<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:blogger='http://schemas.google.com/blogger/2008' xmlns:georss='http://www.georss.org/georss' xmlns:gd="http://schemas.google.com/g/2005" xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-8861905861585113921</id><updated>2026-10-31T15:40:34.416-04:00</updated><category term="Asp .net"/><category term="C #"/><category term="Web service"/><category term=".net"/><category term="Android"/><category term="sql server"/><category term="Videos"/><category term="JAVA"/><category term="JavaScript"/><category term="Python"/><category term="Tutorials"/><category term="AI Agents"/><category term="Deploy Web Sites"/><category term="Windows Forms"/><category term="sql"/><category term="visual Studio"/><category term="C # data base"/><category term="C#"/><category term="IIS SERVER"/><category term="Projects"/><category term="Smart Phone"/><category term="Windows Phone 7"/><category term="XML"/><category term="Gemini"/><category term="Installation"/><category term="Interview Questions"/><category term="Irrlicht 3D  Game"/><category term="NED Itec 2012"/><category term="Oracle"/><category term="jQuery"/><category term="Blogger Tip"/><category term="Data Grid View"/><category term="Email"/><category term="FastAPI"/><category term="Html5"/><category term="Iphone"/><category term="Reports in C#"/><category term="Smart Phone Apps"/><category term="Web"/><category term="Web Server"/><category term="Windows Phone 7.5"/><category term="seo"/><category term="sql server 2008"/><category term="AT commands"/><category term="Ajax"/><category term="Automation"/><category term="Azure"/><category term="Cloud"/><category term="Data Structure"/><category term="Downloads"/><category term="Downloads tutorials"/><category term="Free Source Code"/><category term="Game Programming  Visual C++"/><category term="Google"/><category term="Google Code"/><category term="Google Maps"/><category term="Google Search"/><category term="IT News"/><category term="JSon"/><category term="LINQ"/><category term="MCP"/><category term="Mobile App Development Competition"/><category term="Model Context &#xa;Protocol"/><category term="Objective-C"/><category term="Open Source"/><category term="PHP"/><category term="Regex"/><category term="Site map"/><category term="Small AI Apps"/><category term="Tool Calling"/><category term="Tools"/><category term="Windows Service"/><category term="css"/><category term="flash"/><category term="windows applications"/><title type='text'>Visual Studio Learn</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default?redirect=false'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><link rel='next' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default?start-index=26&amp;max-results=25&amp;redirect=false'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>86</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-6914977922879264301</id><published>2026-10-31T15:33:09.149-04:00</published><updated>2026-10-31T15:40:34.416-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="Open Source"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>Does the 118K-Star AI Trading Agent Actually Work? I Installed and Tested TradingAgents v0.7.1 — Open-Source Agent, Tested #1</title><content type='html'>&lt;p&gt;&lt;strong&gt;Search description:&lt;/strong&gt; I installed TradingAgents v0.7.1 — the 118K-star multi-agent trading framework — and tested it for real. Setup, keys, errors, honest verdict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Labels:&lt;/strong&gt; Python, AI Agents, Open Source, Tutorials&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; TradingAgents &lt;strong&gt;v0.7.1&lt;/strong&gt; (release tag, commit &lt;code&gt;c4544913a14ed3df5f1ed7b9e8483270ce20682a&lt;/code&gt; — the exact code tested here) · Python &lt;strong&gt;3.12.3&lt;/strong&gt; (the project requires Python 3.11 or later) · Repository figures — &lt;strong&gt;118,913 stars, 22,731 forks, 884 watchers&lt;/strong&gt; — read from the project&#39;s GitHub page on &lt;strong&gt;October 31, 2026&lt;/strong&gt;; this repository gains hundreds of stars a day, so treat those numbers as a dated snapshot, not a permanent fact. Licence: Apache-2.0.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A framing note before anything else.&lt;/strong&gt; TradingAgents describes itself, in its own documentation, as a framework &quot;designed for research purposes&quot; that &quot;is not intended as financial, investment, or trading advice.&quot; This post holds it to exactly that standard. This is an engineering test — install it, run it, read what it produces — not a recommendation of any stock, strategy, or tool for real money. The framework&#39;s approved orders go to a &lt;em&gt;simulated&lt;/em&gt; exchange. Nothing here is financial advice.&lt;/p&gt;
&lt;h2&gt;The direct answer&lt;/h2&gt;
&lt;p&gt;TradingAgents is a free, open-source Python framework that splits stock analysis across a team of LLM agents modelled on a trading firm: four analysts research in parallel, two researchers debate the evidence bull-versus-bear, a trader proposes a decision, and a risk check plus a portfolio manager can approve or reject it. It installs with a clone, a virtual environment, and one &lt;code&gt;pip install .&lt;/code&gt; — on the machine used for this test that install finished cleanly, with no errors, and the command-line help runs before any API key exists. The one thing you must supply yourself is a language model: a provider API key, a ChatGPT Plus/Pro sign-in, or a local model.&lt;/p&gt;
&lt;p&gt;And the test result, up front, because this series does not bury it: the full pipeline &lt;strong&gt;did run end to end&lt;/strong&gt; — but it took &lt;strong&gt;five attempts&lt;/strong&gt; to get one complete run, and every failure was a lesson (one quirk of my test machine, one dropped connection, and two hard lessons about free-tier API quotas). When it completed, the framework analysed NVIDIA as of October 9, 2026, debated it properly, and issued a decision — &lt;strong&gt;Overweight, buy the dip, with a strict stop-loss&lt;/strong&gt;. The stock then fell straight through that stop within a week. The framework&#39;s directional call was wrong; its risk discipline was the most valuable thing it produced. The full evidence is below.&lt;/p&gt;
&lt;div style=&quot;text-align:center; margin: 24px 0;&quot;&gt;&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/alakuyGZDNU&quot; title=&quot;Does the 118K-Star AI Trading Agent Actually Work? TradingAgents v0.7.1 Tested&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2&gt;What this test is (and is not)&lt;/h2&gt;
&lt;p&gt;This is the pilot episode of &lt;strong&gt;Open-Source Agent, Tested&lt;/strong&gt;, a series with three fixed rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The version is pinned.&lt;/strong&gt; Every claim below is about v0.7.1 at the commit named above. If the project has released something newer by the time you read this, the post says so rather than quietly drifting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The run is real.&lt;/strong&gt; The install log, the errors, and the output shown here come from an actual session on an actual machine — not from the project&#39;s documentation. If something fails, the failure is the result and it gets reported as one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The verdict is honest.&lt;/strong&gt; Star counts measure popularity, not quality. The end of this post separates what is genuinely well-built from what is demo-grade, and says who should bother.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What it is not: a trading strategy, a signal service, or a promise. See the framing note above.&lt;/p&gt;
&lt;h2&gt;Meet the framework: a firm, not a chatbot&lt;/h2&gt;
&lt;p&gt;Most &quot;AI stock tools&quot; are one model answering one prompt. TradingAgents instead divides the labour the way a trading firm divides it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Analyst Team&lt;/strong&gt; — four specialists who work in parallel:&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Fundamentals&lt;/em&gt; — the company&#39;s financials.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Sentiment&lt;/em&gt; — news headlines, StockTwits, and Reddit.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;News&lt;/em&gt; — global news and macroeconomic events.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Technical&lt;/em&gt; — chart indicators such as MACD and RSI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Researcher Team&lt;/strong&gt; — two researchers take the analysts&#39; reports and argue: one builds the bullish case, the other builds the bearish case. The debate is the point — a claim has to survive an opponent before it travels further.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trader Agent&lt;/strong&gt; — reads the reports and the debate, then composes a concrete proposal: timing and position size.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk Management and Portfolio Manager&lt;/strong&gt; — the proposal is checked for volatility and liquidity risk, and the Portfolio Manager makes the final call: approve or reject. Only an approved order reaches the (simulated) exchange.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That pipeline — specialists gather, opponents argue, a gatekeeper decides — is the design being tested here. Everything else in this post is setup, evidence, and verdict.&lt;/p&gt;
&lt;h2&gt;Environment setup — do this first&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3.11 or later.&lt;/strong&gt; This test used Python 3.12.3. Check yours before anything else:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;bash
  python3 --version&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If it prints 3.10 or lower, install a newer Python first (from python.org, or your OS package manager) — the project&#39;s requirement is 3.11+, and the steps below were verified on 3.12.3.
- &lt;strong&gt;Git&lt;/strong&gt;, to clone the repository.
- &lt;strong&gt;A language-model route&lt;/strong&gt; (covered in its own step below — do not skip it; the analysis cannot run without one).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — clone the repository and pin the tested version:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/TauricResearch/TradingAgents.git
cd TradingAgents
git checkout v0.7.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking out the tag prints Git&#39;s &quot;detached HEAD&quot; notice. That is informational, not an error — it simply confirms you pinned a release tag instead of following the moving main branch. For a test like this one, the pin is what you want: everyone reading this post can reproduce the exact code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — create a virtual environment and install:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 -m venv .venv
source .venv/bin/activate
# Windows PowerShell: py -m venv .venv ; .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Prefer &lt;code&gt;uv&lt;/code&gt;? The project also documents &lt;code&gt;uv venv --python 3.13&lt;/code&gt; followed by &lt;code&gt;uv pip install .&lt;/code&gt;. Prefer Docker? It documents &lt;code&gt;docker compose run --rm tradingagents&lt;/code&gt; with your keys in a &lt;code&gt;.env&lt;/code&gt; file. All three routes install the same v0.7.1 code.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What actually happened on this test machine:&lt;/strong&gt; Python 3.12.3 on Linux, &lt;code&gt;pip install .&lt;/code&gt; completed successfully in roughly four and a half minutes. No errors — none. That is worth stating plainly, because a clean first install is not the open-source default, and this post will not invent friction that did not occur.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — verify the install (no key needed yet):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tradingagents --help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prints the CLI&#39;s options and its three subcommands (&lt;code&gt;backtest&lt;/code&gt;, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;). Notably, it runs with &lt;strong&gt;no API key configured&lt;/strong&gt; — the help does not ask for one. The key only becomes necessary when you run an actual analysis, which is the next step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — give it a language model (zero assumed setup).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An analysis run needs an LLM. The framework accepts several routes; pick ONE:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A provider API key&lt;/strong&gt; — OpenAI, Google, Anthropic, xAI, DeepSeek, DashScope, Zhipu, MiniMax, OpenRouter, Mistral, Moonshot, Groq, NVIDIA, and others are supported. Keys go in a &lt;code&gt;.env&lt;/code&gt; file in the project folder (see below), never in code you commit or share — a key is a password, and anyone who has it can spend your money.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google / Gemini walkthrough (the route this test used):&lt;/strong&gt;
  1. Go to &lt;strong&gt;aistudio.google.com&lt;/strong&gt; and sign in with a Google account.
  2. Open &lt;strong&gt;Get API key&lt;/strong&gt; and choose &lt;strong&gt;Create API key&lt;/strong&gt; (create or pick a Google Cloud project when prompted).
  3. Copy the key Google shows you. You will not be able to view it again in full later — save it directly into your &lt;code&gt;.env&lt;/code&gt; file (next step), not into a note you might share.
  4. Billing note: this test ran &lt;strong&gt;entirely on the free tier and cost $0&lt;/strong&gt; — but read the run section before assuming free is painless. The Gemini free tier meters requests &lt;strong&gt;per day, per project, per model&lt;/strong&gt; (20 requests/day for gemini-3.8-flash; 15/day for gemini-3.5-flash-lite, at the time of this test), and a single multi-agent analysis makes dozens of model calls. The free tier is a real constraint, not a footnote.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChatGPT-subscription sign-in (no API key at all)&lt;/strong&gt; — new in v0.7.0: if you already pay for ChatGPT Plus or Pro, run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;bash
  tradingagents login chatgpt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and sign in with that plan. (&lt;code&gt;tradingagents logout&lt;/code&gt; revokes the machine&#39;s access again.)
- &lt;strong&gt;A local model&lt;/strong&gt; — an Ollama or other OpenAI-compatible local server works too, if you would rather nothing leaves your machine. Expect slower, weaker analysis from small local models; the framework cannot fix a model that cannot reason.&lt;/p&gt;
&lt;p&gt;Create the &lt;code&gt;.env&lt;/code&gt; file in the project folder with the key(s) for the route you chose, following the variable names in the project&#39;s own &lt;code&gt;.env&lt;/code&gt; example. Keep this file out of Git — it is listed in the project&#39;s ignore rules for a reason.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5 — optional data keys.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three optional keys unlock extra data sources: &lt;strong&gt;FRED&lt;/strong&gt; (macroeconomic data), &lt;strong&gt;Alpha Vantage&lt;/strong&gt; (market data), and &lt;strong&gt;TypeSafe&lt;/strong&gt; (sentiment screening). None is required for a first run. This test ran &lt;strong&gt;without&lt;/strong&gt; the optional keys, and the effect is visible in the run below: the framework logged that FRED macro data was unavailable and carried on. Add them later, one at a time, so you can tell which source changed the output.&lt;/p&gt;
&lt;h2&gt;How a run works&lt;/h2&gt;
&lt;p&gt;A run is a single command. The flags most people need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tradingagents --ticker NVDA --date 2026-09-26 --analysts market,news --save --no-show
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ticker&lt;/code&gt; — the symbol to analyse (for example &lt;code&gt;NVDA&lt;/code&gt;, or &lt;code&gt;0700.HK&lt;/code&gt; for a Hong Kong listing).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--date&lt;/code&gt; — the analysis date, &lt;code&gt;YYYY-MM-DD&lt;/code&gt;. A past date is the honest way to test: the framework can only use information available up to that date, and you can compare its decision against what the price actually did afterwards.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--analysts&lt;/code&gt; — a comma-separated subset of the analyst team (for example &lt;code&gt;market,news&lt;/code&gt;); omit it to run the full team.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--save&lt;/code&gt; / &lt;code&gt;--no-show&lt;/code&gt; — save the full report to disk without interactive prompts; &lt;code&gt;--html&lt;/code&gt; also writes the complete report as a single HTML page, which is the comfortable way to read the whole debate afterwards.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--portfolio&lt;/code&gt; — a JSON file of current holdings and cash, so the trader, risk, and portfolio agents size decisions against an actual position instead of a blank slate.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--checkpoint&lt;/code&gt; — saves state after each step so a crashed run can resume instead of starting over. Note this test&#39;s experience in the Errors section: in our session, the checkpoint resume did not pick up and the run restarted from zero. Budget your API requests accordingly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A separate &lt;code&gt;backtest&lt;/code&gt; subcommand scores past decisions across a grid of tickers and dates.&lt;/p&gt;
&lt;p&gt;Under the hood, one run walks the pipeline from &quot;Meet the framework&quot; in order: analysts in parallel → researcher debate → trader proposal → risk evaluation → portfolio manager&#39;s approve/reject → (simulated) fill. Two details from the v0.7.1 release notes deserve attention, because they are correctness fixes of exactly the kind a hype video skips: decisions are now scored &lt;strong&gt;only on final closing prices&lt;/strong&gt;, settlements are handled across long market holidays, and a model-date bug that could let the system read company filings dated &lt;strong&gt;after&lt;/strong&gt; the run date — in plain terms, peek at the future — is fixed. A framework that patches look-ahead bias is taking backtesting seriously.&lt;/p&gt;
&lt;h2&gt;The test run&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The protocol, fixed before any output was seen:&lt;/strong&gt; one well-known ticker (&lt;strong&gt;NVDA&lt;/strong&gt;), one analysis date in the past (&lt;strong&gt;Friday, October 9, 2026&lt;/strong&gt; — chosen so the framework only had information available that day, and so its decision could be scored against the real prices that followed), the &lt;strong&gt;full four-analyst team&lt;/strong&gt;, &lt;strong&gt;one researcher debate round and one risk round&lt;/strong&gt;, the complete report saved, and cost and duration measured. Three questions to answer: does it run end to end, does the debate change anything, and what does it cost?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it actually took: five attempts in 24 minutes.&lt;/strong&gt; The failures are part of the result, and each one is detailed in the Errors section below. In short: attempt 1 died in 7 seconds on a proxy quirk of my test machine; attempt 2 ran ten minutes, completed the analysts and the whole debate, then died at the Research Manager when the Gemini connection dropped; attempt 3 repeated that run and died at the same desk on the free tier&#39;s daily quota (20 requests/day for the deep model); attempt 4 tried a checkpoint resume, which did not resume, and died 33 seconds in on the &lt;em&gt;other&lt;/em&gt; model&#39;s quota (15/day); attempt 5 switched to two untouched free-tier models — &lt;strong&gt;gemini-3.5-flash&lt;/strong&gt; for the deep-thinking agents, &lt;strong&gt;gemini-3.1-flash-lite&lt;/strong&gt; for the quick ones — and &lt;strong&gt;completed the entire pipeline in 107 seconds, exit code 0&lt;/strong&gt;. Everything below is from attempt 5&#39;s saved report. &lt;strong&gt;Measured cost: $0&lt;/strong&gt; — every attempt ran on the free tier. The real currency was quota, not money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What each stage produced (analysis date Oct 9, 2026; NVDA closed that day at $229.28):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Market analyst — uptrend intact, momentum cooling.&lt;/strong&gt; Price above the 50-day average ($221.94) and the 200-day ($201.82) says the structure is bullish; but the MACD histogram has narrowed to 0.118, price dipped below the 10-day EMA, and RSI is a neutral 53. Flagged a possible re-test of support at $226.73.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fundamentals analyst — strongly bullish.&lt;/strong&gt; Quarterly revenue of $96.2 billion, net income of $59.7 billion, over $74 billion of operating cash flow in the first half of the year, $22.4 billion in cash. Its own caveat: growth this large cannot compound forever — watch for deceleration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentiment analyst — neutral, 5.0 out of 10, confidence low — because it had no data.&lt;/strong&gt; For a historical window, its sources (news headlines, StockTwits, Reddit) returned nothing usable — Reddit&#39;s feed was blocked outright (HTTP 403) during these sessions — and the report &lt;strong&gt;says exactly that&lt;/strong&gt; instead of inventing a mood. Honest, and a genuine limitation: sentiment data does not do history, so past-date tests fly blind on this desk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;News analyst — themes, disclosed gaps.&lt;/strong&gt; Real-time news and macro feeds were unavailable (no FRED key configured; the framework logged the fallback and continued), so the report framed NVDA around AI spending, the Fed&#39;s rate path, supply chains and geopolitics — and disclosed the missing data up front.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The debate — the bear nearly won the room.&lt;/strong&gt; The bull argued the moat (an ecosystem hyperscalers cannot abandon), the fortress balance sheet, and a healthy consolidation above both moving averages. The bear argued a blow-off top: hyperscalers are building their own silicon precisely to break NVIDIA&#39;s pricing power, growth on a $96 billion base is mathematically near-impossible to sustain, capex cycles always turn — and if $226.73 and $221.94 break, momentum selling follows. The &lt;strong&gt;Research Manager sided with the bull&lt;/strong&gt; (financial dominance is current fact; the bear&#39;s case is a future risk) but adopted the bear&#39;s warning levels as the trade&#39;s entry zone and stop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trader — BUY.&lt;/strong&gt; Entry $226.73, stop-loss $218.00, size 1.25× standard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk debate — the numbers got moderated.&lt;/strong&gt; The aggressive analyst wanted the full 1.25× (&quot;the real risk is opportunity cost&quot;). The conservative wanted 0.5–0.75× (a stop is only as good as the liquidity that fills it in a fast drop). The neutral proposed the compromise: keep the buy, size at 1.0×, and widen the stop to $215 — at $218, the stop sits less than 1.5× the daily volatility (ATR 5.68) from entry, close enough for ordinary noise to trigger it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portfolio Manager — final decision: OVERWEIGHT.&lt;/strong&gt; Enter between &lt;strong&gt;$226.73 and $221.94&lt;/strong&gt;, size &lt;strong&gt;1.0×–1.25×&lt;/strong&gt;, stop-loss &lt;strong&gt;$215–$218&lt;/strong&gt;, horizon &lt;strong&gt;3–6 months&lt;/strong&gt;, &lt;strong&gt;no price target&lt;/strong&gt;. Compare it with the Trader&#39;s proposal and you can see the debate&#39;s fingerprints: both single numbers became ranges — the neutral risk analyst&#39;s compromise, adopted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The decision vs. reality.&lt;/strong&gt; NVDA&#39;s actual closes after October 9 (Yahoo Finance data): &lt;strong&gt;$226.32 on Oct 12&lt;/strong&gt; — inside the buy zone, so the entry triggered on the first trading day. &lt;strong&gt;$221.30 on Oct 14&lt;/strong&gt; — below the zone, and below the 50-day average the thesis stood on. &lt;strong&gt;$217.23 on Oct 15&lt;/strong&gt; — inside the decision&#39;s own stop-loss band; a rules-following holder exits there. &lt;strong&gt;$214.13 on Oct 16&lt;/strong&gt; — below the whole band; the exit is complete, at roughly &lt;strong&gt;−4% to −6%&lt;/strong&gt; depending on the entry fill. The slide continued: &lt;strong&gt;$204.64 by Oct 30, −10.7%&lt;/strong&gt; from the analysis-date close. The bear&#39;s fragility warning is almost literally what happened next.&lt;/p&gt;
&lt;p&gt;So the scoreboard is split, and this post reports both halves: &lt;strong&gt;the directional call was wrong&lt;/strong&gt; — the stock broke the support the thesis rested on within four trading days — &lt;strong&gt;and the stop-loss was the most valuable line in the decision&lt;/strong&gt;, because obeying it cut the loss roughly in half versus holding (−4% to −6% out versus −10.7% by month&#39;s end). One run, one ticker, one date: an observation about what this framework did here, not evidence of a repeatable edge or a repeatable failure rate.&lt;/p&gt;
&lt;h2&gt;Errors you may hit&lt;/h2&gt;
&lt;p&gt;Every error below was actually observed during this test (install errors: none — the install was clean), with the exact text where it matters. They fall into three buckets, and the bucket matters: framework behaviour, my machine, and the model provider&#39;s free tier.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bucket 1 — provider free-tier quotas (the wall you are most likely to hit):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Daily request quota exhausted — &lt;code&gt;429 RESOURCE_EXHAUSTED&lt;/code&gt;.&lt;/strong&gt; Observed in attempts 3 and 4. The Gemini API free tier allows &lt;strong&gt;20 generate requests per day, per project, per model&lt;/strong&gt; for gemini-3.8-flash, and &lt;strong&gt;15 per day&lt;/strong&gt; for gemini-3.5-flash-lite. Attempt 3&#39;s error named the limit exactly (&quot;Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-3.8-flash&quot;) and quoted a retry delay of 52,503 seconds — about 14.6 hours, i.e. &quot;tomorrow.&quot; A multi-agent analysis makes dozens of calls, so a free-tier day can be spent by a single deep run plus retries. Fixes: spread your runs across models (each model has its own bucket — that is what saved attempt 5), wait for the daily reset, or enable a paid tier and watch the per-run cost instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model at capacity — &lt;code&gt;503 UNAVAILABLE&lt;/code&gt;.&lt;/strong&gt; Observed in attempt 3, immediately before the quota error: &quot;This model is currently experiencing high demand. Spikes in demand are usually temporary.&quot; The framework retried the step in a different mode (free text instead of structured output) on its own — good behaviour — but the retry is what met the quota wall. Fix: wait and retry; the framework already does the graceful part for you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider connection drop — &lt;code&gt;RemoteProtocolError: Server disconnected without sending a response.&lt;/code&gt;&lt;/strong&gt; Observed in attempt 2, at the Research Manager, after ten minutes of completed analyst and debate work. The framework&#39;s one automatic free-text retry hit the same drop. Fix: re-run. This is also the practical case for &lt;code&gt;--save&lt;/code&gt; and saved partial reports — attempt 2&#39;s completed stages were on disk, not lost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bucket 2 — framework behaviour worth knowing before you budget requests:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Checkpoint resume did not resume (observed, attempt 4).&lt;/strong&gt; The resumed run restarted from the beginning instead of picking up attempt 3&#39;s saved state, and promptly re-spent requests against a fresh quota wall. Treat &lt;code&gt;--checkpoint&lt;/code&gt; as a seatbelt you should still test on your own setup, not as a guarantee — and do not plan a quota-tight session assuming resume will save you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data vendors fail, and the framework carries on (observed, attempts 2 and 5).&lt;/strong&gt; Yahoo Finance&#39;s news feed timed out twice in attempt 2 (&quot;Vendor &#39;yfinance&#39; unavailable for get_news: Yahoo Finance request failed: Timeout; trying next vendor.&quot;), Reddit&#39;s RSS returned &quot;HTTP Error 403: Blocked&quot; for the sentiment feeds, and the unconfigured FRED source was skipped with a logged note in attempt 5. In every case the run continued with the remaining sources, and the affected analysts disclosed the gap in their reports rather than papering over it. That is the framework behaving &lt;em&gt;well&lt;/em&gt; — but know that a past-date analysis will have a blind sentiment desk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bucket 3 — my machine (probably not yours):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;httpx &lt;code&gt;InvalidURL&lt;/code&gt; crash at startup — &lt;code&gt;ValueError: invalid literal for int() with base 10: &#39;:1]&#39;&lt;/code&gt;.&lt;/strong&gt; Observed in attempt 1; the run died in 7 seconds before any analysis. Cause: this test machine&#39;s proxy environment sets a &lt;code&gt;NO_PROXY&lt;/code&gt; value containing a bracketed IPv6 address, and httpx&#39;s URL parser chokes on it. Framework: innocent. Fix on a similarly proxied machine: clear &lt;code&gt;NO_PROXY&lt;/code&gt;/&lt;code&gt;no_proxy&lt;/code&gt; from the run&#39;s environment. If your machine has no such proxy variables, you will never see this.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;And the basics, verified during setup:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No LLM provider configured when starting an analysis.&lt;/strong&gt; The help command runs fine without a key, but an unattended analysis start stops with exactly this: &quot;GOOGLE_API_KEY is not set; there is no terminal to ask for it.&quot; (The provider named is whichever one you selected.) Fix: complete Step 4 — add the key to your &lt;code&gt;.env&lt;/code&gt; or finish the ChatGPT sign-in — then re-run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python older than 3.11.&lt;/strong&gt; The project requires 3.11+. This test ran 3.12.3 throughout, so the old-Python failure itself was not reproduced here; the requirement is the project&#39;s own, stated up front. Fix: install Python 3.11 or newer and rebuild the virtual environment from Step 2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Detached HEAD&quot; notice when checking out v0.7.1.&lt;/strong&gt; Observed during this test&#39;s install. It looks like a warning and reads like a problem; it is neither. Git is telling you a tag — a fixed point — is checked out instead of a branch. For reproducing a tested version, that is the goal. No fix needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A provider key in the wrong place.&lt;/strong&gt; Keys belong in the &lt;code&gt;.env&lt;/code&gt; file or your environment — not pasted into a command you later screenshot, and never committed. If a key leaks, revoke it at the provider and issue a new one; treat it like a lost password, because functionally it is one.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The verdict&lt;/h2&gt;
&lt;p&gt;Judged against the three questions this series fixed before the run:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Did it run end to end? Yes — on the fifth attempt, flawlessly.&lt;/strong&gt; The completed run walked the whole firm — four analysts in parallel (19.9s, 12.4s, 20.6s and 18.5s of wall time on their stages), a real debate, a trader, a three-way risk argument, and a portfolio decision with entry, size, stop and horizon — in &lt;strong&gt;107 seconds&lt;/strong&gt;, unattended, with a complete saved report. The four failures were not the framework&#39;s code: one was my machine&#39;s proxy quirk, one a dropped provider connection, two the free tier&#39;s daily quotas. But &quot;eventually&quot; is part of the grade: a tool you test for free will spend your day in request quotas, and its checkpoint resume did not rescue us when we needed it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Did the debate change anything? Yes — measurably, in the final numbers.&lt;/strong&gt; The research debate set the direction: the manager heard the bear&#39;s structural case and ruled for the bull, on current financial dominance — while adopting the bear&#39;s break-levels as the entry zone and stop. The risk debate then moderated the trade itself: the Trader&#39;s single 1.25× size and $218 stop came out of the Portfolio Manager as ranges — 1.0×–1.25× and $215–$218 — the neutral analyst&#39;s compromise, adopted verbatim in spirit. The final decision is not the document any single agent proposed. That is the strongest evidence that the multi-agent structure is doing work a single prompt does not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. What did it cost? $0 — and a day&#39;s quota.&lt;/strong&gt; On the free tier, one deep analysis can consume a model&#39;s entire daily allowance (20 requests for the deep model here). Budget accordingly: free is a planning exercise, paid is a per-run line item — and at 107 seconds of pipeline time, the paid version of this test would be cheap in wall-clock terms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is genuinely well-built:&lt;/strong&gt; the install (clean, pinned, honest separation between browsing and spending); agents that disclose missing data instead of fabricating it (the sentiment desk&#39;s &quot;we have no data, therefore neutral, low confidence&quot; is the single most trustworthy paragraph in the report); vendor fallbacks that keep a run alive; release hygiene that patches look-ahead bias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is demo-grade, today:&lt;/strong&gt; historical sentiment is effectively absent, so past-date tests — the honest kind — run with one desk blind; the news desk without optional data keys works from themes, not facts; checkpoint resume failed its one live test; and the flagship decision this framework produced was directionally wrong within a week, saved from embarrassment only by its own stop-loss. None of that makes it a toy. All of it makes it a research instrument whose output you verify — which is, to be fair, exactly what its authors claim.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who should bother:&lt;/strong&gt; developers and technically comfortable investors who want to see multi-agent orchestration do real work on a real problem, and who will treat the output as a structured second opinion to be checked — starting with its stop-loss. Who should not: anyone looking for a signal to trade on. The framework&#39;s own documentation told you that before I did.&lt;/p&gt;
&lt;h2&gt;Get the code&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Companion code for this episode&lt;/strong&gt; — the exact commands, environment notes, and run configuration used here, in the channel&#39;s public code repository: https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-06-tradingagents-tested&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The project under test&lt;/strong&gt; (credit: Tauric Research): https://github.com/TauricResearch/TradingAgents — star it there, not here, if the test convinces you.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TradingAgents repository (repo page: description, framework overview, installation and CLI documentation; star/fork/watcher counts as of October 31, 2026): https://github.com/TauricResearch/TradingAgents&lt;/li&gt;
&lt;li&gt;TradingAgents releases page (v0.7.1 and v0.7.0 release notes — scoring on final closes, long-holiday settlement, model-date fix; ChatGPT-subscription provider, backtest scoring, release wheels): https://github.com/TauricResearch/TradingAgents/releases&lt;/li&gt;
&lt;li&gt;This test&#39;s own install log and run logs (Python 3.12.3; &lt;code&gt;pip install .&lt;/code&gt;; five run attempts with verbatim error text; the completed attempt&#39;s full report bundle), recorded October 31, 2026, summarised in the setup, run and errors sections above.&lt;/li&gt;
&lt;li&gt;Post-run NVDA daily closing prices, Yahoo Finance chart data, fetched October 31, 2026.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Reminder, in the project&#39;s own spirit and this series&#39; rules: TradingAgents is a research framework. This post is an engineering test report — not financial, investment, or trading advice.&lt;/em&gt;&lt;/p&gt;
&lt;div style=&quot;text-align:center;border:1px solid #e3e6ea;border-radius:8px;padding:18px 14px;margin:28px 0;background:#fbfcfd;&quot;&gt;&lt;h3 style=&quot;margin:0 0 6px;&quot;&gt;Enjoying these tutorials?&lt;/h3&gt;&lt;p style=&quot;margin:0 0 6px;&quot;&gt;Get new Visual Studio Learn posts by email — free, no spam:&lt;/p&gt;&lt;style&gt;
    .followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview {
  display: flex !important;
  flex-direction: column !important;
  justify-content: center !important;
  margin-top: 30px !important;
  padding: clamp(17px, 5%, 40px) clamp(17px, 7%, 50px) !important;
  max-width: none !important;
  border-radius: 6px !important;
  box-shadow: 0 5px 25px rgba(34, 60, 47, 0.25) !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview,
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview *{
  box-sizing: border-box !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading {
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading h5{
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field {
  margin-top: 20px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  width: 100% !important;
  height: 40px !important;
  border-radius: 6px !important;
  border: 2px solid #e9e8e8 !important;
  background-color: #fff;
  outline: none !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  color: #000000 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  text-align: center;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::placeholder {
  color: #000000 !important;
  opacity: 1 !important;
}

.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input:-ms-input-placeholder {
  color: #000000 !important;
}

.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::-ms-input-placeholder {
  color: #000000 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button {
  margin-top: 10px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button {
  width: 100% !important;
  height: 40px !important;
  border: 0 !important;
  border-radius: 6px !important;
  line-height: 0px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button:hover {
  cursor: pointer !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line {
  color: #231f20 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 13px !important;
  font-weight: 400 !important;
  line-height: 25px !important;
  text-align: center !important;
  text-decoration: none !important;
  display: flex !important;
  width: 100% !important;
  justify-content: center !important;
  align-items: center !important;
  margin-top: 10px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line img {
  margin-left: 10px !important;
  height: 1.13em !important;
  max-height: 1.13em !important;
}

  &lt;/style&gt;
  &lt;div class=&quot;followit--follow-form-container&quot; attr-a attr-b attr-c attr-d attr-e attr-f&gt;
    &lt;form data-v-f93f6730=&quot;&quot; action=&quot;https://api.follow.it/subscription-form/SmRpWjEzV1ovU0FOK2R2Y0oxajFBUmtIdW44K1ZiUWZRUm1FZVdpVlRKUHFLSFBKWjE4M1hTSk5yRU5oNmg0RU5zelhuTllNTGU1UmwzRkN0NzZWbnd2YkV5VWFwYnptWFdtekI3eXdwMmhtamtUNit3dlNSblVmRjJxd1JGbjZ8Q05yQWR2eitvYW5HRmNMMzJkbStpM09rNmxXZ0ZjWWhyOERya2VvVXRzZz0=/8&quot; method=&quot;post&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;form-preview&quot; style=&quot;background-color: rgb(255, 255, 255); border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204); position: relative;&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-heading&quot;&gt;&lt;h5 data-v-f93f6730=&quot;&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;&quot;&gt;Get new posts by email:&lt;/h5&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-input-field&quot;&gt;&lt;input data-v-f93f6730=&quot;&quot; type=&quot;email&quot; name=&quot;email&quot; required=&quot;&quot; placeholder=&quot;Enter your email&quot; spellcheck=&quot;false&quot; style=&quot;font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;&quot; /&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-submit-button&quot;&gt;&lt;button data-v-f93f6730=&quot;&quot; type=&quot;submit&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(255, 255, 255); font-size: 16px; text-align: center; background-color: rgb(0, 0, 0); text-transform: none !important;&quot;&gt;Subscribe&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;/form&gt;&lt;a href=&quot;https://follow.it&quot; class=&quot;powered-by-line&quot;&gt;Powered by &lt;img src=&quot;https://follow.it/images/colored-logo.svg&quot; alt=&quot;follow.it&quot; height=&quot;17px&quot;/&gt;&lt;/a&gt;
  &lt;/div&gt;&lt;p style=&quot;margin:12px 0 0;font-size:14px;&quot;&gt;Follow along: &lt;a href=&quot;https://www.youtube.com/@VisualStudioLearn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;YouTube&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://www.facebook.com/VisualStudio.Learn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Facebook&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://visualstudiolearn.blogspot.com/feeds/posts/default?alt=rss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RSS&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/6914977922879264301/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/does-118k-star-ai-trading-agent.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/6914977922879264301'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/6914977922879264301'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/does-118k-star-ai-trading-agent.html' title='Does the 118K-Star AI Trading Agent Actually Work? I Installed and Tested TradingAgents v0.7.1 — Open-Source Agent, Tested #1'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/alakuyGZDNU/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-873346114526941927</id><published>2026-10-29T16:27:21.679-04:00</published><updated>2026-10-29T16:27:21.680-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Azure"/><category scheme="http://www.blogger.com/atom/ns#" term="Cloud"/><category scheme="http://www.blogger.com/atom/ns#" term="FastAPI"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>How Do I Deploy a FastAPI App to Azure App Service? Step by Step, From Zero</title><content type='html'>&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; Python 3.12.3 · Azure CLI 2.91.0 · fastapi 0.162.0 · uvicorn 0.54.0 · gunicorn 26.2.0 · uvicorn-worker 0.4.0 · App Service runtime &lt;code&gt;PYTHON:3.12&lt;/code&gt;. Everything runs from one file, &lt;code&gt;main.py&lt;/code&gt;, printed in full below — the exact file in the companion repo. Recorded October 2026; the free-tier limits and prices below are Microsoft&#39;s published figures as of that date.&lt;/p&gt;&lt;h2&gt;The direct answer&lt;/h2&gt;&lt;p&gt;Deploy a FastAPI app to Azure App Service, step by step, on the free tier: put the app in a folder with &lt;code&gt;requirements.txt&lt;/code&gt; at the root, create a resource group, a free (F1) App Service plan, and a Python 3.12 web app, set two app settings, deploy a zip of the code, and — the step most tutorials skip — set the &lt;strong&gt;startup command&lt;/strong&gt;. On Python 3.13 and earlier, App Service auto-detects Django and Flask only; a FastAPI app with no startup command doesn&#39;t error, it silently serves a built-in default page. The startup command is one line, and this post explains every piece of it. At the end your app answers at &lt;a href=&quot;https://vsl-info-api.azurewebsites.net&quot;&gt;https://vsl-info-api.azurewebsites.net&lt;/a&gt; — for $0, with a delete command that removes everything when you&#39;re done.&lt;/p&gt;&lt;h2&gt;The finished thing first&lt;/h2&gt;&lt;p&gt;Before any setup, here is what the finished deployment looks like. One small FastAPI app — an “App Service Info API” — answering from Azure at &lt;a href=&quot;https://vsl-info-api.azurewebsites.net&quot;&gt;https://vsl-info-api.azurewebsites.net&lt;/a&gt;:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/&quot;&gt;https://vsl-info-api.azurewebsites.net/&lt;/a&gt; — a hello message plus &lt;strong&gt;where the code is running&lt;/strong&gt;: &lt;code&gt;environment: &quot;production&quot;&lt;/code&gt;, &lt;code&gt;hostname: &quot;vsl-info-api.azurewebsites.net&quot;&lt;/code&gt;. The same code on your own machine answers &lt;code&gt;&quot;local&quot;&lt;/code&gt; — that before/after contrast is the whole demo.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/greet/YourName&quot;&gt;https://vsl-info-api.azurewebsites.net/greet/YourName&lt;/a&gt; — a personalised greeting, proving routing and path parameters survived the trip.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/health&quot;&gt;https://vsl-info-api.azurewebsites.net/health&lt;/a&gt; — &lt;code&gt;status: &quot;ok&quot;&lt;/code&gt; plus the Python version and current UTC time, the endpoint you hit to prove the app is really up.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The deploy itself took about 2 minutes 40 seconds from zip upload to live answers, on the free F1 plan. Now, rewind.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzgHu6zkO5XmaAhOBAW1UL_5eDQ3y2FoJ8i7RplrxHCFAb19i6aKwLbQ5lZhRUv7nuPGcY1VlAPEzBYwl9E19C66dwJ9jqFV1ArJQr_16H7dwPwt2TEwEmJYOA_U7FWlrfry6syRX7iUz-F-cWKUAmi5BoFfzZR08hF6YjgkuYlUoFLd_uuvYLT3kKbCQ/s1919/portal-overview.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;Azure portal overview of the vsl-info-api web app, status Running, on the free F1 plan&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzgHu6zkO5XmaAhOBAW1UL_5eDQ3y2FoJ8i7RplrxHCFAb19i6aKwLbQ5lZhRUv7nuPGcY1VlAPEzBYwl9E19C66dwJ9jqFV1ArJQr_16H7dwPwt2TEwEmJYOA_U7FWlrfry6syRX7iUz-F-cWKUAmi5BoFfzZR08hF6YjgkuYlUoFLd_uuvYLT3kKbCQ/s400/portal-overview.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;Azure portal overview of the vsl-info-api web app, status Running, on the free F1 plan.&lt;/p&gt;&lt;h2&gt;What you will build&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;A deployment-check API&lt;/strong&gt; — one file, &lt;code&gt;main.py&lt;/code&gt;, three endpoints, no database, no secrets. Nothing to leak on a screenshot.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The deploy&lt;/strong&gt; — resource group, free plan, web app, app settings, zip deploy, startup command, verification from the browser and the log stream.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The shortcut&lt;/strong&gt; — &lt;code&gt;az webapp up&lt;/code&gt;, the one command that does the whole creation-and-deploy in one go, shown after the long way so you know what it did.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The cleanup&lt;/strong&gt; — one command that deletes everything, and the honest note that this post&#39;s own resources were deleted after filming.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Environment setup — do this first&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Python 3.12 (this post uses 3.12.3), a terminal, and an Azure account. The free account is enough; everything here runs on the free F1 tier and costs $0.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Create the Azure free account, click by click:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to &lt;strong&gt;azure.microsoft.com/free&lt;/strong&gt; and choose &lt;strong&gt;Try Azure for free&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Sign in with a &lt;strong&gt;Microsoft account or a GitHub account&lt;/strong&gt; (create one if you don&#39;t have one).&lt;/li&gt;&lt;li&gt;Fill in the profile details.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Identity verification:&lt;/strong&gt; a &lt;strong&gt;phone number&lt;/strong&gt; and a &lt;strong&gt;credit card or non-prepaid debit card&lt;/strong&gt;. The card is only checked for identity — a &lt;strong&gt;~$1 temporary authorization hold&lt;/strong&gt; may appear and is removed. You are charged nothing unless you later choose pay-as-you-go pricing.&lt;/li&gt;&lt;li&gt;Accept the agreement and finish.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The offer, as Microsoft documents it: a &lt;strong&gt;US$200 credit for your first 30 days&lt;/strong&gt;, plus amounts of popular services free for 12 months (and some always free). If you don&#39;t upgrade within 30 days, or when the credit runs out, the account and services are disabled until you do — nothing keeps billing in the background. This tutorial never needs the credit at all: the F1 plan is $0, and the free account exists here only as the door in.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Install the Azure CLI, then sign in:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Install the Azure CLI for your operating system from Microsoft&#39;s install page (learn.microsoft.com — “Install the Azure CLI”), then verify and sign in:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az --version        # expect azure-cli 2.91.0
az login            # opens a browser sign-in — complete it there
az account show     # read this output: it names the subscription
                    # you are actually about to deploy to&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two optional checks while you&#39;re here. The authoritative list of Python runtimes App Service currently offers is a command, not a page — run it and note that &lt;code&gt;PYTHON:3.12&lt;/code&gt; is in it:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az webapp list-runtimes --os-type linux --runtime python --output table&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(At this post&#39;s production run, on October 28, 2026, the Python rows of that output were &lt;code&gt;PYTHON|3.15&lt;/code&gt;, &lt;code&gt;PYTHON|3.14&lt;/code&gt;, &lt;code&gt;PYTHON|3.13&lt;/code&gt;, &lt;code&gt;PYTHON|3.12&lt;/code&gt;, and &lt;code&gt;PYTHON|3.11&lt;/code&gt;, all Active, plus &lt;code&gt;PYTHON|3.10&lt;/code&gt;, marked Near end of life (support ends 2026-10-31). The list moves — run the command rather than trusting any printed copy, including this one.)&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Create the Python environment and run the app locally first:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;mkdir fastapi-info &amp;amp;&amp;amp; cd fastapi-info
python3 -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate
# Windows PowerShell: py -m venv .venv ; .venv\Scripts\Activate.ps1
pip install -r requirements.txt&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The code goes in this folder as &lt;code&gt;main.py&lt;/code&gt; and &lt;code&gt;requirements.txt&lt;/code&gt; — both printed in full below, and the companion repo has them as ready-made files.)&lt;/p&gt;&lt;pre&gt;&lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Open http://127.0.0.1:8000/ — it answers &lt;code&gt;&quot;environment&quot;: &quot;local&quot;&lt;/code&gt;, &lt;code&gt;&quot;hostname&quot;: &quot;localhost&quot;&lt;/code&gt;. Try &lt;code&gt;/health&lt;/code&gt; and &lt;code&gt;/greet/YourName&lt;/code&gt; too. &lt;strong&gt;Do not skip the local run&lt;/strong&gt;: it separates “my code is broken” from “my deploy is broken” before Azure is involved. The local run for this post, real output:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;{&quot;app&quot;:&quot;App Service Info API&quot;,&quot;message&quot;:&quot;Hello from FastAPI&quot;,&quot;environment&quot;:&quot;local&quot;,&quot;hostname&quot;:&quot;localhost&quot;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Verify-it-works checkpoint:&lt;/strong&gt; local app answers at &lt;code&gt;http://127.0.0.1:8000/&lt;/code&gt;, and &lt;code&gt;az account show&lt;/code&gt; names the subscription you intend to use. Both on screen before you touch Azure&#39;s portal.&lt;/p&gt;&lt;h2&gt;The app — one file, three endpoints&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;App Service Info API: the smallest app that proves a deploy worked.

Companion code for the Visual Studio Learn post and video
&quot;How Do I Deploy a FastAPI App to Azure App Service? Step by Step,
From Zero&quot;.

This one-file FastAPI app exists to make deployment concepts visible:

- GET /        reports where the code is running. It reads the
               APP_ENVIRONMENT app setting (which we set to
               &quot;production&quot; in Azure) and WEBSITE_HOSTNAME, a variable
               App Service defines automatically. Locally, with neither
               set, the same code answers &quot;local&quot; — the before/after
               contrast is the whole demo.
- GET /health  status, the Python version, and the current UTC time:
               the endpoint you hit to prove the app is really up.
- GET /greet/{name}
               a personalised greeting — proof that routing and path
               parameters survived the trip to the cloud.

Requires: Python 3.10 or newer and the packages in requirements.txt.
Run locally:  uvicorn main:app --reload
In Azure it is served by Gunicorn with the Uvicorn worker class — see
startup.txt, which holds the exact command the post sets in App
Service.
&quot;&quot;&quot;

import logging
import os
import platform
from datetime import datetime, timezone

from fastapi import FastAPI

logger = logging.getLogger(&quot;info_api&quot;)
logging.basicConfig(level=logging.INFO, format=&quot;%(levelname)s %(message)s&quot;)

app = FastAPI(title=&quot;App Service Info API&quot;)


def environment() -&amp;gt; str:
    &quot;&quot;&quot;Where this process believes it is running.

    APP_ENVIRONMENT is an ordinary app setting: unset locally (so the
    default &quot;local&quot; answers), set to &quot;production&quot; in Azure.
    &quot;&quot;&quot;
    return os.environ.get(&quot;APP_ENVIRONMENT&quot;, &quot;local&quot;)


def hostname() -&amp;gt; str:
    &quot;&quot;&quot;The site&#39;s public hostname, when the platform provides one.

    App Service auto-defines WEBSITE_HOSTNAME for every web app.
    Locally it does not exist, so the honest answer is &quot;localhost&quot;.
    &quot;&quot;&quot;
    return os.environ.get(&quot;WEBSITE_HOSTNAME&quot;, &quot;localhost&quot;)


@app.get(&quot;/&quot;)
def read_root() -&amp;gt; dict:
    &quot;&quot;&quot;Say hello, and say where from — the deploy&#39;s proof page.&quot;&quot;&quot;
    return {
        &quot;app&quot;: &quot;App Service Info API&quot;,
        &quot;message&quot;: &quot;Hello from FastAPI&quot;,
        &quot;environment&quot;: environment(),
        &quot;hostname&quot;: hostname(),
    }


@app.get(&quot;/health&quot;)
def health() -&amp;gt; dict:
    &quot;&quot;&quot;Liveness plus runtime identity: status, Python version, UTC now.&quot;&quot;&quot;
    return {
        &quot;status&quot;: &quot;ok&quot;,
        &quot;python&quot;: platform.python_version(),
        &quot;utc_time&quot;: datetime.now(timezone.utc).isoformat(),
    }


@app.get(&quot;/greet/{name}&quot;)
def greet(name: str) -&amp;gt; dict:
    &quot;&quot;&quot;A greeting with the visitor&#39;s name — routing, proven live.&quot;&quot;&quot;
    return {
        &quot;greeting&quot;: f&quot;Hello, {name}!&quot;,
        &quot;environment&quot;: environment(),
        &quot;hostname&quot;: hostname(),
    }&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two design choices to notice. The app reads &lt;strong&gt;app settings as ordinary environment variables&lt;/strong&gt; — that is how App Service configuration reaches code, and it is why setting &lt;code&gt;APP_ENVIRONMENT&lt;/code&gt; later flips the answer. And it has &lt;strong&gt;no database, no key, no secret&lt;/strong&gt;: the demo&#39;s only moving parts are the deploy itself.&lt;/p&gt;&lt;h2&gt;The two files Azure cares about&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;requirements.txt&lt;/code&gt; — at the project root, or the build fails:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;fastapi==0.162.0
uvicorn==0.54.0
gunicorn==26.2.0
uvicorn-worker==0.4.0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;App Service&#39;s build looks for the dependency file at the root of the project (or the zip). Without it, the build reports: &lt;em&gt;“Could not find setup.py or requirements.txt; Not running pip install.”&lt;/em&gt; That error text, verbatim, is the giveaway that the file is in the wrong place.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;startup.txt&lt;/code&gt; — the startup command, the step most tutorials skip:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;gunicorn --bind=0.0.0.0:8000 --timeout 600 --workers 2 -k uvicorn_worker.UvicornWorker main:app --access-logfile &#39;-&#39; --error-logfile &#39;-&#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Piece by piece, because each one is a decision:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Why Gunicorn at all?&lt;/strong&gt; App Service&#39;s built-in Python container already runs apps under Gunicorn, behind an Nginx reverse proxy. The startup command customises that, it doesn&#39;t replace the platform.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Why can&#39;t Gunicorn run FastAPI alone?&lt;/strong&gt; Its default worker is a synchronous WSGI worker. Pointed at an ASGI app, it fails in the log stream with &lt;code&gt;TypeError: FastAPI.__call__() missing 1 required positional argument: &#39;send&#39;&lt;/code&gt;. The fix is the &lt;strong&gt;Uvicorn worker class&lt;/strong&gt;, which speaks ASGI.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Why &lt;code&gt;uvicorn_worker.UvicornWorker&lt;/code&gt;, not the &lt;code&gt;uvicorn.workers&lt;/code&gt; class older tutorials show?&lt;/strong&gt; In uvicorn 0.54.0 the &lt;code&gt;uvicorn.workers&lt;/code&gt; module is deprecated — it still runs, but it logs a deprecation warning pointing at the new &lt;code&gt;uvicorn-worker&lt;/code&gt; package. This post uses the new class; the old one is the fallback an older tutorial&#39;s command will show you.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Why port 8000?&lt;/strong&gt; The built-in Python image exposes port 8000 by default. The app must listen there.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Why 2 workers, not the 4 many tutorials show?&lt;/strong&gt; The free F1 plan has 1 GB of RAM shared across the plan. Two workers leave headroom; four would not teach you anything extra.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;What is &lt;em&gt;not&lt;/em&gt; in this command, deliberately: &lt;code&gt;WEBSITES_PORT&lt;/code&gt;.&lt;/strong&gt; That app setting is for custom containers, not the built-in images — it has no effect inside the container, and setting it is cargo-cult configuration this post corrects on sight.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And the callout that decides the whole walkthrough: &lt;strong&gt;on Python 3.14 and later, App Service auto-detects and runs FastAPI apps with no startup command at all.&lt;/strong&gt; We pin Python 3.12 anyway, because 3.12 is a currently documented runtime (Microsoft&#39;s own PostgreSQL tutorials run on it) and because the startup command is the skill every existing app still needs. If you deploy on 3.14, skip the startup-command step — and know that skipping it is the platform&#39;s new behaviour, not a shortcut you invented.&lt;/p&gt;&lt;h2&gt;Step 1 — Create the resources (portal and CLI, side by side)&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The names below are the exact ones from this post&#39;s own run&lt;/strong&gt; — resource group &lt;code&gt;rg-vsl-topic05-cus&lt;/code&gt;, plan &lt;code&gt;plan-vsl-topic05&lt;/code&gt;, app &lt;code&gt;vsl-info-api&lt;/code&gt;. Pick your own equivalents; the app name in particular must be globally unique across Azure, because it becomes your public address.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Portal:&lt;/strong&gt; Azure portal → &lt;strong&gt;Create a resource&lt;/strong&gt; → &lt;strong&gt;Web App&lt;/strong&gt;. Resource group: create new, &lt;code&gt;rg-vsl-topic05-cus&lt;/code&gt;. Region: &lt;strong&gt;Central US&lt;/strong&gt; — on a brand-new free account the region is not a free choice; see the quota note under the CLI commands below. Publishing model: &lt;strong&gt;Code&lt;/strong&gt;. Runtime stack: &lt;strong&gt;Python 3.12&lt;/strong&gt;. Operating system: &lt;strong&gt;Linux&lt;/strong&gt; (Python on App Service runs on Linux only — Windows Python is no longer supported). Pricing plan: create new, and choose the &lt;strong&gt;Free (F1)&lt;/strong&gt; tier. Review + create.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;CLI — the same resources, in three commands:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# A resource group to hold everything (delete the group = delete all)
az group create --name rg-vsl-topic05-cus --location centralus

# The FREE plan — the --sku F1 part is what keeps this at $0
az appservice plan create --name plan-vsl-topic05 \
  --resource-group rg-vsl-topic05-cus --sku F1 --is-linux \
  --location centralus

# The web app on Python 3.12 (the name must be globally unique;
# the app&#39;s public address becomes &amp;lt;name&amp;gt;.azurewebsites.net)
az webapp create --name vsl-info-api --resource-group rg-vsl-topic05-cus \
  --plan plan-vsl-topic05 --runtime &quot;PYTHON:3.12&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Confirm the runtime landed where you think it did:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az webapp config show --name vsl-info-api --resource-group rg-vsl-topic05-cus \
  --query linuxFxVersion&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;The region note, from this post&#39;s own run — read it before you pick a region.&lt;/strong&gt; This walkthrough was produced on a brand-new free-trial account, and creating the F1 plan failed in &lt;strong&gt;Canada Central&lt;/strong&gt;, &lt;strong&gt;East US&lt;/strong&gt;, and &lt;strong&gt;West US 2&lt;/strong&gt;: the account&#39;s free-tier quota there was 0, and the error said so. &lt;strong&gt;Central US&lt;/strong&gt; had quota and accepted the plan immediately. If your region refuses the free plan, you have two free moves: switch regions (Central US worked here), or request the free quota increase for your subscription. What not to do is “fix” it by choosing a paid tier — the quota is an account-youth quirk, not a price of entry.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNW0wLjesxcsRGJzn4o3LDW-m2aDhFF6XLCgtNvFTO9jKW00pCU80VDHN0S-ejB8H6xOQcvf-YOKbhEYaGVmiYf5EB-BRFLVq-IsQCXe1COhzLgop332zvezo_eYx7ADg0W82Fa_DkE7mRIfTvGVYI35Oz2VZfgMEQOnT4RfSyDvPKwj6XGV3Ys7xcNKY/s1919/portal-rg.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;The rg-vsl-topic05-cus resource group holding the plan and the web app&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNW0wLjesxcsRGJzn4o3LDW-m2aDhFF6XLCgtNvFTO9jKW00pCU80VDHN0S-ejB8H6xOQcvf-YOKbhEYaGVmiYf5EB-BRFLVq-IsQCXe1COhzLgop332zvezo_eYx7ADg0W82Fa_DkE7mRIfTvGVYI35Oz2VZfgMEQOnT4RfSyDvPKwj6XGV3Ys7xcNKY/s400/portal-rg.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;The rg-vsl-topic05-cus resource group holding the plan and the web app.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdVCH5vBofy1KdB_V5hiJN3md6Mjoc2-X2iFj5dMOdBAAR8vrRuijnhvWOyORN47xwY2HY8wFAMRm0TqRpHL4_I7gkCNbyk7yCb3b_H56y00n2HOzUwQlIQMoWsWDN0ygoRYJSp4vlbVfVHm5qeq5RWPmf3J452Yw8YLlGwoKbOAc31XXTqFB4r9hlltw/s1919/portal-plan.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;The plan-vsl-topic05 App Service plan page showing the Free F1 pricing tier&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdVCH5vBofy1KdB_V5hiJN3md6Mjoc2-X2iFj5dMOdBAAR8vrRuijnhvWOyORN47xwY2HY8wFAMRm0TqRpHL4_I7gkCNbyk7yCb3b_H56y00n2HOzUwQlIQMoWsWDN0ygoRYJSp4vlbVfVHm5qeq5RWPmf3J452Yw8YLlGwoKbOAc31XXTqFB4r9hlltw/s400/portal-plan.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;The plan-vsl-topic05 App Service plan page showing the Free F1 pricing tier.&lt;/p&gt;&lt;h2&gt;Step 2 — App settings&lt;/h2&gt;&lt;p&gt;Two settings, both set before the deploy:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az webapp config appsettings set --name vsl-info-api \
  --resource-group rg-vsl-topic05-cus \
  --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true APP_ENVIRONMENT=production&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(Portal: your web app → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Environment variables&lt;/strong&gt; → &lt;strong&gt;App settings&lt;/strong&gt; tab, add both, &lt;strong&gt;Save&lt;/strong&gt;. Older portal versions called this &lt;strong&gt;Configuration → Application settings&lt;/strong&gt;.)&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;SCM_DO_BUILD_DURING_DEPLOYMENT=true&lt;/code&gt;&lt;/strong&gt; tells the deployment to run the Oryx build: activate a virtual environment and &lt;code&gt;pip install&lt;/code&gt; requirements.txt. A raw zip deploy must set this itself — without it, the classic symptom is &lt;code&gt;ModuleNotFoundError&lt;/code&gt; at startup, because the packages were never installed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;APP_ENVIRONMENT=production&lt;/code&gt;&lt;/strong&gt; is the &lt;code&gt;/&lt;/code&gt; endpoint&#39;s proof: it is what flips the local answer to the Azure one.&lt;/li&gt;&lt;/ul&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsAQ3v0V6DjAiBQOP49JzfzwITdSgC3gINe3ji6x1Rz6CWptLqVECUijCiAwitOToYHWepzatcllOnMfDng-6J2MMHvFNaBEqkBJF6rFOA4z7gD8peff_K_ya6fC6_tBjJ2TLwM6e9rryWoVINpurW2aJPnUmGkiZWd_HHZ-j9DUvQJpx2AOEF3MjJS20/s1919/portal-appsettings.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;App settings showing APP_ENVIRONMENT set to production&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsAQ3v0V6DjAiBQOP49JzfzwITdSgC3gINe3ji6x1Rz6CWptLqVECUijCiAwitOToYHWepzatcllOnMfDng-6J2MMHvFNaBEqkBJF6rFOA4z7gD8peff_K_ya6fC6_tBjJ2TLwM6e9rryWoVINpurW2aJPnUmGkiZWd_HHZ-j9DUvQJpx2AOEF3MjJS20/s400/portal-appsettings.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;App settings showing APP_ENVIRONMENT set to production.&lt;/p&gt;&lt;h2&gt;Step 3 — Deploy the zip&lt;/h2&gt;&lt;p&gt;Build the zip with the dependency file at the archive root — and &lt;strong&gt;no &lt;code&gt;.venv&lt;/code&gt;, no &lt;code&gt;.git&lt;/code&gt;, no &lt;code&gt;.env&lt;/code&gt; inside it&lt;/strong&gt;. Deploying your local virtual environment is the most common self-inflicted deploy failure in this whole walkthrough:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;zip deploy.zip main.py requirements.txt startup.txt
az webapp deploy --name vsl-info-api --resource-group rg-vsl-topic05-cus \
  --type zip --src-path deploy.zip&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Deployment can take a few minutes; the production run for this post took about 2 minutes 40 seconds.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6Azrz8wIzwdxveOtHSo6qy7BcPyDT3A7tW3O4YCb89GMK5HS4xM5ML9Hsx3VLSPJyvmy11HBtQSOaKCQTiUVzf_clS1VpVilrNbG2FcyTu7_036CKK6B_2EgELDv1jQS5IfnjlOIlydvvOA6i0Ir8MOONIHJiFJH2ml5kQlHABHblclD0NFNs3roJYzU/s1919/portal-deploy.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;Deployment Center log showing the zip deployment succeeded&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6Azrz8wIzwdxveOtHSo6qy7BcPyDT3A7tW3O4YCb89GMK5HS4xM5ML9Hsx3VLSPJyvmy11HBtQSOaKCQTiUVzf_clS1VpVilrNbG2FcyTu7_036CKK6B_2EgELDv1jQS5IfnjlOIlydvvOA6i0Ir8MOONIHJiFJH2ml5kQlHABHblclD0NFNs3roJYzU/s400/portal-deploy.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;Deployment Center log showing the zip deployment succeeded.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Then set the startup command before you judge the result:&lt;/strong&gt; portal → your web app → &lt;strong&gt;Configuration&lt;/strong&gt; → &lt;strong&gt;Stack settings&lt;/strong&gt; tab → &lt;strong&gt;Startup Command&lt;/strong&gt;, paste the line from &lt;code&gt;startup.txt&lt;/code&gt;, &lt;strong&gt;Save&lt;/strong&gt;, then restart the app and wait ~20 seconds. (The &lt;strong&gt;General settings&lt;/strong&gt; tab next to it holds platform and debugging options — the startup command is not there.)&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdIyBLtKgCz1x90_kyZUsH9th6aRqA63KUAKA0sS6KYiad4yRymbaRQyqqjEzvAvtMcnb1UOOK07THZzHPhx-RvIjtICYY0ao4WHEJrQAEIxIXhiajwtnWAiroH7jMFtizf5AJrrurEqdlXsMWQybZRFWArgfbAVd_Fm9DbxaDjYhEi3Yubw_9wq0J-pg/s1919/portal-config.png&quot; style=&quot;display: block; padding: 1em 0; text-align: center; &quot;&gt;&lt;img alt=&quot;Configuration, Stack settings tab, with the gunicorn startup command for FastAPI&quot; border=&quot;0&quot; width=&quot;400&quot; data-original-height=&quot;998&quot; data-original-width=&quot;1919&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdIyBLtKgCz1x90_kyZUsH9th6aRqA63KUAKA0sS6KYiad4yRymbaRQyqqjEzvAvtMcnb1UOOK07THZzHPhx-RvIjtICYY0ao4WHEJrQAEIxIXhiajwtnWAiroH7jMFtizf5AJrrurEqdlXsMWQybZRFWArgfbAVd_Fm9DbxaDjYhEi3Yubw_9wq0J-pg/s400/portal-config.png&quot;/&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;text-align: center; font-size: 14px;&quot;&gt;Configuration, Stack settings tab, with the gunicorn startup command for FastAPI.&lt;/p&gt;&lt;h2&gt;Step 4 — The staged failure (deliberate, and the point of the post)&lt;/h2&gt;&lt;p&gt;Before the startup command was set, the first deploy of this exact app did not error. It served the platform&#39;s built-in default page — the generic one titled &lt;strong&gt;“Microsoft Azure App Service - Welcome”&lt;/strong&gt; — because App Service auto-detects Django and Flask only, and a FastAPI app with no startup command falls through to the platform&#39;s read-only default app. We reproduced this on purpose during production, by clearing the startup command on the working app and restarting it: same deploy, same code, default page — then restored the command and watched the real app come back. If you see a default page after a deploy that reported success, you have not failed: you have reproduced the platform&#39;s documented behaviour, and the fix is the startup command plus a restart.&lt;/p&gt;&lt;p&gt;After the restart, the same URL answers the real app. Verify like an engineer — all three endpoints, in the browser:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/&quot;&gt;https://vsl-info-api.azurewebsites.net/&lt;/a&gt; now reports &lt;code&gt;&quot;environment&quot;: &quot;production&quot;&lt;/code&gt; and &lt;code&gt;&quot;hostname&quot;: &quot;vsl-info-api.azurewebsites.net&quot;&lt;/code&gt; — the local &lt;code&gt;&quot;local&quot;&lt;/code&gt;/&lt;code&gt;&quot;localhost&quot;&lt;/code&gt; answer, replaced by the Azure one, from the same code.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/greet/YourName&quot;&gt;https://vsl-info-api.azurewebsites.net/greet/YourName&lt;/a&gt; — your greeting, from Azure.&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://vsl-info-api.azurewebsites.net/health&quot;&gt;https://vsl-info-api.azurewebsites.net/health&lt;/a&gt; — &lt;code&gt;status: &quot;ok&quot;&lt;/code&gt;, Python 3.12.15 (the patch level the &lt;code&gt;PYTHON:3.12&lt;/code&gt; image currently runs — your local 3.12.x and Azure&#39;s are the same 3.12 line, not necessarily the same patch), the current UTC time.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Then read the log stream while you hit the app, so the deploy stops being a black box:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az webapp log config --name vsl-info-api --resource-group rg-vsl-topic05-cus
az webapp log tail --name vsl-info-api --resource-group rg-vsl-topic05-cus&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;The shortcut — &lt;code&gt;az webapp up&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;Everything above, in one command, for a fresh folder:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az webapp up&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;az webapp up&lt;/code&gt; is a current, generally available Azure CLI command: it creates the resource group, plan, and web app and deploys the code from your local folder, and Python apps are created as Linux apps. Use it when you already understand the steps — the long way above is the version that teaches you what the shortcut did, including which startup command and app settings it leaves you still needing to set yourself.&lt;/p&gt;&lt;h2&gt;Errors you may hit&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The site shows a default/placeholder page, not your app&lt;/strong&gt; — the page titled “Microsoft Azure App Service - Welcome”. FastAPI wasn&#39;t detected (no startup command on Python 3.13 or earlier) and App Service ran its built-in default app. Fix: set the startup command, restart, wait ~20 seconds. One related trap, learned on this post&#39;s own run: &lt;code&gt;az webapp config set --startup-file &quot;&quot;&lt;/code&gt; looks like it clears the startup command but silently leaves the old one in place — to actually clear it, use the portal&#39;s Configuration page or the management API.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;TypeError: FastAPI.__call__() missing 1 required positional argument: &#39;send&#39;&lt;/code&gt;&lt;/strong&gt; in the log stream — Gunicorn&#39;s default sync worker met an ASGI app. Fix: the Uvicorn worker class in the startup command (Step: the two files Azure cares about). This exact error is why that class is in the command.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;“Could not find setup.py or requirements.txt; Not running pip install.”&lt;/strong&gt; — the dependency file is not at the project root / zip root. Fix: move it to the root and redeploy.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ModuleNotFoundError&lt;/code&gt; at startup&lt;/strong&gt; — the packages were never installed, usually because the build never ran or a local venv was deployed with the code. Fix: set &lt;code&gt;SCM_DO_BUILD_DURING_DEPLOYMENT=true&lt;/code&gt; and redeploy; never ship your venv.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;“Service Unavailable” / the first hit hangs&lt;/strong&gt; — two possible causes. The mild one is a cold start: F1 has no Always On, the app unloads when idle, and Microsoft&#39;s docs warn the app “might take longer to start up when you use free tiers… and become responsive after you refresh”. The serious one is Gunicorn up but the app not answering — check the log stream before changing anything (&lt;code&gt;az webapp log tail&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The app stops responding part-way through the day&lt;/strong&gt; — the F1 60-CPU-minutes-per-day quota is exhausted. It resets the next day. That is the free tier&#39;s ceiling working as designed; the first paid tier removes it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A DeprecationWarning about &lt;code&gt;uvicorn.workers&lt;/code&gt; in the logs&lt;/strong&gt; — not a failure. You (or an older tutorial) used the class from the old location; uvicorn 0.54.0 says it moved to the &lt;code&gt;uvicorn-worker&lt;/code&gt; package. Switch before a future uvicorn removes it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The container perpetually restarts&lt;/strong&gt; — the platform&#39;s warmup request must get some HTTP status back. An app that never answers (wrong port, crash on import) restarts forever. Slow starters get a lever: the &lt;code&gt;WEBSITES_CONTAINER_START_TIME_LIMIT&lt;/code&gt; app setting (default 230 seconds, maximum 1800).&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;What it costs&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;F1 is $0&lt;/strong&gt; — and it is a real, currently listed plan, intended for trials, experimentation, and learning the service, with the limits to match:&lt;/p&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;F1 (Free) limits&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Compute&lt;/td&gt;&lt;td&gt;Shared, 60 CPU minutes per day (also capped at 3 minutes per 5 minutes)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Memory&lt;/td&gt;&lt;td&gt;1,024 MB per plan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Storage&lt;/td&gt;&lt;td&gt;1 GB per plan&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Apps per plan&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bandwidth&lt;/td&gt;&lt;td&gt;165 MB per day&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Custom domains&lt;/td&gt;&lt;td&gt;0 — the &lt;code&gt;*.azurewebsites.net&lt;/code&gt; name only (its HTTPS certificate is included)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Always On&lt;/td&gt;&lt;td&gt;No — the app unloads when idle&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SLA&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;p&gt;The first paid step up, &lt;strong&gt;B1 in Central US, was US$0.018 per hour — about US$13.14 per month&lt;/strong&gt; — when this post was prepared (Microsoft&#39;s Retail Prices API, October 2026); B1 gives you a dedicated core, 1.75 GB RAM, and 10 GB storage. (For contrast, Windows B1 in the same region was US$0.075/hour — the Linux/Windows gap is one reason this walkthrough is Linux-only, beside Linux being the only option for Python.)&lt;/p&gt;&lt;p&gt;One billing warning, quoted from Microsoft&#39;s pricing page FAQ because people learn it the expensive way: &lt;strong&gt;“Rates listed apply to apps in stopped state. Please delete apps that are not in use or update tier to Free to avoid charges.”&lt;/strong&gt; App Service plans are charged while they exist, including stopped apps. Deleting is the reliable off switch — which is the next section.&lt;/p&gt;&lt;h2&gt;Delete it when you&#39;re done (yes, really)&lt;/h2&gt;&lt;p&gt;Everything in this walkthrough lives in one resource group, so one command removes all of it — the plan, the app, the settings, everything:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;az group delete --name rg-vsl-topic05-cus --yes&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two honest notes to close the cost story. First: &lt;strong&gt;this post&#39;s own deployment was deleted this way after filming.&lt;/strong&gt; The video&#39;s live site is gone by the time you watch it — that is the owner&#39;s rule for this channel (free tier only, and delete resources when you&#39;re not using them) applied to itself, not a gap in the tutorial. Second: nothing is lost by deleting. The companion repo folder plus the commands in this post redeploy the app from scratch any time you want it back, in the same sitting. Treat free-tier resources as disposable by default, and the cloud bill for learning stays exactly $0.&lt;/p&gt;&lt;h2&gt;Where to go next&lt;/h2&gt;&lt;p&gt;This same one-file app is the running example for the rest of the ship-and-run mini-series: &lt;strong&gt;containerise it with Docker&lt;/strong&gt; (Topic #9), &lt;strong&gt;deploy it on every push with GitHub Actions&lt;/strong&gt; through Deployment Center — which commits a workflow file to your repo and authenticates with a Microsoft Entra identity (Topic #13) — and &lt;strong&gt;watch it with Prometheus and Grafana&lt;/strong&gt; (Topic #10). On App Service itself, the next steps in order: a custom domain and its certificate when you move to a Basic-or-higher plan (the free tier supports none), and the log stream as a daily habit rather than an emergency tool.&lt;/p&gt;&lt;h2&gt;Get the code&lt;/h2&gt;&lt;p&gt;The complete &lt;code&gt;main.py&lt;/code&gt;, pinned &lt;code&gt;requirements.txt&lt;/code&gt;, the &lt;code&gt;startup.txt&lt;/code&gt; command file, and this post&#39;s setup as a README live in the companion repository:&lt;br&gt;&lt;a href=&quot;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-05-deploy-fastapi-azure&quot;&gt;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-05-deploy-fastapi-azure&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Sources&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Python on Azure App Service: container characteristics, startup process, default page behaviour, troubleshooting — &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/app-service/configure-language-python&quot;&gt;https://learn.microsoft.com/en-us/azure/app-service/configure-language-python&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Deploy Python (FastAPI) — Microsoft&#39;s quickstart: zip deploy route, &lt;code&gt;SCM_DO_BUILD_DURING_DEPLOYMENT&lt;/code&gt;, the Python 3.14 FastAPI auto-detection note — &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/app-service/quickstart-python&quot;&gt;https://learn.microsoft.com/en-us/azure/app-service/quickstart-python&lt;/a&gt;&lt;/li&gt;&lt;li&gt;App Service on Linux FAQ (ports, &lt;code&gt;WEBSITES_PORT&lt;/code&gt;, warmup and container start time limit) — &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/app-service/faq-app-service-linux&quot;&gt;https://learn.microsoft.com/en-us/azure/app-service/faq-app-service-linux&lt;/a&gt;&lt;/li&gt;&lt;li&gt;App Service limits, incl. the F1 quota rows — &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits&quot;&gt;https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits&lt;/a&gt;&lt;/li&gt;&lt;li&gt;App Service (Linux) pricing, incl. the Free plan and the stopped-apps FAQ — &lt;a href=&quot;https://azure.microsoft.com/en-us/pricing/details/app-service/linux/&quot;&gt;https://azure.microsoft.com/en-us/pricing/details/app-service/linux/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;B1 Central US prices (Linux US$0.018/hour, Windows US$0.075/hour) — Azure Retail Prices API, &lt;a href=&quot;https://prices.azure.com/api/retail/prices&quot;&gt;https://prices.azure.com/api/retail/prices&lt;/a&gt; (accessed October 28, 2026)&lt;/li&gt;&lt;li&gt;Azure free account offer and signup requirements — &lt;a href=&quot;https://azure.microsoft.com/en-us/free/&quot;&gt;https://azure.microsoft.com/en-us/free/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;az webapp up&lt;/code&gt; and the Azure CLI webapp command reference — &lt;a href=&quot;https://learn.microsoft.com/en-us/cli/azure/webapp&quot;&gt;https://learn.microsoft.com/en-us/cli/azure/webapp&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Deploying a Python FastAPI app to App Service on Linux — Microsoft App Service support team blog, 2024-04-23 — &lt;a href=&quot;https://azureossd.github.io/2024/04/23/Deploying-a-Python-FastAPI-app-to-App-Service-Linux/&quot;&gt;https://azureossd.github.io/2024/04/23/Deploying-a-Python-FastAPI-app-to-App-Service-Linux/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;FastAPI deployment: server workers — &lt;a href=&quot;https://fastapi.tiangolo.com/deployment/server-workers/&quot;&gt;https://fastapi.tiangolo.com/deployment/server-workers/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Uvicorn worker deprecation — the uvicorn 0.54.0 package itself, and the replacement package — &lt;a href=&quot;https://pypi.org/project/uvicorn-worker/&quot;&gt;https://pypi.org/project/uvicorn-worker/&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Package versions — &lt;a href=&quot;https://pypi.org/project/fastapi/&quot;&gt;https://pypi.org/project/fastapi/&lt;/a&gt; (0.162.0), &lt;a href=&quot;https://pypi.org/project/uvicorn/&quot;&gt;https://pypi.org/project/uvicorn/&lt;/a&gt; (0.54.0), &lt;a href=&quot;https://pypi.org/project/gunicorn/&quot;&gt;https://pypi.org/project/gunicorn/&lt;/a&gt; (26.2.0), https://pypi.org/project/azure-cli releases via &lt;a href=&quot;https://github.com/Azure/azure-cli/releases&quot;&gt;https://github.com/Azure/azure-cli/releases&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div style=&quot;text-align: center;&quot;&gt;&lt;h3&gt;Enjoying these tutorials?&lt;/h3&gt;&lt;p&gt;Get new Visual Studio Learn posts by email — free, no spam:&lt;/p&gt;&lt;form action=&quot;https://api.follow.it/subscription-form/SmRpWjEzV1ovU0FOK2R2Y0oxajFBUmtIdW44K1ZiUWZRUm1FZVdpVlRKUHFLSFBKWjE4M1hTSk5yRU5oNmg0RU5zelhuTllNTGU1UmwzRkN0NzZWbnd2YkV5VWFwYnptWFdtekI3eXdwMmhtamtUNit3dlNSblVmRjJxd1JGbjZ8Q05yQWR2eitvYW5HRmNMMzJkbStpM09rNmxXZ0ZjWWhyOERya2VvVXRzZz0=/8&quot; method=&quot;post&quot;&gt;&lt;div class=&quot;form-preview&quot; style=&quot;background-color: white; border-color: rgb(204, 204, 204); border-style: solid; border-width: 1px; position: relative;&quot;&gt;&lt;div class=&quot;preview-heading&quot;&gt;&lt;h5 style=&quot;color: black; font-family: Arial; font-size: 16px; font-weight: bold; text-align: center; text-transform: none;&quot;&gt;Get new posts by email:&lt;/h5&gt;&lt;/div&gt;&lt;div class=&quot;preview-input-field&quot;&gt;&lt;input name=&quot;email&quot; required=&quot;&quot; spellcheck=&quot;false&quot; style=&quot;background-color: white; color: black; font-family: Arial; font-size: 14px; font-weight: normal; text-align: center; text-transform: none;&quot; type=&quot;email&quot; /&gt;&lt;/div&gt;&lt;div class=&quot;preview-submit-button&quot;&gt;&lt;button style=&quot;background-color: black; color: white; font-family: Arial; font-size: 16px; font-weight: bold; text-align: center; text-transform: none;&quot; type=&quot;submit&quot;&gt;Subscribe&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;/form&gt;&lt;a class=&quot;powered-by-line&quot; href=&quot;https://follow.it&quot;&gt;Powered by &lt;img alt=&quot;follow.it&quot; height=&quot;17px&quot; src=&quot;https://follow.it/images/colored-logo.svg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p style=&quot;font-size: 14px; margin: 12px 0px 0px;&quot;&gt;Follow along: &lt;a href=&quot;https://www.youtube.com/@VisualStudioLearn&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;YouTube&lt;/a&gt; · &lt;a href=&quot;https://www.facebook.com/VisualStudio.Learn&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Facebook&lt;/a&gt; · &lt;a href=&quot;https://visualstudiolearn.blogspot.com/feeds/posts/default?alt=rss&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;RSS&lt;/a&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/873346114526941927/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/how-do-i-deploy-fastapi-app-to-azure.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/873346114526941927'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/873346114526941927'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/how-do-i-deploy-fastapi-app-to-azure.html' title='How Do I Deploy a FastAPI App to Azure App Service? Step by Step, From Zero'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzgHu6zkO5XmaAhOBAW1UL_5eDQ3y2FoJ8i7RplrxHCFAb19i6aKwLbQ5lZhRUv7nuPGcY1VlAPEzBYwl9E19C66dwJ9jqFV1ArJQr_16H7dwPwt2TEwEmJYOA_U7FWlrfry6syRX7iUz-F-cWKUAmi5BoFfzZR08hF6YjgkuYlUoFLd_uuvYLT3kKbCQ/s72-c/portal-overview.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-7852476395742725949</id><published>2026-10-27T09:10:29.128-04:00</published><updated>2026-10-27T09:10:29.128-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="Automation"/><category scheme="http://www.blogger.com/atom/ns#" term="Gemini"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>I Built an AI Agent That Reads Release Notes So I Don&#39;t Have To</title><content type='html'>&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; Python 3.12.3 · requests 2.34.2 · google-genai 2.33.0 · Model: &lt;code&gt;gemini-3.6-flash&lt;/code&gt;. Everything runs from one file, &lt;code&gt;digest_agent.py&lt;/code&gt;, printed in full below — the exact file in the companion repo.&lt;/p&gt;&lt;h2&gt;The direct answer&lt;/h2&gt;&lt;p&gt;Build an AI agent in Python, step by step, that reads release notes for you. One small Python file is enough to stop reading changelogs by hand. Think of the agent as the smallest useful harness — three steps with a strict contract between them: &lt;strong&gt;fetch&lt;/strong&gt;, &lt;strong&gt;summarize&lt;/strong&gt;, &lt;strong&gt;write&lt;/strong&gt;. It fetches the newest GitHub release of each project it tracks from the project&#39;s public releases feed, sends the release notes to Gemini with an instruction to return three summary bullets plus any breaking changes as JSON, and writes one Markdown file: &lt;code&gt;digest.md&lt;/code&gt;, with the breaking changes collected at the top. Schedule it for 7:15 every morning and yesterday&#39;s releases are a one-page read with your coffee. No GitHub account, no token, no framework — the only credential is a Gemini API key, and the free tier covers this easily.&lt;/p&gt;&lt;h2&gt;What you will build&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;A feed fetcher&lt;/strong&gt; — reads &lt;code&gt;https://github.com/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/releases.atom&lt;/code&gt; for CPython, FastAPI, and the Azure CLI. The first entry in a releases feed is the newest published release. Feeds are public web pages, so nothing here touches the GitHub REST API and its 60-requests-per-hour unauthenticated limit.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A summariser&lt;/strong&gt; — one function that strips the notes&#39; HTML to text, truncates anything very long, and asks Gemini for exactly three bullets and a breaking-changes list, as JSON.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A digest writer&lt;/strong&gt; — assembles &lt;code&gt;digest.md&lt;/code&gt;: a dated header, a ⚠ Breaking changes section across all projects, one section per project (version, date, bullets, link), and a footer with the run&#39;s real token counts and estimated cost.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A schedule&lt;/strong&gt; — the same script run daily by Windows Task Scheduler or cron (both shown below).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Two design choices are deliberate. The agent reads &lt;strong&gt;feeds, not the API&lt;/strong&gt;: simpler, unauthenticated, and nothing to rate-limit at three projects a day. And it has &lt;strong&gt;no memory&lt;/strong&gt;: it always summarises the newest release in each feed, whether or not you read yesterday&#39;s digest. Remembering where it left off is the first upgrade, listed at the end.&lt;/p&gt;&lt;h2&gt;Environment setup — do this first&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Python 3.10 or newer (this post uses 3.12.3), a terminal, and a Gemini API key. The free tier is enough; no card is needed.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Get your Gemini API key, click by click:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to &lt;strong&gt;aistudio.google.com&lt;/strong&gt; and sign in with a Google account.&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Get API key&lt;/strong&gt; in the left sidebar (the key icon).&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Create API key&lt;/strong&gt;, choose or create a Google Cloud project when asked, and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Copy the key that appears — it stays visible in that same key list.&lt;/li&gt;&lt;li&gt;Keep it secret like a password: never paste it into code, a screenshot, or a public repository. This agent reads it only from an environment variable.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;Create the project and install the pinned dependencies:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;mkdir release-digest &amp;amp;&amp;amp; cd release-digest
python3 -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate
# Windows PowerShell: py -m venv .venv ; .venv\Scripts\Activate.ps1
pip install requests==2.34.2 google-genai==2.33.0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The code goes in this folder as &lt;code&gt;digest_agent.py&lt;/code&gt; — it is printed in full in the steps below, and the companion repo has it as a ready-made file.)&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Set the key, then verify the setup:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;export GEMINI_API_KEY=&amp;lt;redacted&amp;gt;
# Windows PowerShell: $env:GEMINI_API_KEY=&amp;lt;redacted&amp;gt;
python -c &quot;import requests, google.genai; print(&#39;setup ok&#39;)&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If that prints &lt;code&gt;setup ok&lt;/code&gt;, the environment is ready. Every error the run itself can throw is listed in &quot;Errors you may hit&quot; near the end.&lt;/p&gt;&lt;h2&gt;Step 1 — Config, data shapes, and stripping the HTML&lt;/h2&gt;&lt;p&gt;The top of the file is everything you are allowed to tweak: the model name as one constant (you will see why in the errors section), the list of projects to watch, the feed URL pattern, and the published per-token rates the cost footer uses. Below that, two small dataclasses — &lt;code&gt;Release&lt;/code&gt; (what a feed hands us) and &lt;code&gt;Summary&lt;/code&gt; (what the model hands back) — and a minimal HTML-to-text converter, because feed content arrives as HTML and Gemini should read words, not markup.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Release-notes digest agent: read the changelogs so you don&#39;t have to.

Companion code for the Visual Studio Learn post and video
&quot;I Built an AI Agent That Reads Release Notes So I Don&#39;t Have To&quot;.

Once a run, the agent fetches the latest GitHub release of each project
in PROJECTS below, asks Gemini to summarise the release notes in three
bullets and flag anything breaking, and writes one Markdown digest
(digest.md) with a breaking-changes section at the top.

Requires: Python 3.10 or newer, the packages in requirements.txt, and a
Gemini API key in the GEMINI_API_KEY environment variable. See README.md.
&quot;&quot;&quot;

import json
import logging
import os
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import date
from html.parser import HTMLParser

import requests
from google import genai

logger = logging.getLogger(&quot;digest_agent&quot;)
logging.basicConfig(level=logging.INFO, format=&quot;%(levelname)s %(message)s&quot;)

MODEL = &quot;gemini-3.6-flash&quot;

# The projects to watch: (display name, GitHub &quot;owner/repo&quot;). Add or
# remove lines here — nothing else in the file needs to change.
PROJECTS = [
    (&quot;CPython&quot;, &quot;python/cpython&quot;),
    (&quot;FastAPI&quot;, &quot;tiangolo/fastapi&quot;),
    (&quot;Azure CLI&quot;, &quot;Azure/azure-cli&quot;),
]

FEED_URL = &quot;https://github.com/{repo}/releases.atom&quot;
USER_AGENT = &quot;release-digest-agent/1.0 (Visual Studio Learn tutorial)&quot;
FETCH_TIMEOUT_SECONDS = 30
MAX_NOTES_CHARS = 12_000  # truncate very long notes before the model call

# Published pay-as-you-go rates for gemini-3.6-flash, US$ per 1M tokens.
INPUT_RATE_PER_MILLION = 0.75
OUTPUT_RATE_PER_MILLION = 3.75

OUTPUT_FILE = &quot;digest.md&quot;

ATOM = {&quot;a&quot;: &quot;http://www.w3.org/2005/Atom&quot;}
# Tags that mean &quot;new line&quot; when release-note HTML is flattened to text.
BLOCK_TAGS = {&quot;p&quot;, &quot;br&quot;, &quot;div&quot;, &quot;li&quot;, &quot;ul&quot;, &quot;ol&quot;, &quot;h1&quot;, &quot;h2&quot;, &quot;h3&quot;, &quot;h4&quot;, &quot;pre&quot;, &quot;tr&quot;}&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;@dataclass
class Release:
    &quot;&quot;&quot;The latest release of one project, as published in its feed.&quot;&quot;&quot;

    project: str
    title: str  # feed title, e.g. &quot;v3.13.18&quot; or &quot;Azure CLI 2.91.0&quot;
    published: str  # ISO date from the feed, e.g. &quot;2026-10-25&quot;
    url: str
    notes: str  # release notes as plain text (HTML already stripped)


@dataclass
class Summary:
    &quot;&quot;&quot;Gemini&#39;s reading of one release, plus what the call cost.&quot;&quot;&quot;

    bullets: list[str] = field(default_factory=list)
    breaking: list[str] = field(default_factory=list)
    input_tokens: int = 0
    output_tokens: int = 0


class _TextExtractor(HTMLParser):
    &quot;&quot;&quot;Minimal HTML-to-text: keep the words, drop the markup.&quot;&quot;&quot;

    def __init__(self) -&amp;gt; None:
        super().__init__()
        self.parts: list[str] = []

    def handle_starttag(self, tag, attrs):
        if tag in BLOCK_TAGS:
            self.parts.append(&quot;\n&quot;)

    def handle_data(self, data):
        self.parts.append(data)


def html_to_text(html: str) -&amp;gt; str:
    &quot;&quot;&quot;Flatten release-note HTML to readable plain text.&quot;&quot;&quot;
    extractor = _TextExtractor()
    extractor.feed(html)
    lines = (line.strip() for line in &quot;&quot;.join(extractor.parts).splitlines())
    return &quot;\n&quot;.join(line for line in lines if line)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 2 — Fetch the newest release from the feed&lt;/h2&gt;&lt;p&gt;One function per project: GET the feed with a polite user agent and a timeout, parse the XML, take the first entry, and flatten its HTML body to text. &lt;code&gt;raise_for_status()&lt;/code&gt; means a dead or renamed repo fails loudly in the log instead of producing a quietly empty digest.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;def fetch_latest_release(project: str, repo: str) -&amp;gt; Release:
    &quot;&quot;&quot;Fetch the newest entry of a project&#39;s GitHub releases Atom feed.

    The feed lists the 10 most recently published releases; the first
    entry is the newest. No GitHub account or token is needed — the
    feed is a public web page, not the rate-limited REST API.
    &quot;&quot;&quot;
    url = FEED_URL.format(repo=repo)
    logger.info(&quot;Fetching %s&quot;, url)
    response = requests.get(
        url, headers={&quot;User-Agent&quot;: USER_AGENT}, timeout=FETCH_TIMEOUT_SECONDS
    )
    response.raise_for_status()

    root = ET.fromstring(response.content)
    entry = root.find(&quot;a:entry&quot;, ATOM)
    if entry is None:
        raise ValueError(f&quot;{repo}: the releases feed has no entries&quot;)

    title = entry.findtext(&quot;a:title&quot;, default=&quot;&quot;, namespaces=ATOM).strip()
    updated = entry.findtext(&quot;a:updated&quot;, default=&quot;&quot;, namespaces=ATOM)
    link = entry.find(&quot;a:link&quot;, ATOM)
    content = entry.find(&quot;a:content&quot;, ATOM)
    notes_html = content.text if content is not None and content.text else &quot;&quot;

    return Release(
        project=project,
        title=title,
        published=updated[:10],
        url=link.get(&quot;href&quot;, &quot;&quot;) if link is not None else &quot;&quot;,
        notes=html_to_text(notes_html),
    )&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 3 — Ask Gemini for bullets and breaking changes&lt;/h2&gt;&lt;p&gt;This is the only function that talks to the model, and its contract is strict: work only from the notes text provided, return JSON and nothing else — exactly three summary bullets, plus a list of changes that could break existing code, config, or command lines. Two honesty guards live here. If a release ships with &lt;strong&gt;no notes at all&lt;/strong&gt;, the function returns a line saying so &lt;em&gt;without&lt;/em&gt; spending a model call. And if the model&#39;s answer is not the expected JSON, the code keeps the raw text as a single bullet and logs a warning, rather than crashing the morning run.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;def summarize_release(client: genai.Client, release: Release) -&amp;gt; Summary:
    &quot;&quot;&quot;Ask Gemini for three bullets and any breaking changes in a release.

    The model only ever sees the notes text fetched above — it is told
    to work from that text alone and to say so when the notes are thin.
    Its answer comes back as JSON and is parsed defensively.
    &quot;&quot;&quot;
    if not release.notes.strip():
        # Nothing to summarise: don&#39;t spend a model call on an empty page.
        logger.info(&quot;%s: no release notes published — skipping the model call&quot;,
                    release.project)
        return Summary(bullets=[&quot;No release notes were published with this release.&quot;])

    prompt = f&quot;&quot;&quot;You are summarising a software release for a working developer.

Project: {release.project}
Release: {release.title} ({release.published})

Release notes (plain text, converted from the project&#39;s own feed):
---
{release.notes[:MAX_NOTES_CHARS]}
---

Answer with ONLY a JSON object, no markdown fences, in exactly this shape:
{{&quot;summary&quot;: [&quot;bullet&quot;, &quot;bullet&quot;, &quot;bullet&quot;],
  &quot;breaking&quot;: [&quot;one line per breaking change, or empty list&quot;]}}

Rules:
- &quot;summary&quot; is exactly 3 short bullets, based only on the notes above.
- If the notes contain no real change details, say that honestly in one
  bullet instead of inventing changes.
- &quot;breaking&quot; lists only changes that could break existing code, config,
  or command lines — behaviour changes count. Ignore download hashes,
  links, and housekeeping.
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;    response = client.models.generate_content(model=MODEL, contents=prompt)
    usage = response.usage_metadata
    text = (response.text or &quot;&quot;).strip()
    if text.startswith(&quot;```&quot;):  # strip code fences if the model adds them
        text = text.strip(&quot;`&quot;)
        text = text[text.find(&quot;{&quot;):]

    try:
        data = json.loads(text)
        bullets = [str(b) for b in data.get(&quot;summary&quot;, [])][:3]
        breaking = [str(b) for b in data.get(&quot;breaking&quot;, [])]
    except (json.JSONDecodeError, AttributeError):
        logger.warning(&quot;%s: model answer was not the expected JSON; &quot;
                       &quot;keeping the raw text as a single bullet&quot;, release.project)
        bullets = [text] if text else [&quot;The model returned an empty answer.&quot;]
        breaking = []

    return Summary(
        bullets=bullets,
        breaking=breaking,
        input_tokens=usage.prompt_token_count or 0,
        output_tokens=usage.candidates_token_count or 0,
    )


def estimate_cost(input_tokens: int, output_tokens: int) -&amp;gt; float:
    &quot;&quot;&quot;Estimated US$ cost of the run at the published per-token rates.&quot;&quot;&quot;
    return (
        input_tokens * INPUT_RATE_PER_MILLION
        + output_tokens * OUTPUT_RATE_PER_MILLION
    ) / 1_000_000&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 4 — Assemble the digest and run&lt;/h2&gt;&lt;p&gt;&lt;code&gt;build_digest&lt;/code&gt; puts the breaking changes first — across every project — because that is the section that decides whether today is a reading day or an upgrading day. &lt;code&gt;main&lt;/code&gt; loops the projects, and a failure in one (a feed down, a repo renamed) is logged and skipped, not fatal: a digest with two of three projects beats no digest. Only if every project fails does the run exit without writing a file.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;def build_digest(entries: list[tuple[Release, Summary]]) -&amp;gt; str:
    &quot;&quot;&quot;Assemble the one-page Markdown digest from all summaries.&quot;&quot;&quot;
    today = date.today().strftime(&quot;%A, %B %d, %Y&quot;)
    total_in = sum(s.input_tokens for _, s in entries)
    total_out = sum(s.output_tokens for _, s in entries)

    lines = [f&quot;# Release Digest — {today}&quot;, &quot;&quot;]

    breaking_lines = [
        f&quot;- **{release.project} {release.title}** — {item}&quot;
        for release, summary in entries
        for item in summary.breaking
    ]
    lines += [&quot;## ⚠ Breaking changes&quot;, &quot;&quot;]
    lines += breaking_lines or [&quot;None flagged in this run.&quot;]
    lines.append(&quot;&quot;)

    for release, summary in entries:
        lines += [
            f&quot;## {release.project} — {release.title}&quot;,
            &quot;&quot;,
            f&quot;Released: {release.published} · [Release notes]({release.url})&quot;,
            &quot;&quot;,
        ]
        lines += [f&quot;- {bullet}&quot; for bullet in summary.bullets]
        lines.append(&quot;&quot;)

    lines += [
        &quot;---&quot;,
        f&quot;*Generated by digest_agent.py · Model: {MODEL} · &quot;
        f&quot;Tokens: {total_in:,} in / {total_out:,} out · &quot;
        f&quot;Estimated cost: ${estimate_cost(total_in, total_out):.6f}*&quot;,
    ]
    return &quot;\n&quot;.join(lines) + &quot;\n&quot;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;def main() -&amp;gt; int:
    api_key = os.environ.get(&quot;GEMINI_API_KEY&quot;)
    if not api_key:
        logger.error(&quot;GEMINI_API_KEY is not set. Get a key in Google AI &quot;
                     &quot;Studio and set it in the environment — see README.md.&quot;)
        return 1

    client = genai.Client(api_key=api_key)
    entries: list[tuple[Release, Summary]] = []
    failures = 0
    for project, repo in PROJECTS:
        try:
            release = fetch_latest_release(project, repo)
            entries.append((release, summarize_release(client, release)))
            logger.info(&quot;%s: %s (%s)&quot;, project, release.title, release.published)
        except Exception:
            failures += 1
            logger.exception(&quot;%s: fetch or summary failed — continuing &quot;
                             &quot;with the remaining projects&quot;, project)

    if not entries:
        logger.error(&quot;Every project failed; no digest written.&quot;)
        return 1

    with open(OUTPUT_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as fh:
        fh.write(build_digest(entries))
    logger.info(&quot;Wrote %s (%d project(s), %d failure(s))&quot;,
                OUTPUT_FILE, len(entries), failures)
    return 0


if __name__ == &quot;__main__&quot;:
    sys.exit(main())&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 5 — Run it, and read the digest&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;python digest_agent.py&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The log shows each feed fetched and each release found, and the run writes &lt;code&gt;digest.md&lt;/code&gt; in the same folder. When this post was prepared (Sunday, October 25, 2026), the three feeds&#39; newest releases were &lt;strong&gt;CPython v3.13.18&lt;/strong&gt; (published that same day), &lt;strong&gt;FastAPI 0.162.0&lt;/strong&gt; (October 23), and &lt;strong&gt;Azure CLI 2.91.0&lt;/strong&gt; (October 5) — the agent re-fetches on every run, so your digest always carries whatever is newest that morning.&lt;/p&gt;&lt;p&gt;Two of the three feeds teach an honest lesson about release notes. CPython&#39;s GitHub release body is a one-liner (&quot;Python 3.15.2&quot;) — its real changelog lives on docs.python.org — and the Azure CLI&#39;s body is mostly a link to its full notes page plus download hashes. The prompt tells Gemini to say so instead of inventing changes, and to ignore hashes; the summaries for those two are thin &lt;em&gt;because the notes are thin&lt;/em&gt;, and the digest does not pretend otherwise.&lt;/p&gt;&lt;p&gt;Here is the complete &lt;code&gt;digest.md&lt;/code&gt; from the live production run of this exact file, made on Monday, October 26, 2026 — real Gemini bullets, a real breaking-change flag, and the run&#39;s real token counts in the footer. One change from prep is visible in it: the CPython feed&#39;s newest entry had advanced to v3.15.2 (published October 25, after the prep fetch), while FastAPI and Azure CLI stood still — the feed reports what was published last, and the digest prints it as-is:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Release Digest — Monday, October 26, 2026

## ⚠ Breaking changes

- **FastAPI 0.162.0** — FastAPI no longer infers False for omitted boolean form fields; unsubmitted fields without a default will now raise a missing-field error instead of evaluating to False.

## CPython — v3.15.2

Released: 2026-10-25 · [Release notes](https://github.com/python/cpython/releases/tag/v3.15.2)

- Python 3.15.2 release announced on 2026-10-25.
- The provided release notes contain no detailed changelog or change descriptions.
- No specific features, bug fixes, or updates were documented in the notes.

## FastAPI — 0.162.0

Released: 2026-10-23 · [Release notes](https://github.com/fastapi/fastapi/releases/tag/0.162.0)

- Omitted bool and bool | None form fields now use declared defaults or raise missing-field errors instead of automatically inferring False.
- Applies across individual form parameters, form models, and form model unions, bringing behavior in line with standard Pydantic logic.
- Omitted boolean fields are now excluded from model_dump(exclude_unset=True) and no longer count as submitted fields during union selection.

## Azure CLI — Azure CLI 2.91.0

Released: 2026-10-05 · [Release notes](https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.91.0)

- The release notes contain no detailed change log or feature notes.
- Checksums (SHA256) are provided for official Linux, macOS, and Windows release artifacts.
- Artifacts cover arm64 and x86_64 architectures along with MSI and ZIP packages.

---
*Generated by digest_agent.py · Model: gemini-3.6-flash · Tokens: 1,313 in / 252 out · Estimated cost: $0.001930*&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Scheduling it daily&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Windows — Task Scheduler:&lt;/strong&gt; Create Basic Task → Daily, your morning time → Action: Start a program → Program: &lt;code&gt;C:\path\to\release-digest\.venv\Scripts\python.exe&lt;/code&gt;, Arguments: &lt;code&gt;digest_agent.py&lt;/code&gt;, Start in: &lt;code&gt;C:\path\to\release-digest&lt;/code&gt;. Set &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; once as a user environment variable (Settings → Environment Variables) — the scheduled task does not inherit your open PowerShell window&#39;s variables.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;macOS / Linux — cron:&lt;/strong&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;15 7 * * * cd /path/to/release-digest &amp;amp;&amp;amp; GEMINI_API_KEY=&amp;lt;redacted&amp;gt; .venv/bin/python digest_agent.py &amp;gt;&amp;gt; digest.log 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(On a shared machine, keep the key in a protected file or secret store rather than the crontab.)&lt;/p&gt;&lt;h2&gt;Errors you may hit&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;GEMINI_API_KEY is not set&lt;/code&gt;&lt;/strong&gt; — the agent stops before doing any work. The variable must exist in the same shell (or scheduled environment) that runs the script; setting it in one terminal does not set it in another, and Task Scheduler tasks never see your interactive shell&#39;s variables.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;A 404 naming the model — &quot;&lt;code&gt;gemini-…&lt;/code&gt; is no longer available to new users&quot;&lt;/strong&gt; — Google retires model names for &lt;em&gt;new&lt;/em&gt; API keys first: the model can be listed in the docs and still refuse a fresh key. This exact error is why the model is a single constant at the top of the file. Verified example: &lt;code&gt;gemini-2.5-flash&lt;/code&gt; returned it to newly created keys in October 2026. Fix: open Google&#39;s models page, take the current Flash model it names, update &lt;code&gt;MODEL&lt;/code&gt;. This post pins &lt;code&gt;gemini-3.6-flash&lt;/code&gt;, verified current and stable in Google&#39;s docs on October 26, 2026 — the pin moved from &lt;code&gt;gemini-3.8-flash&lt;/code&gt; during production, for the reason in the 503 entry below.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;429 / quota errors from Gemini&lt;/strong&gt; — the free tier has request limits; three small summaries a day sit far below them, but a brand-new key can be throttled and heavy same-day experimenting adds up. Wait for the daily reset, or enable billing.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;503 UNAVAILABLE — &quot;The model is currently experiencing high demand&quot;&lt;/strong&gt; — the model itself is temporarily overloaded; this is not your key, your code, or your quota. This exact error shaped this post: during production on October 26, 2026, &lt;code&gt;gemini-3.8-flash&lt;/code&gt; returned 503s (and quota 429s under retry load) across ten runs of this exact file, completing only single-project digests. The fix applied is the one the file is designed for — the pin moved to &lt;code&gt;gemini-3.6-flash&lt;/code&gt;, one constant, identical published rates — and the unchanged-everything-else file completed the full capture the same day. For a scheduled daily job the softer fix also works: wait, and let the next run try again.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;One project logs &quot;fetch or summary failed&quot; but the run completes&lt;/strong&gt; — that feed was unreachable, the repo was renamed, or the feed returned no entries. A renamed repo empties its old feed; update the path in &lt;code&gt;PROJECTS&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The ⚠ section says &quot;None flagged in this run&quot;&lt;/strong&gt; — that is a result, not an error: the model found nothing in the notes that could break existing code. Skim the bullets before trusting it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Oddly thin bullets for CPython or the Azure CLI&lt;/strong&gt; — expected (see Step 5): their GitHub bodies carry almost no prose. The agent summarises the feed it is given, no more.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Digest shows an older version line than you expected&lt;/strong&gt; — feeds order by &lt;em&gt;publish time&lt;/em&gt;, not version number. On October 25, 2026, CPython published several releases the same day — security fixes for older branches (v3.13.18 among them) and v3.15.2 on the current line; the digest&#39;s CPython line is whichever was published last — v3.15.2 — not the day&#39;s highest-profile release. The digest always prints the version and date so the ordering is visible, and for a &quot;what shipped most recently&quot; digest that ordering is the honest one.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Where to go next&lt;/h2&gt;&lt;p&gt;Three upgrades, in the order a daily tool earns them: &lt;strong&gt;memory&lt;/strong&gt; (a small state file recording the last release seen per project, so the agent only summarises what is actually new and can say &quot;nothing new today&quot;), &lt;strong&gt;delivery&lt;/strong&gt; (mail or message the digest to yourself instead of leaving it as a file — the agent from Topic #1 has the tool-calling pattern for it), and &lt;strong&gt;more sources&lt;/strong&gt; (any GitHub project is one line in &lt;code&gt;PROJECTS&lt;/code&gt;; projects that publish proper changelogs — frameworks, CLIs, SDKs — digest best). Previous in this series of agent builds: Topic #1&#39;s tool-calling agent and Topic #2&#39;s MCP server; the PDF Q&amp;amp;A app (Small AI App #1) is the same Gemini pattern pointed at documents instead of feeds.&lt;/p&gt;&lt;h2&gt;Get the code&lt;/h2&gt;&lt;p&gt;The complete &lt;code&gt;digest_agent.py&lt;/code&gt;, pinned &lt;code&gt;requirements.txt&lt;/code&gt;, and this post&#39;s setup as a README live in the companion repository:&lt;br&gt;&lt;a href=&quot;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-04-release-digest&quot;&gt;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-04-release-digest&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Sources&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;CPython releases feed — &lt;a href=&quot;https://github.com/python/cpython/releases.atom&quot;&gt;https://github.com/python/cpython/releases.atom&lt;/a&gt; (latest at prep: v3.13.18, 2026-10-25)&lt;/li&gt;&lt;li&gt;FastAPI releases feed — &lt;a href=&quot;https://github.com/tiangolo/fastapi/releases.atom&quot;&gt;https://github.com/tiangolo/fastapi/releases.atom&lt;/a&gt; (latest at prep: 0.162.0, 2026-10-23)&lt;/li&gt;&lt;li&gt;Azure CLI releases feed — &lt;a href=&quot;https://github.com/Azure/azure-cli/releases.atom&quot;&gt;https://github.com/Azure/azure-cli/releases.atom&lt;/a&gt; (latest at prep: Azure CLI 2.91.0, 2026-10-05)&lt;/li&gt;&lt;li&gt;GitHub REST API rate limits (60 requests/hour unauthenticated, the reason this agent uses feeds) — &lt;a href=&quot;https://api.github.com/rate_limit&quot;&gt;https://api.github.com/rate_limit&lt;/a&gt; and GitHub&#39;s API docs&lt;/li&gt;&lt;li&gt;Gemini models (gemini-3.6-flash listed as Stable) — &lt;a href=&quot;https://ai.google.dev/gemini-api/docs/models&quot;&gt;https://ai.google.dev/gemini-api/docs/models&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Gemini API pricing ($0.75 / $3.75 per 1M tokens for gemini-3.6-flash through Dec 31, 2026) — &lt;a href=&quot;https://ai.google.dev/gemini-api/docs/pricing&quot;&gt;https://ai.google.dev/gemini-api/docs/pricing&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Package versions — &lt;a href=&quot;https://pypi.org/project/requests/&quot;&gt;https://pypi.org/project/requests/&lt;/a&gt; (2.34.2), &lt;a href=&quot;https://pypi.org/project/google-genai/&quot;&gt;https://pypi.org/project/google-genai/&lt;/a&gt; (2.33.0)&lt;/li&gt;&lt;/ul&gt;&lt;div style=&quot;text-align:center;border:1px solid #e3e6ea;border-radius:8px;padding:18px 14px;margin:28px 0;background:#fbfcfd;&quot;&gt;&lt;h3 style=&quot;margin:0 0 6px;&quot;&gt;Enjoying these tutorials?&lt;/h3&gt;&lt;p style=&quot;margin:0 0 6px;&quot;&gt;Get new Visual Studio Learn posts by email — free, no spam:&lt;/p&gt;&lt;style&gt;
    .followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview {
  display: flex !important;
  flex-direction: column !important;
  justify-content: center !important;
  margin-top: 30px !important;
  padding: clamp(17px, 5%, 40px) clamp(17px, 7%, 50px) !important;
  max-width: none !important;
  border-radius: 6px !important;
  box-shadow: 0 5px 25px rgba(34, 60, 47, 0.25) !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview,
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview *{
  box-sizing: border-box !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading {
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading h5{
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field {
  margin-top: 20px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  width: 100% !important;
  height: 40px !important;
  border-radius: 6px !important;
  border: 2px solid #e9e8e8 !important;
  background-color: #fff;
  outline: none !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  color: #000000 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  text-align: center;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::placeholder {
  color: #000000 !important;
  opacity: 1 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input:-ms-input-placeholder {
  color: #000000 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::-ms-input-placeholder {
  color: #000000 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button {
  margin-top: 10px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button {
  width: 100% !important;
  height: 40px !important;
  border: 0 !important;
  border-radius: 6px !important;
  line-height: 0px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button:hover {
  cursor: pointer !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line {
  color: #231f20 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 13px !important;
  font-weight: 400 !important;
  line-height: 25px !important;
  text-align: center !important;
  text-decoration: none !important;
  display: flex !important;
  width: 100% !important;
  justify-content: center !important;
  align-items: center;
  margin-top: 10px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line img {
  margin-left: 10px !important;
  height: 1.13em !important;
  max-height: 1.13em !important;
}
  &lt;/style&gt;
  &lt;div class=&quot;followit--follow-form-container&quot; attr-a attr-b attr-c attr-d attr-e attr-f&gt;
    &lt;form data-v-f93f6730=&quot;&quot; action=&quot;https://api.follow.it/subscription-form/SmRpWjEzV1ovU0FOK2R2Y0oxajFBUmtIdW44K1ZiUWZRUm1FZVdpVlRKUHFLSFBKWjE4M1hTSk5yRU5oNmg0RU5zelhuTllNTGU1UmwzRkN0NzZWbnd2YkV5VWFwYnptWFdtekI3eXdwMmhtamtUNit3dlNSblVmRjJxd1JGbjZ8Q05yQWR2eitvYW5HRmNMMzJkbStpM09rNmxXZ0ZjWWhyOERya2VvVXRzZz0=/8&quot; method=&quot;post&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;form-preview&quot; style=&quot;background-color: rgb(255, 255, 255); border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204); position: relative;&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-heading&quot;&gt;&lt;h5 data-v-f93f6730=&quot;&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;&quot;&gt;Get new posts by email:&lt;/h5&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-input-field&quot;&gt;&lt;input data-v-f93f6730=&quot;&quot; type=&quot;email&quot; name=&quot;email&quot; required=&quot;&quot; placeholder=&quot;Enter your email&quot; spellcheck=&quot;false&quot; style=&quot;font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;&quot; /&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-submit-button&quot;&gt;&lt;button data-v-f93f6730=&quot;&quot; type=&quot;submit&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(255, 255, 255); font-size: 16px; text-align: center; background-color: rgb(0, 0, 0); text-transform: none !important;&quot;&gt;Subscribe&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;/form&gt;&lt;a href=&quot;https://follow.it&quot; class=&quot;powered-by-line&quot;&gt;Powered by &lt;img src=&quot;https://follow.it/images/colored-logo.svg&quot; alt=&quot;follow.it&quot; height=&quot;17px&quot;/&gt;&lt;/a&gt;
  &lt;/div&gt;&lt;p style=&quot;margin:12px 0 0;font-size:14px;&quot;&gt;Follow along: &lt;a href=&quot;https://www.youtube.com/@VisualStudioLearn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;YouTube&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://www.facebook.com/VisualStudio.Learn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Facebook&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://visualstudiolearn.blogspot.com/feeds/posts/default?alt=rss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RSS&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/7852476395742725949/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/i-built-ai-agent-that-reads-release.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7852476395742725949'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7852476395742725949'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/i-built-ai-agent-that-reads-release.html' title='I Built an AI Agent That Reads Release Notes So I Don&#39;t Have To'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-670509940866109806</id><published>2026-10-24T09:07:00.000-04:00</published><updated>2026-10-24T09:07:00.115-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="FastAPI"/><category scheme="http://www.blogger.com/atom/ns#" term="Gemini"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Small AI Apps"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>Small AI App #1: Build a PDF Q&amp;A App with FastAPI + Gemini (Upload a PDF, Ask It Questions)</title><content type='html'>
&lt;!-- Topic #3 — Small AI App #1: Build a PDF Q&amp;A App with FastAPI + Gemini (Upload a PDF, Ask It Questions)
     Labels: AI Agents, Python, FastAPI, Gemini, Small AI Apps, Tutorials
     Search description: Build a PDF Q&amp;A app with FastAPI and Gemini: upload a PDF, extract and chunk its text, ask questions, and see the real token cost per answer.
     Permalink slug: small-ai-app-1-build-pdf-qa-app-with --&gt;
&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; Python 3.12.3 · FastAPI 0.161.1 · uvicorn 0.54.0 · pypdf 6.21.2 · google-genai 2.33.0 · python-multipart 0.0.32 · Model: &lt;code&gt;gemini-3.8-flash&lt;/code&gt;. Everything runs from one file, &lt;code&gt;app.py&lt;/code&gt;, and the code below is the exact code that was run and tested for this post.&lt;/p&gt;
&lt;!-- VIDEO EMBED: at staging, replace VIDEO_ID in the src below with the uploaded video&#39;s YouTube ID. --&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/QWOKffkimuU&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;h2&gt;The direct answer&lt;/h2&gt;
&lt;p&gt;Yes — one small FastAPI file is enough. Upload a PDF and the server extracts its text with pypdf, splits it into overlapping chunks, and stores them in memory. Ask a question and it picks the chunks sharing the most words with it, sends only those to Gemini with a strict instruction to answer from them alone, and returns the answer with the token counts the API reported and what they cost. No vector database, nothing on top of FastAPI — and if no chunk matches, the app does not call the model at all.&lt;/p&gt;
&lt;p&gt;This is the first episode of &lt;strong&gt;Small AI Apps&lt;/strong&gt;: complete, runnable applications built with FastAPI and Gemini, each small enough to finish in under an hour and honest about what each run costs.&lt;/p&gt;
&lt;h2&gt;What you will build&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /upload&lt;/code&gt; — accepts one PDF, extracts and chunks its text, returns a document id plus the real page, character, and chunk counts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /ask&lt;/code&gt; — answers one question from the best-matching chunks and reports chunks used, input tokens, output tokens, and estimated cost.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /&lt;/code&gt; — a one-page web UI: pick a file, upload it, ask questions, read the cost line under every answer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /health&lt;/code&gt; — a liveness check, because every app you run should have one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two design choices are deliberate. Retrieval is &lt;strong&gt;keyword overlap, not embeddings&lt;/strong&gt;: simpler, free, and honest about its limits — it finds chunks that use your question&#39;s words and can miss a paraphrase. Storage is &lt;strong&gt;in memory&lt;/strong&gt;: restart the server and uploads are gone. Both are right-sized for a first app, and both return at the end as the first production upgrades.&lt;/p&gt;
&lt;h2&gt;Get the code&lt;/h2&gt;
&lt;p&gt;All the code in this post lives in the companion GitHub repository, in the &lt;a href=&quot;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-03-pdf-qa-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;topic-03-pdf-qa-app&lt;/a&gt; folder — the exact &lt;code&gt;app.py&lt;/code&gt; printed below, the pinned requirements file, and a README that covers prerequisites, environment setup including the key walkthrough, install, and how to run and use the app. If you would rather follow along here, every step is also written out below.&lt;/p&gt;
&lt;h2&gt;Environment setup — do this first&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Python 3.10 or newer (this post uses 3.12.3), a terminal, and a Gemini API key. The free tier is enough; no card is needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get your Gemini API key, click by click:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;aistudio.google.com&lt;/strong&gt; and sign in with a Google account.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Get API key&lt;/strong&gt; in the left sidebar (the key icon).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create API key&lt;/strong&gt;, choose or create a Google Cloud project when asked, and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the key that appears — it stays visible in that same key list.&lt;/li&gt;
&lt;li&gt;Keep it secret like a password: never paste it into code, a screenshot, or a public repository. This app reads it only from an environment variable.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Create the project and install the pinned dependencies:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir pdf-qa &amp;amp;&amp;amp; cd pdf-qa
python3 -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate
# Windows PowerShell: py -m venv .venv ; .venv\Scripts\Activate.ps1
pip install fastapi==0.161.1 &amp;quot;uvicorn[standard]==0.54.0&amp;quot; pypdf==6.21.2 google-genai==2.33.0 python-multipart==0.0.32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;python-multipart&lt;/code&gt; is easy to forget: without it, FastAPI cannot accept file uploads at all.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set the key, then verify the setup:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export GEMINI_API_KEY=&amp;quot;paste-your-key-here&amp;quot;
# Windows PowerShell: $env:GEMINI_API_KEY = &amp;quot;paste-your-key-here&amp;quot;
python -c &amp;quot;import fastapi, pypdf, google.genai; print(&amp;#x27;setup ok&amp;#x27;)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;Step 1 — The document pipeline: extract, chunk, retrieve&lt;/h2&gt;
&lt;p&gt;Save the code in this step and the next two as one file, &lt;code&gt;app.py&lt;/code&gt;. This first part turns an uploaded PDF into searchable chunks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Small AI App #1: a PDF Q&amp;amp;A app built with FastAPI and Gemini.

Companion code for the Visual Studio Learn post and video
&amp;quot;Small AI App #1: Build a PDF Q&amp;amp;A App with FastAPI + Gemini&amp;quot;.

Upload a PDF, the app extracts and chunks its text, and each question is
answered by Gemini using only the chunks that match the question — with
the token count and estimated cost of every answer reported back.

Requires: Python 3.12, the packages in requirements.txt, and a Gemini
API key in the GEMINI_API_KEY environment variable. See README.md.
&amp;quot;&amp;quot;&amp;quot;

import logging
import os
import re
import uuid
from dataclasses import dataclass, field
from io import BytesIO

from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.responses import HTMLResponse
from google import genai
from pydantic import BaseModel
from pypdf import PdfReader

logger = logging.getLogger(&amp;quot;pdf_qa&amp;quot;)
logging.basicConfig(level=logging.INFO)

MODEL = &amp;quot;gemini-3.8-flash&amp;quot;
MAX_FILE_BYTES = 10 * 1024 * 1024  # 10 MB upload limit
CHUNK_SIZE = 1000  # characters per chunk
CHUNK_OVERLAP = 150  # characters shared between neighbouring chunks
TOP_CHUNKS = 4  # chunks sent to the model with each question

# Published pay-as-you-go rates for gemini-3.8-flash, US$ per 1M tokens.
INPUT_RATE_PER_MILLION = 0.75
OUTPUT_RATE_PER_MILLION = 3.75


@dataclass
class StoredDocument:
    &amp;quot;&amp;quot;&amp;quot;One uploaded PDF: its name, page count, and text chunks.&amp;quot;&amp;quot;&amp;quot;

    filename: str
    pages: int
    chunks: list[str] = field(default_factory=list)


# In-memory store: doc_id -&amp;gt; document. Deliberately simple — restarting
# the server forgets every upload, which is fine for a learning app and
# is the first thing to replace (with real storage) in a production one.
DOCUMENTS: dict[str, StoredDocument] = {}

app = FastAPI(title=&amp;quot;PDF Q&amp;amp;A — Small AI App #1&amp;quot;)


def extract_text(pdf_bytes: bytes) -&amp;gt; tuple[str, int]:
    &amp;quot;&amp;quot;&amp;quot;Extract all text from PDF bytes. Returns (text, page_count).

    Raises ValueError when the file cannot be parsed as a PDF at all.
    A scanned, image-only PDF parses fine but yields almost no text —
    the caller treats that as its own, separate failure.
    &amp;quot;&amp;quot;&amp;quot;
    try:
        reader = PdfReader(BytesIO(pdf_bytes))
    except Exception as exc:
        raise ValueError(f&amp;quot;Could not read that file as a PDF: {exc}&amp;quot;) from exc
    pages = [page.extract_text() or &amp;quot;&amp;quot; for page in reader.pages]
    return &amp;quot;\n&amp;quot;.join(pages), len(pages)


def chunk_text(text: str, size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -&amp;gt; list[str]:
    &amp;quot;&amp;quot;&amp;quot;Split text into overlapping character chunks.

    Overlap exists so a sentence that straddles a chunk boundary still
    appears whole in at least one chunk the model can read.
    &amp;quot;&amp;quot;&amp;quot;
    cleaned = re.sub(r&amp;quot;\s+&amp;quot;, &amp;quot; &amp;quot;, text).strip()
    chunks = []
    start = 0
    while start &amp;lt; len(cleaned):
        chunks.append(cleaned[start : start + size])
        start += size - overlap
    return [c for c in chunks if c.strip()]


STOPWORDS = {
    &amp;quot;the&amp;quot;, &amp;quot;and&amp;quot;, &amp;quot;for&amp;quot;, &amp;quot;are&amp;quot;, &amp;quot;was&amp;quot;, &amp;quot;what&amp;quot;, &amp;quot;when&amp;quot;, &amp;quot;where&amp;quot;, &amp;quot;which&amp;quot;,
    &amp;quot;does&amp;quot;, &amp;quot;did&amp;quot;, &amp;quot;how&amp;quot;, &amp;quot;why&amp;quot;, &amp;quot;who&amp;quot;, &amp;quot;this&amp;quot;, &amp;quot;that&amp;quot;, &amp;quot;with&amp;quot;, &amp;quot;from&amp;quot;,
    &amp;quot;have&amp;quot;, &amp;quot;has&amp;quot;, &amp;quot;can&amp;quot;, &amp;quot;its&amp;quot;, &amp;quot;it&amp;quot;, &amp;quot;is&amp;quot;, &amp;quot;in&amp;quot;, &amp;quot;on&amp;quot;, &amp;quot;of&amp;quot;, &amp;quot;to&amp;quot;, &amp;quot;a&amp;quot;, &amp;quot;an&amp;quot;,
}


def _meaningful_words(text: str) -&amp;gt; set[str]:
    &amp;quot;&amp;quot;&amp;quot;Lowercase word set with stopwords removed.

    Without this, a question like &amp;quot;What is the…&amp;quot; matches every chunk in
    the document on the words &amp;quot;what&amp;quot; and &amp;quot;the&amp;quot; alone, and the scoring
    below would measure grammar instead of topic.
    &amp;quot;&amp;quot;&amp;quot;
    return {
        w
        for w in re.findall(r&amp;quot;[a-z0-9]+&amp;quot;, text.lower())
        if len(w) &amp;gt; 2 and w not in STOPWORDS
    }


def relevant_chunks(chunks: list[str], question: str, limit: int = TOP_CHUNKS) -&amp;gt; list[str]:
    &amp;quot;&amp;quot;&amp;quot;Pick the chunks that share the most words with the question.

    This is plain keyword overlap, not semantic search — no embeddings,
    no vector database. It is honest about its limits: it finds chunks
    that use the question&amp;#x27;s words, and misses paraphrases. That trade is
    the right size for a first app; embeddings are the upgrade path.
    &amp;quot;&amp;quot;&amp;quot;
    words = _meaningful_words(question)
    if not words:
        return []
    scored = []
    for index, chunk in enumerate(chunks):
        score = len(words &amp;amp; _meaningful_words(chunk))
        if score:
            scored.append((score, index, chunk))
    scored.sort(key=lambda item: (-item[0], item[1]))
    return [chunk for _, _, chunk in scored[:limit]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three details carry this section. &lt;strong&gt;Chunks overlap by 150 characters&lt;/strong&gt; so a sentence that straddles a boundary still appears whole somewhere the model can read it. &lt;strong&gt;Stopwords are stripped before scoring&lt;/strong&gt; — without that, “What is the watering schedule?” matches every chunk on &lt;em&gt;what&lt;/em&gt;, &lt;em&gt;is&lt;/em&gt;, and &lt;em&gt;the&lt;/em&gt;, and the scoring measures grammar instead of topic; that bug appeared in the first test run of this exact code and was fixed before publication. And &lt;strong&gt;extraction and “no text found” are different failures&lt;/strong&gt;: a corrupt file raises in &lt;code&gt;extract_text&lt;/code&gt;, while a scanned PDF parses fine and yields no words — Step 2 gives that case its own, clearer error.&lt;/p&gt;


&lt;h2&gt;Step 2 — The two endpoints&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class AskRequest(BaseModel):
    doc_id: str
    question: str


@app.get(&amp;quot;/health&amp;quot;)
def health() -&amp;gt; dict:
    &amp;quot;&amp;quot;&amp;quot;Tiny liveness check: confirms the server is up.&amp;quot;&amp;quot;&amp;quot;
    return {&amp;quot;status&amp;quot;: &amp;quot;ok&amp;quot;, &amp;quot;documents_loaded&amp;quot;: len(DOCUMENTS)}


@app.post(&amp;quot;/upload&amp;quot;)
async def upload(file: UploadFile = File(...)) -&amp;gt; dict:
    &amp;quot;&amp;quot;&amp;quot;Accept one PDF, extract and chunk its text, and store it.

    Returns the document id the client must send with every question,
    plus the page and chunk counts so the reader can see the pipeline
    actually did the work.
    &amp;quot;&amp;quot;&amp;quot;
    if not (file.filename or &amp;quot;&amp;quot;).lower().endswith(&amp;quot;.pdf&amp;quot;):
        raise HTTPException(status_code=400, detail=&amp;quot;Please upload a PDF file.&amp;quot;)
    pdf_bytes = await file.read()
    if len(pdf_bytes) &amp;gt; MAX_FILE_BYTES:
        raise HTTPException(
            status_code=413,
            detail=f&amp;quot;That file is over the {MAX_FILE_BYTES // (1024 * 1024)} MB limit.&amp;quot;,
        )
    try:
        text, page_count = extract_text(pdf_bytes)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    chunks = chunk_text(text)
    if not chunks:
        raise HTTPException(
            status_code=422,
            detail=(
                &amp;quot;No readable text was found in that PDF. Scanned or &amp;quot;
                &amp;quot;image-only PDFs need OCR before an app like this can &amp;quot;
                &amp;quot;read them.&amp;quot;
            ),
        )
    doc_id = uuid.uuid4().hex[:8]
    DOCUMENTS[doc_id] = StoredDocument(
        filename=file.filename, pages=page_count, chunks=chunks
    )
    logger.info(&amp;quot;stored %s: %d pages, %d chunks&amp;quot;, file.filename, page_count, len(chunks))
    return {
        &amp;quot;doc_id&amp;quot;: doc_id,
        &amp;quot;filename&amp;quot;: file.filename,
        &amp;quot;pages&amp;quot;: page_count,
        &amp;quot;characters&amp;quot;: len(text),
        &amp;quot;chunks&amp;quot;: len(chunks),
    }


@app.post(&amp;quot;/ask&amp;quot;)
def ask(request: AskRequest) -&amp;gt; dict:
    &amp;quot;&amp;quot;&amp;quot;Answer one question from the stored chunks of one document.

    Only the best-matching chunks are sent to Gemini — never the whole
    document — which is what keeps each question cheap and inside the
    model&amp;#x27;s context limits. The response carries the real token counts
    the API reported and the cost they work out to.
    &amp;quot;&amp;quot;&amp;quot;
    document = DOCUMENTS.get(request.doc_id)
    if document is None:
        raise HTTPException(
            status_code=404,
            detail=&amp;quot;Unknown document. Upload a PDF first (uploads are forgotten when the server restarts).&amp;quot;,
        )
    chunks = relevant_chunks(document.chunks, request.question)
    if not chunks:
        return {
            &amp;quot;answer&amp;quot;: (
                &amp;quot;No passage in this document shares words with that &amp;quot;
                &amp;quot;question, so nothing was sent to the model. Try &amp;quot;
                &amp;quot;rephrasing with words the document itself uses.&amp;quot;
            ),
            &amp;quot;chunks_used&amp;quot;: 0,
            &amp;quot;input_tokens&amp;quot;: 0,
            &amp;quot;output_tokens&amp;quot;: 0,
            &amp;quot;estimated_cost_usd&amp;quot;: 0.0,
        }

    context = &amp;quot;\n\n---\n\n&amp;quot;.join(chunks)
    prompt = (
        &amp;quot;Answer the question using only the document passages below. &amp;quot;
        &amp;quot;If the passages do not contain the answer, say so plainly &amp;quot;
        &amp;quot;instead of guessing.\n\n&amp;quot;
        f&amp;quot;Passages:\n{context}\n\nQuestion: {request.question}&amp;quot;
    )
    api_key = os.environ.get(&amp;quot;GEMINI_API_KEY&amp;quot;)
    if not api_key:
        raise HTTPException(
            status_code=500,
            detail=&amp;quot;GEMINI_API_KEY is not set on the server. See the setup steps in the README.&amp;quot;,
        )
    try:
        client = genai.Client(api_key=api_key)
        response = client.models.generate_content(model=MODEL, contents=prompt)
    except Exception as exc:  # surface API failures as a clean 502, not a stack trace
        logger.exception(&amp;quot;Gemini call failed&amp;quot;)
        raise HTTPException(status_code=502, detail=f&amp;quot;Gemini API error: {exc}&amp;quot;) from exc

    usage = response.usage_metadata
    input_tokens = usage.prompt_token_count or 0
    output_tokens = usage.candidates_token_count or 0
    cost = (
        input_tokens * INPUT_RATE_PER_MILLION
        + output_tokens * OUTPUT_RATE_PER_MILLION
    ) / 1_000_000
    logger.info(
        &amp;quot;ask on %s: %d chunks, %d in / %d out tokens&amp;quot;,
        request.doc_id,
        len(chunks),
        input_tokens,
        output_tokens,
    )
    return {
        &amp;quot;answer&amp;quot;: response.text,
        &amp;quot;chunks_used&amp;quot;: len(chunks),
        &amp;quot;input_tokens&amp;quot;: input_tokens,
        &amp;quot;output_tokens&amp;quot;: output_tokens,
        &amp;quot;estimated_cost_usd&amp;quot;: round(cost, 6),
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read &lt;code&gt;/ask&lt;/code&gt; closely, because it is where the money is spent — and saved. Only the four best-matching chunks ever reach the model, never the whole document, which keeps each question small whatever the PDF’s size. If &lt;strong&gt;no&lt;/strong&gt; chunk matches, the endpoint answers without an API call: in testing, asking the sample handbook &lt;em&gt;“What is the airspeed velocity of an unladen swallow?”&lt;/em&gt; returned exactly that no-passage answer with &lt;code&gt;chunks_used: 0&lt;/code&gt; and zero tokens — grounding enforced by code, not by hoping the prompt behaves. When a call does happen, the answer returns with the API’s own usage numbers, and the cost is simple arithmetic at the published &lt;code&gt;gemini-3.8-flash&lt;/code&gt; rates (US$0.75 per million input tokens, US$3.75 per million output). In the live run for this post, the question &lt;em&gt;“When is the lottery draw, and what does a plot cost?”&lt;/em&gt; came back with the handbook’s own facts — the draw takes place on March 30, 2026, and each plot costs 45 dollars for the season — answered from a single chunk at 277 input tokens and 35 output tokens: an estimated &lt;strong&gt;US$0.000339&lt;/strong&gt;, roughly a thirtieth of a cent. The app prints the exact figure for &lt;em&gt;your&lt;/em&gt; question. API failures return as a clean &lt;code&gt;502&lt;/code&gt; carrying the API’s own error text, and a missing key returns a &lt;code&gt;500&lt;/code&gt; that names exactly what to set — both quoted verbatim below.&lt;/p&gt;


&lt;h2&gt;Step 3 — The web page&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.get(&amp;quot;/&amp;quot;, response_class=HTMLResponse)
def home() -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Serve the one-page UI: upload a PDF, then ask it questions.&amp;quot;&amp;quot;&amp;quot;
    return &amp;quot;&amp;quot;&amp;quot;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&amp;quot;utf-8&amp;quot;&amp;gt;
&amp;lt;title&amp;gt;PDF Q&amp;amp;amp;A — Small AI App #1&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
  body { font-family: system-ui, sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; }
  h1 { font-size: 24px; }
  .box { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 16px 0; }
  #answer { white-space: pre-wrap; }
  .meta { color: #555; font-size: 14px; }
  button { padding: 8px 16px; cursor: pointer; }
  input[type=text] { width: 75%; padding: 8px; }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;PDF Q&amp;amp;amp;A&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Upload a PDF, then ask it questions. Answers come only from the
document, and every answer shows what it cost.&amp;lt;/p&amp;gt;

&amp;lt;div class=&amp;quot;box&amp;quot;&amp;gt;
  &amp;lt;input type=&amp;quot;file&amp;quot; id=&amp;quot;pdf&amp;quot; accept=&amp;quot;application/pdf&amp;quot;&amp;gt;
  &amp;lt;button onclick=&amp;quot;uploadPdf()&amp;quot;&amp;gt;Upload PDF&amp;lt;/button&amp;gt;
  &amp;lt;p class=&amp;quot;meta&amp;quot; id=&amp;quot;uploadStatus&amp;quot;&amp;gt;No document uploaded yet.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class=&amp;quot;box&amp;quot;&amp;gt;
  &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;question&amp;quot; placeholder=&amp;quot;Ask a question about the document&amp;quot;&amp;gt;
  &amp;lt;button onclick=&amp;quot;askQuestion()&amp;quot;&amp;gt;Ask&amp;lt;/button&amp;gt;
  &amp;lt;p id=&amp;quot;answer&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;p class=&amp;quot;meta&amp;quot; id=&amp;quot;askMeta&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
let docId = null;

async function uploadPdf() {
  const fileInput = document.getElementById(&amp;#x27;pdf&amp;#x27;);
  const status = document.getElementById(&amp;#x27;uploadStatus&amp;#x27;);
  if (!fileInput.files.length) { status.textContent = &amp;#x27;Choose a PDF first.&amp;#x27;; return; }
  const form = new FormData();
  form.append(&amp;#x27;file&amp;#x27;, fileInput.files[0]);
  status.textContent = &amp;#x27;Uploading and reading…&amp;#x27;;
  const res = await fetch(&amp;#x27;/upload&amp;#x27;, { method: &amp;#x27;POST&amp;#x27;, body: form });
  const data = await res.json();
  if (!res.ok) { status.textContent = &amp;#x27;Upload failed: &amp;#x27; + data.detail; return; }
  docId = data.doc_id;
  status.textContent = &amp;#x27;Loaded &amp;quot;&amp;#x27; + data.filename + &amp;#x27;&amp;quot;: &amp;#x27; + data.pages +
    &amp;#x27; pages, &amp;#x27; + data.characters + &amp;#x27; characters, split into &amp;#x27; +
    data.chunks + &amp;#x27; chunks. You can ask questions now.&amp;#x27;;
}

async function askQuestion() {
  const answer = document.getElementById(&amp;#x27;answer&amp;#x27;);
  const meta = document.getElementById(&amp;#x27;askMeta&amp;#x27;);
  if (!docId) { answer.textContent = &amp;#x27;Upload a PDF first.&amp;#x27;; return; }
  const question = document.getElementById(&amp;#x27;question&amp;#x27;).value;
  answer.textContent = &amp;#x27;Thinking…&amp;#x27;;
  const res = await fetch(&amp;#x27;/ask&amp;#x27;, {
    method: &amp;#x27;POST&amp;#x27;,
    headers: { &amp;#x27;Content-Type&amp;#x27;: &amp;#x27;application/json&amp;#x27; },
    body: JSON.stringify({ doc_id: docId, question: question })
  });
  const data = await res.json();
  if (!res.ok) { answer.textContent = &amp;#x27;Error: &amp;#x27; + data.detail; meta.textContent = &amp;#x27;&amp;#x27;; return; }
  answer.textContent = data.answer;
  meta.textContent = &amp;#x27;Used &amp;#x27; + data.chunks_used + &amp;#x27; chunks · &amp;#x27; +
    data.input_tokens + &amp;#x27; input tokens · &amp;#x27; + data.output_tokens +
    &amp;#x27; output tokens · about $&amp;#x27; + data.estimated_cost_usd.toFixed(6);
}
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&amp;quot;&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page is deliberately plain: one file input, one question box, and a status line that repeats the server’s own counts back to you. That line is your first debugging tool — it proves the server read the file &lt;em&gt;before&lt;/em&gt; you blame the model for a bad answer.&lt;/p&gt;


&lt;h2&gt;Step 4 — Run it and watch it work&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;uvicorn app:app --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;strong&gt;http://127.0.0.1:8000&lt;/strong&gt;. The test run behind this post uploaded a two-page community-garden handbook written as a sample document. The server’s real response:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;{&quot;doc_id&quot;: &quot;979808d3&quot;, &quot;filename&quot;: &quot;sample.pdf&quot;, &quot;pages&quot;: 2, &quot;characters&quot;: 856, &quot;chunks&quot;: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1080&quot; data-original-width=&quot;1920&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhYLYITUJGToLf1DCMYbYTf9pJcYSq0jLTfFCH1etAhXG64KD5cw4xUoL04SPgeCXogMqwWmCIXyAm7wSvx_6AS12oZr3beQWcCp5O0-pytlLazq8kGpm5rHYiDcVlZdHwdKpWHN4hpYYJ-VV76g7p6ABGk_qV62aRdX22N2CpvLctU4QM0KiG0wE5B70k/s1600/screenshot-1-upload.png&quot; /&gt;&lt;/div&gt;
&lt;p&gt;Two pages became 856 characters became two chunks — small numbers, because the sample is small, but the pipeline is identical at 200 pages. The same flow works without the browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -F &amp;quot;file=@sample.pdf&amp;quot; http://127.0.0.1:8000/upload
curl -X POST http://127.0.0.1:8000/ask -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#x27;{&amp;quot;doc_id&amp;quot;: &amp;quot;YOUR_DOC_ID&amp;quot;, &amp;quot;question&amp;quot;: &amp;quot;When is the lottery draw?&amp;quot;}&amp;#x27;
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1080&quot; data-original-width=&quot;1920&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJRZJ4ltxzfXxq_6Mt4Md63yFqph7q5JHZ81_OpICgRTdKIq3ou9Zu56omNzSj3pLfB_U91rbB5-8Xt3Czp17cwby5pkEa_M8Vwnkz1iloeEIb7nXaic8VQTO8SzuqNWlRpqhpo6tD_bEJteuqH13ljLRNrwTSxEcuTWUKYKMxY13_2rJEJwDEogaUn7w/s1600/screenshot-2-live-response.png&quot; /&gt;&lt;/div&gt;


&lt;h2&gt;Errors you may hit&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;422&lt;/code&gt; — “No readable text was found in that PDF.”&lt;/strong&gt; The file parsed but has no text layer — the classic scanned or image-only PDF. This app does not OCR; run the document through an OCR tool first. (Verified: an image-only test PDF returns exactly this.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;413&lt;/code&gt; — “That file is over the 10 MB limit.”&lt;/strong&gt; The cap is a constant at the top of &lt;code&gt;app.py&lt;/code&gt;. Raise it deliberately if needed — a bigger PDF means more chunks, and retrieval, not the context window, keeps questions cheap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;400&lt;/code&gt; — “Please upload a PDF file.”&lt;/strong&gt; The filename does not end in &lt;code&gt;.pdf&lt;/code&gt;. A renamed text file fails here or in extraction, with “Could not read that file as a PDF”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;404&lt;/code&gt; — “Unknown document.”&lt;/strong&gt; The id is wrong, or the server restarted and forgot every upload — the in-memory store working as designed. Upload again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;500&lt;/code&gt; — “GEMINI_API_KEY is not set on the server.”&lt;/strong&gt; The key is missing from the &lt;em&gt;server’s&lt;/em&gt; environment. Setting it in a different terminal does not count; set it in the window running &lt;code&gt;uvicorn&lt;/code&gt;, then restart.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;502&lt;/code&gt; — “Gemini API error: …”&lt;/strong&gt; The call failed — invalid key, exhausted free-tier quota, or a network problem. The text after the colon is the API’s own error; fix that, not the app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A &lt;code&gt;404&lt;/code&gt; inside that 502 — “This model … is no longer available to new users.”&lt;/strong&gt; Google retires model names for new keys first. If your key is recent and the error names a replacement model, update the &lt;code&gt;MODEL&lt;/code&gt; constant at the top of &lt;code&gt;app.py&lt;/code&gt; to the one it names. This post pins &lt;code&gt;gemini-3.8-flash&lt;/code&gt; for exactly this reason: the first test run pointed at &lt;code&gt;gemini-2.5-flash&lt;/code&gt;, and a freshly created key could not reach it at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The answer misses your question’s meaning.&lt;/strong&gt; Retrieval found chunks that share words but not intent, or the passage uses different words than your question. Rephrase with the document’s own vocabulary — or take it as the signal to build the embeddings upgrade below.&lt;/p&gt;
&lt;h2&gt;Where to go next&lt;/h2&gt;
&lt;p&gt;Three upgrades, in the order a production version needs them: &lt;strong&gt;persistent storage&lt;/strong&gt; (a database or disk store instead of the in-memory dictionary), &lt;strong&gt;embeddings-based retrieval&lt;/strong&gt; (semantic matching instead of word overlap), and &lt;strong&gt;authentication&lt;/strong&gt; (this app trusts everyone who can reach the port).&lt;/p&gt;
&lt;p&gt;Previous: MCP Servers Explained — Give Your AI Agent Real Tools. Next in Small AI Apps: decided by what readers actually run — tell us in the comments.&lt;/p&gt;


&lt;div style=&quot;text-align:center;border:1px solid #e3e6ea;border-radius:8px;padding:18px 14px;margin:28px 0;background:#fbfcfd;&quot;&gt;&lt;h3 style=&quot;margin:0 0 6px;&quot;&gt;Enjoying these tutorials?&lt;/h3&gt;&lt;p style=&quot;margin:0 0 6px;&quot;&gt;Get new Visual Studio Learn posts by email — free, no spam:&lt;/p&gt;&lt;style&gt;
    .followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview {
  display: flex !important;
  flex-direction: column !important;
  justify-content: center !important;
  margin-top: 30px !important;
  padding: clamp(17px, 5%, 40px) clamp(17px, 7%, 50px) !important;
  max-width: none !important;
  border-radius: 6px !important;
  box-shadow: 0 5px 25px rgba(34, 60, 47, 0.25) !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview,
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview *{
  box-sizing: border-box !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading {
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-heading h5{
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field {
  margin-top: 20px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  width: 100% !important;
  height: 40px !important;
  border-radius: 6px !important;
  border: 2px solid #e9e8e8 !important;
  background-color: #fff;
  outline: none !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input {
  color: #000000 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  text-align: center;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::placeholder {
  color: #000000 !important;
  opacity: 1 !important;
}

.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input:-ms-input-placeholder {
  color: #000000 !important;
}

.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-input-field input::-ms-input-placeholder {
  color: #000000 !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button {
  margin-top: 10px !important;
  width: 100% !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button {
  width: 100% !important;
  height: 40px !important;
  border: 0 !important;
  border-radius: 6px !important;
  line-height: 0px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .form-preview .preview-submit-button button:hover {
  cursor: pointer !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line {
  color: #231f20 !important;
  font-family: &quot;Montserrat&quot; !important;
  font-size: 13px !important;
  font-weight: 400 !important;
  line-height: 25px !important;
  text-align: center !important;
  text-decoration: none !important;
  display: flex !important;
  width: 100% !important;
  justify-content: center !important;
  align-items: center !important;
  margin-top: 10px !important;
}
.followit--follow-form-container[attr-a][attr-b][attr-c][attr-d][attr-e][attr-f] .powered-by-line img {
  margin-left: 10px !important;
  height: 1.13em !important;
  max-height: 1.13em !important;
}

  &lt;/style&gt;
  &lt;div class=&quot;followit--follow-form-container&quot; attr-a attr-b attr-c attr-d attr-e attr-f&gt;
    &lt;form data-v-f93f6730=&quot;&quot; action=&quot;https://api.follow.it/subscription-form/SmRpWjEzV1ovU0FOK2R2Y0oxajFBUmtIdW44K1ZiUWZRUm1FZVdpVlRKUHFLSFBKWjE4M1hTSk5yRU5oNmg0RU5zelhuTllNTGU1UmwzRkN0NzZWbnd2YkV5VWFwYnptWFdtekI3eXdwMmhtamtUNit3dlNSblVmRjJxd1JGbjZ8Q05yQWR2eitvYW5HRmNMMzJkbStpM09rNmxXZ0ZjWWhyOERya2VvVXRzZz0=/8&quot; method=&quot;post&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;form-preview&quot; style=&quot;background-color: rgb(255, 255, 255); border-style: solid; border-width: 1px; border-color: rgb(204, 204, 204); position: relative;&quot;&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-heading&quot;&gt;&lt;h5 data-v-f93f6730=&quot;&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(0, 0, 0); font-size: 16px; text-align: center; text-transform: none !important;&quot;&gt;Get new posts by email:&lt;/h5&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-input-field&quot;&gt;&lt;input data-v-f93f6730=&quot;&quot; type=&quot;email&quot; name=&quot;email&quot; required=&quot;&quot; placeholder=&quot;Enter your email&quot; spellcheck=&quot;false&quot; style=&quot;font-family: Arial; font-weight: normal; color: rgb(0, 0, 0); font-size: 14px; text-align: center; background-color: rgb(255, 255, 255); text-transform: none !important;&quot; /&gt;&lt;/div&gt;&lt;div data-v-f93f6730=&quot;&quot; class=&quot;preview-submit-button&quot;&gt;&lt;button data-v-f93f6730=&quot;&quot; type=&quot;submit&quot; style=&quot;font-family: Arial; font-weight: bold; color: rgb(255, 255, 255); font-size: 16px; text-align: center; background-color: rgb(0, 0, 0); text-transform: none !important;&quot;&gt;Subscribe&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;/form&gt;&lt;a href=&quot;https://follow.it&quot; class=&quot;powered-by-line&quot;&gt;Powered by &lt;img src=&quot;https://follow.it/images/colored-logo.svg&quot; alt=&quot;follow.it&quot; height=&quot;17px&quot;/&gt;&lt;/a&gt;
  &lt;/div&gt;&lt;p style=&quot;margin:12px 0 0;font-size:14px;&quot;&gt;Follow along: &lt;a href=&quot;https://www.youtube.com/@VisualStudioLearn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;YouTube&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://www.facebook.com/VisualStudio.Learn&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Facebook&lt;/a&gt; &amp;middot; &lt;a href=&quot;https://visualstudiolearn.blogspot.com/feeds/posts/default?alt=rss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RSS&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;
</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/670509940866109806/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/small-ai-app-1-build-pdf-qa-app-with.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/670509940866109806'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/670509940866109806'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/small-ai-app-1-build-pdf-qa-app-with.html' title='Small AI App #1: Build a PDF Q&amp;A App with FastAPI + Gemini (Upload a PDF, Ask It Questions)'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/QWOKffkimuU/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-8981327061337745507</id><published>2026-10-22T09:07:00.000-04:00</published><updated>2026-10-22T09:07:00.120-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="MCP"/><category scheme="http://www.blogger.com/atom/ns#" term="Model Context &#xa;Protocol"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>MCP Servers Explained: Give Your AI Agent Real Tools</title><content type='html'>&lt;p&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;div class=&quot;BLOG_html_snippet&quot;&gt;&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; Python 3.12 · MCP Python SDK &lt;code&gt;mcp&lt;/code&gt; 2.3.0 (specification revision 2026-07-28) · Transport: stdio · No API key, no account. Version note that matters: SDK v2 renamed the class most older tutorials use — code importing &lt;code&gt;FastMCP&lt;/code&gt; was written for v1 and will not run on v2. This post uses &lt;code&gt;MCPServer&lt;/code&gt; throughout.&lt;/p&gt;
&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/SA-J4OARAzk&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;
&lt;h2&gt;The direct answer&lt;/h2&gt;
&lt;p&gt;MCP — the Model Context Protocol — is a standard way for an AI application to discover and call tools that live outside the model. Instead of every framework inventing its own plugin format, a tool is packaged once as an MCP server, and any MCP-aware host can list its tools, read their input schemas, and call them over a defined protocol.&lt;/p&gt;
&lt;p&gt;In the previous post, the tools lived inside the same file as the agent. That works for one program. MCP is for when a tool should outlive it — usable by a second agent, a different host, or next month&#39;s project.&lt;/p&gt;
&lt;h2&gt;Host, client, server — in plain words&lt;/h2&gt;
&lt;p&gt;Three roles appear in every MCP setup, and most confusion comes from mixing them up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Host&lt;/strong&gt; — the application the person uses: an AI coding assistant, a chat app with tool support, or your own agent program. It owns the model connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt; — the connector inside the host, one per server. It opens the connection, performs the handshake, and forwards tool calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server&lt;/strong&gt; — a separate program that offers tools and answers when called. It has no model of its own. Ours offers two tools over a local SQLite database. Client and server exchange JSON-RPC messages, here over &lt;strong&gt;stdio&lt;/strong&gt;: the server is a subprocess, talking over its standard input and output. Streamable HTTP exists for shared servers; stdio is the right start — no port, no hosting, nothing exposed beyond your machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What you will build&lt;/h2&gt;
&lt;p&gt;A server named &lt;code&gt;project-notes&lt;/code&gt; with two read-only tools over a tiny notes database the code creates and seeds itself — &lt;strong&gt;search_notes&lt;/strong&gt; (finds notes containing a keyword) and &lt;strong&gt;recent_notes&lt;/strong&gt; (lists the newest) — plus a small Python client that starts the server and calls both: the same handshake a full AI host performs, with no model in between.&lt;/p&gt;
&lt;h2&gt;Get the code&lt;/h2&gt;
&lt;p&gt;All the code in this post lives in the companion GitHub repository, in the &lt;a href=&quot;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-02-mcp-server&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;topic-02-mcp-server&lt;/a&gt; folder — with a README that covers prerequisites, environment setup, install, and how to run and use it. If you would rather follow along here, every step is also written out below.&lt;/p&gt;
&lt;h2&gt;Step 1 — Set up (no key, no account, nothing to buy)&lt;/h2&gt;
&lt;p&gt;This build needs &lt;strong&gt;no API key and no account&lt;/strong&gt;: an MCP server is ordinary local code, and testing it involves no model at all. If you later connect it to an AI host, signing in is the host&#39;s own setup.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check you have Python 3.10 or newer (this post uses 3.12): run &lt;code&gt;python --version&lt;/code&gt;. On Windows, if the Store opens instead, install Python from python.org and tick &quot;Add python.exe to PATH&quot;.&lt;/li&gt;
&lt;li&gt;Create a folder called &lt;code&gt;mcp-notes&lt;/code&gt;, open a terminal in it, and create a virtual environment:
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
python3 -m venv .venv &amp;amp;&amp;amp; source .venv/bin/activate

# Windows PowerShell
py -m venv .venv ; .venv\Scripts\Activate.ps1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;Install the SDK, pinned (the &lt;code&gt;[cli]&lt;/code&gt; extra adds the &lt;code&gt;mcp&lt;/code&gt; command Step 3 uses):
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install &quot;mcp[cli]==2.3.0&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step 2 — Write the server&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;&quot;&quot;A tiny MCP server that searches a local project-notes database.&quot;&quot;&quot;

import json
import sqlite3
from pathlib import Path

from mcp.server import MCPServer

DB_PATH = Path(__file__).with_name(&quot;notes.db&quot;)

SEED_NOTES = [
    (&quot;Deploy checklist&quot;, &quot;Build the image, run the tests, tag the release, then deploy.&quot;, &quot;deploy&quot;),
    (&quot;Database backup&quot;, &quot;Take a full backup before any schema change and verify the restore.&quot;, &quot;database&quot;),
    (&quot;Grafana alert idea&quot;, &quot;Alert when the error rate stays above two percent for ten minutes.&quot;, &quot;monitoring&quot;),
    (&quot;FastAPI startup fix&quot;, &quot;The startup error came from a missing environment variable, not the code.&quot;, &quot;fastapi&quot;),
]


def _connect() -&amp;gt; sqlite3.Connection:
    &quot;&quot;&quot;Open the notes database, creating and seeding it on first use.&quot;&quot;&quot;
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        &quot;CREATE TABLE IF NOT EXISTS notes &quot;
        &quot;(id INTEGER PRIMARY KEY, title TEXT, body TEXT, tag TEXT)&quot;
    )
    count = conn.execute(&quot;SELECT COUNT(*) FROM notes&quot;).fetchone()[0]
    if count == 0:
        conn.executemany(
            &quot;INSERT INTO notes (title, body, tag) VALUES (?, ?, ?)&quot;, SEED_NOTES
        )
        conn.commit()
    return conn


mcp = MCPServer(&quot;project-notes&quot;)


@mcp.tool()
def search_notes(keyword: str, limit: int = 5) -&amp;gt; str:
    &quot;&quot;&quot;Search project notes for a keyword in the title or body.

    Args:
        keyword: Word or phrase to look for, for example &quot;backup&quot;.
        limit: Maximum number of notes to return, between 1 and 20.
    &quot;&quot;&quot;
    limit = max(1, min(limit, 20))
    like = f&quot;%{keyword}%&quot;
    with _connect() as conn:
        rows = conn.execute(
            &quot;SELECT id, title, body, tag FROM notes &quot;
            &quot;WHERE title LIKE ? OR body LIKE ? ORDER BY id LIMIT ?&quot;,
            (like, like, limit),
        ).fetchall()
    if not rows:
        return json.dumps({&quot;matches&quot;: [], &quot;message&quot;: f&quot;No notes match &#39;{keyword}&#39;.&quot;})
    return json.dumps(
        {
            &quot;matches&quot;: [
                {&quot;id&quot;: r[0], &quot;title&quot;: r[1], &quot;body&quot;: r[2], &quot;tag&quot;: r[3]} for r in rows
            ]
        }
    )


@mcp.tool()
def recent_notes(limit: int = 3) -&amp;gt; str:
    &quot;&quot;&quot;List the most recently added project notes.

    Args:
        limit: How many recent notes to return, between 1 and 20.
    &quot;&quot;&quot;
    limit = max(1, min(limit, 20))
    with _connect() as conn:
        rows = conn.execute(
            &quot;SELECT id, title, tag FROM notes ORDER BY id DESC LIMIT ?&quot;, (limit,)
        ).fetchall()
    return json.dumps(
        {&quot;notes&quot;: [{&quot;id&quot;: r[0], &quot;title&quot;: r[1], &quot;tag&quot;: r[2]} for r in rows]}
    )


if __name__ == &quot;__main__&quot;:
    mcp.run(transport=&quot;stdio&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save it as &lt;code&gt;server.py&lt;/code&gt;. Three things do more work than they appear to. &lt;strong&gt;The type hints are the contract:&lt;/strong&gt; &lt;code&gt;keyword: str&lt;/code&gt; and &lt;code&gt;limit: int = 5&lt;/code&gt; become the tool&#39;s JSON input schema automatically — no schema is written by hand. &lt;strong&gt;The docstring is the description the model reads&lt;/strong&gt;, so write it as instructions to a careful colleague. And &lt;strong&gt;the SQL is parameterized&lt;/strong&gt;: tool arguments are untrusted input — they may come from a model acting on text someone else wrote — and binding the keyword as a parameter keeps a hostile keyword from becoming hostile SQL. Both tools are read-only, and &lt;code&gt;limit&lt;/code&gt; is clamped.&lt;/p&gt;
&lt;h2&gt;Step 3 — Look at your server in the Inspector&lt;/h2&gt;
&lt;p&gt;Before writing client code, inspect the server the way a host sees it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mcp dev server.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This starts the MCP Inspector, a local web page that connects as a client. Open the address it prints, choose &lt;strong&gt;List Tools&lt;/strong&gt;, and you should see both tools with their generated schemas. Call &lt;code&gt;search_notes&lt;/code&gt; with the keyword &lt;code&gt;backup&lt;/code&gt;. If it works here, the server half is done — later failures live in the client or host configuration.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;python server.py&lt;/code&gt; directly appears to do nothing. That is correct, not a hang: a stdio server waits silently for a client to speak first.&lt;/p&gt;
&lt;h2&gt;Step 4 — Connect a client and call a tool&lt;/h2&gt;
&lt;p&gt;Save this as &lt;code&gt;client.py&lt;/code&gt; in the same folder:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;&quot;&quot;A small MCP client that connects to server.py and calls its tools.&quot;&quot;&quot;

import asyncio
import sys

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main() -&amp;gt; None:
    params = StdioServerParameters(
        command=sys.executable,
        args=[&quot;server.py&quot;],
    )
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            tools = await session.list_tools()
            print(&quot;Tools:&quot;, [tool.name for tool in tools.tools])

            result = await session.call_tool(
                &quot;search_notes&quot;, arguments={&quot;keyword&quot;: &quot;backup&quot;}
            )
            print(&quot;search_notes -&amp;gt;&quot;, result.content[0].text)

            result = await session.call_tool(
                &quot;recent_notes&quot;, arguments={&quot;limit&quot;: 2}
            )
            print(&quot;recent_notes -&amp;gt;&quot;, result.content[0].text)


if __name__ == &quot;__main__&quot;:
    asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note &lt;code&gt;command=sys.executable&lt;/code&gt;: the client starts the server with the same Python running the client, so the server inherits the venv where the SDK is installed. Pointing a host at a &lt;code&gt;python&lt;/code&gt; that resolves elsewhere is the most common reason a working server &quot;cannot connect.&quot;&lt;/p&gt;
&lt;p&gt;Run it with &lt;code&gt;python client.py&lt;/code&gt;. Actual output, run against SDK 2.3.0:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tools: [&#39;search_notes&#39;, &#39;recent_notes&#39;]
search_notes -&amp;gt; {&quot;matches&quot;: [{&quot;id&quot;: 2, &quot;title&quot;: &quot;Database backup&quot;, &quot;body&quot;: &quot;Take a full backup before any schema change and verify the restore.&quot;, &quot;tag&quot;: &quot;database&quot;}]}
recent_notes -&amp;gt; {&quot;notes&quot;: [{&quot;id&quot;: 4, &quot;title&quot;: &quot;FastAPI startup fix&quot;, &quot;tag&quot;: &quot;fastapi&quot;}, {&quot;id&quot;: 3, &quot;title&quot;: &quot;Grafana alert idea&quot;, &quot;tag&quot;: &quot;monitoring&quot;}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read it as a conversation: subprocess started, handshake completed, two tools discovered, two calls answered with parseable JSON. Put a model in the host&#39;s seat and nothing about the server changes. That separation is the entire point of the protocol.&lt;/p&gt;
&lt;p&gt;To use the server from a real host, give the host the same launch information in its configuration (file locations differ per host; use absolute paths):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;mcpServers&quot;: {
    &quot;project-notes&quot;: {
      &quot;command&quot;: &quot;C:\\path\\to\\mcp-notes\\.venv\\Scripts\\python.exe&quot;,
      &quot;args&quot;: [&quot;C:\\path\\to\\mcp-notes\\server.py&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, fully restart the host, and the two tools should appear in its tool list.&lt;/p&gt;
&lt;h2&gt;Errors you may hit&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ModuleNotFoundError: No module named &#39;mcp.server.fastmcp&#39;&lt;/code&gt;, with a note that FastMCP was renamed.&lt;/strong&gt; You — or your tutorial — are running v1 code on SDK v2. Change the import to &lt;code&gt;from mcp.server import MCPServer&lt;/code&gt;, as here, or pin the old line with &lt;code&gt;pip install &quot;mcp&amp;lt;2&quot;&lt;/code&gt;. The generations cannot be mixed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The server &quot;hangs&quot; and prints nothing.&lt;/strong&gt; It is waiting on stdio for a client; test it with &lt;code&gt;mcp dev&lt;/code&gt; or &lt;code&gt;client.py&lt;/code&gt;. The mirror-image mistake is worse: a debugging &lt;code&gt;print()&lt;/code&gt; inside the server corrupts the JSON-RPC stream on stdout. Log to stderr, or not at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A validation error names the argument you got wrong.&lt;/strong&gt; These are real outputs from this server: a missing argument returns &lt;code&gt;Field required&lt;/code&gt; for &lt;code&gt;keyword&lt;/code&gt;; passing &lt;code&gt;&quot;five&quot;&lt;/code&gt; where an integer belongs returns &lt;code&gt;Input should be a valid integer, unable to parse string as an integer&lt;/code&gt;. Both arrive as tool errors, not crashes, so the caller can retry corrected. An unregistered name returns &lt;code&gt;Unknown tool:&lt;/code&gt; plus the name — print &lt;code&gt;list_tools()&lt;/code&gt; first, as &lt;code&gt;client.py&lt;/code&gt; does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The host cannot start the server, or finds nothing.&lt;/strong&gt; In order: the Python launched is not the venv&#39;s Python (use its full path); the &lt;code&gt;server.py&lt;/code&gt; path is relative or wrong (use absolute paths, then restart the host — most read this configuration only at startup); or the SDK sits in a different environment. The Step 3 test separates these: if &lt;code&gt;mcp dev&lt;/code&gt; works from your venv, the server is innocent and the launch line is at fault.&lt;/p&gt;
&lt;h2&gt;Where to go next&lt;/h2&gt;
&lt;p&gt;Grow this server as real work pulls it: swap the seed data for your own notes, add a third tool only when a real question needs it, and keep every tool narrow, read-only until a write is required, and honest in its docstring.&lt;/p&gt;
&lt;p&gt;Previous: building the tool-calling loop by hand, in Python. Next: a complete small AI app — a PDF Q&amp;amp;A tool built with FastAPI.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&amp;nbsp;&lt;p&gt;&lt;/p&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/8981327061337745507/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/mcp-servers-explained-give-your-ai.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/8981327061337745507'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/8981327061337745507'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/mcp-servers-explained-give-your-ai.html' title='MCP Servers Explained: Give Your AI Agent Real Tools'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/SA-J4OARAzk/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-442795928269777322</id><published>2026-10-20T09:07:00.000-04:00</published><updated>2026-10-21T09:16:56.739-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="AI Agents"/><category scheme="http://www.blogger.com/atom/ns#" term="Gemini"/><category scheme="http://www.blogger.com/atom/ns#" term="Python"/><category scheme="http://www.blogger.com/atom/ns#" term="Tool Calling"/><category scheme="http://www.blogger.com/atom/ns#" term="Tutorials"/><title type='text'>Build Your First AI Agent in Python (Tool-Calling, Step by Step)</title><content type='html'>&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&amp;nbsp;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;div class=&quot;BLOG_html_snippet&quot;&gt;&lt;p&gt;&lt;strong&gt;Versions used in this post — read this first:&lt;/strong&gt; Python 3.12 · &lt;code&gt;google-genai&lt;/code&gt; 1.35.0 · model &lt;code&gt;gemini-2.5-flash&lt;/code&gt; · No framework, no LangChain. Everything below runs from one file. If a newer SDK changes a method name, the loop in Step 4 stays the same — only the call syntax moves.&lt;/p&gt;
&lt;iframe allowfullscreen=&quot;&quot; frameborder=&quot;0&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/r0nY3zq9YfE&quot; width=&quot;560&quot;&gt;&lt;/iframe&gt;

&lt;h2&gt;The direct answer&lt;/h2&gt;
&lt;p&gt;An AI agent is not a special kind of model. It is a normal program that does three things in a loop: it sends your question and a list of tools to a model, it runs whichever tool the model asks for, and it sends the tool result back so the model can answer — or ask for another tool. Execution stays with your program from start to finish — the model only ever asks.&lt;/p&gt;
&lt;p&gt;This post builds that loop by hand, on purpose. Frameworks hide the loop, and the loop is the part you need when something breaks in production.&lt;/p&gt;
&lt;h2&gt;What you will build&lt;/h2&gt;
&lt;p&gt;A small command-line agent with two tools:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Weather lookup&lt;/strong&gt; — gets the current temperature and wind for a city from the free Open-Meteo service, which needs no API key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calculator&lt;/strong&gt; — evaluates arithmetic safely, without using Python&#39;s &lt;code&gt;eval()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can ask it a question that needs both, such as: &lt;em&gt;&quot;Is it warmer in Ottawa or Toronto right now, and what is the difference times two?&quot;&lt;/em&gt; The agent should call the weather tool twice, call the calculator once, and then answer in plain language.&lt;/p&gt;
&lt;h2&gt;Get the code&lt;/h2&gt; &lt;p&gt;All the code in this post lives in the companion GitHub repository, in the &lt;a href=&quot;https://github.com/sajjad037/visual-studio-learn-code/tree/main/topic-01-first-ai-agent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;topic-01-first-ai-agent&lt;/a&gt; folder — with a README that covers prerequisites, environment setup, install, and how to run and use it. If you would rather follow along here, every step is also written out below.&lt;/p&gt; &lt;h2&gt;Step 1 — Install and set your key&lt;/h2&gt;
&lt;p&gt;Create a folder, then install the single dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install google-genai==1.35.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Get a Gemini API key from Google AI Studio and set it as an environment variable. Never paste a key into source code that you might commit.&lt;/p&gt; &lt;p&gt;&lt;strong&gt;How to get your free Gemini API key (about 2 minutes, no card required):&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;Open &lt;a href=&quot;https://aistudio.google.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google AI Studio&lt;/a&gt; and sign in with your Google account. Accept the terms if it is your first visit.&lt;/li&gt; &lt;li&gt;Click &lt;strong&gt;Get API key&lt;/strong&gt; (in the left sidebar, or on the dashboard).&lt;/li&gt; &lt;li&gt;Click &lt;strong&gt;Create API key&lt;/strong&gt;, then choose &lt;strong&gt;Create API key in new project&lt;/strong&gt; — the simplest option for a first key.&lt;/li&gt; &lt;li&gt;Google generates the key and shows it to you. Click &lt;strong&gt;Copy&lt;/strong&gt;. A Gemini key starts with &quot;AIza&quot;.&lt;/li&gt; &lt;li&gt;Treat the key like a password: anyone who has it can use your quota. Never post it publicly, never commit it to GitHub, and if it ever leaks, delete it on the same page and create a fresh one.&lt;/li&gt; &lt;li&gt;Now set it as the &lt;code&gt;GEMINI_API_KEY&lt;/code&gt; environment variable with the commands below — the code reads it from there, so the key never lives in your source.&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;The free tier is enough for everything in this post. You only need billing if you outgrow the free rate limits — and the 429 error in the errors section below is how you will know.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
export GEMINI_API_KEY=&quot;your-key-here&quot;

# Windows PowerShell
$env:GEMINI_API_KEY=&quot;your-key-here&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2 — Write the two tools&lt;/h2&gt;
&lt;p&gt;A tool is just a Python function with a clear name, typed arguments, and a short docstring. The docstring matters more than usual here: the model reads it to decide when the tool applies, so write it for the model, not only for humans.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import ast
import json
import operator
import urllib.parse
import urllib.request


def get_weather(city: str) -&amp;gt; dict:
    &quot;&quot;&quot;Get the current weather for a city.

    Args:
        city: City name, for example &quot;Ottawa&quot; or &quot;Toronto&quot;.
    Returns:
        Temperature in Celsius, wind speed, and a condition code.
    &quot;&quot;&quot;
    query = urllib.parse.quote(city)
    geo_url = f&quot;https://geocoding-api.open-meteo.com/v1/search?name={query}&amp;amp;count=1&quot;
    with urllib.request.urlopen(geo_url, timeout=10) as response:
        places = json.load(response).get(&quot;results&quot;)
    if not places:
        return {&quot;error&quot;: f&quot;No location found for {city}&quot;}

    place = places[0]
    weather_url = (
        &quot;https://api.open-meteo.com/v1/forecast&quot;
        f&quot;?latitude={place[&#39;latitude&#39;]}&amp;amp;longitude={place[&#39;longitude&#39;]}&quot;
        &quot;&amp;amp;current=temperature_2m,wind_speed_10m,weather_code&quot;
)
    with urllib.request.urlopen(weather_url, timeout=10) as response:
        current = json.load(response)[&quot;current&quot;]
    return {
        &quot;city&quot;: place[&quot;name&quot;],
        &quot;temperature_c&quot;: current[&quot;temperature_2m&quot;],
        &quot;wind_kmh&quot;: current[&quot;wind_speed_10m&quot;],
        &quot;weather_code&quot;: current[&quot;weather_code&quot;],
}


_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.USub: operator.neg,
}


def _eval_node(node):
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in _OPERATORS:
        return _OPERATORS[type(node.op)](_eval_node(node.left), _eval_node(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in _OPERATORS:
        return _OPERATORS[type(node.op)](_eval_node(node.operand))
    raise ValueError(&quot;Expression contains something the calculator does not allow&quot;)


def calculate(expression: str) -&amp;gt; dict:
    &quot;&quot;&quot;Calculate a basic arithmetic expression.

    Args:
        expression: Arithmetic only, for example &quot;18.5 - 16.0&quot; or &quot;(4 + 6) * 2&quot;.
    Returns:
        The numeric result, or an error message if the expression is not allowed.
    &quot;&quot;&quot;
    try:
        tree = ast.parse(expression, mode=&quot;eval&quot;)
        return {&quot;expression&quot;: expression, &quot;result&quot;: _eval_node(tree.body)}
    except Exception as exc:
        return {&quot;error&quot;: f&quot;Could not calculate &#39;{expression}&#39;: {exc}&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the calculator&#39;s design: it parses the expression into a tree and allows only numbers and five operators. Passing a model&#39;s output into &lt;code&gt;eval()&lt;/code&gt; would let a bad prompt become arbitrary code execution. Treat every tool argument as untrusted input, because it is.&lt;/p&gt;
&lt;h2&gt;Step 3 — Write the agent loop&lt;/h2&gt;
&lt;p&gt;This is the whole agent. Automatic function calling is switched off deliberately, so you can watch each round trip instead of having the SDK hide it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os

from google import genai
from google.genai import types

TOOLS = {&quot;get_weather&quot;: get_weather, &quot;calculate&quot;: calculate}
MAX_STEPS = 6


def run_agent(question: str) -&amp;gt; str:
    client = genai.Client(api_key=os.environ[&quot;GEMINI_API_KEY&quot;])
    config = types.GenerateContentConfig(
        tools=list(TOOLS.values()),
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
        system_instruction=(
            &quot;Use the tools when you need live weather or arithmetic. &quot;
            &quot;Do not guess temperatures or do maths in your head.&quot;
),
)
    contents = [question]

    for step in range(1, MAX_STEPS + 1):
        response = client.models.generate_content(
            model=&quot;gemini-2.5-flash&quot;,
            contents=contents,
            config=config,
)
        if not response.function_calls:
            return response.text

        contents.append(response.candidates[0].content)
        for call in response.function_calls:
            tool = TOOLS.get(call.name)
            if tool is None:
                result = {&quot;error&quot;: f&quot;Unknown tool: {call.name}&quot;}
            else:
                result = tool(**call.args)
            print(f&quot; step {step}: {call.name}({call.args}) -&amp;gt; {result}&quot;)
            contents.append(
                types.Part.from_function_response(name=call.name, response=result)
)

    return &quot;Stopped: the agent used all of its steps without finishing.&quot;


if __name__ == &quot;__main__&quot;:
    answer = run_agent(
        &quot;Is it warmer in Ottawa or Toronto right now, &quot;
        &quot;and what is the temperature difference times two?&quot;
)
    print(&quot;\nAnswer:&quot;, answer)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 4 — How the loop works, round by round&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;You send the question plus the tool list.&lt;/strong&gt; The model sees the function names, arguments, and docstrings — not the function bodies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The model replies with a request, not an answer.&lt;/strong&gt; For our question, the first reply asks for &lt;code&gt;get_weather&lt;/code&gt; for Ottawa and Toronto. Nothing has run yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your code validates and runs the tools.&lt;/strong&gt; This is your security boundary. Check the tool name, check the arguments, and only then execute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You send the results back.&lt;/strong&gt; The results become new entries in &lt;code&gt;contents&lt;/code&gt;, so the model can reason over real numbers instead of inventing them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The loop repeats or ends.&lt;/strong&gt; The model may ask for the calculator next. When a reply contains no function call, that reply is the final answer and the loop stops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A step cap protects you.&lt;/strong&gt; &lt;code&gt;MAX_STEPS = 6&lt;/code&gt; guarantees the program ends. An agent without a cap can loop — and bill you — for a very long time.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How to run it and what you should see&lt;/h2&gt;
&lt;p&gt;Save both parts in one file named &lt;code&gt;first_agent.py&lt;/code&gt;, then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python first_agent.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact temperatures will differ every run, but the shape should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt; step 1: get_weather({&#39;city&#39;: &#39;Ottawa&#39;}) -&amp;gt; {&#39;city&#39;: &#39;Ottawa&#39;, &#39;temperature_c&#39;: 9.4,...}
  step 1: get_weather({&#39;city&#39;: &#39;Toronto&#39;}) -&amp;gt; {&#39;city&#39;: &#39;Toronto&#39;, &#39;temperature_c&#39;: 12.1,...}
  step 2: calculate({&#39;expression&#39;: &#39;(12.1 - 9.4) * 2&#39;}) -&amp;gt; {&#39;result&#39;: 5.4}

Answer: Toronto is warmer right now, at 12.1°C compared with 9.4°C in Ottawa — a difference of 2.7 degrees, which doubled is 5.4.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the printed tool calls are missing, the model answered from memory instead of using the tools. Strengthen the system instruction rather than adding more code — wording is a real control surface here.&lt;/p&gt;
&lt;h2&gt;Errors you may hit&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;KeyError: &#39;GEMINI_API_KEY&#39;&lt;/code&gt; or a 400 API key error.&lt;/strong&gt; The variable is not set in the same terminal session that runs the script, or the key was copied with a trailing space. Check only whether the variable exists — never print the key — and restart the terminal after setting it system-wide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The model asks for a tool that does not exist.&lt;/strong&gt; Models occasionally invent a plausible function name. That is why the loop looks the name up in &lt;code&gt;TOOLS&lt;/code&gt; and returns an error result instead of crashing. The model usually corrects itself on the next step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Malformed or missing arguments.&lt;/strong&gt; A call may arrive with a city missing or an expression that is not arithmetic. Both tools above return an &lt;code&gt;{&quot;error&quot;:...}&lt;/code&gt; dictionary rather than raising, so the agent gets a chance to recover and the conversation does not die on one bad call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;429 rate-limit or quota errors.&lt;/strong&gt; Free-tier keys have per-minute and per-day limits, and a loop multiplies requests: one question here costs three or four model calls. Add a short pause and retry on 429, lower &lt;code&gt;MAX_STEPS&lt;/code&gt;, and expect this error during busy testing rather than treating it as a code bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;503 or a temporary model error.&lt;/strong&gt; Transient failures happen on hosted models. Retry a few times with increasing waits before changing your code — and log which step failed, so you know whether the failure was the model call or your own tool.&lt;/p&gt;
&lt;h2&gt;What does one run actually cost?&lt;/h2&gt;
&lt;p&gt;Honest answer: very little — but not zero, and the loop is why. A two-city question typically uses roughly 1,000–1,500 input tokens and 150–250 output tokens across all its model calls. At the published &lt;code&gt;gemini-2.5-flash&lt;/code&gt; paid rates of US$0.30 per million input tokens and US$2.50 per million output tokens, that is well under a tenth of a cent per question. On the free tier, the same run counts against your daily quota instead.&lt;/p&gt;
&lt;p&gt;Do not trust my estimate for your own app. Every response carries usage data, so build the habit on day one of logging &lt;code&gt;response.usage_metadata&lt;/code&gt; for each step. Cost surprises almost never come from one expensive call. They come from a loop that quietly ran forty cheap ones.&lt;/p&gt;
&lt;h2&gt;Where to go next&lt;/h2&gt;
&lt;p&gt;Add one tool that touches your own world — reading a folder of notes, querying a small database, or checking a calendar — and keep the same discipline: typed arguments, errors returned as data, a hard step cap, and a log line for every call. That pattern, not any framework, carries from this script to larger agent systems, including the Gemini integration behind my own AI Portfolio Manager app.&lt;/p&gt;
&lt;p&gt;Next in this series: MCP servers — the standard way to package tools like these so more than one agent can use them.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;br /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/442795928269777322/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/build-your-first-ai-agent-in-python.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/442795928269777322'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/442795928269777322'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2026/10/build-your-first-ai-agent-in-python.html' title='Build Your First AI Agent in Python (Tool-Calling, Step by Step)'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img.youtube.com/vi/r0nY3zq9YfE/default.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-2104785960283910823</id><published>2016-07-20T23:30:00.000-04:00</published><updated>2016-07-20T23:30:41.680-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Videos"/><category scheme="http://www.blogger.com/atom/ns#" term="visual Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="Windows Forms"/><title type='text'>Create Installer Projects in Visual Studio 2013</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5U4TLPB7hUyatRIBROspiyBN2JyYFDtk_IsKJU_1X0r4MEloa8ot_F-n2ewgZbr4R8wVllG_40Q0p5naWvHPSztF1a2OSjpLZUj7dMCnUk52NYPgZTHUajW6HHRzjGvXKXV_3zsz8jWk/s1600/msi-exe-i1%255B1%255D.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;186&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5U4TLPB7hUyatRIBROspiyBN2JyYFDtk_IsKJU_1X0r4MEloa8ot_F-n2ewgZbr4R8wVllG_40Q0p5naWvHPSztF1a2OSjpLZUj7dMCnUk52NYPgZTHUajW6HHRzjGvXKXV_3zsz8jWk/s320/msi-exe-i1%255B1%255D.png&quot; title=&quot;Create Project Installer in visual studio 2013&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
After completing the development of your desktop Application. The next phase is to deployment or installation of that application.&lt;br&gt;
&lt;br&gt;
For this you need to create a setup of your project, then you just need to give the setup file to the users or your client so they can install and use your application.&lt;br&gt;
&lt;br&gt;
In Visual Studio 10 or prior edition, has built in functionality to create setup file using Installer Projects. But in Visual Studio 13 and next edition you need to install an extension first and then you can create. &lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2016/07/create-installer-projects-in-visual.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/2104785960283910823/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/create-installer-projects-in-visual.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2104785960283910823'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2104785960283910823'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/create-installer-projects-in-visual.html' title='Create Installer Projects in Visual Studio 2013'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5U4TLPB7hUyatRIBROspiyBN2JyYFDtk_IsKJU_1X0r4MEloa8ot_F-n2ewgZbr4R8wVllG_40Q0p5naWvHPSztF1a2OSjpLZUj7dMCnUk52NYPgZTHUajW6HHRzjGvXKXV_3zsz8jWk/s72-c/msi-exe-i1%255B1%255D.png" height="72" width="72"/><thr:total>0</thr:total><georss:featurename>Montreal, QC, Canada</georss:featurename><georss:point>45.5016889 -73.567255999999986</georss:point><georss:box>45.1459249 -74.212702999999991 45.8574529 -72.921808999999982</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-8563921201470357008</id><published>2016-07-18T23:30:00.000-04:00</published><updated>2016-07-19T09:53:44.591-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Android"/><category scheme="http://www.blogger.com/atom/ns#" term="Videos"/><title type='text'>Android Development Step by step</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNRO9wFLS-2WT5hMvbVMXF4CQQbRBd59aUbWo6K2eU5-HM-gbkLAOEGnQFve3CDzrNECATNESOAGZio7LMMOvzkpO9Bl2jadLMnFIwn2n9ktip1s3Bcp-2z7sWaDz9jkqFxLmCzfPFhC0/s1600/learn-android-development-840x420%255B1%255D.jpg&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNRO9wFLS-2WT5hMvbVMXF4CQQbRBd59aUbWo6K2eU5-HM-gbkLAOEGnQFve3CDzrNECATNESOAGZio7LMMOvzkpO9Bl2jadLMnFIwn2n9ktip1s3Bcp-2z7sWaDz9jkqFxLmCzfPFhC0/s320/learn-android-development-840x420%255B1%255D.jpg&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Android_(operating_system)&quot;&gt;Android &lt;/a&gt;is a mobile operating system developed by Google. It is primarily designed for Touchscreen mobile devices such as smartphone and tablets.&lt;br&gt;
As we know, a large number  of  audience is using Android smartphone. So you have a huge market to publish your android apps on play store.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2016/07/android-development-step-by-step.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/8563921201470357008/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/android-development-step-by-step.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/8563921201470357008'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/8563921201470357008'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/android-development-step-by-step.html' title='Android Development Step by step'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNRO9wFLS-2WT5hMvbVMXF4CQQbRBd59aUbWo6K2eU5-HM-gbkLAOEGnQFve3CDzrNECATNESOAGZio7LMMOvzkpO9Bl2jadLMnFIwn2n9ktip1s3Bcp-2z7sWaDz9jkqFxLmCzfPFhC0/s72-c/learn-android-development-840x420%255B1%255D.jpg" height="72" width="72"/><thr:total>1</thr:total><georss:featurename>Montreal, QC, Canada</georss:featurename><georss:point>45.5016889 -73.567255999999986</georss:point><georss:box>45.1459249 -74.212702999999991 45.8574529 -72.921808999999982</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-7064269883475300816</id><published>2016-07-16T23:30:00.000-04:00</published><updated>2026-10-16T18:35:03.416-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="JavaScript"/><category scheme="http://www.blogger.com/atom/ns#" term="Tools"/><title type='text'>Online Base64 Encode and Decode</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlhGR-BxBAzoO62a8_kklKtL3uVPB9pCZfRu-5c39MZcBz_A_hpv5aEtnSWaXHcqa-qtVLANGoynw-S5y-WmD5mqoviHOKXNC6r2FWbK0E23RFUNj0P8hPulewXFARB6X5RxA2DwM89ZA/s1600/Online+base64+Encoder+and+Decoder.png&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;255&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlhGR-BxBAzoO62a8_kklKtL3uVPB9pCZfRu-5c39MZcBz_A_hpv5aEtnSWaXHcqa-qtVLANGoynw-S5y-WmD5mqoviHOKXNC6r2FWbK0E23RFUNj0P8hPulewXFARB6X5RxA2DwM89ZA/s320/Online+base64+Encoder+and+Decoder.png&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
In my previous post, I have shared the basic introduction of &lt;b&gt;base64 encode and decode&lt;/b&gt; and it uses.&lt;br&gt;
&lt;br&gt;
I have also shared the code &lt;a href=&quot;http://visualstudiolearn.blogspot.ca/2016/07/base64-encode-and-decode.html&quot;&gt;&lt;b&gt;How you can base64 encode and decode a string in C#, Java, and Object-c&lt;/b&gt;&lt;/a&gt;.&lt;br&gt;
&lt;br&gt;
I have built a simple java script based tool,  which online converts the string to Base64 encode and base64 decode. At the end of this post will get the source code of this tool and the code snippet  to share this tool on your own site.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2016/07/oline-base64-encode-and-decode.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/7064269883475300816/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/oline-base64-encode-and-decode.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7064269883475300816'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7064269883475300816'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/oline-base64-encode-and-decode.html' title='Online Base64 Encode and Decode'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlhGR-BxBAzoO62a8_kklKtL3uVPB9pCZfRu-5c39MZcBz_A_hpv5aEtnSWaXHcqa-qtVLANGoynw-S5y-WmD5mqoviHOKXNC6r2FWbK0E23RFUNj0P8hPulewXFARB6X5RxA2DwM89ZA/s72-c/Online+base64+Encoder+and+Decoder.png" height="72" width="72"/><thr:total>0</thr:total><georss:featurename>Montreal, QC, Canada</georss:featurename><georss:point>45.5016889 -73.567255999999986</georss:point><georss:box>45.1459249 -74.212702999999991 45.8574529 -72.921808999999982</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-209060201097519748</id><published>2016-07-14T23:30:00.000-04:00</published><updated>2026-10-16T14:29:07.241-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="C#"/><category scheme="http://www.blogger.com/atom/ns#" term="JAVA"/><category scheme="http://www.blogger.com/atom/ns#" term="Objective-C"/><title type='text'>Base64 Encode and Decode</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggBqdKY1vmQ1NC5zaMsKr7GmDojhi8Km-gPL2zGTYvxUXfhED-jNT_FZ3D5QhmPS1SnwY4Pn61Mr4E-Do45i8zhJ7l717ru0jdGpcnRb89KbMAqjsKR7jDm6QaPOWvA3DSnZZ-Z5EdV1I/s1600/base64%255B1%255D.png&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;180&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggBqdKY1vmQ1NC5zaMsKr7GmDojhi8Km-gPL2zGTYvxUXfhED-jNT_FZ3D5QhmPS1SnwY4Pn61Mr4E-Do45i8zhJ7l717ru0jdGpcnRb89KbMAqjsKR7jDm6QaPOWvA3DSnZZ-Z5EdV1I/s320/base64%255B1%255D.png&quot; width=&quot;300&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Base64 is used for converting arbitrary binary data to ASCII text so, that it&amp;#39;s become easier to transmitted the data over the network like e-mail and HTML form data.&lt;br&gt;
&lt;br&gt;
In simple words, it turns binary data into plain ASCII text, so the data can travel safely through systems that only handle text, such as email or HTML forms. (Source: &lt;a href=&quot;https://en.wikipedia.org/wiki/Base64&quot;&gt;Wikipedia&lt;/a&gt;).&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2016/07/base64-encode-and-decode.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/209060201097519748/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/base64-encode-and-decode.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/209060201097519748'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/209060201097519748'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/base64-encode-and-decode.html' title='Base64 Encode and Decode'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggBqdKY1vmQ1NC5zaMsKr7GmDojhi8Km-gPL2zGTYvxUXfhED-jNT_FZ3D5QhmPS1SnwY4Pn61Mr4E-Do45i8zhJ7l717ru0jdGpcnRb89KbMAqjsKR7jDm6QaPOWvA3DSnZZ-Z5EdV1I/s72-c/base64%255B1%255D.png" height="72" width="72"/><thr:total>0</thr:total><georss:featurename>Montreal, QC, Canada</georss:featurename><georss:point>45.5016889 -73.567255999999986</georss:point><georss:box>45.1459249 -74.212702999999991 45.8574529 -72.921808999999982</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-7266969290493692784</id><published>2016-07-13T00:00:00.000-04:00</published><updated>2026-10-16T14:30:05.603-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term=".net"/><category scheme="http://www.blogger.com/atom/ns#" term="C#"/><category scheme="http://www.blogger.com/atom/ns#" term="visual Studio"/><category scheme="http://www.blogger.com/atom/ns#" term="Web service"/><title type='text'>Auto Generate Classes from JSON or XML In Visual studio .Net</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzYFFwAiNDeWqJRZPHEwTMqLK3cx3yHQWHLNVMitjcGhuFQb7T7QglYgFK4v7olOP21LxFujVLBnOTJSY1EnPQLl7HSCuet4-Wp-p9YO2FAMECA-4t2Mfw3L6srPdwVqwLnkvOEGvMZLg/s1600/generate_json_to_C%2523_Classes_300_260.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzYFFwAiNDeWqJRZPHEwTMqLK3cx3yHQWHLNVMitjcGhuFQb7T7QglYgFK4v7olOP21LxFujVLBnOTJSY1EnPQLl7HSCuet4-Wp-p9YO2FAMECA-4t2Mfw3L6srPdwVqwLnkvOEGvMZLg/s1600/generate_json_to_C%2523_Classes_300_260.png&quot;&gt;&lt;/a&gt;&lt;/div&gt;
It is very hard to convert the &lt;b&gt;JSON or XML&lt;/b&gt; output of &lt;a href=&quot;http://visualstudiolearn.blogspot.ca/search/label/Web%20service&quot;&gt;Web service&lt;/a&gt; or &lt;b&gt;Web API&lt;/b&gt; to Model or Object Classes manually.&lt;br&gt;
&lt;br&gt;
The best way to handle service response data is, to &lt;b&gt;Deserialize &lt;/b&gt;the &lt;a href=&quot;http://visualstudiolearn.blogspot.ca/2013/05/access-aspnet-j-son-web-service-on.html&quot;&gt;JSON&lt;/a&gt; or XML response into class Object.&lt;br&gt;
&lt;br&gt;
For this, you need to &lt;b&gt;create mapping&lt;/b&gt; classes for that JSON response. Today I will show you how you can easily and quickly create these classes which are auto-generated by a &lt;a href=&quot;https://visualstudiolearn.blogspot.com/search/label/visual%20Studio&quot;&gt;Visual studio&lt;/a&gt;.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2016/07/auto-generate-classes-from-json-or-xml.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/7266969290493692784/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/auto-generate-classes-from-json-or-xml.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7266969290493692784'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7266969290493692784'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2016/07/auto-generate-classes-from-json-or-xml.html' title='Auto Generate Classes from JSON or XML In Visual studio .Net'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzYFFwAiNDeWqJRZPHEwTMqLK3cx3yHQWHLNVMitjcGhuFQb7T7QglYgFK4v7olOP21LxFujVLBnOTJSY1EnPQLl7HSCuet4-Wp-p9YO2FAMECA-4t2Mfw3L6srPdwVqwLnkvOEGvMZLg/s72-c/generate_json_to_C%2523_Classes_300_260.png" height="72" width="72"/><thr:total>0</thr:total><georss:featurename>Montreal, QC, Canada</georss:featurename><georss:point>45.5016889 -73.567255999999986</georss:point><georss:box>45.1459249 -74.212702999999991 45.8574529 -72.921808999999982</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-2002445483530498632</id><published>2014-06-30T15:00:00.000-04:00</published><updated>2026-10-16T14:30:08.264-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="JavaScript"/><category scheme="http://www.blogger.com/atom/ns#" term="jQuery"/><title type='text'>jQuery Form Validation Sample Example</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhm65FdfvuFlXSzolGgS58PI3OGTOJLhzmGxl8vFvGAe2tiGofchWgO_CE1JMMh5AV5OgCRtQMBdyVTdP00caO1w-HozGvoEgCGCOML954svCBxqL-kib4sYFDUnZkSrEQAFtbbPgTPzQ8/s1600/JQuery+Validation.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhm65FdfvuFlXSzolGgS58PI3OGTOJLhzmGxl8vFvGAe2tiGofchWgO_CE1JMMh5AV5OgCRtQMBdyVTdP00caO1w-HozGvoEgCGCOML954svCBxqL-kib4sYFDUnZkSrEQAFtbbPgTPzQ8/s1600/JQuery+Validation.png&quot; height=&quot;200&quot; width=&quot;183&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;b&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/jQuery&quot;&gt;jQuery&lt;/a&gt;&lt;/b&gt; is&lt;b&gt; &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/JavaScript&quot;&gt;JavaScript&lt;/a&gt;&lt;/b&gt; library containing a lot of important and usable function which facilitate our Application. Java script is client side language and which run on client browser.&lt;br&gt;
&lt;br&gt;
In most of web application we have to validate user input before submitting to Server. You can validate user input on Client side using Java script or &lt;b&gt;jQuery&lt;/b&gt; and on Server Side  using server side programming language like &lt;b&gt;PHP, &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/Asp%20.net&quot;&gt;Asp.net&lt;/a&gt;, &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/JAVA&quot;&gt;Java&lt;/a&gt;&lt;/b&gt; and etc.But when you validate input to server side then first request send to server with all user Inputs, then it check whether  these input are correct or not the return to client to display error.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/07/jquery-form-validation-sample-example.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/2002445483530498632/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/07/jquery-form-validation-sample-example.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2002445483530498632'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2002445483530498632'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/07/jquery-form-validation-sample-example.html' title='jQuery Form Validation Sample Example'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhm65FdfvuFlXSzolGgS58PI3OGTOJLhzmGxl8vFvGAe2tiGofchWgO_CE1JMMh5AV5OgCRtQMBdyVTdP00caO1w-HozGvoEgCGCOML954svCBxqL-kib4sYFDUnZkSrEQAFtbbPgTPzQ8/s72-c/JQuery+Validation.png" height="72" width="72"/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-2240800271246777468</id><published>2014-06-28T14:30:00.000-04:00</published><updated>2026-10-16T14:27:52.740-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Html5"/><title type='text'>How to add hyperlink to table row &amp;lt;tr&amp;gt; ?</title><content type='html'>
&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQIQRfAxM8gJsuuydD2Ljosy7ttCeMipr15GH7csNXTeL8ksAnuLseZMP8jt90P6ewSUj0spgya1eTpzimw37i4puseeKRglUVpyh4gBaLiBIIiGLihQ0JJBMJhgc9QtCsefuSgn1cLmQ/s1600/How+to+add+hyperlink+to+table+row+t.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQIQRfAxM8gJsuuydD2Ljosy7ttCeMipr15GH7csNXTeL8ksAnuLseZMP8jt90P6ewSUj0spgya1eTpzimw37i4puseeKRglUVpyh4gBaLiBIIiGLihQ0JJBMJhgc9QtCsefuSgn1cLmQ/s1600/How+to+add+hyperlink+to+table+row+t.png&quot; height=&quot;200&quot; width=&quot;200&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class=&quot;post-text&quot; itemprop=&quot;description&quot;&gt;
&lt;br&gt;
If you having a table with its table row &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; generating in a loop to form multiple rows.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
If you  want to give separate &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; link to each &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;. Since in table we can add only add data to &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt; only, you are not able to achieve that.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
There is a way to achieve it which is following. &lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/06/how-to-add-hyperlink-to-table-row.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/2240800271246777468/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/how-to-add-hyperlink-to-table-row.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2240800271246777468'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2240800271246777468'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/how-to-add-hyperlink-to-table-row.html' title='How to add hyperlink to table row &amp;lt;tr&amp;gt; ?'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQIQRfAxM8gJsuuydD2Ljosy7ttCeMipr15GH7csNXTeL8ksAnuLseZMP8jt90P6ewSUj0spgya1eTpzimw37i4puseeKRglUVpyh4gBaLiBIIiGLihQ0JJBMJhgc9QtCsefuSgn1cLmQ/s72-c/How+to+add+hyperlink+to+table+row+t.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-3441931306928365509</id><published>2014-06-26T15:00:00.000-04:00</published><updated>2014-06-26T23:34:30.837-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Asp .net"/><category scheme="http://www.blogger.com/atom/ns#" term="Deploy Web Sites"/><title type='text'>Free Host or Deploy Asp.net Web Application on somee</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvch-awGmi62-FAeybvsujtHAX1wkUlYGzPsJT4mUUlcSZuVFbzdxme71YmbZYTpif8nlJoeBy9xKQwKGte1aVTTRcew02-SeFgjXw6HczY4lU-TYlZHHGwyXP7Izl-FH3oSXj1NrUjUE/s1600/Free+Host+Asp.net+Web+Application+on+Soome.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvch-awGmi62-FAeybvsujtHAX1wkUlYGzPsJT4mUUlcSZuVFbzdxme71YmbZYTpif8nlJoeBy9xKQwKGte1aVTTRcew02-SeFgjXw6HczY4lU-TYlZHHGwyXP7Izl-FH3oSXj1NrUjUE/s1600/Free+Host+Asp.net+Web+Application+on+Soome.png&quot;&gt;&lt;/a&gt;&lt;/div&gt;
During graduation we had created a lot of academics &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/.net&quot;&gt;.net&lt;/a&gt; web application project which are small but interesting. We could not deploying these project to &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/Asp%20.net&quot;&gt;Asp.net&lt;/a&gt; hosting  Server because it was very expensive and we just deploy it locally and presented it to our teacher.&lt;br&gt;
&lt;br&gt;
 But now there are a lot of site which allow us to freely deploy asp.net web application. &lt;a href=&quot;http://somee.com/&quot;&gt;somee.com&lt;/a&gt; is one of them.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/06/frree-host-or-deploy-aspnet-web.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/3441931306928365509/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/frree-host-or-deploy-aspnet-web.html#comment-form' title='11 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/3441931306928365509'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/3441931306928365509'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/frree-host-or-deploy-aspnet-web.html' title='Free Host or Deploy Asp.net Web Application on somee'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvch-awGmi62-FAeybvsujtHAX1wkUlYGzPsJT4mUUlcSZuVFbzdxme71YmbZYTpif8nlJoeBy9xKQwKGte1aVTTRcew02-SeFgjXw6HczY4lU-TYlZHHGwyXP7Izl-FH3oSXj1NrUjUE/s72-c/Free+Host+Asp.net+Web+Application+on+Soome.png" height="72" width="72"/><thr:total>11</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-7190899725782892553</id><published>2014-06-25T15:00:00.000-04:00</published><updated>2014-06-25T15:00:03.998-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Blogger Tip"/><category scheme="http://www.blogger.com/atom/ns#" term="css"/><category scheme="http://www.blogger.com/atom/ns#" term="Google Code"/><category scheme="http://www.blogger.com/atom/ns#" term="JavaScript"/><title type='text'>Host JavaScript and CSS files for Blogger and How to use Google Code?</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqViTM6oeUkpBu4sqdiD7oxTaLW0e4wb-yPyiNx5LZ4RzRp3jQHb56ju3RM92lUQClV-ekr1rDq0X4YYf-ALpfHMeSMikxomncFL-NZaBIBpMCsI3nqH8Zm64AT2eKa40aX7T8xLu9uR8/s1600/Where+to+store+JavaScript+and+CSS+files+for+Blogger,+How+to+use+Google+Code.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqViTM6oeUkpBu4sqdiD7oxTaLW0e4wb-yPyiNx5LZ4RzRp3jQHb56ju3RM92lUQClV-ekr1rDq0X4YYf-ALpfHMeSMikxomncFL-NZaBIBpMCsI3nqH8Zm64AT2eKa40aX7T8xLu9uR8/s1600/Where+to+store+JavaScript+and+CSS+files+for+Blogger,+How+to+use+Google+Code.png&quot; height=&quot;240&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/Blogger%20Tip&quot;&gt;Blogger&lt;/a&gt; and web designer know how much we should care about coding assets, &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/JavaScript&quot;&gt;JavaScript&lt;/a&gt; files  and css etc where to host it ? and manage it.&lt;br&gt;
&lt;br&gt;
Now in present user experience is more important if user feel bad experience form UI then it will effect site visit and ranking.&lt;br&gt;
&lt;br&gt;
There are many  widget JavaScript and  CSS script that makes you site better.&lt;br&gt;
&lt;br&gt;
But most off time we have to Upload or host some of these script file to a hosting server.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/06/host-javascript-and-css-files-for.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/7190899725782892553/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/host-javascript-and-css-files-for.html#comment-form' title='6 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7190899725782892553'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7190899725782892553'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/host-javascript-and-css-files-for.html' title='Host JavaScript and CSS files for Blogger and How to use Google Code?'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqViTM6oeUkpBu4sqdiD7oxTaLW0e4wb-yPyiNx5LZ4RzRp3jQHb56ju3RM92lUQClV-ekr1rDq0X4YYf-ALpfHMeSMikxomncFL-NZaBIBpMCsI3nqH8Zm64AT2eKa40aX7T8xLu9uR8/s72-c/Where+to+store+JavaScript+and+CSS+files+for+Blogger,+How+to+use+Google+Code.png" height="72" width="72"/><thr:total>6</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-1777448742593740404</id><published>2014-06-24T15:00:00.000-04:00</published><updated>2014-06-24T15:00:01.699-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Google Search"/><category scheme="http://www.blogger.com/atom/ns#" term="seo"/><title type='text'>Google Search Tip and access site through FTP anonymously.</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrXX93zWoOrgF4KiW-Rz5pUPl3ppGofWEzB-LQYlMdsBv62q_1qGy71N6tFWrLoq0-hBtljc3jzYEOocMbppwkFD9424QvKcyWkKamLrFsYNIe_me2JGkg-OOg1dHLqxgnEX0YE4GtAdU/s1600/Google+Search+Tip+VisualStudioLearn.jpg&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrXX93zWoOrgF4KiW-Rz5pUPl3ppGofWEzB-LQYlMdsBv62q_1qGy71N6tFWrLoq0-hBtljc3jzYEOocMbppwkFD9424QvKcyWkKamLrFsYNIe_me2JGkg-OOg1dHLqxgnEX0YE4GtAdU/s1600/Google+Search+Tip+VisualStudioLearn.jpg&quot; height=&quot;155&quot; width=&quot;200&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Google is giant of Technologies. &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/seo&quot;&gt;Google Search &lt;/a&gt;engine is now helping every one students, teachers etc to his respective search.Students use it for school, business people use it for research, and millions more use it for entertainment.&lt;br&gt;
&lt;br&gt;
In this tutorial I will show widely use &lt;b&gt;Google Search Tip and How to access site through FTP anonymously.&lt;/b&gt;&lt;br&gt;
&lt;br&gt;
Did you know that you may not be using Google search to its full potential ? &lt;br&gt;
&lt;br&gt;
There are few amazing tip by using you can access interesting and relevant  data quickly and efficiently.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/06/google-search-tip-and-access-site.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/1777448742593740404/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/google-search-tip-and-access-site.html#comment-form' title='2 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/1777448742593740404'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/1777448742593740404'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/06/google-search-tip-and-access-site.html' title='Google Search Tip and access site through FTP anonymously.'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjrXX93zWoOrgF4KiW-Rz5pUPl3ppGofWEzB-LQYlMdsBv62q_1qGy71N6tFWrLoq0-hBtljc3jzYEOocMbppwkFD9424QvKcyWkKamLrFsYNIe_me2JGkg-OOg1dHLqxgnEX0YE4GtAdU/s72-c/Google+Search+Tip+VisualStudioLearn.jpg" height="72" width="72"/><thr:total>2</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-698163300479768463</id><published>2014-01-04T14:30:00.000-05:00</published><updated>2014-01-05T01:19:05.122-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Blogger Tip"/><category scheme="http://www.blogger.com/atom/ns#" term="seo"/><category scheme="http://www.blogger.com/atom/ns#" term="Site map"/><title type='text'>Howe to create site map for Blogger </title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9qo-938Any6PTIVneZ3Y-8iGmAHG4fMxws8-1TY6v2mzaCiSXqqOLqU2DVUH8ZnyVgrD4v0B_5TuaUSdpDXs2ELlpPBflDt8WuFZfbBvjpAvYXELvoVJzwF7UWuml68ZVKO2wOb7_WGs/s1600/Visual+studio+learn+Generate+Site+Map+for+blogger.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;272&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9qo-938Any6PTIVneZ3Y-8iGmAHG4fMxws8-1TY6v2mzaCiSXqqOLqU2DVUH8ZnyVgrD4v0B_5TuaUSdpDXs2ELlpPBflDt8WuFZfbBvjpAvYXELvoVJzwF7UWuml68ZVKO2wOb7_WGs/s320/Visual+studio+learn+Generate+Site+Map+for+blogger.png&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today! I will discuss a tutorial about on How To &lt;b&gt;Create A Sitemap For Blogger Blog&lt;/b&gt;. For those who don’t know, Why do we need to create sitemap to our blog?&lt;br&gt;
Basically &lt;a href=&quot;http://visualstudiolearn.blogspot.com/p/download.html&quot;&gt;&lt;b&gt;Sitemap&lt;/b&gt;&lt;/a&gt; is a list of pages (directory or table of content) of our blog site to easily navigate our blog post.&lt;br&gt;
&lt;br&gt;
Actually this is not a sitemap in Google &lt;b&gt;Webmaster Tool&lt;/b&gt; this is only a widget developed by Visual studio learn.&lt;br&gt;
&lt;b&gt;Site map&lt;/b&gt; get Attention of &lt;a href=&quot;http://visualstudiolearn.blogspot.com/2011/03/seo.html&quot;&gt;Search Engine&lt;/a&gt; Robot&amp;#39;s in your Blogger Blog using Blogger Site Map And RSS/Atom Feed, Sitemap basically help to improving your Blog visibility to Search Engine, And also help to increasing your Tariff and Page ranking of Site/blog,  here be describe a simple way to Create a visible Site map for Your Blogger Blog, and add to in menu Bar and any where you Want, and Get Fully optimization of Search Engine Robot&amp;#39;s, Follow Some Below Steps to Create a Site Map For Blogger&lt;b&gt;.&lt;/b&gt;&lt;br&gt;
&lt;b&gt;&lt;/b&gt;&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/01/howe-to-create-site-map-for-blogger.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/698163300479768463/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/01/howe-to-create-site-map-for-blogger.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/698163300479768463'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/698163300479768463'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/01/howe-to-create-site-map-for-blogger.html' title='Howe to create site map for Blogger '/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi9qo-938Any6PTIVneZ3Y-8iGmAHG4fMxws8-1TY6v2mzaCiSXqqOLqU2DVUH8ZnyVgrD4v0B_5TuaUSdpDXs2ELlpPBflDt8WuFZfbBvjpAvYXELvoVJzwF7UWuml68ZVKO2wOb7_WGs/s72-c/Visual+studio+learn+Generate+Site+Map+for+blogger.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-9042259936761930939</id><published>2014-01-01T14:00:00.000-05:00</published><updated>2014-01-02T07:32:04.243-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="JavaScript"/><title type='text'>Java Script TRIM, LTRIM and RTRIM Function</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhR2pN14VtafPHYgsR4NlrbMYrI8ydXK6exF4ajSy1zipKFNAA6NMHApnVrJPVX5J124JMzOu99DKxdXSCBSBajl4aqQ4-SY2kSIapn0XIxf0tKunqZftBBuxUCNvtnQaU4_6-q8vFTg0/s1600/LTRIM_RTRIM+in+Java+Script.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;107&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhR2pN14VtafPHYgsR4NlrbMYrI8ydXK6exF4ajSy1zipKFNAA6NMHApnVrJPVX5J124JMzOu99DKxdXSCBSBajl4aqQ4-SY2kSIapn0XIxf0tKunqZftBBuxUCNvtnQaU4_6-q8vFTg0/s320/LTRIM_RTRIM+in+Java+Script.png&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today I will show you how to make &lt;b&gt;TRIM&lt;/b&gt; Function in &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/JavaScript&quot;&gt;Java Script&lt;/a&gt; .&lt;br&gt;
From this tutorial you can easily write &lt;b&gt;RTRIM&lt;/b&gt; and &lt;b&gt;LTRIM&lt;/b&gt; function.&lt;br&gt;
This Function is very useful you can also make you own TRIM function to filtering user input, Preventing Basic SQL Injection and verify or correct user input.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2014/01/java-script-trim-ltrim-and-rtrim.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/9042259936761930939/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/01/java-script-trim-ltrim-and-rtrim.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/9042259936761930939'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/9042259936761930939'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2014/01/java-script-trim-ltrim-and-rtrim.html' title='Java Script TRIM, LTRIM and RTRIM Function'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhR2pN14VtafPHYgsR4NlrbMYrI8ydXK6exF4ajSy1zipKFNAA6NMHApnVrJPVX5J124JMzOu99DKxdXSCBSBajl4aqQ4-SY2kSIapn0XIxf0tKunqZftBBuxUCNvtnQaU4_6-q8vFTg0/s72-c/LTRIM_RTRIM+in+Java+Script.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-4314339403163345448</id><published>2013-12-19T14:00:00.000-05:00</published><updated>2026-10-16T14:32:44.608-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="C #"/><category scheme="http://www.blogger.com/atom/ns#" term="JAVA"/><title type='text'>Convert number to words</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgy8JUxiEeZmk_0wQYcrI22HgCW2gmifPyHav3duKVocKt0Rq6dd-wMxjwX1u7lojS3wJsXgBKDvYcxYv3feXLfL0ZwJKjnenrJ6mfV7eqRivz9AKbDRddTc97s3Jh5KAEJRMb_I3gP5DM/s1600/Convert+Number+into+word+www.visualstudiolearn.blogspot.com+.jpg&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgy8JUxiEeZmk_0wQYcrI22HgCW2gmifPyHav3duKVocKt0Rq6dd-wMxjwX1u7lojS3wJsXgBKDvYcxYv3feXLfL0ZwJKjnenrJ6mfV7eqRivz9AKbDRddTc97s3Jh5KAEJRMb_I3gP5DM/s1600/Convert+Number+into+word+www.visualstudiolearn.blogspot.com+.jpg&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today I will show you how to Convert numbers or Amount in words.&lt;br&gt;
&lt;br&gt;
This is simple but mostly used tool / function in many Applications.&lt;br&gt;
&lt;br&gt;
It is also used in reporting to display amount in words form. &lt;br&gt;
&lt;br&gt;
I am writing simple programs which converts numbers into word in &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/JAVA&quot; target=&quot;_blank&quot;&gt;Java&lt;/a&gt; but can also make it in other languages like &lt;a href=&quot;http://visualstudiolearn.blogspot.com/search/label/C%20%23&quot; target=&quot;_blank&quot;&gt;C#&lt;/a&gt;, Php, JavaScript etc easily.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/12/convert-number-to-words.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/4314339403163345448/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/12/convert-number-to-words.html#comment-form' title='3 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/4314339403163345448'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/4314339403163345448'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/12/convert-number-to-words.html' title='Convert number to words'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgy8JUxiEeZmk_0wQYcrI22HgCW2gmifPyHav3duKVocKt0Rq6dd-wMxjwX1u7lojS3wJsXgBKDvYcxYv3feXLfL0ZwJKjnenrJ6mfV7eqRivz9AKbDRddTc97s3Jh5KAEJRMb_I3gP5DM/s72-c/Convert+Number+into+word+www.visualstudiolearn.blogspot.com+.jpg" height="72" width="72"/><thr:total>3</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-7879311452999011982</id><published>2013-12-04T14:00:00.000-05:00</published><updated>2013-12-05T05:57:51.182-05:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Email"/><category scheme="http://www.blogger.com/atom/ns#" term="JAVA"/><title type='text'>Send Email in Java</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmum10a9W8d6viYKB-KHORP5VrHuPCAEGoHTnyAAxm9BPm5HMNkJRh94_CnzZlkWu7kolSq8MXcLy2-tyUVYIT0okvHtCc34bsbmdKiKMJLAZDISmdU9PyfvBVMNCX1giZNsXNP6CN2lA/s1600/Send+Email+trhough+Java.jpg&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;214&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmum10a9W8d6viYKB-KHORP5VrHuPCAEGoHTnyAAxm9BPm5HMNkJRh94_CnzZlkWu7kolSq8MXcLy2-tyUVYIT0okvHtCc34bsbmdKiKMJLAZDISmdU9PyfvBVMNCX1giZNsXNP6CN2lA/s320/Send+Email+trhough+Java.jpg&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today I will share how to Send Email in java programming language.&lt;br&gt;
&lt;br&gt;
In this post I use G mail SMTP to send Email.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/12/send-email-in-java.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/7879311452999011982/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/12/send-email-in-java.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7879311452999011982'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/7879311452999011982'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/12/send-email-in-java.html' title='Send Email in Java'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhmum10a9W8d6viYKB-KHORP5VrHuPCAEGoHTnyAAxm9BPm5HMNkJRh94_CnzZlkWu7kolSq8MXcLy2-tyUVYIT0okvHtCc34bsbmdKiKMJLAZDISmdU9PyfvBVMNCX1giZNsXNP6CN2lA/s72-c/Send+Email+trhough+Java.jpg" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-2295183194375130265</id><published>2013-08-25T15:00:00.000-04:00</published><updated>2013-08-26T01:49:02.444-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Oracle"/><category scheme="http://www.blogger.com/atom/ns#" term="sql"/><title type='text'>Auto increment Column in Oracle</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWb8y4qfAitPD65od07h1bqHlqwvekzrSl2fE4NPFWu74QcIouL87STWH7v1EKVjEQ2n3j_-OC4d5kCb0y5sIXJbAwkQrxQ7H4_PWali6ncs0XEOeTGMPM75W3MvmioTweBnGlQGBgOSA/s1600/Auto+increment+Coloumn+in+oracle.jpg&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;195&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWb8y4qfAitPD65od07h1bqHlqwvekzrSl2fE4NPFWu74QcIouL87STWH7v1EKVjEQ2n3j_-OC4d5kCb0y5sIXJbAwkQrxQ7H4_PWali6ncs0XEOeTGMPM75W3MvmioTweBnGlQGBgOSA/s200/Auto+increment+Coloumn+in+oracle.jpg&quot; width=&quot;200&quot;&gt;&lt;/a&gt;&lt;/div&gt;
To day I will show you how to create Auto increment column in oracle which may be use as Primary Key .&lt;br&gt;
&lt;br&gt;
In My Sql and Sql Server Data bases it very easy to create auto increment column but in oracle you must have to follow some step to Create an Auto increment column.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/08/auto-increment-column-in-oracle.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/2295183194375130265/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/auto-increment-column-in-oracle.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2295183194375130265'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2295183194375130265'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/auto-increment-column-in-oracle.html' title='Auto increment Column in Oracle'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWb8y4qfAitPD65od07h1bqHlqwvekzrSl2fE4NPFWu74QcIouL87STWH7v1EKVjEQ2n3j_-OC4d5kCb0y5sIXJbAwkQrxQ7H4_PWali6ncs0XEOeTGMPM75W3MvmioTweBnGlQGBgOSA/s72-c/Auto+increment+Coloumn+in+oracle.jpg" height="72" width="72"/><thr:total>0</thr:total><georss:featurename>Karachi, Pakistan</georss:featurename><georss:point>24.893379 67.028060900000014</georss:point><georss:box>24.432913499999998 66.38261390000001 25.3538445 67.673507900000018</georss:box></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-5242375189154614870</id><published>2013-08-21T03:02:00.000-04:00</published><updated>2026-10-16T14:33:28.137-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Oracle"/><category scheme="http://www.blogger.com/atom/ns#" term="sql"/><category scheme="http://www.blogger.com/atom/ns#" term="sql server"/><title type='text'>Log/Audit Table in Oracle and SQL SERVER</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgssK-r8JWQgm1G_98urMRn5UsgRmAd2si78YPcs1gNezN8yhmp7jZGEb0VxkuIen8tEhwqU3zMED-pEa-h0ChPhzHJtcHT0AdxNTO7o4ErVzDy9kvgs0KMUcgSJ2K9ircsf5BJNrzPxIo/s1600/Lg+and+Audit.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;259&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgssK-r8JWQgm1G_98urMRn5UsgRmAd2si78YPcs1gNezN8yhmp7jZGEb0VxkuIen8tEhwqU3zMED-pEa-h0ChPhzHJtcHT0AdxNTO7o4ErVzDy9kvgs0KMUcgSJ2K9ircsf5BJNrzPxIo/s320/Lg+and+Audit.png&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today i will explain you Why we  create Log/Audit table, How it helps us in various situations and How to create Log table and audit table.&lt;br&gt;
&lt;br&gt;
In this tutorial we will Learn  How to create Trigger  in Oracle and Sql server.&lt;br&gt;
&lt;br&gt;
In this article I use both databases&lt;a href=&quot;http://www.visualstudiolearn.com/search/label/Oracle&quot; target=&quot;_blank&quot;&gt; Oracle&lt;/a&gt; and &lt;a href=&quot;http://www.visualstudiolearn.com/search/label/sql%20server%202008&quot; target=&quot;_blank&quot;&gt;SQL Server&lt;/a&gt;.&lt;br&gt;
&lt;br&gt;
By creating log tables, &lt;a href=&quot;http://www.visualstudiolearn.com/search/label/C%20%23%20data%20base&quot; target=&quot;_blank&quot;&gt;data base&lt;/a&gt; automatically logging of changes to several of our database tables.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/08/logaudit-table-in-oracle-and-sql-server.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/5242375189154614870/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/logaudit-table-in-oracle-and-sql-server.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/5242375189154614870'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/5242375189154614870'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/logaudit-table-in-oracle-and-sql-server.html' title='Log/Audit Table in Oracle and SQL SERVER'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgssK-r8JWQgm1G_98urMRn5UsgRmAd2si78YPcs1gNezN8yhmp7jZGEb0VxkuIen8tEhwqU3zMED-pEa-h0ChPhzHJtcHT0AdxNTO7o4ErVzDy9kvgs0KMUcgSJ2K9ircsf5BJNrzPxIo/s72-c/Lg+and+Audit.png" height="72" width="72"/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-2734498509043426753</id><published>2013-08-20T03:04:00.000-04:00</published><updated>2026-10-16T14:34:18.810-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Oracle"/><category scheme="http://www.blogger.com/atom/ns#" term="sql"/><category scheme="http://www.blogger.com/atom/ns#" term="sql server"/><title type='text'>Get Client IP Address, Host name and Current User in Oracle and SQL Server</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-l5TucIWHd0KiSmjZcnEraHiF2lBVoiwg-0Jf-7BVh9iaP-s4x9tFRDnAXdmiuDg3NVZtF2dcyIvmr_C_lqHqultG7OeLS0Whv_ytHXD3HiGBUxWqT3b5PFTK5MV2Z572i-BIivdXOAw/s1600/Host+Name,IP+address+and+Current+User+in+oracle.png&quot; imageanchor=&quot;1&quot; style=&quot;clear: left; float: left; margin-bottom: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;160&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-l5TucIWHd0KiSmjZcnEraHiF2lBVoiwg-0Jf-7BVh9iaP-s4x9tFRDnAXdmiuDg3NVZtF2dcyIvmr_C_lqHqultG7OeLS0Whv_ytHXD3HiGBUxWqT3b5PFTK5MV2Z572i-BIivdXOAw/s320/Host+Name,IP+address+and+Current+User+in+oracle.png&quot; width=&quot;320&quot;&gt;&lt;/a&gt;&lt;/div&gt;
Today I show how to get Client IP Address, Host name and Current Data base user in &lt;a href=&quot;http://www.visualstudiolearn.com/search/label/sql%20server&quot; target=&quot;_blank&quot;&gt;Sql Server&lt;/a&gt; and &lt;a href=&quot;http://www.visualstudiolearn.com/search/label/Oracle&quot; target=&quot;_blank&quot;&gt;Oracle&lt;/a&gt;.&lt;br&gt;
&lt;br&gt;
It is very useful for security purposes and in case of any illegal or unauthorized activity is performed on your database so you can Track and Trace how, when  and who performed that  illegal or unauthorized activity.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/08/get-client-ip-adrres-host-name-and.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/2734498509043426753/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/get-client-ip-adrres-host-name-and.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2734498509043426753'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/2734498509043426753'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/08/get-client-ip-adrres-host-name-and.html' title='Get Client IP Address, Host name and Current User in Oracle and SQL Server'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-l5TucIWHd0KiSmjZcnEraHiF2lBVoiwg-0Jf-7BVh9iaP-s4x9tFRDnAXdmiuDg3NVZtF2dcyIvmr_C_lqHqultG7OeLS0Whv_ytHXD3HiGBUxWqT3b5PFTK5MV2Z572i-BIivdXOAw/s72-c/Host+Name,IP+address+and+Current+User+in+oracle.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-5676246506537344087</id><published>2013-05-28T03:00:00.000-04:00</published><updated>2013-05-28T03:00:09.051-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Asp .net"/><category scheme="http://www.blogger.com/atom/ns#" term="Deploy Web Sites"/><category scheme="http://www.blogger.com/atom/ns#" term="IIS SERVER"/><title type='text'>Unrecognized attribute &#39;targetFramework&#39;. Note that attribute names are case-sensitive</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZeFaGY4_CnXix332_dmIAYJMy9Fivsp6DBof46ZHf6bcYoAzsPVszZBuWYwrvwl7u20Y9E1Op-2mPmzl_h5W-djuhIwKCqW7bo4J1Ko_46DSTiB5Xnt_beVFMRljBGvQZF3cf4w2BkU8/s1600/UWZ66.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;214&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZeFaGY4_CnXix332_dmIAYJMy9Fivsp6DBof46ZHf6bcYoAzsPVszZBuWYwrvwl7u20Y9E1Op-2mPmzl_h5W-djuhIwKCqW7bo4J1Ko_46DSTiB5Xnt_beVFMRljBGvQZF3cf4w2BkU8/s640/UWZ66.png&quot; width=&quot;640&quot;&gt;&lt;/a&gt;&lt;/div&gt;
This Error would be occurred due to wrong  Framework settings in Web.config file or in IIS Server.&lt;br&gt;
Following are cases Which will help you solving this error.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/05/unrecognized-attribute-targetframework.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/5676246506537344087/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/05/unrecognized-attribute-targetframework.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/5676246506537344087'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/5676246506537344087'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/05/unrecognized-attribute-targetframework.html' title='Unrecognized attribute &#39;targetFramework&#39;. Note that attribute names are case-sensitive'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZeFaGY4_CnXix332_dmIAYJMy9Fivsp6DBof46ZHf6bcYoAzsPVszZBuWYwrvwl7u20Y9E1Op-2mPmzl_h5W-djuhIwKCqW7bo4J1Ko_46DSTiB5Xnt_beVFMRljBGvQZF3cf4w2BkU8/s72-c/UWZ66.png" height="72" width="72"/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8861905861585113921.post-6638791454088150125</id><published>2013-05-26T23:29:00.000-04:00</published><updated>2013-05-26T23:29:49.142-04:00</updated><category scheme="http://www.blogger.com/atom/ns#" term="Asp .net"/><category scheme="http://www.blogger.com/atom/ns#" term="Deploy Web Sites"/><category scheme="http://www.blogger.com/atom/ns#" term="IIS SERVER"/><title type='text'>The requested page cannot be accessed because the related configuration data for the page is invalid</title><content type='html'>&lt;div dir=&quot;ltr&quot; style=&quot;text-align: left;&quot; trbidi=&quot;on&quot;&gt;
&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;
&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4dIBkNIBK6SKqlzNx2XjrYAaD7hwc-BJHIfmZmsCoQ0vBwg4PKQtzAOdzPRz2ZGLyL97m8_FJo_7Hq98uAzD_AR0egmKWkp-6MT9mNtsJcRpXaWoYP1vuoHOfEWD5RhYuDovL1Ixa7m4/s1600/sitefinity-community-edition-faq-895-HTTP+500.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; height=&quot;364&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4dIBkNIBK6SKqlzNx2XjrYAaD7hwc-BJHIfmZmsCoQ0vBwg4PKQtzAOdzPRz2ZGLyL97m8_FJo_7Hq98uAzD_AR0egmKWkp-6MT9mNtsJcRpXaWoYP1vuoHOfEWD5RhYuDovL1Ixa7m4/s640/sitefinity-community-edition-faq-895-HTTP+500.png&quot; width=&quot;640&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
This Error would be occurred by some of Following reasons.&lt;br&gt;
&lt;br&gt;
&lt;/div&gt;&lt;a href=&quot;http://visualstudiolearn.blogspot.com/2013/05/the-requested-page-cannot-be-accessed.html#more&quot;&gt;READ MORE&lt;/a&gt;</content><link rel='replies' type='application/atom+xml' href='http://visualstudiolearn.blogspot.com/feeds/6638791454088150125/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/05/the-requested-page-cannot-be-accessed.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/6638791454088150125'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8861905861585113921/posts/default/6638791454088150125'/><link rel='alternate' type='text/html' href='http://visualstudiolearn.blogspot.com/2013/05/the-requested-page-cannot-be-accessed.html' title='The requested page cannot be accessed because the related configuration data for the page is invalid'/><author><name>The Loonie Lesson</name><uri>http://www.blogger.com/profile/02243798795439866634</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='32' height='32' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRY4V9cFLhqMxHWBOWCGBiGUqjan14Vda_dy5rz-C41kApceM-u6jAxS2_6AIRZtGkQeyiQolq1gCeSBhpo573lYq37MLmMGgSKDQLasowic65VoitivYCzqrmUDUqxJuqCTyzah8JuALYtW0Iz7rXIvIcj-ovhVpEuEhQJ6yG1FUQ-A/s1600/loonie-lesson-logo-profile.png'/></author><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4dIBkNIBK6SKqlzNx2XjrYAaD7hwc-BJHIfmZmsCoQ0vBwg4PKQtzAOdzPRz2ZGLyL97m8_FJo_7Hq98uAzD_AR0egmKWkp-6MT9mNtsJcRpXaWoYP1vuoHOfEWD5RhYuDovL1Ixa7m4/s72-c/sitefinity-community-edition-faq-895-HTTP+500.png" height="72" width="72"/><thr:total>1</thr:total></entry></feed>