<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Gonzalo Ayuso &#8211; Web Architect</title>
	<atom:link href="https://gonzalo123.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://gonzalo123.com</link>
	<description>gonzalo123.com</description>
	<lastBuildDate>Mon, 29 Jun 2026 12:13:27 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://s0.wp.com/i/webclip.png</url>
	<title>Gonzalo Ayuso &#8211; Web Architect</title>
	<link>https://gonzalo123.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">6282145</site>	<item>
		<title>A Pokédex in the terminal, but agentic, with LangChain</title>
		<link>https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/</link>
					<comments>https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 12:13:18 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[LangChain]]></category>
		<category><![CDATA[Pokemon]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88579</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>This project is a spin-off of a small that we started during
the Katayuno on June 20, 2024. Katayuno is a Saturday morning programming kata where the
conversation, debate and retrospective normally matter more than finishing the
exercise. That time, helped by AI, almost every team actually finished.</p>
<p>My version used React, TypeScript and Vite, with PokeAPI directly from the
browser <a href="https://github.com/gonzalo123/pokemon">React Pokédex</a>. It had the usual things: list, details, comparison, shiny sprites and
even a small battle mode.</p>
<p>But that application was deterministic. Click here, fetch this, render that.</p>
<p>This time I wanted something slightly different. I wanted a Pokémon professor
in my terminal. Not a chatbot that hallucinates Pokémon facts. A small agent
that uses <a href="https://pokeapi.co/">PokeAPI</a> as the source of truth and an LLM only
as the reasoning layer.</p>
<blockquote>
<p>The LLM is not the database. The LLM is the reasoning layer.</p>
</blockquote>
<p>I don’t want the model to remember Pikachu’s Speed. I want the model to ask the
tool.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?ssl=1"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="656" height="369" data-attachment-id="88581" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/logo-10/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?fit=1672%2C941&amp;ssl=1" data-orig-size="1672,941" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?fit=656%2C369&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=656%2C369&#038;ssl=1" alt="" class="wp-image-88581" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=1536%2C864&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?resize=1200%2C675&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?w=1672&amp;ssl=1 1672w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/logo.png?w=1312&amp;ssl=1 1312w" sizes="(max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h2>The idea</h2>
<p>The project is a Python 3.13 CLI built with Click, Rich, httpx, Pydantic and
LangChain. AWS Bedrock is the default model provider, but it lives behind one
small factory, so changing the provider does not require changing the PokeAPI
code.</p>
<p>The flow is deliberately boring:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="465" height="440" data-attachment-id="88584" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/image-3/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?fit=465%2C440&amp;ssl=1" data-orig-size="465,440" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="image" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?fit=465%2C440&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?resize=465%2C440&#038;ssl=1" alt="" class="wp-image-88584" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?w=465&amp;ssl=1 465w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/image.png?resize=300%2C284&amp;ssl=1 300w" sizes="(max-width: 465px) 100vw, 465px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The agent cannot fetch arbitrary URLs. It only receives five tools:</p>
<ul>
<li><code>get_pokemon</code></li>
<li><code>get_type</code></li>
<li><code>compare_pokemon</code></li>
<li><code>get_evolution_chain</code></li>
<li><code>get_type_matchup</code></li>
</ul>
<p>This is not magic. The tools are normal Python functions returning small,
normalized dictionaries. LangChain decides when to call them and the model
reasons with their output.</p>
<h2>The CLI</h2>
<p>The deterministic commands work without AWS credentials:</p>
<pre><code class="language-bash">uv run python -m cli pokemon pikachu
uv run python -m cli search charizrad
uv run python -m cli compare charizard blastoise
</code></pre>
<p>The commands involving reasoning can use Bedrock:</p>
<pre><code class="language-bash">uv run python -m cli compare charizard blastoise --explain
uv run python -m cli battle charizard venusaur
uv run python -m cli ask &quot;Which Pokémon is faster, Gengar or Alakazam?&quot;
</code></pre>
<h2>The boring deterministic part</h2>
<p>Getting a Pokémon by name does not need an LLM. This is one of those examples
where using an LLM for everything would be a mistake.</p>
<p>The PokeAPI adapter turns the large API response into a small Pydantic model:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">PokemonSummary</span><span class="tok-punctuation">(</span><span class="tok-variableName">BaseModel</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">id</span>: <span class="tok-variableName">int</span></div><div class="cm-line">    <span class="tok-variableName">name</span>: <span class="tok-variableName">str</span></div><div class="cm-line">    <span class="tok-variableName">types</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-variableName">height</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-variableName">Field</span><span class="tok-punctuation">(</span><span class="tok-variableName">description</span><span class="tok-operator">=</span><span class="tok-string">&quot;Height in decimetres, as returned by PokeAPI&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">weight</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-variableName">Field</span><span class="tok-punctuation">(</span><span class="tok-variableName">description</span><span class="tok-operator">=</span><span class="tok-string">&quot;Weight in hectograms, as returned by PokeAPI&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">base_experience</span>: <span class="tok-variableName">int</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span></div><div class="cm-line">    <span class="tok-variableName">stats</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">PokemonStat</span><span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-variableName">abilities</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">stat</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">name</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">int</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">next</span><span class="tok-punctuation">(</span><span class="tok-punctuation">(</span><span class="tok-variableName">stat</span><span class="tok-operator">.</span><span class="tok-propertyName">value</span> <span class="tok-keyword">for</span> <span class="tok-variableName">stat</span> <span class="tok-keyword">in</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">stats</span> <span class="tok-keyword">if</span> <span class="tok-variableName">stat</span><span class="tok-operator">.</span><span class="tok-propertyName">name</span> <span class="tok-operator">==</span> <span class="tok-variableName">name</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">0</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-meta">@</span><span class="tok-variableName">property</span></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">total_stats</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">int</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">sum</span><span class="tok-punctuation">(</span><span class="tok-variableName">stat</span><span class="tok-operator">.</span><span class="tok-propertyName">value</span> <span class="tok-keyword">for</span> <span class="tok-variableName">stat</span> <span class="tok-keyword">in</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">stats</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The <code>pokemon</code> command fetches the data and Rich renders it. The <code>compare</code>
command fetches two Pokémon and compares their base stats with plain Python.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="fadeIn"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="656" height="416" data-attachment-id="88587" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/pokemon/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?fit=2400%2C1520&amp;ssl=1" data-orig-size="2400,1520" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="pokemon" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?fit=656%2C416&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=656%2C416&#038;ssl=1" alt="" class="wp-image-88587" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=1024%2C649&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=300%2C190&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=768%2C486&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=1536%2C973&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=2048%2C1297&amp;ssl=1 2048w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?resize=1200%2C760&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?w=1312&amp;ssl=1 1312w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/pokemon.png?w=1968&amp;ssl=1 1968w" sizes="(max-width: 656px) 100vw, 656px" /></a></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="303" data-attachment-id="88588" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/compare/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?fit=2400%2C1108&amp;ssl=1" data-orig-size="2400,1108" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="compare" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?fit=656%2C303&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=656%2C303&#038;ssl=1" alt="" class="wp-image-88588" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=1024%2C473&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=300%2C139&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=768%2C355&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=1536%2C709&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=2048%2C945&amp;ssl=1 2048w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?resize=1200%2C554&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?w=1312&amp;ssl=1 1312w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/compare.png?w=1968&amp;ssl=1 1968w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The deterministic part is boring. And that is good.</p>
<p>It is easy to test and easy to understand when something goes wrong.</p>
<p>Typos are deterministic too. A failed lookup downloads the compact species-name
index from PokeAPI and keeps it in memory for the current process. Python’s
<code>SequenceMatcher</code> then ranks names locally:</p>
<pre><code class="language-bash">uv run python -m cli search charizrad
</code></pre>
<pre><code class="language-text">charizard 89% similarity
</code></pre>
<p>Normal commands use the same matcher when PokeAPI returns a 404:</p>
<pre><code class="language-text">╭──────────────────────────── Pokémon not found ─────────────────────────────╮
│ I couldn't find 'charizrad'.                                               │
│                                                                            │
│ Best match: Charizard  (89% similarity)                                    │
╰────────────────────────────────────────────────────────────────────────────╯
Press Enter to use Charizard, type another name, or q to cancel:
</code></pre>
<p>Pressing Enter accepts the suggestion. Typing another name retries the lookup,
and <code>q</code> cancels. I deliberately do not ask the LLM and I do not silently replace
the name. In non-interactive scripts the command keeps returning a normal error
instead of waiting forever for input.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="545" data-attachment-id="88590" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/typo-correction/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?fit=2400%2C1992&amp;ssl=1" data-orig-size="2400,1992" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="typo-correction" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?fit=656%2C545&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=656%2C545&#038;ssl=1" alt="" class="wp-image-88590" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=1024%2C850&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=300%2C249&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=768%2C637&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=1536%2C1275&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=2048%2C1700&amp;ssl=1 2048w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?resize=1200%2C996&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?w=1312&amp;ssl=1 1312w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/typo-correction.png?w=1968&amp;ssl=1 1968w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h2>The agentic part</h2>
<p>The agentic part starts when the question is no longer a direct API call.</p>
<p>For example:</p>
<pre><code class="language-text">Which Pokémon is faster, Gengar or Alakazam?
Can Pikachu beat Squirtle?
What are Dragonite weaknesses?
Tell me the evolution chain of Eevee
</code></pre>
<p>Here LangChain’s <code>create_agent</code> receives the Bedrock chat model, the controlled
tools and a system prompt:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">create_chat_model</span><span class="tok-punctuation">(</span><span class="tok-variableName">settings</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-variableName">build_tools</span><span class="tok-punctuation">(</span><span class="tok-variableName">client</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">SYSTEM_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-variableName">result</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-operator">.</span><span class="tok-propertyName">invoke</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-punctuation">{</span><span class="tok-string">&quot;messages&quot;</span>: <span class="tok-punctuation">[</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;role&quot;</span>: <span class="tok-string">&quot;user&quot;</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;content&quot;</span>: <span class="tok-variableName">question</span><span class="tok-punctuation">}</span><span class="tok-punctuation">]</span><span class="tok-punctuation">}</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The important part is not <code>create_agent</code>. The important part is the boundary.
Facts come from tools. The model decides which facts it needs and explains the
result.</p>
<p>The prompt says it explicitly:</p>
<pre><code class="language-text">You must never invent Pokémon data. Use the available tools to retrieve facts
from PokeAPI before answering factual questions.

The LLM is not the database. The LLM is the reasoning layer.
</code></pre>
<p>A prompt is not a security boundary, of course. That is why the agent only gets
small, explicit tools and never gets a generic HTTP client.</p>
<h2>Tools</h2>
<p>The tools are created around the PokeAPI client. This makes them small and also
makes tests simple because I can inject an <code>httpx.MockTransport</code>.</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">get_type_matchup</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">attacker_type</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">defender_types</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">dict</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">Any</span><span class="tok-punctuation">]</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Calculate the damage multiplier for one attacking type against defender types.&quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">client</span><span class="tok-operator">.</span><span class="tok-propertyName">get_type_matchup</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">attacker_type</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">defender_types</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>I don’t return the complete PokeAPI JSON. Agents work better when tools return
the information needed for the task instead of a small novel containing every
field an API has accumulated over the years.</p>
<h2>Structured output</h2>
<p>The battle command is intentionally limited. It is not a competitive Pokémon
simulator. It considers the Pokémon types, type multipliers, base Speed,
offensive stats and defensive stats. It does not consider moves, levels,
abilities, held items, natures, weather or battle format.</p>
<p>The local heuristic first creates a valid prediction:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">BattlePrediction</span><span class="tok-punctuation">(</span><span class="tok-variableName">BaseModel</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">winner</span>: <span class="tok-variableName">str</span></div><div class="cm-line">    <span class="tok-variableName">confidence</span>: <span class="tok-variableName">float</span> <span class="tok-operator">=</span> <span class="tok-variableName">Field</span><span class="tok-punctuation">(</span><span class="tok-variableName">ge</span><span class="tok-operator">=</span><span class="tok-number">0</span><span class="tok-punctuation">,</span> <span class="tok-variableName">le</span><span class="tok-operator">=</span><span class="tok-number">1</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">reasons</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-variableName">caveats</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-variableName">recommended_attack_types</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">]</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>In Bedrock mode I pass that prediction and the normalized PokeAPI facts to a
second LangChain agent using <code>response_format=BattlePrediction</code>. The result is
validated by Pydantic instead of parsing an optimistic blob of JSON from a
string.</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">create_chat_model</span><span class="tok-punctuation">(</span><span class="tok-variableName">settings</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">BATTLE_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">response_format</span><span class="tok-operator">=</span><span class="tok-variableName">ToolStrategy</span><span class="tok-punctuation">(</span><span class="tok-variableName">BattlePrediction</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The model can improve the explanation, but it does not get permission to
invent a Flamethrower, an item or a hidden ability.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="fadeIn"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="431" data-attachment-id="88596" data-permalink="https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/battle/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?fit=2400%2C1578&amp;ssl=1" data-orig-size="2400,1578" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}" data-image-title="battle" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?fit=656%2C431&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=656%2C431&#038;ssl=1" alt="" class="wp-image-88596" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=1024%2C673&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=300%2C197&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=768%2C505&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=1536%2C1010&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=2048%2C1347&amp;ssl=1 2048w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?resize=1200%2C789&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?w=1312&amp;ssl=1 1312w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/06/battle.png?w=1968&amp;ssl=1 1968w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>I use LangChain’s <code>ToolStrategy</code> explicitly here. Bedrock’s native structured
output currently rejects some numeric JSON Schema constraints generated by
Pydantic, such as the minimum and maximum for <code>confidence</code>. Tool calling still
returns a validated <code>BattlePrediction</code> without depending on that provider
limitation.</p>
<h2>Rich output</h2>
<p>Click handles the command-line interface and Rich handles tables, panels,
colours and stat bars.</p>
<p>Rich is not needed, but terminals should still look decent.</p>
<p>The visual layer is also separate from the data layer. <code>render.py</code> receives
Pydantic models. It does not know how PokeAPI works and it does not call the
LLM.</p>
<h2>When not to use the LLM</h2>
<p>I think this is the useful part of the experiment.</p>
<p>There is no model call in:</p>
<ul>
<li><code>pokemon</code></li>
<li><code>compare</code> without <code>--explain</code></li>
<li>typo suggestions and Pokémon name search</li>
<li>type multiplier calculation</li>
<li>the first battle prediction</li>
<li>tests</li>
</ul>
<p>An LLM is useful when the user asks an open question and the application needs
to choose tools, combine facts and explain a conclusion. It is not useful for
adding six integers or reading Pikachu’s height from JSON.</p>
<p>Using less AI here makes the agentic part easier to see.</p>
<h2>Running the project</h2>
<p>I normally use Poetry. For this small project I wanted to try <code>uv</code>, so it owns
Python installation, dependency resolution, command execution and the lock
file. I am not starting a package-manager religion here. It’s just a test.</p>
<pre><code class="language-bash">git clone https://github.com/gonzalo123/pokemon_cli.git
cd pokemon_cli

uv python install 3.13
uv sync --extra dev
</code></pre>
<p>That is enough for the deterministic commands.</p>
<p>For AWS Bedrock:</p>
<pre><code class="language-bash">uv sync --extra dev --extra bedrock
cp .env.example .env
</code></pre>
<p>Then configure the environment:</p>
<pre><code class="language-dotenv">AWS_PROFILE=sandbox
AWS_REGION=eu-west-1
BEDROCK_MODEL_ID=global.anthropic.claude-sonnet-4-6
</code></pre>
<p>No AWS key is stored in the repository. The AWS SDK uses the selected profile
or its normal credential chain.</p>
<p>Now the examples:</p>
<pre><code class="language-bash">uv run python -m cli pokemon pikachu
uv run python -m cli compare charizard blastoise
uv run python -m cli battle charizard venusaur
uv run python -m cli ask \
  &quot;Which Pokémon is faster, Gengar or Alakazam?&quot;
</code></pre>
<p>There is also an installed command:</p>
<pre><code class="language-bash">uv run pokemon-professor pokemon pikachu
</code></pre>
<h2>Things I liked</h2>
<p>The separation is small but useful. PokeAPI owns the facts, Pydantic owns the
shape, Python owns deterministic calculations, Rich owns presentation and the
LLM owns a narrow reasoning task.</p>
<h2>Things that still feel awkward</h2>
<p>The battle result is only a heuristic. A real battle model needs moves, abilities, levels, items, natures, generation rules, and probably much more. Adding all that while pretending the result is still simple would be dishonest.</p>
<p>I’m not a Pokémon expert. I have only been playing with the Pokémon API because of the Katayuno. This is just an excuse to use AI everywhere, just like we developers seem to be doing these days. Please don’t judge me too harshly.</p>
<h2>Final thoughts</h2>
<p>Agentic does not mean replacing every function with an LLM call. For me it
means giving the model a small set of reliable capabilities and letting it use
them when a deterministic route is no longer enough.</p>
<p>The Pokémon facts do not belong in the prompt and they do not belong in the
model’s memory. They belong in PokeAPI.</p>
<p>The LLM is not the database. The LLM is the reasoning layer.</p>
<p>And that’s all. Full source code is available in my GitHub <a href="https://github.com/gonzalo123/pokemon_cli">account</a>.</p>
</div>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/06/29/a-pokedex-in-the-terminal-but-agentic-with-langchain/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88579</post-id>	</item>
		<item>
		<title>What Homer Would Reply to Your Email: A Classical Quote Recommender with Bedrock, RAG, and Strands Agents</title>
		<link>https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/</link>
					<comments>https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 08 Jun 2026 12:18:45 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RAG]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88361</guid>

					<description><![CDATA[Say the input is &#8220;Thanks for your feedback. I think we can find a middle ground&#8221;. Instead of just searching for those words, the system searches for negotiation, conciliatory, guarded optimism, diplomatic and bridge-building, all at once. The search doesn&#8217;t just look for passages about feedback. It looks for passages that feel the same way &#8230; <a href="https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/" class="more-link">Continue reading <span class="screen-reader-text">What Homer Would Reply to Your Email: A Classical Quote Recommender with Bedrock, RAG, and Strands Agents</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>What if every email you write could carry the rhetorical weight of Homer? Not as a gimmick, as a real tool that understands the tone, intent, and emotion of your message and finds the classical passage that fits.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="slideInLeft"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="438" data-attachment-id="88363" data-permalink="https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/image-2/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?fit=1536%2C1024&amp;ssl=1" data-orig-size="1536,1024" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="image" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?fit=656%2C438&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?resize=656%2C438&#038;ssl=1" alt="" class="wp-image-88363" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?resize=1024%2C683&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?resize=1200%2C800&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?w=1536&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/image.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>That’s the idea behind this PoC: a system that takes a short text (an email, a Slack message, a reply to a tricky thread) and recommends the most fitting quote from classical literature. Right now the corpus is the Iliad and the Odyssey, nearly 5,000 passages of Homer, indexed and searchable by meaning, not just by keywords.</p>
<p>The interesting part isn’t the concept. It’s how the pieces fit together.</p>
<h2>The Architecture</h2>
<p>The system runs as a FastAPI service with a five-stage pipeline:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-full coblocks-animate" data-coblocks-animation="slideInLeft"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="538" data-attachment-id="88366" data-permalink="https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/gonzalo123_classical-quote-recfor_short_emails_and_messages_/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?fit=868%2C712&amp;ssl=1" data-orig-size="868,712" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="gonzalo123_classical-quote-rec…for_short_emails_and_messages_" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?fit=656%2C538&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?resize=656%2C538&#038;ssl=1" alt="" class="wp-image-88366" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?w=868&amp;ssl=1 868w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?resize=300%2C246&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_classical-quote-rec%E2%80%A6for_short_emails_and_messages_.png?resize=768%2C630&amp;ssl=1 768w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The key idea: the two ends of the pipeline (understanding your message and choosing the final quote) use an LLM, but the middle part, finding and ranking candidates, is pure code, no AI involved. That means the core search is predictable and inspectable. Bedrock handles the parts that need language understanding; everything else stays local.</p>
<h2>Query Enrichment: Not Just What You Said, But What You Meant</h2>
<p>When you search a typical RAG system, you take the user’s text and look for similar documents. Here I do something different: before searching, the system enriches the query with everything it learned during the rhetorical analysis.</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">HybridRetriever</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_build_query</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">text</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">analysis</span>: <span class="tok-variableName">InputAnalysis</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">        <span class="tok-variableName">parts</span> <span class="tok-operator">=</span> <span class="tok-punctuation">[</span></div><div class="cm-line">            <span class="tok-variableName">text</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">summary</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">main_theme</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-string">&quot; &quot;</span><span class="tok-operator">.</span><span class="tok-propertyName">join</span><span class="tok-punctuation">(</span><span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">secondary_themes</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">tone</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">intent</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">dominant_emotion</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">analysis</span><span class="tok-operator">.</span><span class="tok-propertyName">recommended_quote_type</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">]</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-string">&quot; &quot;</span><span class="tok-operator">.</span><span class="tok-propertyName">join</span><span class="tok-punctuation">(</span><span class="tok-variableName">part</span> <span class="tok-keyword">for</span> <span class="tok-variableName">part</span> <span class="tok-keyword">in</span> <span class="tok-variableName">parts</span> <span class="tok-keyword">if</span> <span class="tok-variableName">part</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">strip</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Say the input is <em>&#8220;Thanks for your feedback. I think we can find a middle ground&#8221;</em>. Instead of just searching for those words, the system searches for <code>negotiation</code>, <code>conciliatory</code>, <code>guarded optimism</code>, <code>diplomatic and bridge-building</code>, all at once. The search doesn&#8217;t just look for passages about feedback. It looks for passages that feel the same way as the message.</p>



<h2 class="wp-block-heading">Zero-Dependency Vector Store</h2>



<p class="wp-block-paragraph">I deliberately avoided FAISS, Chroma, Pinecone, or any external vector database. The entire search index is a single NumPy matrix:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">NumpyVectorStore</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">search</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">query_vector</span>: <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">ndarray</span><span class="tok-punctuation">,</span> <span class="tok-variableName">top_k</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">tuple</span><span class="tok-punctuation">[</span><span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">float</span><span class="tok-punctuation">]</span><span class="tok-punctuation">]</span>:</div><div class="cm-line">        <span class="tok-variableName">query</span> <span class="tok-operator">=</span> <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">asarray</span><span class="tok-punctuation">(</span><span class="tok-variableName">query_vector</span><span class="tok-punctuation">,</span> <span class="tok-variableName">dtype</span><span class="tok-operator">=</span><span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">float32</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">scores</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">vectors</span> <span class="tok-operator">@</span> <span class="tok-variableName">query</span></div><div class="cm-line">        <span class="tok-variableName">indices</span> <span class="tok-operator">=</span> <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">argsort</span><span class="tok-punctuation">(</span><span class="tok-variableName">scores</span><span class="tok-punctuation">)</span><span class="tok-punctuation">[</span>::<span class="tok-operator">-</span><span class="tok-number">1</span><span class="tok-punctuation">]</span><span class="tok-punctuation">[</span>:<span class="tok-variableName">top_k</span><span class="tok-punctuation">]</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-punctuation">[</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">ids</span><span class="tok-punctuation">[</span><span class="tok-variableName">index</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span> <span class="tok-variableName">float</span><span class="tok-punctuation">(</span><span class="tok-variableName">scores</span><span class="tok-punctuation">[</span><span class="tok-variableName">index</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span> <span class="tok-keyword">for</span> <span class="tok-variableName">index</span> <span class="tok-keyword">in</span> <span class="tok-variableName">indices</span><span class="tok-punctuation">]</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">One line does the work: <code>scores = self.vectors @ query</code>. This is a matrix multiplication that compares the query against every passage in the corpus at once. Because the embedding model produces normalized vectors, this simple operation gives you cosine similarity, a standard way to measure how close two texts are in meaning.</p>



<p class="wp-block-paragraph">The full index is a matrix of 4,841 rows (one per passage) and 384 columns (one per dimension of the embedding). It loads in milliseconds and fits in memory easily. For a corpus of this size, a full-blown vector database would be overkill.</p>



<h2 class="wp-block-heading">Why all-MiniLM-L6-v2</h2>



<p class="wp-block-paragraph">The model that turns text into numbers (embeddings) is <code>all-MiniLM-L6-v2</code> from Sentence Transformers. It takes any piece of text and produces a list of 384 numbers that represent its meaning. Texts that say similar things end up with similar numbers, even if they use completely different words.</p>



<p class="wp-block-paragraph">It&#8217;s a small model, only 22 million parameters and 6 layers, but it was trained on over a billion pairs of sentences, so it&#8217;s surprisingly good at capturing semantic similarity. It loads in under a second on a regular CPU and processes the entire Homer corpus in a few seconds.</p>



<p class="wp-block-paragraph">There are bigger models (like <code>all-mpnet-base-v2</code> with 110M parameters) that would be slightly more precise, but for this project the bottleneck isn&#8217;t the embedding quality, it&#8217;s how well the query enrichment captures the intent of the message. The small model is more than enough.</p>



<h2 class="wp-block-heading">The Reranker: Five Signals, Calibrated Weights</h2>



<p class="wp-block-paragraph">After the search phase finds candidate passages using both meaning and keywords, a rule-based reranker scores each one across five signals:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">rerank_score</span> <span class="tok-operator">=</span> <span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-number">0.45</span> <span class="tok-operator">*</span> <span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">hybrid_score</span></div><div class="cm-line">    <span class="tok-operator">+</span> <span class="tok-number">0.20</span> <span class="tok-operator">*</span> <span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">thematic_fit</span></div><div class="cm-line">    <span class="tok-operator">+</span> <span class="tok-number">0.15</span> <span class="tok-operator">*</span> <span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">tonal_fit</span></div><div class="cm-line">    <span class="tok-operator">+</span> <span class="tok-number">0.10</span> <span class="tok-operator">*</span> <span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">clarity_fit</span></div><div class="cm-line">    <span class="tok-operator">+</span> <span class="tok-number">0.10</span> <span class="tok-operator">*</span> <span class="tok-variableName">candidate</span><span class="tok-operator">.</span><span class="tok-propertyName">rhetorical_fit</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">Each signal measures something different: how well the topic matches, whether the tone is right, whether the passage would work rhetorically. The most product-shaped one is <code>clarity_fit</code>. It now favors excerpts that are short, sentence-bounded, and easy to paste into an email without further editing.</p>



<p class="wp-block-paragraph">That matters because pure semantic similarity misses a very obvious real-world constraint: a 200-word passage from Homer might be relevant, but it&#8217;s still useless if what you need is a quotable closing line.</p>



<h2 class="wp-block-heading">Strands Agents with Structured Output</h2>



<p class="wp-block-paragraph">The analyzer and selector use <a href="https://github.com/strands-agents/sdk-python">Strands Agents</a>, an open-source SDK for building AI agents. The key feature I rely on is structured output: instead of asking the LLM to return free text and then parsing it, the SDK forces the model to fill in a typed Python object directly.</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">Agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">model</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-string">&quot;You analyze short emails or messages for a rhetorical quote recommender. &quot;</span></div><div class="cm-line">        <span class="tok-string">&quot;First infer the input language from the text itself. &quot;</span></div><div class="cm-line">        <span class="tok-string">&quot;Return the detected language plus concise, grounded rhetorical analysis.&quot;</span></div><div class="cm-line">    <span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-variableName">result</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">prompt</span><span class="tok-punctuation">,</span> <span class="tok-variableName">structured_output_model</span><span class="tok-operator">=</span><span class="tok-variableName">AnalyzerResult</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The <code>AnalyzerResult</code> is a Pydantic model with fields like <code>main_theme</code>, <code>tone</code>, <code>intent</code>, <code>dominant_emotion</code>, and <code>recommended_quote_type</code>. The model doesn&#8217;t write prose, it fills in a form. This eliminates a whole category of bugs related to parsing LLM output.</p>



<p class="wp-block-paragraph">The selector agent works the same way: it receives the ranked candidates as JSON and returns exactly three choices, each with a <code>why_it_fits</code> explanation written in the language of the original message.</p>



<h2 class="wp-block-heading">Compact Quotes, Not Paragraphs</h2>



<p class="wp-block-paragraph">One thing became obvious very quickly: finding a semantically relevant passage is not the same as finding a quotable one. For emails and short messages, a 70-word block of Homer is basically unusable.</p>



<p class="wp-block-paragraph">So before the selector sees the candidates, the reranker extracts a compact quote window from each passage, usually one or two sentences, under 32 words. The quote still comes verbatim from the indexed corpus, but the system stops treating the full chunk as the thing you&#8217;d actually paste into an email.</p>



<p class="wp-block-paragraph">That small layer makes a big difference. It biases the output toward something you can actually use without turning your message into a wall of text.</p>



<h2 class="wp-block-heading">Representative Examples</h2>



<p class="wp-block-paragraph">Here are two representative examples of the kind of output the system is designed to produce.</p>



<h3 class="wp-block-heading">Example 1</h3>



<p class="wp-block-paragraph"><strong>Input</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Thanks for your feedback. I do not fully agree with the proposal, but I think we can still find a middle ground and move forward.</p>
</blockquote>



<p class="wp-block-paragraph"><strong>Analysis</strong></p>



<ul class="wp-block-list">
<li>Summary: Polite disagreement looking for a workable compromise</li>



<li>Main theme: Negotiation</li>



<li>Tone: Conciliatory</li>



<li>Intent: Negotiate</li>



<li>Dominant emotion: Controlled tension</li>
</ul>



<p class="wp-block-paragraph"><strong>Recommended quote</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">But now let each becalm his troubled breast,<br>Wash, and partake serene the friendly feast.</p>
</blockquote>



<p class="wp-block-paragraph">Homer, <em>The Odyssey</em></p>



<p class="wp-block-paragraph"><strong>Why it fits</strong></p>



<p class="wp-block-paragraph">It cools the temperature without sounding weak. The quote shifts the message away from friction and toward calm, shared ground, and continued conversation.</p>



<h3 class="wp-block-heading">Example 2</h3>



<p class="wp-block-paragraph"><strong>Input</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">We need to stay focused, make a decision, and keep moving even if the road is rough.</p>
</blockquote>



<p class="wp-block-paragraph"><strong>Analysis</strong></p>



<ul class="wp-block-list">
<li>Summary: Call for disciplined action under pressure</li>



<li>Main theme: Leadership</li>



<li>Tone: Resolute</li>



<li>Intent: Persuade</li>



<li>Dominant emotion: Focus</li>
</ul>



<p class="wp-block-paragraph"><strong>Recommended quote</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">In battle calm he guides the rapid storm,<br>Wise to resolve, and patient to perform.</p>
</blockquote>



<p class="wp-block-paragraph">Homer, <em>The Odyssey</em></p>



<p class="wp-block-paragraph"><strong>Why it fits</strong></p>



<p class="wp-block-paragraph">It is short, memorable, and action-oriented. The line matches a message that asks for composure, judgment, and forward motion at the same time.</p>



<h2 class="wp-block-heading">Bedrock as the Language Brain</h2>



<p class="wp-block-paragraph">Language detection, rhetorical analysis, quote selection, and translation all go through AWS Bedrock (running Claude Sonnet 4). I tried a local language detector first, but short real-world messages are messy: mixed languages, ticket IDs, URLs, corporate jargon. Bedrock handles that ambiguity much better and keeps the pipeline simpler.</p>



<p class="wp-block-paragraph">The deterministic part of the system is still retrieval and reranking. But for anything that requires actually understanding language, Bedrock does the heavy lifting.</p>



<h2 class="wp-block-heading">Reading EPUBs With Zero Dependencies</h2>



<p class="wp-block-paragraph">The corpus loader handles EPUB files using only Python&#8217;s standard library, <code>zipfile</code>, <code>xml.etree.ElementTree</code>, and <code>html.parser</code>:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_load_epub</span><span class="tok-punctuation">(</span><span class="tok-variableName">path</span>: <span class="tok-variableName">Path</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">LoadedDocument</span>:</div><div class="cm-line">    <span class="tok-keyword">with</span> <span class="tok-variableName">zipfile</span><span class="tok-operator">.</span><span class="tok-propertyName">ZipFile</span><span class="tok-punctuation">(</span><span class="tok-variableName">path</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;r&quot;</span><span class="tok-punctuation">)</span> <span class="tok-keyword">as</span> <span class="tok-variableName">archive</span>:</div><div class="cm-line">        <span class="tok-variableName">container_xml</span> <span class="tok-operator">=</span> <span class="tok-variableName">archive</span><span class="tok-operator">.</span><span class="tok-propertyName">read</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;META-INF/container.xml&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">container_root</span> <span class="tok-operator">=</span> <span class="tok-variableName">ET</span><span class="tok-operator">.</span><span class="tok-propertyName">fromstring</span><span class="tok-punctuation">(</span><span class="tok-variableName">container_xml</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">rootfile</span> <span class="tok-operator">=</span> <span class="tok-variableName">container_root</span><span class="tok-operator">.</span><span class="tok-propertyName">find</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;.//c:rootfile&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">CONTAINER_NS</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-variableName">opf_root</span> <span class="tok-operator">=</span> <span class="tok-variableName">ET</span><span class="tok-operator">.</span><span class="tok-propertyName">fromstring</span><span class="tok-punctuation">(</span><span class="tok-variableName">archive</span><span class="tok-operator">.</span><span class="tok-propertyName">read</span><span class="tok-punctuation">(</span><span class="tok-variableName">opf_path</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-comment"># Parse manifest, follow the spine, extract XHTML chapters</span></div><div class="cm-line">        <span class="tok-keyword">for</span> <span class="tok-variableName">chapter_path</span> <span class="tok-keyword">in</span> <span class="tok-variableName">spine_paths</span>:</div><div class="cm-line">            <span class="tok-variableName">chapter_html</span> <span class="tok-operator">=</span> <span class="tok-variableName">archive</span><span class="tok-operator">.</span><span class="tok-propertyName">read</span><span class="tok-punctuation">(</span><span class="tok-variableName">chapter_path</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">decode</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;utf-8&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">errors</span><span class="tok-operator">=</span><span class="tok-string">&quot;ignore&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-variableName">text</span> <span class="tok-operator">=</span> <span class="tok-variableName">_extract_epub_text</span><span class="tok-punctuation">(</span><span class="tok-variableName">chapter_html</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-variableName">chapters</span><span class="tok-operator">.</span><span class="tok-propertyName">append</span><span class="tok-punctuation">(</span><span class="tok-variableName">text</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">An EPUB is really just a ZIP file with XML and HTML inside. The loader opens the ZIP, reads the table of contents (the OPF file), follows the chapter order (the spine), and extracts clean text from each HTML chapter. No external libraries needed, just Python&#8217;s built-in tools.</p>



<h2 class="wp-block-heading">The HashingEmbedder: Deterministic Tests Without Models</h2>



<p class="wp-block-paragraph">Running the full pipeline in tests means loading Sentence Transformers, which means downloading a 90MB model. Instead, there&#8217;s a <code>HashingEmbedder</code> that produces fake-but-consistent embeddings using SHA1:</p>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">HashingEmbedder</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_embed</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">text</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">ndarray</span>:</div><div class="cm-line">        <span class="tok-variableName">vector</span> <span class="tok-operator">=</span> <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">zeros</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">dimension</span><span class="tok-punctuation">,</span> <span class="tok-variableName">dtype</span><span class="tok-operator">=</span><span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">float32</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-keyword">for</span> <span class="tok-variableName">token</span> <span class="tok-keyword">in</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_tokenize</span><span class="tok-punctuation">(</span><span class="tok-variableName">text</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">            <span class="tok-variableName">digest</span> <span class="tok-operator">=</span> <span class="tok-variableName">hashlib</span><span class="tok-operator">.</span><span class="tok-propertyName">sha1</span><span class="tok-punctuation">(</span><span class="tok-variableName">token</span><span class="tok-operator">.</span><span class="tok-propertyName">encode</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;utf-8&quot;</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">hexdigest</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-variableName">bucket</span> <span class="tok-operator">=</span> <span class="tok-variableName">int</span><span class="tok-punctuation">(</span><span class="tok-variableName">digest</span><span class="tok-punctuation">[</span>:<span class="tok-number">8</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span> <span class="tok-number">16</span><span class="tok-punctuation">)</span> <span class="tok-operator">%</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">dimension</span></div><div class="cm-line">            <span class="tok-variableName">sign</span> <span class="tok-operator">=</span> <span class="tok-number">1.0</span> <span class="tok-keyword">if</span> <span class="tok-variableName">int</span><span class="tok-punctuation">(</span><span class="tok-variableName">digest</span><span class="tok-punctuation">[</span><span class="tok-number">8</span>:<span class="tok-number">10</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span> <span class="tok-number">16</span><span class="tok-punctuation">)</span> <span class="tok-operator">%</span> <span class="tok-number">2</span> <span class="tok-operator">==</span> <span class="tok-number">0</span> <span class="tok-keyword">else</span> <span class="tok-operator">-</span><span class="tok-number">1.0</span></div><div class="cm-line">            <span class="tok-variableName">vector</span><span class="tok-punctuation">[</span><span class="tok-variableName">bucket</span><span class="tok-punctuation">]</span> <span class="tok-operator">+=</span> <span class="tok-variableName">sign</span></div><div class="cm-line">        <span class="tok-variableName">norm</span> <span class="tok-operator">=</span> <span class="tok-variableName">np</span><span class="tok-operator">.</span><span class="tok-propertyName">linalg</span><span class="tok-operator">.</span><span class="tok-propertyName">norm</span><span class="tok-punctuation">(</span><span class="tok-variableName">vector</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-keyword">if</span> <span class="tok-variableName">norm</span> <span class="tok-operator">&gt;</span> <span class="tok-number">0</span>:</div><div class="cm-line">            <span class="tok-variableName">vector</span> <span class="tok-operator">/=</span> <span class="tok-variableName">norm</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">vector</span></div></code></pre>
		</div>
	</div>
</div>


<p class="wp-block-paragraph">The idea: each word gets hashed to a position and a direction in the vector. The same word always produces the same result, so the tests are reproducible. These embeddings don&#8217;t understand meaning, &#8220;king&#8221; and &#8220;queen&#8221; won&#8217;t be close, but the whole pipeline runs exactly the same way, with real numbers flowing through every step. Tests stay fast, offline, and predictable.</p>



<h2 class="wp-block-heading">Source code</h2>



<p class="wp-block-paragraph">Full source code available in my <a href="https://github.com/gonzalo123/classical-quote-recommender">GitHub repository</a>.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/06/08/what-homer-would-reply-to-your-email-a-classical-quote-recommender-with-bedrock-rag-and-strands-agents/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88361</post-id>	</item>
		<item>
		<title>AI-Powered CloudWatch Logs Analysis with Python and Strands Agents</title>
		<link>https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/</link>
					<comments>https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 11 May 2026 12:24:03 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[cloudwatch]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=87732</guid>

					<description><![CDATA[Configure known log groups in settings.py: Now you can ask specific questions about your logs: With custom CloudWatch Insights query: Using natural language time ranges: The project also allows us to launch an interactive session for exploratory analysis: This ensures you can analyze any time range without manual intervention, regardless of log volume. For large &#8230; <a href="https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/" class="more-link">Continue reading <span class="screen-reader-text">AI-Powered CloudWatch Logs Analysis with Python and Strands Agents</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>When you’re debugging production issues at 3 AM, the last thing you want is to scroll through thousands of CloudWatch log entries trying to find that one error. I’ve built a CLI tool that uses AWS Bedrock (Claude Sonnet 4.5) to analyze CloudWatch logs intelligently. You ask questions in natural language, and it gives you insights instead of raw log dumps.</p>
<p>This project is an exploration of combining AWS services with AI agents. Yes, it’s probably over-engineered for simple log queries, but it demonstrates interesting patterns for handling large datasets with parallel AI processing.</p>
<h2>The Problem</h2>
<p>CloudWatch Logs Insights is powerful, but it has limitations:</p>
<ul>
<li>You need to know the query syntax</li>
<li>Results are raw data, not insights</li>
<li>Large result sets are overwhelming</li>
<li>Pattern recognition requires manual analysis</li>
</ul>
<p>What if you could ask: “What errors occurred in the last 2 hours?” and get an intelligent summary instead of 10,000 raw log entries?</p>
<h2>Architecture</h2>
<p>The tool implements two interaction modes: direct CLI queries and an interactive agent.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/12/online_flowchart___diagrams_editor_-_mermaid_live_editor.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="448" height="732" data-attachment-id="87753" data-permalink="https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/online_flowchart___diagrams_editor_-_mermaid_live_editor/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/12/online_flowchart___diagrams_editor_-_mermaid_live_editor.png?fit=448%2C732&amp;ssl=1" data-orig-size="448,732" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="Online_FlowChart___Diagrams_Editor_-_Mermaid_Live_Editor" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/12/online_flowchart___diagrams_editor_-_mermaid_live_editor.png?fit=448%2C732&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/12/online_flowchart___diagrams_editor_-_mermaid_live_editor.png?resize=448%2C732&#038;ssl=1" alt="" class="wp-image-87753" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h3>Key Components</h3>
<p><strong>CloudWatch Insights Query Layer</strong> (<code>modules/logs/main.py</code>)</p>
<ul>
<li>Recursively subdivides time ranges when hitting AWS’s 10,000 result limit</li>
<li>Parses natural language time ranges (“last 2 hours”, “since yesterday”)</li>
<li>Supports custom CloudWatch Insights query syntax</li>
</ul>
<p><strong>Smart Dataset Routing</strong></p>
<ul>
<li>Small datasets (2,000 logs): Parallel worker-coordinator pattern</li>
<li>Configurable chunk size and max workers</li>
</ul>
<p><strong>Worker-Coordinator Pattern</strong>
Each worker agent analyzes a chunk of logs (2,000 records), then a coordinator agent synthesizes all analyses into a coherent answer. This architecture allows processing 10,000+ log records efficiently while staying within Claude’s context limits.</p>
<p><strong>Interactive Agent</strong> (<code>agents/log_agent.py</code>)
A specialized agent with access to the <code>analyze_cloudwatch_logs</code> tool, configured with known log groups and time parsing capabilities.</p>
<h2>Technology Stack</h2>
<ul>
<li><strong>Python 3.13</strong> with type hints and Pydantic models</li>
<li><strong>boto3</strong> for AWS CloudWatch Logs API</li>
<li><strong>AWS Bedrock</strong> (Claude Sonnet 4.5) for AI analysis</li>
<li><strong>Strands Agents</strong> for agent orchestration and tool integration</li>
<li><strong>Click</strong> for CLI interface</li>
</ul>
<p>Create your AWS credentials and configure the tool:</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# Environment variables
AWS_REGION=eu-central-1
AWS_PROFILE_NAME=your-profile
MAX_CHUNKS_TO_PROCESS=5  # Safety limit for cost control

# Optional: Define known log groups
KNOWN_LOG_GROUPS=&quot;/aws/lambda/api,/aws/ecs/backend&quot;
</pre></div>


<p class="wp-block-paragraph">Configure known log groups in <code>settings.py</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
class LogGroups(StrEnum):
    &quot;&quot;&quot;Known CloudWatch log groups for type-safe references.&quot;&quot;&quot;
    API_LAMBDA = &quot;/aws/lambda/api&quot;
    BACKEND_ECS = &quot;/aws/ecs/backend-service&quot;
    DATABASE_RDS = &quot;/aws/rds/instance/prod/postgresql&quot;
</pre></div>


<p class="wp-block-paragraph">Now you can ask specific questions about your logs:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
poetry run python src/cli.py log \
  --group &quot;/aws/lambda/api-handler&quot; \
  --question &quot;What errors occurred?&quot; \
  --start &quot;2025-12-20T10:00:00&quot; \
  --end &quot;2025-12-20T12:00:00&quot;
</pre></div>


<p class="wp-block-paragraph">With custom CloudWatch Insights query:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
poetry run python src/cli.py log \
  --group &quot;/aws/lambda/payment&quot; \
  --question &quot;Analyze payment failures&quot; \
  --start &quot;2025-12-20&quot; \
  --query &quot;fields @timestamp, @message, userId | filter @message like /ERROR/&quot;
</pre></div>


<p class="wp-block-paragraph">Using natural language time ranges:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
poetry run python src/cli.py log \
  --group &quot;/aws/ecs/backend&quot; \
  --question &quot;What performance issues occurred?&quot; \
  --start &quot;last 2 hours&quot;
</pre></div>


<p class="wp-block-paragraph">The project also allows us to launch an interactive session for exploratory analysis:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
poetry run python src/cli.py agent
</pre></div>


<div class="wp-block-jetpack-markdown"><p>Example interaction:</p>
<pre><code>============================================================
CloudWatch Logs Analysis Agent
============================================================
Ask me about your CloudWatch logs!

Examples:
  - What errors occurred in /aws/lambda/api in the last hour?
  - Analyze /aws/ecs/backend-service from last 2 hours for memory issues
  - Show me exceptions in /aws/lambda/payment-api since yesterday

Type 'exit', 'quit', or 'q' to quit.
============================================================

&amp;gt; What errors happened in /aws/lambda/api in the last hour?

[Agent analyzes logs and provides intelligent summary]

&amp;gt; Were there any timeouts?

[Agent refines analysis based on context]
</code></pre>
<p>The agent mode maintains conversation context and can refine analyses based on follow-up questions.</p>
<p>CloudWatch Insights limits results to 10,000 records per query. The tool automatically subdivides time ranges when hitting this limit:</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
def query_chunk_recursively(log_group: str, start: datetime, end: datetime,
                           query: str, depth: int = 0) -&gt; list&#x5B;list&#x5B;dict]]:
    &quot;&quot;&quot;
    Queries a time chunk and subdivides it recursively if it hits the result limit.
    Returns all log entries for the given time range.
    &quot;&quot;&quot;
    status, rows = insights_query(log_group, start=start, end=end,
                                 query=query, limit=MAX_RESULTS_PER_QUERY)

    if len(rows) &gt;= MAX_RESULTS_PER_QUERY:
        # Subdivide in half
        midpoint = start + (end - start) / 2
        first_half = query_chunk_recursively(log_group, start, midpoint, query, depth + 1)
        second_half = query_chunk_recursively(log_group, midpoint, end, query, depth + 1)
        return first_half + second_half

    return rows
</pre></div>


<p class="wp-block-paragraph">This ensures you can analyze any time range without manual intervention, regardless of log volume.</p>



<p class="wp-block-paragraph">For large datasets, logs are split into chunks and processed in parallel:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
def analyze_chunk_with_worker(
    chunk: LogChunk, question: str, log_group: str, global_metadata: dict
) -&gt; ChunkAnalysisResult:
    &quot;&quot;&quot;
    Analyze a single chunk of logs using a worker agent.
    Each worker gets chunk-specific context and the user&#039;s question.
    &quot;&quot;&quot;
    worker_prompt = WORKER_AGENT_PROMPT.format(
        chunk_index=chunk.chunk_index + 1,
        total_chunks=chunk.total_chunks,
        chunk_size=chunk.chunk_size,
        time_range=chunk.get_time_range_description(),
        question=question,
    )

    worker_agent = create_agent(
        system_prompt=worker_prompt,
        model=Models.CLAUDE_45,
        temperature=0.3,
        read_timeout=WORKER_TIMEOUT_SECONDS,
    )

    chunk_context = {
        &quot;metadata&quot;: {...},
        &quot;logs&quot;: chunk.logs,
    }

    result = worker_agent(prompt=&#x5B;
        {&quot;text&quot;: f&quot;Question: {question}&quot;},
        {&quot;text&quot;: f&quot;Log context: {json.dumps(chunk_context)}&quot;},
        {&quot;text&quot;: &quot;Analyze this chunk of logs according to the guidelines in your system prompt.&quot;},
    ])

    return ChunkAnalysisResult(...)
</pre></div>


<p class="wp-block-paragraph">Workers run concurrently using ThreadPoolExecutor:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
with ThreadPoolExecutor(max_workers=MAX_PARALLEL_WORKERS) as executor:
    future_to_chunk = {
        executor.submit(analyze_chunk_with_worker, chunk, question, log_group, global_metadata): chunk
        for chunk in chunks
    }

    for future in as_completed(future_to_chunk):
        result = future.result()
        chunk_results.append(result)
</pre></div>


<div class="wp-block-jetpack-markdown"><p>After workers complete, a coordinator agent synthesizes their analyses:</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
def consolidate_with_coordinator(
    chunk_results: list&#x5B;ChunkAnalysisResult],
    question: str,
    log_group: str,
    start: datetime,
    end: datetime,
    total_records: int,
) -&gt; str:
    &quot;&quot;&quot;
    Use coordinator agent to synthesize chunk analyses into final answer.
    &quot;&quot;&quot;
    coordinator_context = {
        &quot;metadata&quot;: {
            &quot;log_group&quot;: log_group,
            &quot;time_range&quot;: f&quot;{start.isoformat()} to {end.isoformat()}&quot;,
            &quot;total_records&quot;: total_records,
            &quot;total_chunks&quot;: len(chunk_results),
        },
        &quot;chunk_analyses&quot;: &#x5B;
            {
                &quot;chunk_index&quot;: r.chunk_index + 1,
                &quot;time_range&quot;: r.chunk_time_range,
                &quot;analysis&quot;: r.analysis,
            }
            for r in successful_results
        ],
    }

    result = coordinator(prompt=&#x5B;
        {&quot;text&quot;: f&quot;Original Question: {question}&quot;},
        {&quot;text&quot;: f&quot;Chunk Analyses: {json.dumps(coordinator_context)}&quot;},
        {&quot;text&quot;: &quot;Synthesize these chunk analyses to answer the user&#039;s question.&quot;},
    ])

    return str(result)
</pre></div>


<p class="wp-block-paragraph">This pattern allows analyzing datasets far exceeding Claude&#8217;s context window while maintaining coherent insights.</p>



<p class="wp-block-paragraph">The tool supports flexible time specifications:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# modules/logs/time_parser.py
def parse_time_range(time_range: str) -&gt; tuple&#x5B;datetime, datetime]:
    &quot;&quot;&quot;
    Parse natural language time ranges:
    - &quot;last 2 hours&quot;
    - &quot;since yesterday&quot;
    - &quot;2025-12-10 to 2025-12-12&quot;
    - &quot;last 7 days&quot;
    &quot;&quot;&quot;
    # Implementation handles various patterns
    pass
</pre></div>


<p class="wp-block-paragraph">The <code>analyze_cloudwatch_logs</code> tool integrates with Strands agents:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
@tool
def analyze_cloudwatch_logs(
    log_group: Annotated&#x5B;Union&#x5B;LogGroups, str], &quot;CloudWatch log group name&quot;],
    question: Annotated&#x5B;str, &quot;Question to answer about the logs&quot;],
    time_range: Annotated&#x5B;Optional&#x5B;str], &quot;Time range examples: &#039;last 2 hours&#039;, &#039;since yesterday&#039;&quot;] = None,
    cloudwatch_sql: Annotated&#x5B;Optional&#x5B;str], &quot;CloudWatch Insights query string&quot;] = None,
) -&gt; dict:
    &quot;&quot;&quot;
    Analyze AWS CloudWatch Logs to answer questions about application behavior.
    Automatically handles large datasets through parallel chunking.
    &quot;&quot;&quot;
    # Parse time range
    start_dt, end_dt = parse_time_range(time_range or &quot;last 24 hours&quot;)

    # Call the existing analysis function
    analysis, metadata = ask_to_log(log_group, question, start_dt, end_dt,
                                   cloudwatch_sql=cloudwatch_sql or DEFAULT_CW_SQL)

    return {
        &quot;status&quot;: &quot;success&quot;,
        &quot;content&quot;: &#x5B;{&quot;text&quot;: f&quot;Analysis for log group &#039;{log_group}&#039;:\n\n{analysis}&quot;}],
        &quot;metadata&quot;: metadata,
    }
</pre></div>


<p class="wp-block-paragraph">This tool can be composed with other agent tools for more sophisticated workflows.</p>



<p class="wp-block-paragraph">Each worker agent receives context about its role in the larger analysis:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
WORKER_AGENT_PROMPT = &quot;&quot;&quot;You are a CloudWatch Logs Analysis Worker Agent.

Role: Analyze a specific chunk of logs (part {chunk_index} of {total_chunks})
Time range: {time_range}
Chunk size: {chunk_size} log records

Your task:
1. Analyze this chunk for patterns, errors, anomalies related to: {question}
2. Provide factual observations, not speculation
3. Note timestamps for important events
4. Be concise - a coordinator will synthesize all chunks

Focus on:
- Error messages and stack traces
- Unusual patterns or spikes
- Performance indicators
- User-impacting events

Output format: Concise bullet points with timestamps.
&quot;&quot;&quot;
</pre></div>


<p class="wp-block-paragraph">The coordinator synthesizes worker outputs into coherent insights:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
COORDINATOR_AGENT_PROMPT = &quot;&quot;&quot;You are a CloudWatch Logs Coordinator Agent.

Role: Synthesize analyses from {chunks_processed} worker agents
Dataset: {total_records} total log records
Time range: {time_range}

You&#039;ve received chunk-level analyses. Your task:
1. Identify patterns across all chunks
2. Synthesize a coherent narrative answering the user&#039;s question
3. Highlight critical findings
4. Provide actionable insights

Output format:
- Executive summary
- Key findings (chronological if relevant)
- Patterns or trends observed
- Recommendations (if applicable)

Be direct and actionable. Focus on what matters.
&quot;&quot;&quot;
</pre></div>


<p class="wp-block-paragraph">Processing large log volumes with AI can get expensive. The tool includes configurable safety limits:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">

</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# settings.py
MAX_CHUNKS_TO_PROCESS = int(os.getenv(&quot;MAX_CHUNKS_TO_PROCESS&quot;, &quot;5&quot;))

# In main.py
if len(chunks) &gt; MAX_CHUNKS_TO_PROCESS:
    error_msg = (
        f&quot;Dataset would generate {len(chunks)} chunks, which exceeds the maximum limit &quot;
        f&quot;of {MAX_CHUNKS_TO_PROCESS} chunks.\n\n&quot;
        f&quot;Options:\n&quot;
        f&quot;  1. Reduce time range to analyze fewer logs\n&quot;
        f&quot;  2. Increase MAX_CHUNKS_TO_PROCESS in settings\n&quot;
        f&quot;  3. Use more specific CloudWatch Insights filters&quot;
    )
    return f&quot;ERROR: {error_msg}&quot;, {...}
</pre></div>


<p class="wp-block-paragraph">With default settings (chunk size = 2,000, max chunks = 5), you can analyze up to 10,000 log records per query. Adjust these values based on your budget and requirements.</p>



<p class="wp-block-paragraph">The project uses Pydantic for all data structures:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# modules/logs/models.py
class LogChunk(BaseModel):
    &quot;&quot;&quot;Represents a chunk of logs for parallel processing.&quot;&quot;&quot;
    chunk_index: int
    total_chunks: int
    chunk_size: int
    start_timestamp: str | None
    end_timestamp: str | None
    logs: list&#x5B;dict&#x5B;str, str]]

    def get_time_range_description(self) -&gt; str:
        if self.start_timestamp and self.end_timestamp:
            return f&quot;{self.start_timestamp} to {self.end_timestamp}&quot;
        return &quot;Unknown time range&quot;


class ChunkAnalysisResult(BaseModel):
    &quot;&quot;&quot;Result from a worker agent analyzing a chunk.&quot;&quot;&quot;
    chunk_index: int
    chunk_time_range: str
    chunk_size: int
    analysis: str
    success: bool = True
    error_message: str | None = None
    processing_time_seconds: float = 0.0
</pre></div>


<p class="wp-block-paragraph">Let&#8217;s say you&#8217;re investigating a production incident:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
# Step 1: Check what happened in the last hour
poetry run python src/cli.py log \
  --group &quot;/aws/lambda/payment-api&quot; \
  --question &quot;What errors occurred?&quot; \
  --start &quot;last 1 hour&quot;

# Output:
# Analysis for log group &#039;/aws/lambda/payment-api&#039; from 2025-12-20T14:00:00 to 2025-12-20T15:00:00:
#
# Key Findings:
# - 47 payment timeout errors between 14:23 and 14:45
# - Errors clustered around Stripe API calls
# - No database connection issues observed
# - Timeout duration: consistently 30 seconds
#
# Pattern: All failures occurred during userId sessions starting with &#039;eu-&#039;
# suggesting regional routing issue.
#
# &#x5B;Metadata: 8,432 records, 5 chunks, 23.4s]
</pre></div>


<p class="wp-block-paragraph">The agent identified the pattern (regional issue) and specific time window without you writing complex queries or manually reviewing logs.</p>



<p class="wp-block-paragraph">This project is deliberately over-engineered. For simple log queries, CloudWatch Insights is sufficient. But building this taught me about:</p>



<ul class="wp-block-list">
<li>Managing AI context window limits at scale</li>



<li>Worker-coordinator patterns for parallel processing</li>



<li>Designing tools for agent consumption</li>



<li>Balancing cost vs. capability in AI systems</li>
</ul>



<p class="wp-block-paragraph">We can use this tool effectively in scenarios like:</p>



<ul class="wp-block-list">
<li>Debugging complex incidents requiring pattern recognition</li>



<li>Onboarding new team members who don&#8217;t know your query syntax</li>



<li>Exploratory analysis where you don&#8217;t know what you&#8217;re looking for</li>



<li>Generating incident reports from raw logs</li>
</ul>



<p class="wp-block-paragraph">When NOT to Use This:</p>



<ul class="wp-block-list">
<li>Real-time monitoring (use CloudWatch alarms)</li>



<li>Known queries you run repeatedly (use saved Insights queries)</li>



<li>Cost-sensitive environments (AI analysis adds expense)</li>
</ul>



<p class="wp-block-paragraph">AI agents transform log analysis from query construction to question asking. Instead of learning CloudWatch Insights syntax, you describe what you want to know. The worker-coordinator pattern demonstrates how to scale AI analysis beyond single-agent context limits.</p>



<p class="wp-block-paragraph">Is it practical for every use case? No. Is it interesting to build and explore? Absolutely.</p>



<p class="wp-block-paragraph">The complete implementation is available in my <a href="https://github.com/gonzalo123/cw.logs">GitHub</a> account.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">87732</post-id>	</item>
		<item>
		<title>Removing Clickbait from News Articles with an AI Agent, Python, Strands Agents, and AWS Bedrock</title>
		<link>https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/</link>
					<comments>https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 04 May 2026 12:15:05 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88343</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>The web is full of articles that do not want to tell you what happened too soon. The headline hints at something. The first paragraphs add suspense. The useful information is somewhere below the fold, after the cookie banner, the newsletter box, a couple of related links, and enough scrolling to make the advertising model happy.</p>
<p>That is annoying when all we want is the news.</p>
<p>That’s my PoC. A small command-line application that receives the URL of a news article, converts the page into clean Markdown, and asks an AI agent to rewrite it as clear journalism: direct headline, concise lead, short paragraphs, no clickbait.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="369" data-attachment-id="88346" data-permalink="https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/logo-9/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?fit=1672%2C941&amp;ssl=1" data-orig-size="1672,941" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?fit=656%2C369&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=656%2C369&#038;ssl=1" alt="" class="wp-image-88346" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=1024%2C576&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=1536%2C864&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?resize=1200%2C675&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?w=1672&amp;ssl=1 1672w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/logo-1.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The idea is simple:</p>
<pre><code class="language-bash">plainnews rewrite &quot;https://example.com/news/article&quot;
</code></pre>
<p>The CLI does not scrape the page directly. It gives the URL to a Strands Agent. The agent has one tool, <code>fetch_url_as_markdown</code>, and the model decides when to use it. Once the article is available as Markdown, the agent rewrites it following a focused system prompt.</p>
<h2>The architecture</h2>
<p>The flow is straightforward:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="slideInLeft"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="161" data-attachment-id="88348" data-permalink="https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/gonzalo123_plainnews__rewritetrands_agents_and_aws_bedrock/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?fit=1374%2C338&amp;ssl=1" data-orig-size="1374,338" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="gonzalo123_plainnews__Rewrite…trands_Agents_and_AWS_Bedrock" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?fit=656%2C161&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?resize=656%2C161&#038;ssl=1" alt="" class="wp-image-88348" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?resize=1024%2C252&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?resize=300%2C74&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?resize=768%2C189&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?resize=1200%2C295&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?w=1374&amp;ssl=1 1374w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/gonzalo123_plainnews__Rewrite%E2%80%A6trands_Agents_and_AWS_Bedrock.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The important part is the boundary between the agent and the tool. Fetching a web page, removing navigation, and converting HTML into Markdown is deterministic Python code. Deciding how to rewrite the story is the LLM’s job.</p>
<p>This keeps the PoC small and easy to reason about.</p>
<h2>Project structure</h2>
<p>I like to keep configuration in <code>settings.py</code>. It is a pattern I borrowed years ago from Django and I still use it in small prototypes because it keeps things simple:</p>
<pre><code class="language-text">src/
  cli.py
  settings.py
  commands/
    rewrite.py
  lib/
    agent.py
    prompts.py
    tools.py
    ui.py
  env/
    local/
      .env.example
tests/
</code></pre>
<p>The responsibilities are intentionally small:</p>
<ul>
<li><code>src/commands/rewrite.py</code> contains the Click command.</li>
<li><code>src/lib/tools.py</code> contains the Strands tool and the HTML-to-Markdown pipeline.</li>
<li><code>src/lib/agent.py</code> wires Strands Agents with AWS Bedrock.</li>
<li><code>src/lib/prompts.py</code> keeps the editor prompt and the user task prompt.</li>
<li><code>src/lib/ui.py</code> renders Markdown in the terminal with Rich.</li>
</ul>
<h2>Fetching a URL as Markdown</h2>
<p>The agent only gets one tool. It fetches the URL, removes noisy page elements, selects the main content, converts it to Markdown, and truncates the result to 100K characters:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">fetch_url_as_markdown</span><span class="tok-punctuation">(</span><span class="tok-variableName">url</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;</span></div><div class="cm-line"><span class="tok-string">    Fetch an HTTP or HTTPS URL, remove navigation, ads, scripts and layout noise,</span></div><div class="cm-line"><span class="tok-string">    extract the main article content, convert it to Markdown, and return up to</span></div><div class="cm-line"><span class="tok-string">    100K characters of clean text.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Use this tool when the user pastes a URL or asks you to analyze a web page.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">fetch_url_as_markdown_impl</span><span class="tok-punctuation">(</span><span class="tok-variableName">url</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">clean_html_to_markdown</span><span class="tok-punctuation">(</span><span class="tok-variableName">html</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-keyword">*</span><span class="tok-punctuation">,</span> <span class="tok-variableName">max_chars</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-number">100_000</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-variableName">soup</span> <span class="tok-operator">=</span> <span class="tok-variableName">BeautifulSoup</span><span class="tok-punctuation">(</span><span class="tok-variableName">html</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;html.parser&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">for</span> <span class="tok-variableName">selector</span> <span class="tok-keyword">in</span> <span class="tok-variableName">NOISY_SELECTORS</span>:</div><div class="cm-line">        <span class="tok-keyword">for</span> <span class="tok-variableName">tag</span> <span class="tok-keyword">in</span> <span class="tok-variableName">soup</span><span class="tok-operator">.</span><span class="tok-propertyName">select</span><span class="tok-punctuation">(</span><span class="tok-variableName">selector</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">            <span class="tok-variableName">tag</span><span class="tok-operator">.</span><span class="tok-propertyName">decompose</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">content</span> <span class="tok-operator">=</span> <span class="tok-variableName">soup</span><span class="tok-operator">.</span><span class="tok-propertyName">find</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;main&quot;</span><span class="tok-punctuation">)</span> <span class="tok-keyword">or</span> <span class="tok-variableName">soup</span><span class="tok-operator">.</span><span class="tok-propertyName">find</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;article&quot;</span><span class="tok-punctuation">)</span> <span class="tok-keyword">or</span> <span class="tok-variableName">soup</span><span class="tok-operator">.</span><span class="tok-propertyName">body</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">content</span> <span class="tok-keyword">is</span> <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-string">&quot;&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">markdown</span> <span class="tok-operator">=</span> <span class="tok-variableName">md</span><span class="tok-punctuation">(</span><span class="tok-variableName">str</span><span class="tok-punctuation">(</span><span class="tok-variableName">content</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-variableName">heading_style</span><span class="tok-operator">=</span><span class="tok-string">&quot;ATX&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">bullets</span><span class="tok-operator">=</span><span class="tok-string">&quot;-&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">strip</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;a&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">markdown</span> <span class="tok-operator">=</span> <span class="tok-variableName">normalize_markdown</span><span class="tok-punctuation">(</span><span class="tok-variableName">markdown</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">len</span><span class="tok-punctuation">(</span><span class="tok-variableName">markdown</span><span class="tok-punctuation">)</span> <span class="tok-operator">&gt;</span> <span class="tok-variableName">max_chars</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">markdown</span><span class="tok-punctuation">[</span>:<span class="tok-variableName">max_chars</span><span class="tok-punctuation">]</span><span class="tok-operator">.</span><span class="tok-propertyName">rstrip</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-operator">+</span> <span class="tok-string">&quot;</span><span class="tok-string2">\n</span><span class="tok-string2">\n</span><span class="tok-string">[Content truncated]&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">markdown</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>I am not trying to build a perfect browser engine here. This is a PoC. The goal is to get enough readable article content for the agent to work with. For many news pages, removing scripts, navigation, cookie boxes, newsletter blocks, related links and advertising containers is enough.</p>
<h2>The agent</h2>
<p>The agent uses Claude on AWS Bedrock through Strands Agents:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_agent</span><span class="tok-punctuation">(</span><span class="tok-keyword">*</span><span class="tok-punctuation">,</span> <span class="tok-variableName">settings</span>: <span class="tok-variableName">Settings</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">Agent</span>:</div><div class="cm-line">    <span class="tok-variableName">boto_session</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_boto_session</span><span class="tok-punctuation">(</span><span class="tok-variableName">settings</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">Agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">BedrockModel</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-variableName">boto_session</span><span class="tok-operator">=</span><span class="tok-variableName">boto_session</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">model_id</span><span class="tok-operator">=</span><span class="tok-variableName">settings</span><span class="tok-operator">.</span><span class="tok-propertyName">resolved_bedrock_model_id</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-variableName">fetch_url_as_markdown</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">SYSTEM_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The system prompt is the editorial policy. It tells the model to preserve only facts supported by the fetched article, answer in the requested output language, put the most important information first, remove suspense and filler, and write in a neutral tone.</p>
<p>The output format is Markdown:</p>
<ul>
<li>a direct H1 headline</li>
<li>a concise lead paragraph</li>
<li>short factual paragraphs</li>
<li>a final <code>What changed</code> section, translated to the requested output language, explaining
what noise was removed</li>
</ul>
<p>That last section is useful during development. It gives us a quick sanity check: did the model actually remove clickbait, or did it just paraphrase the article?</p>
<h2>The CLI</h2>
<p>The command is intentionally small:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">command</span><span class="tok-punctuation">(</span><span class="tok-variableName">name</span><span class="tok-operator">=</span><span class="tok-string">&quot;rewrite&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">argument</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;url&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">runtime_options</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">rewrite_command</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">url</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">aws_profile</span>: <span class="tok-variableName">str</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">region</span>: <span class="tok-variableName">str</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">model</span>: <span class="tok-variableName">str</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">language</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">is_supported_url</span><span class="tok-punctuation">(</span><span class="tok-variableName">url</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-keyword">raise</span> <span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-propertyName">ClickException</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;URL must start with http:// or https://&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">settings</span> <span class="tok-operator">=</span> <span class="tok-variableName">resolve_settings</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">aws_profile</span><span class="tok-operator">=</span><span class="tok-variableName">aws_profile</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">aws_region</span><span class="tok-operator">=</span><span class="tok-variableName">region</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">bedrock_model_id</span><span class="tok-operator">=</span><span class="tok-variableName">model</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">settings</span><span class="tok-operator">=</span><span class="tok-variableName">settings</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">result</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">build_rewrite_prompt</span><span class="tok-punctuation">(</span><span class="tok-variableName">url</span><span class="tok-punctuation">,</span> <span class="tok-variableName">language</span><span class="tok-operator">=</span><span class="tok-variableName">language</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">print_result</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;PlainNews&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">str</span><span class="tok-punctuation">(</span><span class="tok-variableName">result</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The CLI validates the URL, creates the agent, sends the URL in the prompt, and renders the final Markdown with Rich.</p>
<p>The tool is not called manually from the command. That is the point of this PoC: the URL is part of the task, and the agent decides to call <code>fetch_url_as_markdown</code> because the tool description says it should be used when the user pastes a URL or asks to analyze a web page.</p>
<h2>Usage</h2>
<p>Run the command:</p>
<pre><code class="language-bash">poetry run plainnews rewrite &quot;https://example.com/news/article&quot;
</code></pre>
<p>By default, PlainNews writes the rewritten article in English. You can choose a
different output language with <code>--language</code>:</p>
<pre><code class="language-bash">poetry run plainnews rewrite &quot;https://example.com/news/article&quot; --language Spanish
</code></pre>
<p>The output is rendered as Markdown in the terminal.</p>
<p>Example terminal output:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="slideInLeft"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="299" data-attachment-id="88352" data-permalink="https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/demo/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?fit=1524%2C695&amp;ssl=1" data-orig-size="1524,695" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="demo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?fit=656%2C299&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=656%2C299&#038;ssl=1" alt="" class="wp-image-88352" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=1024%2C467&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=300%2C137&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=768%2C350&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=1200%2C547&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?resize=656%2C300&amp;ssl=1 656w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?w=1524&amp;ssl=1 1524w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/05/demo.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h2>Tech stack</h2>
<ul>
<li><strong>Python</strong> with Poetry</li>
<li><strong>Strands Agents</strong> for tool-based agent orchestration</li>
<li><strong>AWS Bedrock</strong> for the LLM runtime</li>
<li><strong>BeautifulSoup</strong> for HTML cleanup</li>
<li><strong>markdownify</strong> for HTML-to-Markdown conversion</li>
<li><strong>Click</strong> for the command-line interface</li>
<li><strong>Rich</strong> for Markdown terminal rendering</li>
<li><strong>pytest</strong> for tests</li>
</ul>
<h2>A couple of notes</h2>
<p>This is not a product and it is not a universal paywall remover. It is a small agentic workflow for a very specific frustration: articles that make readers work too hard to understand the basic facts.</p>
<p>Even in this small version, the pattern is useful: deterministic Python code prepares clean context, and the AI agent performs the editorial rewrite with a tight prompt.</p>
<p>And that’s all. Full source code available on <a href="https://github.com/gonzalo123/plainnews">GitHub</a>.</p>
</div>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/05/04/removing-clickbait-from-news-articles-with-an-ai-agent-python-strands-agents-and-aws-bedrock/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88343</post-id>	</item>
		<item>
		<title>Production-Ready Logging with AWS CloudWatch for Python applications</title>
		<link>https://gonzalo123.com/2026/04/27/production-ready-logging-with-aws-cloudwatch-for-python-applications/</link>
					<comments>https://gonzalo123.com/2026/04/27/production-ready-logging-with-aws-cloudwatch-for-python-applications/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 27 Apr 2026 12:13:22 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=87746</guid>

					<description><![CDATA[from typing import Literal, Unionfrom pathlib import Path def setup_logging(env: Literal[&#8216;local&#8217;, &#8216;production&#8217;],app: str,log_path: Union[Path, str],process: str = &#8216;main&#8217;,log_level: str = &#8216;INFO&#8217;) -&#62; None:&#8220;&#8221;&#8221;Configure logging with environment-specific settings.&#8221;&#8221;&#8221;if env == &#8216;local&#8217;:logging.basicConfig(format=&#8217;%(asctime)s [%(levelname)s] %(message)s&#8217;,level=log_level,datefmt=&#8217;%d/%m/%Y %X&#8217;)else:# Console handler with human-readable formatconsole_handler = logging.StreamHandler()console_formatter = ConsoleFormatter(fmt=&#8217;%(asctime)s [%(levelname)s] %(name)s &#8211; %(message)s&#8217;,datefmt=&#8217;%Y-%m-%d %H:%M:%S&#8217;)console_handler.setFormatter(console_formatter) Console output: JSON output (CloudWatch): The CloudWatch Agent &#8230; <a href="https://gonzalo123.com/2026/04/27/production-ready-logging-with-aws-cloudwatch-for-python-applications/" class="more-link">Continue reading <span class="screen-reader-text">Production-Ready Logging with AWS CloudWatch for Python applications</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>Today we’re going to build a production-grade logging system for Python applications using. We’re going to use CloudWatch Agent with it’s <code>auto_removal</code> feature to automatically delete log files after they’ve been uploaded to CloudWatch Logs.</p>
<p>This architectural constraint requires careful design of your logging pipeline.</p>
<p>The solution uses a dual-formatter approach:</p>
<ul>
<li><strong>Console output</strong>: Human-readable format for <code>docker compose logs</code></li>
<li><strong>File output</strong>: Structured JSON for CloudWatch with hourly rotation</li>
<li><strong>CloudWatch Agent</strong>: Reads rotated files and automatically deletes them after upload</li>
</ul>
<pre><code>Flask App
    ↓
Logging System (dual formatters)
    ├─→ Console Handler → Human-readable + extras
    └─→ File Handler → JSON (hourly rotation)
            ↓
        Rotated files (app.log.YYYY-MM-DD_HH)
            ↓
        CloudWatch Agent (auto_removal: true)
            ↓
        AWS CloudWatch Logs
</code></pre>
<p>The logging system uses two custom formatters to serve different purposes:</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
from datetime import datetime
from logging.handlers import TimedRotatingFileHandler
from pythonjsonlogger.json import JsonFormatter
import logging

class CloudWatchJsonFormatter(JsonFormatter):
    &quot;&quot;&quot;JSON formatter for CloudWatch logs with custom metadata fields.&quot;&quot;&quot;

    def __init__(self, app: str, process: str, *args, **kwargs):
        self.app = app
        self.process = process
        super().__init__(*args, **kwargs)

    def add_fields(self, log_record, record, message_dict):
        &quot;&quot;&quot;Add CloudWatch-specific fields to log record.&quot;&quot;&quot;
        log_record&#x5B;&#039;@timestamp&#039;] = datetime.fromtimestamp(record.created).isoformat()
        log_record&#x5B;&#039;level&#039;] = record.levelname
        log_record&#x5B;&#039;app&#039;] = self.app
        log_record&#x5B;&#039;logger&#039;] = record.name
        log_record&#x5B;&#039;process&#039;] = self.process
        super(CloudWatchJsonFormatter, self).add_fields(log_record, record, message_dict)


class ConsoleFormatter(logging.Formatter):
    &quot;&quot;&quot;Console formatter that includes extra fields.&quot;&quot;&quot;

    RESERVED_ATTRS = {
        &#039;name&#039;, &#039;msg&#039;, &#039;args&#039;, &#039;created&#039;, &#039;filename&#039;, &#039;funcName&#039;, &#039;levelname&#039;,
        &#039;levelno&#039;, &#039;lineno&#039;, &#039;module&#039;, &#039;msecs&#039;, &#039;message&#039;, &#039;pathname&#039;, &#039;process&#039;,
        &#039;processName&#039;, &#039;relativeCreated&#039;, &#039;thread&#039;, &#039;threadName&#039;, &#039;exc_info&#039;,
        &#039;exc_text&#039;, &#039;stack_info&#039;, &#039;asctime&#039;
    }

    def format(self, record):
        base_message = super().format(record)

        # Extract extra fields
        extras = {
            key: value
            for key, value in record.__dict__.items()
            if key not in self.RESERVED_ATTRS
        }

        if extras:
            extras_str = &#039; &#039;.join(f&#039;{k}={v}&#039; for k, v in extras.items())
            return f&#039;{base_message} | {extras_str}&#039;

        return base_message
</pre></div>


<div class="wp-block-jetpack-markdown"><p>The <code>ConsoleFormatter</code> automatically appends extra fields to the log message, making debugging easier. The <code>CloudWatchJsonFormatter</code> creates structured JSON logs with CloudWatch-specific metadata.</p>
<p>The key to making <code>auto_removal</code> work is using <code>TimedRotatingFileHandler</code> with hourly rotation:</p>
</div>



<p class="wp-block-paragraph">from typing import Literal, Union<br>from pathlib import Path</p>



<p class="wp-block-paragraph">def setup_logging(<br>env: Literal[&#8216;local&#8217;, &#8216;production&#8217;],<br>app: str,<br>log_path: Union[Path, str],<br>process: str = &#8216;main&#8217;,<br>log_level: str = &#8216;INFO&#8217;<br>) -&gt; None:<br>&#8220;&#8221;&#8221;Configure logging with environment-specific settings.&#8221;&#8221;&#8221;<br>if env == &#8216;local&#8217;:<br>logging.basicConfig(<br>format=&#8217;%(asctime)s [%(levelname)s] %(message)s&#8217;,<br>level=log_level,<br>datefmt=&#8217;%d/%m/%Y %X&#8217;<br>)<br>else:<br># Console handler with human-readable format<br>console_handler = logging.StreamHandler()<br>console_formatter = ConsoleFormatter(<br>fmt=&#8217;%(asctime)s [%(levelname)s] %(name)s &#8211; %(message)s&#8217;,<br>datefmt=&#8217;%Y-%m-%d %H:%M:%S&#8217;<br>)<br>console_handler.setFormatter(console_formatter)</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
    # JSON formatter for file (CloudWatch)
    json_formatter = CloudWatchJsonFormatter(
        app=app,
        process=process,
        fmt=&#039;%(levelname)s %(name)s %(message)s&#039;
    )

    # File handler with hourly rotation
    file_handler = TimedRotatingFileHandler(
        log_path,
        when=&#039;H&#039;,        # Hourly rotation
        interval=1,      # Every 1 hour
        backupCount=2,   # Keep 2 backups
        encoding=&#039;utf-8&#039;
    )
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(json_formatter)

    logging.basicConfig(
        level=log_level,
        handlers=&#x5B;console_handler, file_handler]
    )
</pre></div>


<div class="wp-block-jetpack-markdown"><p>Why hourly rotation? CloudWatch Agent’s <code>auto_removal</code> only deletes complete files. With daily rotation, you’d have up to 24 hours of logs accumulating. Hourly rotation minimizes disk usage to just 1-2 hours of logs at any time.</p>
<p><strong>Important</strong>: Do not use minute-level rotation (<code>when='M'</code>). Fast rotation intervals cause timing issues where CloudWatch Agent cannot properly track file inodes during rotation, leading to log loss or incorrect file deletion. AWS documentation recommends hourly or longer rotation intervals for reliable <code>auto_removal</code> behavior.</p>
<p>Using the logger is straightforward. Extra fields are automatically handled:</p>
</div>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
import logging
from flask import Flask
from lib.logger import setup_logging
from settings import APP, PROCESS, LOG_PATH, ENVIRONMENT

app = Flask(__name__)
logger = logging.getLogger(__name__)

setup_logging(
    env=ENVIRONMENT,
    app=APP,
    process=PROCESS,
    log_path=LOG_PATH
)

@app.get(&quot;/&quot;)
def health():
    logger.info(&quot;GET /&quot;, extra=dict(
        user_id=123,
        response_time_ms=45
    ))
    return {&#039;status&#039;: &#039;ok&#039;}
</pre></div>


<p class="wp-block-paragraph"><strong>Console output:</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
2025-12-13 12:30:45 &#x5B;INFO] app - GET / | user_id=123 response_time_ms=45
</pre></div>


<p class="wp-block-paragraph"><strong>JSON output (CloudWatch):</strong></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;@timestamp&quot;: &quot;2025-12-13T12:30:45.123456&quot;,
  &quot;level&quot;: &quot;INFO&quot;,
  &quot;logger&quot;: &quot;app&quot;,
  &quot;app&quot;: &quot;cw_demo&quot;,
  &quot;process&quot;: &quot;cw_demo&quot;,
  &quot;message&quot;: &quot;GET /&quot;,
  &quot;user_id&quot;: 123,
  &quot;response_time_ms&quot;: 45
}
</pre></div>


<p class="wp-block-paragraph">The CloudWatch Agent uses <code>auto_removal</code> to delete rotated files automatically:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: jscript; title: ; notranslate">
{
  &quot;agent&quot;: {
    &quot;debug&quot;: false
  },
  &quot;logs&quot;: {
    &quot;logs_collected&quot;: {
      &quot;files&quot;: {
        &quot;collect_list&quot;: &#x5B;
          {
            &quot;file_path&quot;: &quot;/logs/*.log*&quot;,
            &quot;log_group_name&quot;: &quot;${LOG_GROUP_NAME}&quot;,
            &quot;log_stream_name&quot;: &quot;{hostname}&quot;,
            &quot;auto_removal&quot;: true
          }
        ]
      }
    }
  }
}
</pre></div>


<p class="wp-block-paragraph">The wildcard pattern <code>/logs/*.log*</code> matches any log file and its rotations (e.g., <code>app.log</code>, <code>cw.log</code>, <code>worker.log</code> and their rotated versions like <code>app.log.2025-12-13_12</code>). This allows different applications to use different log file names based on their <code>app_id</code>.</p>



<p class="wp-block-paragraph">The <code>LOG_GROUP_NAME</code> environment variable is injected at runtime by the entrypoint script. If not provided, it defaults to <code>/app/logs</code>.</p>



<p class="wp-block-paragraph">The application runs alongside CloudWatch Agent with a shared volume:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
services:
  api:
    build:
      context: .
    volumes:
      - logs_volume:/src/logs
    environment:
      - ENVIRONMENT=production
      - PROCESS_ID=api
    ports:
      - 5000:5000
    command: gunicorn -w 1 app:app -b 0.0.0.0:5000 --timeout 180

  cloudwatch-agent:
    build:
      context: .docker/cw
    volumes:
      - logs_volume:/logs
    environment:
      - LOG_GROUP_NAME=/mi-proyecto/app  # Optional: defaults to /app/logs if not set

volumes:
  logs_volume:
</pre></div>


<p class="wp-block-paragraph">The CloudWatch Agent container&#8217;s entrypoint script handles the <code>LOG_GROUP_NAME</code> variable with a default value:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
#!/bin/sh
set -e

# Set default value for LOG_GROUP_NAME if not provided
LOG_GROUP_NAME=${LOG_GROUP_NAME:-/app/logs}

# Replace LOG_GROUP_NAME environment variable in the config file
sed &quot;s|\${LOG_GROUP_NAME}|${LOG_GROUP_NAME}|g&quot; \
    /opt/aws/amazon-cloudwatch-agent/bin/config.template.json &gt; /opt/aws/amazon-cloudwatch-agent/bin/default_linux_config.json

echo &quot;CloudWatch Agent starting with LOG_GROUP_NAME=${LOG_GROUP_NAME}&quot;

# Start the CloudWatch Agent
exec /opt/aws/amazon-cloudwatch-agent/bin/start-amazon-cloudwatch-agent
</pre></div>


<p class="wp-block-paragraph">This ensures the agent always has a valid log group name, even if the environment variable is not explicitly set.</p>



<p class="wp-block-paragraph">When using CloudWatch Insights, remember to use the actual field names from your JSON structure:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
fields @timestamp, level, logger, message, user_id, response_time_ms
| filter level = &quot;ERROR&quot;
| sort @timestamp desc
| limit 100
</pre></div>


<p class="wp-block-paragraph">This logging architecture uses sidecar patterns to decouple application logic from logging concerns, ensuring robust, production-ready logging with minimal disk usage and automatic log management via AWS CloudWatch.</p>



<p class="wp-block-paragraph">full code in my GitHub <a href="https://github.com/gonzalo123/py2cw">account</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/04/27/production-ready-logging-with-aws-cloudwatch-for-python-applications/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">87746</post-id>	</item>
		<item>
		<title>What if you could ask questions to any GitHub repository? Building a repository-aware AI agent with Python, Strands Agents, and Bedrock</title>
		<link>https://gonzalo123.com/2026/04/06/what-if-you-could-ask-questions-to-any-github-repository-building-a-repository-aware-ai-agent-with-python-strands-agents-and-bedrock/</link>
					<comments>https://gonzalo123.com/2026/04/06/what-if-you-could-ask-questions-to-any-github-repository-building-a-repository-aware-ai-agent-with-python-strands-agents-and-bedrock/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 06 Apr 2026 14:19:41 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88206</guid>

					<description><![CDATA[Full code in my github]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>Sometimes we land on an unfamiliar GitHub repository and the first problem is not writing code. The real problem is understanding the project fast enough. Is this a REST API? Where are the entrypoints? How is the application wired? Are there obvious risks in the codebase? If the repository is big enough, answering those questions manually is slow and boring.</p>
<p>That’s just my PoC. An interactive command-line application that can inspect any public GitHub repository and answer questions about it.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/github.com/gonzalo123/github.kb/raw/main/assets/logo.png?w=656&#038;ssl=1" alt="logo"/></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>I have the feeling this workflow should exist natively on GitHub. Once repositories become large enough, being able to ask architecture, audit, or API questions feels like a natural evolution of code search and Copilot. Maybe the reason it does not exist yet is cost, scope, or product complexity. In the meantime, a CLI-first open source approach feels like a good place to start: simple, scriptable, hackable, and based on bring-your-own-model credentials so each user keeps control of their own usage and billing.</p>
<p>The idea is simple. We give a GitHub repository to a CLI application. The CLI creates a local checkout, exposes a small set of repository-aware tools to a Strands Agent, and lets the agent inspect the project with AWS Bedrock. Because the agent can list directories, search code and read files, we can ask practical questions such as:</p>
<ul>
<li>Explain how the project works</li>
<li>Audit the codebase looking for risks</li>
<li>List the API endpoints</li>
<li>Describe the execution flow of a specific module</li>
</ul>
<p>This is not a vector database project and it is not a RAG pipeline. It is a much simpler approach. We let the agent explore the repository directly, file by file, using tools.</p>
<h2>The architecture</h2>
<p>The flow is straightforward:</p>
<ol>
<li>The user calls the CLI with a GitHub repository.</li>
<li>The repository is cloned into a local cache.</li>
<li>A Strands Agent is created with a Bedrock model.</li>
<li>The agent receives a system prompt plus four tools:
<code>get_directory_tree</code>, <code>list_directory</code>, <code>search_code</code> and <code>read_file</code>.</li>
<li>The agent inspects the repository and returns the final answer in Markdown.</li>
</ol>
<p>This is enough for a surprising number of use cases. If the system prompt is focused on architecture, the answer becomes an explanation. If the prompt is focused on risk, the answer becomes a code audit. If the prompt is focused on HTTP routes, the answer becomes an API inventory.</p>
<h2>Project structure</h2>
<p>I like to keep configuration in <code>settings.py</code>. It is a pattern I borrowed years ago from Django and I still use it in small prototypes because it keeps things simple:</p>
<pre><code class="language-text">src/
└── github_kb/
    ├── cli.py
    ├── settings.py
    ├── commands/
    │   ├── ask.py
    │   ├── audit.py
    │   ├── chat.py
    │   ├── endpoints.py
    │   └── explain.py
    ├── lib/
    │   ├── agent.py
    │   ├── github.py
    │   ├── models.py
    │   ├── prompts.py
    │   ├── repository.py
    │   └── ui.py
    └── env/
        └── local/
            └── .env.example
</code></pre>
<p>The responsibilities are small and explicit:</p>
<ul>
<li><code>github_kb/commands/</code> contains the Click commands.</li>
<li><code>github_kb/lib/github.py</code> resolves the GitHub repository and manages the local checkout.</li>
<li><code>github_kb/lib/repository.py</code> contains the repository exploration logic used by the agent tools.</li>
<li><code>github_kb/lib/agent.py</code> wires Strands Agents with AWS Bedrock.</li>
<li><code>github_kb/lib/prompts.py</code> keeps the system prompt and the task-specific prompts in one place.</li>
</ul>
<h2>Why this works</h2>
<p>Large repositories are difficult because we rarely need the whole repository at once. We normally need a guided exploration strategy. A tree view helps us identify the shape of the project. Search helps us jump to the interesting files. Reading files gives us the final confirmation.</p>
<p>That sequence maps very well to tool-based agents.</p>
<p>Instead of trying to send the whole repository in one prompt, the model can progressively inspect only the relevant parts. It is cheaper, easier to reason about, and much closer to how we inspect an unknown codebase ourselves.</p>
<h2>Install</h2>
<p>The intended installation flow is:</p>
<pre><code class="language-bash">pipx install github-kb
</code></pre>
<h2>Quick start</h2>
<p>The happy path should look like this:</p>
<pre><code class="language-bash">aws sso login --profile sandbox
AWS_PROFILE=sandbox AWS_REGION=us-west-2 github-kb doctor
AWS_PROFILE=sandbox AWS_REGION=us-west-2 github-kb chat gonzalo123/autofix
</code></pre>
<p>The CLI is designed to work out of the box with the standard AWS credential chain. That means it can use:</p>
<ul>
<li><code>AWS_PROFILE</code></li>
<li><code>AWS_REGION</code></li>
<li><code>aws sso login</code></li>
<li>regular access keys if they are already configured in the environment</li>
</ul>
<p>By default, <code>github-kb</code> uses <code>global.anthropic.claude-sonnet-4-6</code> unless <code>BEDROCK_MODEL_ID</code> or <code>--model</code> says otherwise.</p>
<p>You can also override the runtime explicitly with CLI flags such as <code>--aws-profile</code>, <code>--region</code>, and <code>--model</code>.</p>
<h2>Usage</h2>
<p>Now we can ask questions:</p>
<pre><code class="language-bash">github-kb ask gonzalo123/autofix &quot;How does the automated fix flow work?&quot;
github-kb chat gonzalo123/autofix
github-kb explain gonzalo123/autofix --topic architecture
github-kb audit gonzalo123/autofix --focus github
github-kb endpoints gonzalo123/autofix
github-kb doctor
</code></pre>
<p>If we want to keep the same conversation alive across multiple questions in one terminal session:</p>
<pre><code class="language-bash">github-kb chat gonzalo123/autofix
</code></pre>
<p>It also accepts full GitHub URLs:</p>
<pre><code class="language-bash">github-kb ask https://github.com/gonzalo123/autofix &quot;Where is the application bootstrapped?&quot;
</code></pre>
<p>If we want to refresh the local cache:</p>
<pre><code class="language-bash">github-kb audit gonzalo123/autofix --refresh
</code></pre>
<p>We can also pass the AWS runtime explicitly:</p>
<pre><code class="language-bash">github-kb chat gonzalo123/autofix --aws-profile sandbox --region eu-central-1
github-kb ask gonzalo123/autofix &quot;Explain the architecture&quot; --model global.anthropic.claude-sonnet-4-6
</code></pre>
<h2>Demo screenshots</h2>
<p>Here are a few real screenshots generated against one of my own repositories, <a href="https://github.com/gonzalo123/autofix"><code>gonzalo123/autofix</code></a>.</p>
<p>The screenshots below are embedded as PNG files:</p>
<h3><code>explain</code></h3>
</div>



<figure class="wp-block-image"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/github.com/gonzalo123/github.kb/raw/main/assets/demo-explain.png?w=656&#038;ssl=1" alt="Explain demo"/></figure>



<div class="wp-block-jetpack-markdown"><h3><code>endpoints</code></h3>
</div>



<figure class="wp-block-image"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/github.com/gonzalo123/github.kb/raw/main/assets/demo-endpoints.png?w=656&#038;ssl=1" alt="Endpoints demo"/></figure>



<div class="wp-block-jetpack-markdown"><h3><code>audit</code></h3>
</div>



<figure class="wp-block-image"><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/github.com/gonzalo123/github.kb/raw/main/assets/demo-audit.png?w=656&#038;ssl=1" alt="Audit demo"/></figure>



<div class="wp-block-jetpack-markdown"><h2>A couple of notes</h2>
<p>This is still a PoC. The goal is not to build a perfect repository analysis platform. The goal is to validate a simple idea: an agent with a tiny set of well-chosen tools can already be useful for code understanding.</p>
<p>There are several obvious next steps:</p>
<ul>
<li>add more repository-aware tools</li>
<li>persist analysis sessions</li>
<li>summarize previous findings before starting a new question</li>
<li>support GitHub authentication for private repositories</li>
<li>add specialized prompts for security reviews or framework-specific inspections</li>
</ul>
<p>Even in its current state, it is already a nice example of how tool-based agents can help with a very real developer problem.</p>
</div>



<p class="wp-block-paragraph">Full code in my <a href="https://github.com/gonzalo123/github.kb">github</a></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/04/06/what-if-you-could-ask-questions-to-any-github-repository-building-a-repository-aware-ai-agent-with-python-strands-agents-and-bedrock/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88206</post-id>	</item>
		<item>
		<title>Exposing a REST API Through MCP: Turning Any API into an AI Tool</title>
		<link>https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/</link>
					<comments>https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 23 Mar 2026 13:34:54 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[mcp]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[rest]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88035</guid>

					<description><![CDATA[That&#8217;s exactly what this project does. We take a standard Flask REST API and wrap it with an MCP server using FastMCP. Any MCP-compatible client, Claude Code, Cursor, Windsurf, can discover and call the API endpoints as tools, without knowing there&#8217;s a REST layer underneath.]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>Most organizations already have REST APIs. They power internal dashboards, connect microservices, expose data to mobile apps. They work. But now you want an AI agent to use those same services, and agents don’t speak REST. They speak MCP. Do you rewrite everything? No. You build a thin adapter layer that translates between the two protocols, and your existing API stays untouched.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="438" data-attachment-id="88043" data-permalink="https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/logo-5/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?fit=1536%2C1024&amp;ssl=1" data-orig-size="1536,1024" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?fit=656%2C438&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?resize=656%2C438&#038;ssl=1" alt="" class="wp-image-88043" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?resize=1024%2C683&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?resize=1200%2C800&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?w=1536&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<p class="wp-block-paragraph">That&#8217;s exactly what this project does. We take a standard Flask REST API and wrap it with an MCP server using FastMCP. Any MCP-compatible client, Claude Code, Cursor, Windsurf, can discover and call the API endpoints as tools, without knowing there&#8217;s a REST layer underneath.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="120" data-attachment-id="88041" data-permalink="https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/gonzalo123_rest2mcp__exposing_a_rest_api_through_mcp__turning_any_api_into_an_ai_tool/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?fit=1035%2C190&amp;ssl=1" data-orig-size="1035,190" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?fit=656%2C120&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?resize=656%2C120&#038;ssl=1" alt="" class="wp-image-88041" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?resize=1024%2C188&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?resize=300%2C55&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?resize=768%2C141&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool.png?w=1035&amp;ssl=1 1035w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h2>The REST API</h2>
<p>The API is a standard Flask application with CRUD endpoints for managing notes. Nothing special here, just the kind of REST service you’d find in any organization:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">notes_bp</span> <span class="tok-operator">=</span> <span class="tok-variableName">Blueprint</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;notes&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">__name__</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">notes_bp</span><span class="tok-operator">.</span><span class="tok-variableName">route</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;/api/notes&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">methods</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;GET&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">list_notes</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-variableName">get_all_notes</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">notes_bp</span><span class="tok-operator">.</span><span class="tok-variableName">route</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;/api/notes/&lt;int:note_id&gt;&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">methods</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;GET&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">read_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">note</span> <span class="tok-operator">=</span> <span class="tok-variableName">get_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">note</span> <span class="tok-keyword">is</span> <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;error&quot;</span>: <span class="tok-string">&quot;Note not found&quot;</span><span class="tok-punctuation">}</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">404</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-variableName">note</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">notes_bp</span><span class="tok-operator">.</span><span class="tok-variableName">route</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;/api/notes&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">methods</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;POST&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">add_note</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">data</span> <span class="tok-operator">=</span> <span class="tok-variableName">request</span><span class="tok-operator">.</span><span class="tok-propertyName">get_json</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">data</span> <span class="tok-keyword">or</span> <span class="tok-string">&quot;title&quot;</span> <span class="tok-keyword">not</span> <span class="tok-keyword">in</span> <span class="tok-variableName">data</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;error&quot;</span>: <span class="tok-string">&quot;title is required&quot;</span><span class="tok-punctuation">}</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">400</span></div><div class="cm-line">    <span class="tok-variableName">note</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">title</span><span class="tok-operator">=</span><span class="tok-variableName">data</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;title&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span> <span class="tok-variableName">body</span><span class="tok-operator">=</span><span class="tok-variableName">data</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;body&quot;</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;&quot;</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-variableName">note</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">201</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">notes_bp</span><span class="tok-operator">.</span><span class="tok-variableName">route</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;/api/notes/&lt;int:note_id&gt;&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">methods</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;PUT&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">edit_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">data</span> <span class="tok-operator">=</span> <span class="tok-variableName">request</span><span class="tok-operator">.</span><span class="tok-propertyName">get_json</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">note</span> <span class="tok-operator">=</span> <span class="tok-variableName">update_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">,</span> <span class="tok-variableName">title</span><span class="tok-operator">=</span><span class="tok-variableName">data</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;title&quot;</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-variableName">body</span><span class="tok-operator">=</span><span class="tok-variableName">data</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;body&quot;</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">note</span> <span class="tok-keyword">is</span> <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;error&quot;</span>: <span class="tok-string">&quot;Note not found&quot;</span><span class="tok-punctuation">}</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">404</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-variableName">note</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">notes_bp</span><span class="tok-operator">.</span><span class="tok-variableName">route</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;/api/notes/&lt;int:note_id&gt;&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">methods</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;DELETE&quot;</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">remove_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">delete_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;status&quot;</span>: <span class="tok-string">&quot;deleted&quot;</span><span class="tok-punctuation">}</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">jsonify</span><span class="tok-punctuation">(</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;error&quot;</span>: <span class="tok-string">&quot;Note not found&quot;</span><span class="tok-punctuation">}</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-number">404</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>Five endpoints: list, get, create, update, delete. The data store is an in-memory dictionary for simplicity, but in a real scenario this would be your existing database, your internal service, your legacy system. The point is that the REST API already exists and works. We don’t want to change it.</p>
<h2>The MCP server</h2>
<p>This is the core of the project. The MCP server uses <a href="https://gofastmcp.com/">FastMCP</a> to expose each REST endpoint as an MCP tool. It uses <code>requests</code> to call the Flask API over HTTP:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">import</span> <span class="tok-variableName">requests</span></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">server</span><span class="tok-operator">.</span><span class="tok-variableName">fastmcp</span> <span class="tok-keyword">import</span> <span class="tok-variableName">FastMCP</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">settings</span> <span class="tok-keyword">import</span> <span class="tok-variableName">API_BASE_URL</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">mcp</span> <span class="tok-operator">=</span> <span class="tok-variableName">FastMCP</span><span class="tok-punctuation">(</span><span class="tok-variableName">name</span><span class="tok-operator">=</span><span class="tok-string">&quot;notes-api&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">BASE</span> <span class="tok-operator">=</span> <span class="tok-variableName">API_BASE_URL</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">tool</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">list_notes</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;List all notes stored in the system.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Returns a JSON array of note objects, each containing:</span></div><div class="cm-line"><span class="tok-string">    id, title, body, and created_at fields.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">requests</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">BASE</span><span class="tok-punctuation">}</span><span class="tok-string2">/api/notes&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">response</span><span class="tok-operator">.</span><span class="tok-propertyName">text</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">tool</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">get_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Get a single note by its ID.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Returns the note object with id, title, body, and created_at fields.</span></div><div class="cm-line"><span class="tok-string">    Returns an error if the note is not found.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">requests</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">BASE</span><span class="tok-punctuation">}</span><span class="tok-string2">/api/notes/</span><span class="tok-punctuation">{</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">response</span><span class="tok-operator">.</span><span class="tok-propertyName">text</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">tool</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">title</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">body</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&quot;</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Create a new note.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Args:</span></div><div class="cm-line"><span class="tok-string">        title: The title of the note (required).</span></div><div class="cm-line"><span class="tok-string">        body: The body content of the note (optional, defaults to empty string).</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Returns the created note object with its assigned id.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">requests</span><span class="tok-operator">.</span><span class="tok-propertyName">post</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">BASE</span><span class="tok-punctuation">}</span><span class="tok-string2">/api/notes&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">json</span><span class="tok-operator">=</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;title&quot;</span>: <span class="tok-variableName">title</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;body&quot;</span>: <span class="tok-variableName">body</span><span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">response</span><span class="tok-operator">.</span><span class="tok-propertyName">text</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">tool</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">update_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">,</span> <span class="tok-variableName">title</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">body</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&quot;</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Update an existing note by its ID.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Args:</span></div><div class="cm-line"><span class="tok-string">        note_id: The ID of the note to update.</span></div><div class="cm-line"><span class="tok-string">        title: New title for the note (optional, send empty string to keep current).</span></div><div class="cm-line"><span class="tok-string">        body: New body for the note (optional, send empty string to keep current).</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Returns the updated note object, or an error if not found.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">payload</span> <span class="tok-operator">=</span> <span class="tok-punctuation">{</span><span class="tok-punctuation">}</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">title</span>:</div><div class="cm-line">        <span class="tok-variableName">payload</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;title&quot;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-variableName">title</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">body</span>:</div><div class="cm-line">        <span class="tok-variableName">payload</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;body&quot;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-variableName">body</span></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">requests</span><span class="tok-operator">.</span><span class="tok-propertyName">put</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">BASE</span><span class="tok-punctuation">}</span><span class="tok-string2">/api/notes/</span><span class="tok-punctuation">{</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">json</span><span class="tok-operator">=</span><span class="tok-variableName">payload</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">response</span><span class="tok-operator">.</span><span class="tok-propertyName">text</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-variableName">tool</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">delete_note</span><span class="tok-punctuation">(</span><span class="tok-variableName">note_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Delete a note by its ID.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Args:</span></div><div class="cm-line"><span class="tok-string">        note_id: The ID of the note to delete.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">    Returns a status confirmation or an error if the note is not found.</span></div><div class="cm-line"><span class="tok-string">    &quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">requests</span><span class="tok-operator">.</span><span class="tok-propertyName">delete</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">BASE</span><span class="tok-punctuation">}</span><span class="tok-string2">/api/notes/</span><span class="tok-punctuation">{</span><span class="tok-variableName">note_id</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">response</span><span class="tok-operator">.</span><span class="tok-propertyName">text</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> <span class="tok-variableName">__name__</span> <span class="tok-operator">==</span> <span class="tok-string">&quot;__main__&quot;</span>:</div><div class="cm-line">    <span class="tok-variableName">mcp</span><span class="tok-operator">.</span><span class="tok-propertyName">run</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>Each <code>@mcp.tool()</code> maps to one REST endpoint. The decorator extracts parameter types from the function signature to build the JSON schema that MCP clients use to understand what parameters to send. The docstring becomes the tool description that the AI agent reads to decide when and how to call each tool. When you run <code>python src/server/main.py</code>, it starts listening on stdio for MCP requests.</p>
<p>The pattern is straightforward: receive the MCP call, translate it into an HTTP request, forward it to the REST API, and return the response. The MCP server knows nothing about the business logic. The REST API knows nothing about MCP. Each side does its job.</p>
<h2>The adapter pattern</h2>
<p>This is the <strong>Adapter Pattern</strong> applied at the protocol level. The MCP server adapts the REST interface into the MCP protocol. The REST API doesn’t need to change. The MCP client doesn’t need to know it’s talking to a REST service. The adapter handles the translation:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="314" data-attachment-id="88046" data-permalink="https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/gonzalo123_rest2mcp__exposing_a_rest_api_through_mcp__turning_any_api_into_an_ai_tool-2/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?fit=760%2C364&amp;ssl=1" data-orig-size="760,364" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?fit=656%2C314&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?resize=656%2C314&#038;ssl=1" alt="" class="wp-image-88046" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?w=760&amp;ssl=1 760w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/gonzalo123_rest2mcp__Exposing_a_REST_API_Through_MCP__Turning_Any_API_into_an_AI_Tool-1.png?resize=300%2C144&amp;ssl=1 300w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The MCP client calls <code>create_note(&quot;Meeting notes&quot;, &quot;Discussed Q3 roadmap&quot;)</code>. The MCP server translates this into a <code>POST /api/notes</code> with a JSON body. The Flask API processes it, creates the note, and returns the result. The MCP server passes the response back to the client. The agent sees a tool that creates notes. It doesn’t know or care that there’s an HTTP call in between.</p>
<h2>Configuration</h2>
<p>To use the MCP server from Claude Code, create a <code>.mcp.json</code> file in your project root:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-json"><div class="cm-line"><span class="tok-punctuation">{</span></div><div class="cm-line">  <span class="tok-propertyName">&quot;mcpServers&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-propertyName">&quot;notes-api&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">{</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;command&quot;</span><span class="tok-punctuation">:</span> <span class="tok-string">&quot;/path/to/venv/bin/python&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">      <span class="tok-propertyName">&quot;args&quot;</span><span class="tok-punctuation">:</span> <span class="tok-punctuation">[</span><span class="tok-string">&quot;/path/to/src/server/main.py&quot;</span><span class="tok-punctuation">]</span></div><div class="cm-line">    <span class="tok-punctuation">}</span></div><div class="cm-line">  <span class="tok-punctuation">}</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>Claude Code reads this file, launches the MCP server as a subprocess, performs the MCP handshake, and discovers the five tools automatically. The same server works with Cursor, Windsurf, VS Code with Copilot, or any other MCP-compatible client, just point it to the same Python script.</p>
<h2>Running it</h2>
<p>First, install dependencies:</p>
<pre><code class="language-bash">poetry install
</code></pre>
<p>Start the Flask API in one terminal:</p>
<pre><code class="language-bash">make api
</code></pre>
<p>You can verify it works with curl:</p>
<pre><code class="language-bash">curl -X POST http://127.0.0.1:5000/api/notes \
  -H &quot;Content-Type: application/json&quot; \
  -d '{&quot;title&quot;: &quot;First note&quot;, &quot;body&quot;: &quot;Hello from REST&quot;}'

curl http://127.0.0.1:5000/api/notes
</code></pre>
<p>With the API running and <code>.mcp.json</code> in place, open Claude Code in the project directory. It discovers the <code>notes-api</code> MCP server and makes all five tools available. You can ask things like “Create a note about the deployment we did today” or “List all my notes” and the agent calls the MCP tools, which call your REST API, automatically.</p>
<h2>Taking it further</h2>
<p>This POC uses a simple notes API, but the same pattern works with any existing REST service. Your internal APIs, third-party integrations, legacy systems, anything with HTTP endpoints can be wrapped with a thin MCP layer. The REST API stays unchanged, the MCP server handles the translation, and suddenly your existing services become tools that any AI agent can use.</p>
<p>You could also add authentication headers in the MCP server (forwarding API keys or tokens to the REST API), error handling with retry logic, or caching for read-heavy endpoints. The adapter layer is the right place for these cross-cutting concerns.</p>
<p>And that’s all. With a thin MCP adapter on top of any REST API, your existing services become tools that any AI agent can discover and use. The REST API stays unchanged, the MCP server handles the protocol translation, and the standard connects them. Build the adapter once, use it from any MCP client.</p>
<p>Full code in my <a href="https://github.com/gonzalo123/rest2mcp">github</a> account.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/03/23/exposing-a-rest-api-through-mcp-turning-any-api-into-an-ai-tool/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88035</post-id>	</item>
		<item>
		<title>AI Eurobeat Producer: Generating Music in Real-Time with AI Agents, Python, and MIDI</title>
		<link>https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/</link>
					<comments>https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 13:13:39 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[MIDI]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88078</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>What if you could describe the music you want to hear and have an AI produce it in real-time, sending MIDI notes directly to your DAW? That’s exactly what I built: a Python application that uses AI agents to generate Eurobeat and 90s techno patterns, outputting them as live MIDI to Akai’s MPC Beats.</p>
<p>I’m not a musician. I enjoy playing guitar from time to time, but I have zero experience with music production software. However, I’m gifted myself a Akai MPK mini Plus MIDI controller, which has 8 knobs and 8 pads, and I experimented with using it to control a music generation agent. No idea what I’m doing, but it’s fun.</p>
<p>As Akai MIDI controller can be connected to a laptop, and there I’ve got Python, this saturday morning I decided to build a simple prototype that connects an AI agent to MIDI output. The idea is simple. You write a prompt like “Energetic eurobeat in Am, Daft Punk style”, and an AI agent powered by Claude on AWS Bedrock generates patterns for 8 tracks: two drum kits, bass, rhodes, pluck, pad, and a lead melody. The patterns are sent as MIDI messages to MPC Beats, where each track is routed to a different virtual instrument. You can then modify the music live by writing new instructions, and use the physical knobs and pads on an Akai MPK Mini Plus to mute/unmute tracks, regenerate patterns, or reset the session.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="438" data-attachment-id="88080" data-permalink="https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/logo-6/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?fit=1536%2C1024&amp;ssl=1" data-orig-size="1536,1024" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?fit=656%2C438&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?resize=656%2C438&#038;ssl=1" alt="" class="wp-image-88080" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?resize=1024%2C683&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?w=1536&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>I’m using the MPC Beats because it’s free and has a simple MIDI setup, but in theory this could work with any DAW that accepts MIDI input. The whole system is built in Python using Strands Agents for the AI orchestration, mido + python-rtmidi for MIDI I/O, and Rich for the terminal UI.</p>
<h2>The Architecture</h2>
<p>The flow is straightforward:</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="147" data-attachment-id="88082" data-permalink="https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/eurobeat_readme_md_at_main_%c2%b7_gonzalo123_eurobeat/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?fit=1377%2C309&amp;ssl=1" data-orig-size="1377,309" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="eurobeat_README_md_at_main_·_gonzalo123_eurobeat" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?fit=656%2C147&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?resize=656%2C147&#038;ssl=1" alt="" class="wp-image-88082" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?resize=1024%2C230&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?resize=300%2C67&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?resize=768%2C172&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?w=1377&amp;ssl=1 1377w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><h2>Project Structure</h2>
<pre><code>src/
  settings.py           # Configuration: BPM, tracks, MIDI devices
  cli.py                # Click CLI entry point
  commands/play.py      # Main play command
  agent/
    prompts.py          # System prompts for the AI producer
    tools.py            # PatternStore + @tool functions
    factory.py          # Agent creation
  midi/
    device.py           # MIDI device detection
    melody_player.py    # Threaded melody loop player
    drum_player.py      # Threaded drum loop player
  session/
    state.py            # State machine (IDLE/GENERATING/PLAYING)
    session.py          # Session orchestrator
  ui/
    menu.py             # Interactive terminal menu
</code></pre>
<h2>Configuration</h2>
<p>Everything starts with <code>settings.py</code>. The MIDI devices and AWS region are loaded from environment variables, while the musical parameters are defined as constants:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-cython"><div class="cm-line"><span class="tok-variableName">BPM</span> <span class="tok-operator">=</span> <span class="tok-number">122</span></div><div class="cm-line"><span class="tok-variableName">BAR_DURATION</span> <span class="tok-operator">=</span> <span class="tok-variableName">round</span>((<span class="tok-number">60</span> <span class="tok-operator">/</span> <span class="tok-variableName">BPM</span>) <span class="tok-operator">*</span> <span class="tok-number">4</span>, <span class="tok-number">3</span>)</div><div class="cm-line"><span class="tok-variableName">LOOP_BARS</span> <span class="tok-operator">=</span> <span class="tok-number">4</span></div><div class="cm-line"><span class="tok-variableName">LOOP_DURATION</span> <span class="tok-operator">=</span> <span class="tok-variableName">round</span>(<span class="tok-variableName">BAR_DURATION</span> <span class="tok-operator">*</span> <span class="tok-variableName">LOOP_BARS</span>, <span class="tok-number">3</span>)</div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">TRACKS</span> <span class="tok-operator">=</span> {</div><div class="cm-line">    <span class="tok-number">1</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Drums&quot;</span>,         <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">0</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;drums&quot;</span>},</div><div class="cm-line">    <span class="tok-number">2</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Drums Detroit&quot;</span>, <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">1</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;drums&quot;</span>},</div><div class="cm-line">    <span class="tok-number">3</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Rhodes&quot;</span>,        <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">2</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">    <span class="tok-number">4</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Pluck&quot;</span>,         <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">3</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">    <span class="tok-number">5</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Bass&quot;</span>,          <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">4</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">    <span class="tok-number">6</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Org Bass&quot;</span>,      <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">5</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">    <span class="tok-number">7</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Pad&quot;</span>,           <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">6</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">    <span class="tok-number">8</span>: {<span class="tok-string">&quot;name&quot;</span>: <span class="tok-string">&quot;Lead&quot;</span>,          <span class="tok-string">&quot;channel&quot;</span>: <span class="tok-number">7</span>, <span class="tok-string">&quot;type&quot;</span>: <span class="tok-string">&quot;melody&quot;</span>},</div><div class="cm-line">}</div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>Each track maps to a MIDI channel. Tracks 1-2 are drum kits (offset-based timing), tracks 3-8 are melodic instruments (duration-based timing). The MPC Beats “House Template” provides the virtual instruments: a Classic drum kit, a Detroit percussion kit, Electric Rhodes, Tube Pluck, Bassline, Organ Bass, Tube Pad, and an Instant Go lead synth.</p>
<h2>The Bridge Between AI and MIDI: PatternStore and Tools</h2>
<p>The core of the system is the <code>PatternStore</code>, a simple shared store where the AI writes patterns and the MIDI players read them:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">PatternStore</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">__init__</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_patterns</span>: <span class="tok-variableName">dict</span><span class="tok-punctuation">[</span><span class="tok-variableName">int</span><span class="tok-punctuation">,</span> <span class="tok-variableName">list</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-punctuation">{</span><span class="tok-punctuation">}</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">set</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">track_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">,</span> <span class="tok-variableName">pattern</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_patterns</span><span class="tok-punctuation">[</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-variableName">pattern</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">get</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">track_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">list</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_patterns</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">clear</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_patterns</span><span class="tok-operator">.</span><span class="tok-propertyName">clear</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The Strands <code>@tool</code> functions are created via a factory that closes over the store:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_tools</span><span class="tok-punctuation">(</span><span class="tok-variableName">store</span>: <span class="tok-variableName">PatternStore</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">list</span>:</div><div class="cm-line">    <span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">set_melody_pattern</span><span class="tok-punctuation">(</span><span class="tok-variableName">track_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">,</span> <span class="tok-variableName">pattern</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">        <span class="tok-string">&quot;&quot;&quot;Define a melodic line for a specific track.&quot;&quot;&quot;</span></div><div class="cm-line">        <span class="tok-variableName">data</span> <span class="tok-operator">=</span> <span class="tok-variableName">json</span><span class="tok-operator">.</span><span class="tok-propertyName">loads</span><span class="tok-punctuation">(</span><span class="tok-variableName">pattern</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">store</span><span class="tok-operator">.</span><span class="tok-propertyName">set</span><span class="tok-punctuation">(</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">,</span> <span class="tok-variableName">data</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">total</span> <span class="tok-operator">=</span> <span class="tok-variableName">sum</span><span class="tok-punctuation">(</span><span class="tok-variableName">n</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;duration&quot;</span><span class="tok-punctuation">]</span> <span class="tok-keyword">for</span> <span class="tok-variableName">n</span> <span class="tok-keyword">in</span> <span class="tok-variableName">data</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">name</span> <span class="tok-operator">=</span> <span class="tok-variableName">TRACKS</span><span class="tok-punctuation">[</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">]</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;name&quot;</span><span class="tok-punctuation">]</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-string2">f&quot;OK - </span><span class="tok-punctuation">{</span><span class="tok-variableName">name</span><span class="tok-punctuation">}</span><span class="tok-string2">: </span><span class="tok-punctuation">{</span><span class="tok-variableName">len</span><span class="tok-punctuation">(</span><span class="tok-variableName">data</span><span class="tok-punctuation">)</span><span class="tok-punctuation">}</span><span class="tok-string2"> notes, total duration </span><span class="tok-punctuation">{</span><span class="tok-variableName">total</span>:<span class="tok-keyword">.3f</span><span class="tok-punctuation">}</span><span class="tok-string2">s&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">set_drum_pattern</span><span class="tok-punctuation">(</span><span class="tok-variableName">track_id</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">,</span> <span class="tok-variableName">pattern</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">str</span>:</div><div class="cm-line">        <span class="tok-string">&quot;&quot;&quot;Define a drum pattern for a specific drum track.&quot;&quot;&quot;</span></div><div class="cm-line">        <span class="tok-variableName">data</span> <span class="tok-operator">=</span> <span class="tok-variableName">json</span><span class="tok-operator">.</span><span class="tok-propertyName">loads</span><span class="tok-punctuation">(</span><span class="tok-variableName">pattern</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">store</span><span class="tok-operator">.</span><span class="tok-propertyName">set</span><span class="tok-punctuation">(</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">,</span> <span class="tok-variableName">data</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">name</span> <span class="tok-operator">=</span> <span class="tok-variableName">TRACKS</span><span class="tok-punctuation">[</span><span class="tok-variableName">track_id</span><span class="tok-punctuation">]</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;name&quot;</span><span class="tok-punctuation">]</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-string2">f&quot;OK - </span><span class="tok-punctuation">{</span><span class="tok-variableName">name</span><span class="tok-punctuation">}</span><span class="tok-string2">: </span><span class="tok-punctuation">{</span><span class="tok-variableName">len</span><span class="tok-punctuation">(</span><span class="tok-variableName">data</span><span class="tok-punctuation">)</span><span class="tok-punctuation">}</span><span class="tok-string2"> hits&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-punctuation">[</span><span class="tok-variableName">set_drum_pattern</span><span class="tok-punctuation">,</span> <span class="tok-variableName">set_melody_pattern</span><span class="tok-punctuation">]</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>A melody pattern is a JSON array of <code>{note, duration, velocity}</code> objects where the sum of durations must equal <code>LOOP_DURATION</code> (4 bars). A drum pattern uses <code>{note, velocity, offset}</code> where offset is the time in seconds from the loop start. The note value <code>-1</code> represents silence, which is crucial for creating space in the arrangement.</p>
<h2>The Agent</h2>
<p>The agent is a Strands Agent using Claude Sonnet on AWS Bedrock. The system prompt is heavily detailed with music production instructions: frequency ranges for each track, velocity guidelines, and structural rules. The key instruction is “less is more” – not all tracks should play notes all the time:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">store</span>: <span class="tok-variableName">PatternStore</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">Agent</span>:</div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">Agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">BedrockModel</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-variableName">model_id</span><span class="tok-operator">=</span><span class="tok-variableName">Models</span><span class="tok-operator">.</span><span class="tok-propertyName">CLAUDE_SONNET</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">region_name</span><span class="tok-operator">=</span><span class="tok-variableName">AWS_REGION</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-variableName">create_tools</span><span class="tok-punctuation">(</span><span class="tok-variableName">store</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">SYSTEM_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">callback_handler</span><span class="tok-operator">=</span><span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>There are two agents: one for initial generation (calls all 8 tools) and one for live modifications (only modifies the tracks that need to change). A third, lighter agent using Haiku generates the menu suggestions to keep latency and cost low.</p>
<h2>MIDI Players</h2>
<p>Two player classes handle the actual MIDI output. The <code>MelodyLoopPlayer</code> iterates through note events with durations:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_loop</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">melody</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">while</span> <span class="tok-keyword">not</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">stop_event</span><span class="tok-operator">.</span><span class="tok-propertyName">is_set</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">current</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">store</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">track_id</span><span class="tok-punctuation">)</span> <span class="tok-keyword">or</span> <span class="tok-variableName">melody</span></div><div class="cm-line">        <span class="tok-keyword">for</span> <span class="tok-variableName">ev</span> <span class="tok-keyword">in</span> <span class="tok-variableName">current</span>:</div><div class="cm-line">            <span class="tok-keyword">if</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">stop_event</span><span class="tok-operator">.</span><span class="tok-propertyName">is_set</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">                <span class="tok-keyword">break</span></div><div class="cm-line">            <span class="tok-variableName">note</span> <span class="tok-operator">=</span> <span class="tok-variableName">ev</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;note&quot;</span><span class="tok-punctuation">]</span></div><div class="cm-line">            <span class="tok-variableName">vel</span> <span class="tok-operator">=</span> <span class="tok-variableName">ev</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;velocity&quot;</span><span class="tok-punctuation">,</span> <span class="tok-number">80</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-keyword">if</span> <span class="tok-variableName">note</span> <span class="tok-operator">&gt;=</span> <span class="tok-number">0</span>:</div><div class="cm-line">                <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_send</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;note_on&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">note</span><span class="tok-operator">=</span><span class="tok-variableName">note</span><span class="tok-punctuation">,</span> <span class="tok-variableName">velocity</span><span class="tok-operator">=</span><span class="tok-variableName">vel</span><span class="tok-punctuation">,</span> <span class="tok-variableName">channel</span><span class="tok-operator">=</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">channel</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-variableName">deadline</span> <span class="tok-operator">=</span> <span class="tok-variableName">time</span><span class="tok-operator">.</span><span class="tok-propertyName">time</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-operator">+</span> <span class="tok-variableName">ev</span><span class="tok-punctuation">[</span><span class="tok-string">&quot;duration&quot;</span><span class="tok-punctuation">]</span></div><div class="cm-line">            <span class="tok-keyword">while</span> <span class="tok-keyword">not</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">stop_event</span><span class="tok-operator">.</span><span class="tok-propertyName">is_set</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-keyword">and</span> <span class="tok-variableName">time</span><span class="tok-operator">.</span><span class="tok-propertyName">time</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-operator">&lt;</span> <span class="tok-variableName">deadline</span>:</div><div class="cm-line">                <span class="tok-variableName">time</span><span class="tok-operator">.</span><span class="tok-propertyName">sleep</span><span class="tok-punctuation">(</span><span class="tok-number">0.02</span><span class="tok-punctuation">)</span></div><div class="cm-line">            <span class="tok-keyword">if</span> <span class="tok-variableName">note</span> <span class="tok-operator">&gt;=</span> <span class="tok-number">0</span>:</div><div class="cm-line">                <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_send</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;note_off&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">note</span><span class="tok-operator">=</span><span class="tok-variableName">note</span><span class="tok-punctuation">,</span> <span class="tok-variableName">velocity</span><span class="tok-operator">=</span><span class="tok-number">0</span><span class="tok-punctuation">,</span> <span class="tok-variableName">channel</span><span class="tok-operator">=</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">channel</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The <code>DrumLoopPlayer</code> uses offset-based timing instead, scheduling hits at specific points within the loop. Both players read from the <code>PatternStore</code> on each loop iteration, which enables hot-swapping patterns during live modifications.</p>
<h2>The Session</h2>
<p>The <code>Session</code> class orchestrates everything. It manages the state machine (IDLE -&gt; GENERATING -&gt; PLAYING), owns the <code>PatternStore</code>, creates the agents, and handles MIDI input from the controller:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">Session</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">__init__</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">state</span> <span class="tok-operator">=</span> <span class="tok-variableName">State</span><span class="tok-operator">.</span><span class="tok-propertyName">IDLE</span></div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">store</span> <span class="tok-operator">=</span> <span class="tok-variableName">PatternStore</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">store</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">live_agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_live_agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">store</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_agent_busy</span> <span class="tok-operator">=</span> <span class="tok-variableName">threading</span><span class="tok-operator">.</span><span class="tok-propertyName">Lock</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>When generation completes, playback starts with a progressive intro – tracks are unmuted one by one with a 2-bar delay between each, creating a build-up effect:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_start_playback</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">state</span> <span class="tok-operator">=</span> <span class="tok-variableName">State</span><span class="tok-operator">.</span><span class="tok-propertyName">PLAYING</span></div><div class="cm-line">    <span class="tok-keyword">for</span> <span class="tok-variableName">tid</span> <span class="tok-keyword">in</span> <span class="tok-variableName">TRACKS</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">players</span><span class="tok-punctuation">[</span><span class="tok-variableName">tid</span><span class="tok-punctuation">]</span><span class="tok-operator">.</span><span class="tok-propertyName">muted</span> <span class="tok-operator">=</span> <span class="tok-bool">True</span></div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">players</span><span class="tok-punctuation">[</span><span class="tok-variableName">tid</span><span class="tok-punctuation">]</span><span class="tok-operator">.</span><span class="tok-propertyName">start</span><span class="tok-punctuation">(</span><span class="tok-variableName">patterns</span><span class="tok-punctuation">[</span><span class="tok-variableName">tid</span><span class="tok-punctuation">]</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">intro_delay</span> <span class="tok-operator">=</span> <span class="tok-variableName">BAR_DURATION</span> <span class="tok-operator">*</span> <span class="tok-number">2</span></div><div class="cm-line">    <span class="tok-keyword">for</span> <span class="tok-variableName">i</span><span class="tok-punctuation">,</span> <span class="tok-variableName">tid</span> <span class="tok-keyword">in</span> <span class="tok-variableName">enumerate</span><span class="tok-punctuation">(</span><span class="tok-variableName">INTRO_ORDER</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">timer</span> <span class="tok-operator">=</span> <span class="tok-variableName">threading</span><span class="tok-operator">.</span><span class="tok-propertyName">Timer</span><span class="tok-punctuation">(</span><span class="tok-variableName">intro_delay</span> <span class="tok-operator">*</span> <span class="tok-variableName">i</span><span class="tok-punctuation">,</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_unmute_track</span><span class="tok-punctuation">,</span> <span class="tok-variableName">args</span><span class="tok-operator">=</span><span class="tok-punctuation">(</span><span class="tok-variableName">tid</span><span class="tok-punctuation">,</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">timer</span><span class="tok-operator">.</span><span class="tok-propertyName">start</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large coblocks-animate" data-coblocks-animation="fadeIn"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="281" data-attachment-id="88089" data-permalink="https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/setup/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?fit=1806%2C773&amp;ssl=1" data-orig-size="1806,773" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="setup" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?fit=656%2C281&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?resize=656%2C281&#038;ssl=1" alt="" class="wp-image-88089" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?resize=1024%2C438&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?resize=300%2C128&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?resize=768%2C329&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?resize=1536%2C657&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?w=1806&amp;ssl=1 1806w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><h2>How It Works</h2>
<ol>
<li>Run <code>python cli.py play</code></li>
<li>The app detects your MPK Mini Plus and shows a menu with AI-generated suggestions</li>
<li>Select a suggestion or write your own prompt</li>
<li>The AI generates 8 track patterns (takes a few seconds)</li>
<li>Playback begins with a progressive build-up</li>
<li>Write new instructions to modify the music live</li>
<li>Use knobs K1-K8 to mute/unmute individual tracks</li>
<li>PAD 1 regenerates with the same prompt, PAD 2 resets everything</li>
</ol>
<h2>Tech Stack</h2>
<ul>
<li><strong>Python 3.13</strong> with Poetry</li>
<li><strong>Strands Agents</strong> for AI agent orchestration</li>
<li><strong>AWS Bedrock</strong> (Claude Sonnet + Haiku) for pattern generation</li>
<li><strong>mido</strong> + <strong>python-rtmidi</strong> for MIDI I/O</li>
<li><strong>Akai MPK Mini Plus</strong> as MIDI controller</li>
<li><strong>MPC Beats</strong> as the DAW/sound engine</li>
<li><strong>Rich</strong> for terminal UI</li>
<li><strong>Click</strong> for CLI</li>
</ul>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="361" data-attachment-id="88091" data-permalink="https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/mpc_beats/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?fit=1507%2C830&amp;ssl=1" data-orig-size="1507,830" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="MPC_Beats" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?fit=656%2C361&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?resize=656%2C361&#038;ssl=1" alt="" class="wp-image-88091" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?resize=1024%2C564&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?resize=300%2C165&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?resize=768%2C423&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?w=1507&amp;ssl=1 1507w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>And that’s all. Full source code available on <a href="https://github.com/gonzalo123/eurobeat">GitHub</a>.</p>
</div>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/03/09/ai-eurobeat-producer-generating-music-in-real-time-with-ai-agents-python-and-midi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88078</post-id>	</item>
		<item>
		<title>Predicting the future: time series forecasting with AI Agents and Amazon Chronos-Bolt</title>
		<link>https://gonzalo123.com/2026/03/02/predicting-the-future-time-series-forecasting-with-ai-agents-and-amazon-chronos-bolt/</link>
					<comments>https://gonzalo123.com/2026/03/02/predicting-the-future-time-series-forecasting-with-ai-agents-and-amazon-chronos-bolt/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 02 Mar 2026 13:45:14 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=88004</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>Predicting the future is something we all try to do. Whether it’s energy consumption, sensor readings, or production metrics, having a reliable forecast helps us make better decisions. The problem is that building a good forecasting model traditionally requires deep statistical knowledge, and a lot of tuning. What if we could just hand our data to an AI agent and ask “what’s going to happen next”?</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="438" data-attachment-id="88018" data-permalink="https://gonzalo123.com/2026/03/02/predicting-the-future-time-series-forecasting-with-ai-agents-and-amazon-chronos-bolt/logo-4/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?fit=1536%2C1024&amp;ssl=1" data-orig-size="1536,1024" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?fit=656%2C438&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?resize=656%2C438&#038;ssl=1" alt="" class="wp-image-88018" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?resize=1024%2C683&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?resize=1200%2C800&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?w=1536&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>That’s exactly what this project does. It combines <a href="https://github.com/strands-agents/sdk-python">Strands Agents</a> with <a href="https://aws.amazon.com/bedrock/marketplace/">Amazon Chronos-Bolt</a>, a foundation model for time series forecasting available on AWS Bedrock Marketplace, to create an AI agent that can forecast any numerical time series through natural language.</p>
<h2>The architecture</h2>
<p>The idea is simple. We have a Strands Agent powered by Claude (via AWS Bedrock) that understands natural language. When the user asks for a forecast, the agent calls a custom tool that invokes Chronos-Bolt to generate predictions. The agent then interprets the results and explains them in plain language.</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="316" data-attachment-id="88015" data-permalink="https://gonzalo123.com/2026/03/02/predicting-the-future-time-series-forecasting-with-ai-agents-and-amazon-chronos-bolt/forecast_readme_md_at_main_%c2%b7_gonzalo123_forecast/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?fit=1071%2C517&amp;ssl=1" data-orig-size="1071,517" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="forecast_README_md_at_main_·_gonzalo123_forecast" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?fit=656%2C316&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?resize=656%2C316&#038;ssl=1" alt="" class="wp-image-88015" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?resize=1024%2C494&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?resize=300%2C145&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?resize=768%2C371&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?w=1071&amp;ssl=1 1071w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><p>The key here is that the agent doesn’t just return raw numbers. It understands the context, explains trends, and presents the confidence intervals in a way that makes sense.</p>
<h2>The forecast tool</h2>
<p>The tool is defined using the <code>@tool</code> decorator from Strands. This decorator turns a regular Python function into something the agent can discover and invoke on its own:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">forecast_time_series</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">values</span>: <span class="tok-variableName">Annotated</span><span class="tok-punctuation">[</span></div><div class="cm-line">        <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">float</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;Historical time series values in chronological order. &quot;</span></div><div class="cm-line">        <span class="tok-string">&quot;Values should be evenly spaced (e.g., hourly, daily). Minimum 10 values.&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">prediction_length</span>: <span class="tok-variableName">Annotated</span><span class="tok-punctuation">[</span></div><div class="cm-line">        <span class="tok-variableName">int</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;Number of future steps to predict. &quot;</span></div><div class="cm-line">        <span class="tok-string">&quot;Uses the same time unit as the input data.&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">quantile_levels</span>: <span class="tok-variableName">Annotated</span><span class="tok-punctuation">[</span></div><div class="cm-line">        <span class="tok-variableName">Optional</span><span class="tok-punctuation">[</span><span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">float</span><span class="tok-punctuation">]</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;Quantile levels for confidence intervals. Default: [0.1, 0.5, 0.9]. &quot;</span></div><div class="cm-line">        <span class="tok-string">&quot;0.5 is the median forecast, 0.1 and 0.9 define the 80% confidence band.&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">dict</span>:</div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The <code>Annotated</code> type hints serve a dual purpose: they validate types at runtime and provide descriptions that the LLM reads to understand how to use the tool. This means the agent knows it needs a list of floats, a prediction length, and optionally custom quantile levels, all from the type annotations alone.</p>
<p>The tool validates the input (minimum 10 values, maximum 50,000, prediction length between 1 and 1,000), filters out NaN values, and then calls the Chronos-Bolt client:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-variableName">result</span> <span class="tok-operator">=</span> <span class="tok-variableName">invoke_chronos</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">values</span><span class="tok-operator">=</span><span class="tok-variableName">clean_values</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">prediction_length</span><span class="tok-operator">=</span><span class="tok-variableName">prediction_length</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">quantile_levels</span><span class="tok-operator">=</span><span class="tok-variableName">quantile_levels</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">return</span> <span class="tok-punctuation">{</span></div><div class="cm-line">    <span class="tok-string">&quot;status&quot;</span>: <span class="tok-string">&quot;success&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-string">&quot;content&quot;</span>: <span class="tok-punctuation">[</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;text&quot;</span>: <span class="tok-string">&quot;</span><span class="tok-string2">\n</span><span class="tok-string">&quot;</span><span class="tok-operator">.</span><span class="tok-propertyName">join</span><span class="tok-punctuation">(</span><span class="tok-variableName">summary_lines</span><span class="tok-punctuation">)</span><span class="tok-punctuation">}</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-string">&quot;metadata&quot;</span>: <span class="tok-punctuation">{</span></div><div class="cm-line">        <span class="tok-string">&quot;quantiles&quot;</span>: <span class="tok-variableName">result</span><span class="tok-operator">.</span><span class="tok-propertyName">quantiles</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;prediction_length&quot;</span>: <span class="tok-variableName">result</span><span class="tok-operator">.</span><span class="tok-propertyName">prediction_length</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;history_length&quot;</span>: <span class="tok-variableName">result</span><span class="tok-operator">.</span><span class="tok-propertyName">history_length</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">}</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The response includes both a human-readable summary (in <code>content</code>) and the raw quantile data (in <code>metadata</code>), so the agent can reference exact numbers when explaining the forecast.</p>
<h2>The Chronos-Bolt client</h2>
<p>Chronos-Bolt is accessed through the Bedrock runtime API. The client sends the historical values and receives predictions at different quantile levels:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">invoke_chronos</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">values</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">float</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">prediction_length</span>: <span class="tok-variableName">int</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">quantile_levels</span>: <span class="tok-variableName">list</span><span class="tok-punctuation">[</span><span class="tok-variableName">float</span><span class="tok-punctuation">]</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span> <span class="tok-operator">=</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">ForecastResult</span>:</div><div class="cm-line">    <span class="tok-variableName">client</span> <span class="tok-operator">=</span> <span class="tok-variableName">_get_bedrock_runtime_client</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">payload</span> <span class="tok-operator">=</span> <span class="tok-punctuation">{</span></div><div class="cm-line">        <span class="tok-string">&quot;inputs&quot;</span>: <span class="tok-punctuation">[</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;target&quot;</span>: <span class="tok-variableName">values</span><span class="tok-punctuation">}</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-string">&quot;parameters&quot;</span>: <span class="tok-punctuation">{</span></div><div class="cm-line">            <span class="tok-string">&quot;prediction_length&quot;</span>: <span class="tok-variableName">prediction_length</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-string">&quot;quantile_levels&quot;</span>: <span class="tok-variableName">quantiles</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">}</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">client</span><span class="tok-operator">.</span><span class="tok-propertyName">invoke_model</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">modelId</span><span class="tok-operator">=</span><span class="tok-variableName">CHRONOS_ENDPOINT_ARN</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">body</span><span class="tok-operator">=</span><span class="tok-variableName">json</span><span class="tok-operator">.</span><span class="tok-propertyName">dumps</span><span class="tok-punctuation">(</span><span class="tok-variableName">payload</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">contentType</span><span class="tok-operator">=</span><span class="tok-string">&quot;application/json&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">accept</span><span class="tok-operator">=</span><span class="tok-string">&quot;application/json&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The <code>invoke_model</code> call uses the SageMaker endpoint ARN deployed through Bedrock Marketplace. Chronos-Bolt returns predictions organized by quantile levels, by default, the 10th, 50th (median), and 90th percentiles. This gives us not just a single forecast line, but a confidence band: the 80% interval between the 10th and 90th percentiles tells us how uncertain the model is about its predictions.</p>
<p>The Bedrock runtime client is configured with generous timeouts (120s read, 30s connect) and automatic retries, since inference on time series data can take a moment depending on the history length:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_get_bedrock_runtime_client</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">boto3</span><span class="tok-operator">.</span><span class="tok-propertyName">client</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-string">&quot;bedrock-runtime&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">region_name</span><span class="tok-operator">=</span><span class="tok-variableName">AWS_REGION</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">config</span><span class="tok-operator">=</span><span class="tok-variableName">Config</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-variableName">read_timeout</span><span class="tok-operator">=</span><span class="tok-number">120</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">connect_timeout</span><span class="tok-operator">=</span><span class="tok-number">30</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">retries</span><span class="tok-operator">=</span><span class="tok-punctuation">{</span><span class="tok-string">&quot;max_attempts&quot;</span>: <span class="tok-number">3</span><span class="tok-punctuation">}</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><h2>The agent</h2>
<p>Wiring everything together is straightforward. We create a <code>BedrockModel</code> pointing to Claude and pass our forecast tool to the <code>Agent</code>:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">strands</span> <span class="tok-keyword">import</span> <span class="tok-variableName">Agent</span></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">strands</span><span class="tok-operator">.</span><span class="tok-variableName">models</span><span class="tok-operator">.</span><span class="tok-variableName">bedrock</span> <span class="tok-keyword">import</span> <span class="tok-variableName">BedrockModel</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">settings</span> <span class="tok-keyword">import</span> <span class="tok-variableName">AWS_REGION</span><span class="tok-punctuation">,</span> <span class="tok-variableName">Models</span></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">forecast</span> <span class="tok-keyword">import</span> <span class="tok-variableName">forecast_time_series</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">SYSTEM_PROMPT</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&quot;&quot;You are a time series forecasting assistant powered by Amazon Chronos-Bolt.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">You help users predict future values from historical numerical data. When a user provides</span></div><div class="cm-line"><span class="tok-string">time series data or describes a scenario, use the forecast_time_series tool to generate</span></div><div class="cm-line"><span class="tok-string">predictions.</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-string">When presenting results:</span></div><div class="cm-line"><span class="tok-string">- Show the median forecast (quantile 0.5) as the main prediction</span></div><div class="cm-line"><span class="tok-string">- Explain the confidence band (quantiles 0.1 and 0.9) as the uncertainty range</span></div><div class="cm-line"><span class="tok-string">- Summarize trends in plain language</span></div><div class="cm-line"><span class="tok-string">&quot;&quot;&quot;</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_agent</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">Agent</span>:</div><div class="cm-line">    <span class="tok-variableName">bedrock_model</span> <span class="tok-operator">=</span> <span class="tok-variableName">BedrockModel</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">model_id</span><span class="tok-operator">=</span><span class="tok-variableName">Models</span><span class="tok-operator">.</span><span class="tok-propertyName">CLAUDE_SONNET</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">region_name</span><span class="tok-operator">=</span><span class="tok-variableName">AWS_REGION</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">Agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">bedrock_model</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">SYSTEM_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-variableName">forecast_time_series</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The system prompt is important here. It tells Claude that it has forecasting capabilities and how to present the results. Without it, the agent would still call the tool correctly (thanks to the <code>Annotated</code> descriptions), but it might not explain the confidence bands or summarize trends as clearly.</p>
<h2>Running it</h2>
<p>The CLI entry point (<code>cli.py</code>) registers commands and wires everything together. The <code>forecast</code> command generates synthetic hourly data (a sine wave with noise) by default and asks the agent to forecast. You can also pass a custom prompt.</p>
<p>The entry point is minimal:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">import</span> <span class="tok-variableName">click</span></div><div class="cm-line"><span class="tok-keyword">from</span> <span class="tok-variableName">commands</span><span class="tok-operator">.</span><span class="tok-variableName">forecast</span> <span class="tok-keyword">import</span> <span class="tok-variableName">run</span> <span class="tok-keyword">as</span> <span class="tok-variableName">forecast</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">group</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">cli</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">pass</span></div><div class="cm-line"></div><div class="cm-line"></div><div class="cm-line"><span class="tok-variableName">cli</span><span class="tok-operator">.</span><span class="tok-propertyName">add_command</span><span class="tok-punctuation">(</span><span class="tok-variableName">cmd</span><span class="tok-operator">=</span><span class="tok-variableName">forecast</span><span class="tok-punctuation">,</span> <span class="tok-variableName">name</span><span class="tok-operator">=</span><span class="tok-string">&quot;forecast&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-keyword">if</span> <span class="tok-variableName">__name__</span> <span class="tok-operator">==</span> <span class="tok-string">&quot;__main__&quot;</span>:</div><div class="cm-line">    <span class="tok-variableName">cli</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The actual command lives in <code>commands/forecast.py</code>:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">command</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">option</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;--prompt&quot;</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;-p&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">default</span><span class="tok-operator">=</span><span class="tok-keyword">None</span><span class="tok-punctuation">,</span> <span class="tok-variableName">help</span><span class="tok-operator">=</span><span class="tok-string">&quot;Custom prompt for the agent.&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">run</span><span class="tok-punctuation">(</span><span class="tok-variableName">prompt</span>: <span class="tok-variableName">str</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">prompt</span> <span class="tok-keyword">is</span> <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">values</span> <span class="tok-operator">=</span> <span class="tok-variableName">generate_sample_data</span><span class="tok-punctuation">(</span><span class="tok-variableName">num_points</span><span class="tok-operator">=</span><span class="tok-number">100</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">values_str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;, &quot;</span><span class="tok-operator">.</span><span class="tok-propertyName">join</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">v</span>:<span class="tok-keyword">.2f</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span> <span class="tok-keyword">for</span> <span class="tok-variableName">v</span> <span class="tok-keyword">in</span> <span class="tok-variableName">values</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-variableName">prompt</span> <span class="tok-operator">=</span> <span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-string2">f&quot;I have the following hourly sensor readings from the last 100 hours:\n&quot;</span></div><div class="cm-line">            <span class="tok-string2">f&quot;[</span><span class="tok-punctuation">{</span><span class="tok-variableName">values_str</span><span class="tok-punctuation">}</span><span class="tok-string2">]\n\n&quot;</span></div><div class="cm-line">            <span class="tok-string2">f&quot;Please forecast the next 24 hours and explain the predicted trend.&quot;</span></div><div class="cm-line">        <span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">prompt</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-propertyName">echo</span><span class="tok-punctuation">(</span><span class="tok-variableName">response</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The sine wave is a good choice for a demo because it has a clear periodic pattern that Chronos-Bolt should capture well. With 100 hours of history (about 4 full cycles of a 24-hour pattern), the model has enough data to identify the periodicity and project it forward.</p>
<h2>Example</h2>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-shell"><div class="cm-line">(venv) ➜  src python cli.py forecast</div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:16,471 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Found credentials <span class="tok-keyword">in</span> shared credentials file: ~/.aws/credentials</div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:16,506 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Creating Strands MetricsClient</div><div class="cm-line">Sure! Let me run the forecast on your <span class="tok-number">100</span><span class="tok-propertyName">-hour</span> sensor readings right away.</div><div class="cm-line">Tool <span class="tok-comment">#1: forecast_time_series</span></div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:22,981 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Starting forecast: <span class="tok-variableName tok-definition">history</span><span class="tok-operator">=</span><span class="tok-number">100</span>, <span class="tok-variableName tok-definition">prediction_length</span><span class="tok-operator">=</span><span class="tok-number">24</span></div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:22,981 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Invoking Chronos-Bolt: <span class="tok-variableName tok-definition">history_length</span><span class="tok-operator">=</span><span class="tok-number">100</span>, <span class="tok-variableName tok-definition">prediction_length</span><span class="tok-operator">=</span><span class="tok-number">24</span>, <span class="tok-variableName tok-definition">quantiles</span><span class="tok-operator">=</span>[0.1, <span class="tok-number">0</span>.5, <span class="tok-number">0</span>.9]</div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:22,994 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Found credentials <span class="tok-keyword">in</span> shared credentials file: ~/.aws/credentials</div><div class="cm-line"><span class="tok-number">2026</span><span class="tok-propertyName">-02-27</span> <span class="tok-number">14</span>:11:23,697 <span class="tok-propertyName">-</span> INFO <span class="tok-propertyName">-</span> Forecast complete: <span class="tok-number">24</span> steps, <span class="tok-number">4</span> quantiles</div><div class="cm-line">Here are the results of your <span class="tok-number">24</span><span class="tok-propertyName">-hour</span> forecast:</div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><h3>Script response</h3>
<h4><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 24-Hour Sensor Forecast</h4>
<table>
<thead>
<tr>
<th>Hour</th>
<th>Low (10th %)</th>
<th>Median (50th %)</th>
<th>High (90th %)</th>
</tr>
</thead>
<tbody>
<tr>
<td>+1</td>
<td>~-0.29</td>
<td>~0.85</td>
<td>~0.99</td>
</tr>
<tr>
<td>+2</td>
<td>~0.29</td>
<td>~0.95</td>
<td>~0.99</td>
</tr>
<tr>
<td>+3</td>
<td>~0.49</td>
<td>~0.99</td>
<td>~0.99</td>
</tr>
<tr>
<td>+4</td>
<td>~0.47</td>
<td>~0.83</td>
<td>~0.99</td>
</tr>
<tr>
<td>+5</td>
<td>~0.24</td>
<td>~0.72</td>
<td>~0.96</td>
</tr>
<tr>
<td>+6</td>
<td>~0.09</td>
<td>~0.53</td>
<td>~0.83</td>
</tr>
<tr>
<td>+7</td>
<td>~-0.25</td>
<td>~0.19</td>
<td>~0.59</td>
</tr>
<tr>
<td>+8</td>
<td>~-0.60</td>
<td>~-0.22</td>
<td>~0.25</td>
</tr>
<tr>
<td>+9</td>
<td>~-0.91</td>
<td>~-0.51</td>
<td>~-0.04</td>
</tr>
<tr>
<td>+10</td>
<td>~-0.99</td>
<td>~-0.76</td>
<td>~-0.30</td>
</tr>
<tr>
<td>+11</td>
<td>~-0.99</td>
<td>~-0.87</td>
<td>~-0.46</td>
</tr>
<tr>
<td>+12</td>
<td>~-0.99</td>
<td>~-0.93</td>
<td>~-0.56</td>
</tr>
<tr>
<td>+13</td>
<td>~-0.99</td>
<td>~-0.96</td>
<td>~-0.57</td>
</tr>
<tr>
<td>+14</td>
<td>~-0.99</td>
<td>~-0.93</td>
<td>~-0.52</td>
</tr>
<tr>
<td>+15</td>
<td>~-0.99</td>
<td>~-0.87</td>
<td>~-0.44</td>
</tr>
<tr>
<td>+16</td>
<td>~-0.99</td>
<td>~-0.79</td>
<td>~-0.35</td>
</tr>
<tr>
<td>+17</td>
<td>~-0.96</td>
<td>~-0.63</td>
<td>~-0.16</td>
</tr>
<tr>
<td>+18</td>
<td>~-0.86</td>
<td>~-0.46</td>
<td>~0.02</td>
</tr>
<tr>
<td>+19</td>
<td>~-0.66</td>
<td>~-0.24</td>
<td>~0.21</td>
</tr>
<tr>
<td>+20</td>
<td>~-0.44</td>
<td>~-0.01</td>
<td>~0.42</td>
</tr>
<tr>
<td>+21</td>
<td>~-0.21</td>
<td>~0.22</td>
<td>~0.60</td>
</tr>
<tr>
<td>+22</td>
<td>~-0.04</td>
<td>~0.45</td>
<td>~0.78</td>
</tr>
<tr>
<td>+23</td>
<td>~0.16</td>
<td>~0.66</td>
<td>~0.90</td>
</tr>
<tr>
<td>+24</td>
<td>~0.31</td>
<td>~0.80</td>
<td>~0.96</td>
</tr>
</tbody>
</table>
<hr>
<h4><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Trend Explanation</h4>
<p>Your sensor data clearly exhibits a <strong>sinusoidal/oscillating pattern</strong> — cycling between peaks near <strong>+1.0</strong> and troughs near <strong>-1.0</strong> roughly every <strong>~24 hours</strong>. The model has picked this up beautifully! Here’s what to expect:</p>
<ol>
<li>
<p><strong>Hours +1 to +4 — Continued Peak <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53c.png" alt="🔼" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: The signal is currently rising (ending at ~0.67–0.70), and the model predicts it will crest near <strong>~+0.85 to +0.99</strong> in the first few hours — completing the current positive phase.</p>
</li>
<li>
<p><strong>Hours +5 to +13 — Descent &amp; Trough <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53d.png" alt="🔽" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: The signal then descends sharply, hitting a <strong>trough around hours +12 to +14</strong> with a median near <strong>-0.93 to -0.96</strong> — matching the negative peaks seen in the historical data.</p>
</li>
<li>
<p><strong>Hours +14 to +24 — Recovery <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53c.png" alt="🔼" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: After bottoming out, the signal climbs back up, reaching approximately <strong>+0.80</strong> by hour +24, setting up the next positive cycle.</p>
</li>
</ol>
<h5><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4d0.png" alt="📐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Confidence Band</h5>
<p>The <strong>80% confidence interval</strong> (low–high columns) is relatively <strong>tight</strong>, reflecting the model’s high confidence in the periodic nature of this signal. The widest uncertainty occurs around the <strong>transition zones</strong> (hours +7–+9 and +17–+19), which is typical for oscillating signals near the zero-crossing points.</p>
<blockquote>
<p><strong>In short</strong>: your sensor is behaving like a clean oscillating signal with an ~24-hour period, and the next full cycle looks very consistent with historical behavior.Here are the results of your 24-hour forecast:</p>
</blockquote>
<hr>
<h4><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 24-Hour Sensor Forecast</h4>
<table>
<thead>
<tr>
<th>Hour</th>
<th>Low (10th %)</th>
<th>Median (50th %)</th>
<th>High (90th %)</th>
</tr>
</thead>
<tbody>
<tr>
<td>+1</td>
<td>~-0.29</td>
<td>~0.85</td>
<td>~0.99</td>
</tr>
<tr>
<td>+2</td>
<td>~0.29</td>
<td>~0.95</td>
<td>~0.99</td>
</tr>
<tr>
<td>+3</td>
<td>~0.49</td>
<td>~0.99</td>
<td>~0.99</td>
</tr>
<tr>
<td>+4</td>
<td>~0.47</td>
<td>~0.83</td>
<td>~0.99</td>
</tr>
<tr>
<td>+5</td>
<td>~0.24</td>
<td>~0.72</td>
<td>~0.96</td>
</tr>
<tr>
<td>+6</td>
<td>~0.09</td>
<td>~0.53</td>
<td>~0.83</td>
</tr>
<tr>
<td>+7</td>
<td>~-0.25</td>
<td>~0.19</td>
<td>~0.59</td>
</tr>
<tr>
<td>+8</td>
<td>~-0.60</td>
<td>~-0.22</td>
<td>~0.25</td>
</tr>
<tr>
<td>+9</td>
<td>~-0.91</td>
<td>~-0.51</td>
<td>~-0.04</td>
</tr>
<tr>
<td>+10</td>
<td>~-0.99</td>
<td>~-0.76</td>
<td>~-0.30</td>
</tr>
<tr>
<td>+11</td>
<td>~-0.99</td>
<td>~-0.87</td>
<td>~-0.46</td>
</tr>
<tr>
<td>+12</td>
<td>~-0.99</td>
<td>~-0.93</td>
<td>~-0.56</td>
</tr>
<tr>
<td>+13</td>
<td>~-0.99</td>
<td>~-0.96</td>
<td>~-0.57</td>
</tr>
<tr>
<td>+14</td>
<td>~-0.99</td>
<td>~-0.93</td>
<td>~-0.52</td>
</tr>
<tr>
<td>+15</td>
<td>~-0.99</td>
<td>~-0.87</td>
<td>~-0.44</td>
</tr>
<tr>
<td>+16</td>
<td>~-0.99</td>
<td>~-0.79</td>
<td>~-0.35</td>
</tr>
<tr>
<td>+17</td>
<td>~-0.96</td>
<td>~-0.63</td>
<td>~-0.16</td>
</tr>
<tr>
<td>+18</td>
<td>~-0.86</td>
<td>~-0.46</td>
<td>~0.02</td>
</tr>
<tr>
<td>+19</td>
<td>~-0.66</td>
<td>~-0.24</td>
<td>~0.21</td>
</tr>
<tr>
<td>+20</td>
<td>~-0.44</td>
<td>~-0.01</td>
<td>~0.42</td>
</tr>
<tr>
<td>+21</td>
<td>~-0.21</td>
<td>~0.22</td>
<td>~0.60</td>
</tr>
<tr>
<td>+22</td>
<td>~-0.04</td>
<td>~0.45</td>
<td>~0.78</td>
</tr>
<tr>
<td>+23</td>
<td>~0.16</td>
<td>~0.66</td>
<td>~0.90</td>
</tr>
<tr>
<td>+24</td>
<td>~0.31</td>
<td>~0.80</td>
<td>~0.96</td>
</tr>
</tbody>
</table>
<hr>
<h4><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Trend Explanation</h4>
<p>Your sensor data clearly exhibits a <strong>sinusoidal/oscillating pattern</strong> — cycling between peaks near <strong>+1.0</strong> and troughs near <strong>-1.0</strong> roughly every <strong>~24 hours</strong>. The model has picked this up beautifully! Here’s what to expect:</p>
<ol>
<li>
<p><strong>Hours +1 to +4 — Continued Peak <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53c.png" alt="🔼" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: The signal is currently rising (ending at ~0.67–0.70), and the model predicts it will crest near <strong>~+0.85 to +0.99</strong> in the first few hours — completing the current positive phase.</p>
</li>
<li>
<p><strong>Hours +5 to +13 — Descent &amp; Trough <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53d.png" alt="🔽" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: The signal then descends sharply, hitting a <strong>trough around hours +12 to +14</strong> with a median near <strong>-0.93 to -0.96</strong> — matching the negative peaks seen in the historical data.</p>
</li>
<li>
<p><strong>Hours +14 to +24 — Recovery <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f53c.png" alt="🔼" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong>: After bottoming out, the signal climbs back up, reaching approximately <strong>+0.80</strong> by hour +24, setting up the next positive cycle.</p>
</li>
</ol>
<h3><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4d0.png" alt="📐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Confidence Band</h3>
<p>The <strong>80% confidence interval</strong> (low–high columns) is relatively <strong>tight</strong>, reflecting the model’s high confidence in the periodic nature of this signal. The widest uncertainty occurs around the <strong>transition zones</strong> (hours +7–+9 and +17–+19), which is typical for oscillating signals near the zero-crossing points.</p>
<blockquote>
<p><strong>In short</strong>: your sensor is behaving like a clean oscillating signal with an ~24-hour period, and the next full cycle looks very consistent with historical behavior.</p>
</blockquote>
<hr>
<p>And that’s all! Full code in my <a href="https://github.com/gonzalo123/forecast">GitHub</a> account.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/03/02/predicting-the-future-time-series-forecasting-with-ai-agents-and-amazon-chronos-bolt/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">88004</post-id>	</item>
		<item>
		<title>Transforming Raw Spreadsheets into Professional Excel Reports with AI Agents and Python</title>
		<link>https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/</link>
					<comments>https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 09 Feb 2026 13:11:01 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Excel]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<category><![CDATA[xlsx]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=87898</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>We all deal with spreadsheets. They’re everywhere, financial reports, sales data, operational metrics. But raw data in a flat table is just that: raw data. To extract insights, you need dashboards, charts, KPIs, conditional formatting, and executive summaries. Doing this manually is tedious. What if an AI agent could take any raw <code>.xlsx</code> file and transform it into a professional, multi-sheet workbook with formulas, charts, and insights, automatically?</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="438" data-attachment-id="87904" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/logo-2/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?fit=1536%2C1024&amp;ssl=1" data-orig-size="1536,1024" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="logo" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?fit=656%2C438&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?resize=656%2C438&#038;ssl=1" alt="" class="wp-image-87904" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?resize=1024%2C683&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?resize=300%2C200&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?resize=768%2C512&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?w=1536&amp;ssl=1 1536w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><p>That’s exactly what this project does. The idea is simple: you give it a spreadsheet, and an AI agent running Python inside a AWS sandbox analyzes the data, builds a Dashboard with KPI formulas, formats the source data, generates an executive summary with real insights, and creates analysis sheets with charts, all using Excel formulas, never hardcoded values.</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="99" data-attachment-id="87908" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/gonzalo123_xlsx__transforming_raw_spreadsheets_into_professional_excel_reports_with_ai_agents_and_python/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?fit=1414%2C212&amp;ssl=1" data-orig-size="1414,212" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?fit=656%2C99&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?resize=656%2C99&#038;ssl=1" alt="" class="wp-image-87908" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?resize=1024%2C154&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?resize=300%2C45&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?resize=768%2C115&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?resize=1200%2C180&amp;ssl=1 1200w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?w=1414&amp;ssl=1 1414w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/gonzalo123_xlsx__Transforming_Raw_Spreadsheets_into_Professional_Excel_Reports_with_AI_Agents_and_Python.png?w=1312&amp;ssl=1 1312w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><h2>The two-agent pattern</h2>
<p>The core of the system is a <strong>two-agent architecture</strong>. An outer orchestrator agent (Claude Sonnet) manages the workflow, while an inner agent (Claude Opus) does the actual Excel work inside an AWS Bedrock Code Interpreter sandbox. This separation keeps the orchestration clean and lets the inner agent focus entirely on writing Python code with openpyxl.</p>
<p>The CLI entry point uses Click. When you run the command, it creates the orchestrator agent with the <code>xlsx_enhancer</code> tool:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">command</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">argument</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;input_file&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">type</span><span class="tok-operator">=</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-propertyName">Path</span><span class="tok-punctuation">(</span><span class="tok-variableName">exists</span><span class="tok-operator">=</span><span class="tok-bool">True</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-variableName">argument</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;output_file&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">type</span><span class="tok-operator">=</span><span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-propertyName">Path</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span><span class="tok-punctuation">,</span> <span class="tok-variableName">required</span><span class="tok-operator">=</span><span class="tok-bool">False</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">run</span><span class="tok-punctuation">(</span><span class="tok-variableName">input_file</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">output_file</span>: <span class="tok-variableName">str</span> <span class="tok-operator">|</span> <span class="tok-keyword">None</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">output_file</span>:</div><div class="cm-line">        <span class="tok-variableName">p</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span><span class="tok-punctuation">(</span><span class="tok-variableName">input_file</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">output_file</span> <span class="tok-operator">=</span> <span class="tok-variableName">str</span><span class="tok-punctuation">(</span><span class="tok-variableName">p</span><span class="tok-operator">.</span><span class="tok-propertyName">parent</span> <span class="tok-operator">/</span> <span class="tok-string2">f&quot;enhanced_</span><span class="tok-punctuation">{</span><span class="tok-variableName">p</span><span class="tok-operator">.</span><span class="tok-propertyName">name</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">ORCHESTRATOR_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-variableName">xlsx_enhancer</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">hooks</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-variableName">ToolProgressHook</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-string2">f&quot;Process the Excel file at </span><span class="tok-punctuation">{</span><span class="tok-variableName">input_file</span><span class="tok-punctuation">}</span><span class="tok-string2"> and save the enhanced version to </span><span class="tok-punctuation">{</span><span class="tok-variableName">output_file</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">click</span><span class="tok-operator">.</span><span class="tok-propertyName">echo</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;Done: </span><span class="tok-punctuation">{</span><span class="tok-variableName">str</span><span class="tok-punctuation">(</span><span class="tok-variableName">response</span><span class="tok-punctuation">)</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The agent factory wraps the Strands SDK configuration, model selection, retry logic, sliding window conversation management:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">create_agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">    <span class="tok-variableName">system_prompt</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">model</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-variableName">Models</span><span class="tok-operator">.</span><span class="tok-propertyName">CLAUDE_45</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">tools</span>: <span class="tok-variableName">Optional</span><span class="tok-punctuation">[</span><span class="tok-variableName">List</span><span class="tok-punctuation">[</span><span class="tok-variableName">Any</span><span class="tok-punctuation">]</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">hooks</span>: <span class="tok-variableName">Optional</span><span class="tok-punctuation">[</span><span class="tok-variableName">List</span><span class="tok-punctuation">[</span><span class="tok-variableName">HookProvider</span><span class="tok-punctuation">]</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">temperature</span>: <span class="tok-variableName">float</span> <span class="tok-operator">=</span> <span class="tok-number">0.3</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">read_timeout</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-number">300</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">connect_timeout</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-number">60</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">max_attempts</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-number">10</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">maximum_messages_to_keep</span>: <span class="tok-variableName">int</span> <span class="tok-operator">=</span> <span class="tok-number">30</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">should_truncate_results</span>: <span class="tok-variableName">bool</span> <span class="tok-operator">=</span> <span class="tok-bool">True</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-variableName">callback_handler</span>: <span class="tok-variableName">Any</span> <span class="tok-operator">=</span> <span class="tok-keyword">None</span><span class="tok-punctuation">,</span></div><div class="cm-line"><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">Agent</span>:</div><div class="cm-line">    <span class="tok-variableName">bedrock_model</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_bedrock_model</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">model</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">temperature</span><span class="tok-operator">=</span><span class="tok-variableName">temperature</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">read_timeout</span><span class="tok-operator">=</span><span class="tok-variableName">read_timeout</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">connect_timeout</span><span class="tok-operator">=</span><span class="tok-variableName">connect_timeout</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">max_attempts</span><span class="tok-operator">=</span><span class="tok-variableName">max_attempts</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">return</span> <span class="tok-variableName">Agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">        <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">system_prompt</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">bedrock_model</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">conversation_manager</span><span class="tok-operator">=</span><span class="tok-variableName">SlidingWindowConversationManager</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-variableName">window_size</span><span class="tok-operator">=</span><span class="tok-variableName">maximum_messages_to_keep</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">should_truncate_results</span><span class="tok-operator">=</span><span class="tok-variableName">should_truncate_results</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">)</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-variableName">tools</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">hooks</span><span class="tok-operator">=</span><span class="tok-variableName">hooks</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-variableName">callback_handler</span><span class="tok-operator">=</span><span class="tok-variableName">callback_handler</span><span class="tok-punctuation">,</span></div><div class="cm-line">    <span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><h2>The xlsx_enhancer tool</h2>
<p>This is the centerpiece. It’s a Strands <code>@tool</code> that orchestrates a 4-step pipeline: upload the file to the sandbox, run the inner agent, verify the output, and download the result from the sandbox.</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-meta">@</span><span class="tok-variableName">tool</span></div><div class="cm-line"><span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">xlsx_enhancer</span><span class="tok-punctuation">(</span><span class="tok-variableName">input_file</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">output_file</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">instructions</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;&quot;</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-variableName">dict</span>:</div><div class="cm-line">    <span class="tok-string">&quot;&quot;&quot;Enhance an Excel file with professional formatting, dashboards, charts, and analysis sheets.&quot;&quot;&quot;</span></div><div class="cm-line">    <span class="tok-variableName">input_path</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span><span class="tok-punctuation">(</span><span class="tok-variableName">input_file</span><span class="tok-punctuation">)</span></div><div class="cm-line">    <span class="tok-variableName">output_path</span> <span class="tok-operator">=</span> <span class="tok-variableName">Path</span><span class="tok-punctuation">(</span><span class="tok-variableName">output_file</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">input_path</span><span class="tok-operator">.</span><span class="tok-propertyName">exists</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">XlsxResult</span><span class="tok-punctuation">(</span><span class="tok-variableName">success</span><span class="tok-operator">=</span><span class="tok-bool">False</span><span class="tok-punctuation">,</span> <span class="tok-variableName">error</span><span class="tok-operator">=</span><span class="tok-string2">f&quot;Input file not found: </span><span class="tok-punctuation">{</span><span class="tok-variableName">input_file</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">input_path</span><span class="tok-operator">.</span><span class="tok-propertyName">suffix</span><span class="tok-operator">.</span><span class="tok-propertyName">lower</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-operator">!=</span> <span class="tok-string">&quot;.xlsx&quot;</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">XlsxResult</span><span class="tok-punctuation">(</span><span class="tok-variableName">success</span><span class="tok-operator">=</span><span class="tok-bool">False</span><span class="tok-punctuation">,</span> <span class="tok-variableName">error</span><span class="tok-operator">=</span><span class="tok-string2">f&quot;Input file must be .xlsx, got: </span><span class="tok-punctuation">{</span><span class="tok-variableName">input_path</span><span class="tok-operator">.</span><span class="tok-propertyName">suffix</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-variableName">user_prompt</span> <span class="tok-operator">=</span> <span class="tok-variableName">USER_PROMPT</span></div><div class="cm-line">    <span class="tok-keyword">if</span> <span class="tok-variableName">instructions</span><span class="tok-operator">.</span><span class="tok-propertyName">strip</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">user_prompt</span> <span class="tok-operator">=</span> <span class="tok-string2">f&quot;</span><span class="tok-punctuation">{</span><span class="tok-variableName">USER_PROMPT</span><span class="tok-punctuation">}</span><span class="tok-string2">\n\n## Additional Instructions\n</span><span class="tok-punctuation">{</span><span class="tok-variableName">instructions</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">try</span>:</div><div class="cm-line">        <span class="tok-variableName">code_tool</span> <span class="tok-operator">=</span> <span class="tok-variableName">AgentCoreCodeInterpreter</span><span class="tok-punctuation">(</span><span class="tok-variableName">region</span><span class="tok-operator">=</span><span class="tok-variableName">AWS_REGION</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">sandbox</span> <span class="tok-operator">=</span> <span class="tok-variableName">SandboxIO</span><span class="tok-punctuation">(</span><span class="tok-variableName">code_tool</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-comment"># 1. Upload</span></div><div class="cm-line">        <span class="tok-variableName">sandbox</span><span class="tok-operator">.</span><span class="tok-propertyName">upload</span><span class="tok-punctuation">(</span><span class="tok-variableName">input_path</span><span class="tok-punctuation">,</span> <span class="tok-variableName">SANDBOX_INPUT</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-comment"># 2. Run the inner XLSX agent</span></div><div class="cm-line">        <span class="tok-variableName">agent</span> <span class="tok-operator">=</span> <span class="tok-variableName">create_agent</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-variableName">system_prompt</span><span class="tok-operator">=</span><span class="tok-variableName">SYSTEM_PROMPT</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">model</span><span class="tok-operator">=</span><span class="tok-variableName">Models</span><span class="tok-operator">.</span><span class="tok-propertyName">CLAUDE_46_OPUS</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-variableName">tools</span><span class="tok-operator">=</span><span class="tok-punctuation">[</span><span class="tok-variableName">code_tool</span><span class="tok-operator">.</span><span class="tok-propertyName">code_interpreter</span><span class="tok-punctuation">]</span><span class="tok-punctuation">,</span></div><div class="cm-line">        <span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">response</span> <span class="tok-operator">=</span> <span class="tok-variableName">agent</span><span class="tok-punctuation">(</span><span class="tok-variableName">user_prompt</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-comment"># 3. Verify output exists in sandbox</span></div><div class="cm-line">        <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">sandbox</span><span class="tok-operator">.</span><span class="tok-propertyName">verify_exists</span><span class="tok-punctuation">(</span><span class="tok-variableName">SANDBOX_OUTPUT</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">            <span class="tok-keyword">return</span> <span class="tok-variableName">XlsxResult</span><span class="tok-punctuation">(</span></div><div class="cm-line">                <span class="tok-variableName">success</span><span class="tok-operator">=</span><span class="tok-bool">False</span><span class="tok-punctuation">,</span></div><div class="cm-line">                <span class="tok-variableName">error</span><span class="tok-operator">=</span><span class="tok-string2">f&quot;The XLSX agent did not produce &apos;</span><span class="tok-punctuation">{</span><span class="tok-variableName">SANDBOX_OUTPUT</span><span class="tok-punctuation">}</span><span class="tok-string2">&apos;&quot;</span><span class="tok-punctuation">,</span></div><div class="cm-line">            <span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-comment"># 4. Download</span></div><div class="cm-line">        <span class="tok-variableName">output_path</span><span class="tok-operator">.</span><span class="tok-propertyName">parent</span><span class="tok-operator">.</span><span class="tok-propertyName">mkdir</span><span class="tok-punctuation">(</span><span class="tok-variableName">parents</span><span class="tok-operator">=</span><span class="tok-bool">True</span><span class="tok-punctuation">,</span> <span class="tok-variableName">exist_ok</span><span class="tok-operator">=</span><span class="tok-bool">True</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">sandbox</span><span class="tok-operator">.</span><span class="tok-propertyName">download</span><span class="tok-punctuation">(</span><span class="tok-variableName">SANDBOX_OUTPUT</span><span class="tok-punctuation">,</span> <span class="tok-variableName">output_path</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">XlsxResult</span><span class="tok-punctuation">(</span><span class="tok-variableName">success</span><span class="tok-operator">=</span><span class="tok-bool">True</span><span class="tok-punctuation">,</span> <span class="tok-variableName">output_path</span><span class="tok-operator">=</span><span class="tok-variableName">str</span><span class="tok-punctuation">(</span><span class="tok-variableName">output_path</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">except</span> <span class="tok-variableName">SandboxIOError</span> <span class="tok-keyword">as</span> <span class="tok-variableName">e</span>:</div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">XlsxResult</span><span class="tok-punctuation">(</span><span class="tok-variableName">success</span><span class="tok-operator">=</span><span class="tok-bool">False</span><span class="tok-punctuation">,</span> <span class="tok-variableName">error</span><span class="tok-operator">=</span><span class="tok-string2">f&quot;Sandbox I/O failed: </span><span class="tok-punctuation">{</span><span class="tok-variableName">e</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span><span class="tok-operator">.</span><span class="tok-propertyName">model_dump</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The inner agent receives two carefully crafted prompts. The system prompt enforces hard rules about Excel integrity, formulas instead of hardcoded values, sheet name constraints, error handling. The user prompt defines the exact structure: Dashboard with KPI formulas, formatted Data sheet, executive Summary with LLM-generated insights, and Analysis sheets with charts.</p>
<h2>The formula-first philosophy</h2>
<p>One of the most important design decisions is that the agent <strong>never hardcodes computed values</strong> in cells. Every number in the output workbook comes from an Excel formula:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-comment"># FORBIDDEN - Computing in Python</span></div><div class="cm-line"><span class="tok-variableName">total</span> <span class="tok-operator">=</span> <span class="tok-variableName">df</span><span class="tok-punctuation">[</span><span class="tok-string">&apos;Sales&apos;</span><span class="tok-punctuation">]</span><span class="tok-operator">.</span><span class="tok-propertyName">sum</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line"><span class="tok-variableName">sheet</span><span class="tok-punctuation">[</span><span class="tok-string">&apos;B10&apos;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-variableName">total</span>  <span class="tok-comment"># Hardcodes a value</span></div><div class="cm-line"></div><div class="cm-line"><span class="tok-comment"># REQUIRED - Excel formulas</span></div><div class="cm-line"><span class="tok-variableName">sheet</span><span class="tok-punctuation">[</span><span class="tok-string">&apos;B10&apos;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-string">&apos;=SUM(Data!D:D)&apos;</span></div><div class="cm-line"><span class="tok-variableName">sheet</span><span class="tok-punctuation">[</span><span class="tok-string">&apos;C10&apos;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-string">&apos;=SUMIF(Data!A:A,&quot;Category&quot;,Data!B:B)&apos;</span></div><div class="cm-line"><span class="tok-variableName">sheet</span><span class="tok-punctuation">[</span><span class="tok-string">&apos;D10&apos;</span><span class="tok-punctuation">]</span> <span class="tok-operator">=</span> <span class="tok-string">&apos;=IFERROR(AVERAGEIF(Data!A:A,A10,Data!D:D),0)&apos;</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>This means the resulting Excel file is <strong>alive</strong>,  change a value in the Data sheet and every KPI, every analysis table, every chart updates automatically. The IFERROR wrapping prevents #DIV/0! errors that would otherwise break AVERAGEIF formulas when a category has no data.</p>
<h2>Handling binary files in the sandbox</h2>
<p>The AWS Bedrock Code Interpreter sandbox runs Python in an isolated environment. Uploading the source file is straightforward, the bedrock client handles binary blobs natively. But downloading the result is trickier: the <code>download_file</code> method decodes everything as UTF-8, which corrupts binary xlsx files.</p>
<p>The solution is to base64-encode the file inside the sandbox and extract the text from the stream:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">SandboxIO</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">__init__</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">code_tool</span>: <span class="tok-variableName">AgentCoreCodeInterpreter</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_code_tool</span> <span class="tok-operator">=</span> <span class="tok-variableName">code_tool</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">_get_client</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">        <span class="tok-variableName">session_name</span><span class="tok-punctuation">,</span> <span class="tok-variableName">error</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_code_tool</span><span class="tok-operator">.</span><span class="tok-propertyName">_ensure_session</span><span class="tok-punctuation">(</span><span class="tok-keyword">None</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-keyword">if</span> <span class="tok-variableName">error</span>:</div><div class="cm-line">            <span class="tok-keyword">raise</span> <span class="tok-variableName">SandboxIOError</span><span class="tok-punctuation">(</span><span class="tok-string2">f&quot;Failed to ensure session: </span><span class="tok-punctuation">{</span><span class="tok-variableName">error</span><span class="tok-punctuation">}</span><span class="tok-string2">&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">session_info</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_code_tool</span><span class="tok-operator">.</span><span class="tok-propertyName">_sessions</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-variableName">session_name</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-keyword">return</span> <span class="tok-variableName">session_info</span><span class="tok-operator">.</span><span class="tok-propertyName">client</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">upload</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">local_path</span>: <span class="tok-variableName">Path</span><span class="tok-punctuation">,</span> <span class="tok-variableName">sandbox_name</span>: <span class="tok-variableName">str</span> <span class="tok-operator">=</span> <span class="tok-string">&quot;input.xlsx&quot;</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">file_bytes</span> <span class="tok-operator">=</span> <span class="tok-variableName">local_path</span><span class="tok-operator">.</span><span class="tok-propertyName">read_bytes</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">client</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_get_client</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">client</span><span class="tok-operator">.</span><span class="tok-propertyName">upload_file</span><span class="tok-punctuation">(</span><span class="tok-variableName">path</span><span class="tok-operator">=</span><span class="tok-variableName">sandbox_name</span><span class="tok-punctuation">,</span> <span class="tok-variableName">content</span><span class="tok-operator">=</span><span class="tok-variableName">file_bytes</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">download</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">sandbox_name</span>: <span class="tok-variableName">str</span><span class="tok-punctuation">,</span> <span class="tok-variableName">local_path</span>: <span class="tok-variableName">Path</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">client</span> <span class="tok-operator">=</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_get_client</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">result</span> <span class="tok-operator">=</span> <span class="tok-variableName">client</span><span class="tok-operator">.</span><span class="tok-propertyName">execute_code</span><span class="tok-punctuation">(</span></div><div class="cm-line">            <span class="tok-string">&quot;import base64, os</span><span class="tok-string2">\n</span><span class="tok-string">&quot;</span></div><div class="cm-line">            <span class="tok-string2">f&quot;p = &apos;</span><span class="tok-punctuation">{</span><span class="tok-variableName">sandbox_name</span><span class="tok-punctuation">}</span><span class="tok-string2">&apos;\n&quot;</span></div><div class="cm-line">            <span class="tok-string">&quot;data = open(p, &apos;rb&apos;).read()</span><span class="tok-string2">\n</span><span class="tok-string">&quot;</span></div><div class="cm-line">            <span class="tok-string">&quot;print(base64.b64encode(data).decode())</span><span class="tok-string2">\n</span><span class="tok-string">&quot;</span></div><div class="cm-line">        <span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">b64_text</span> <span class="tok-operator">=</span> <span class="tok-variableName">_extract_stream_text</span><span class="tok-punctuation">(</span><span class="tok-variableName">result</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">file_bytes</span> <span class="tok-operator">=</span> <span class="tok-variableName">base64</span><span class="tok-operator">.</span><span class="tok-propertyName">b64decode</span><span class="tok-punctuation">(</span><span class="tok-variableName">b64_text</span><span class="tok-operator">.</span><span class="tok-propertyName">strip</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-keyword">if</span> <span class="tok-keyword">not</span> <span class="tok-variableName">file_bytes</span><span class="tok-operator">.</span><span class="tok-propertyName">startswith</span><span class="tok-punctuation">(</span><span class="tok-string">b&quot;PK</span><span class="tok-string2">\x03</span><span class="tok-string2">\x04</span><span class="tok-string">&quot;</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">            <span class="tok-keyword">raise</span> <span class="tok-variableName">SandboxIOError</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;Downloaded file is not a valid xlsx&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">        <span class="tok-variableName">local_path</span><span class="tok-operator">.</span><span class="tok-propertyName">write_bytes</span><span class="tok-punctuation">(</span><span class="tok-variableName">file_bytes</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>The <code>PK\x03\x04</code> check validates the ZIP magic bytes — every xlsx file is a ZIP archive internally.</p>
<h2>The original xlsx file</h2>
<p>This is the original file we feed into the agent. It’s a flat table with rows and columns. No formatting, no formulas, just bored raw data.</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="408" data-attachment-id="87905" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/original-3/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?fit=1261%2C785&amp;ssl=1" data-orig-size="1261,785" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="original" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?fit=656%2C408&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?resize=656%2C408&#038;ssl=1" alt="" class="wp-image-87905" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?resize=1024%2C637&amp;ssl=1 1024w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?resize=300%2C187&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?resize=768%2C478&amp;ssl=1 768w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?w=1261&amp;ssl=1 1261w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><h2>What the agent produces</h2>
<p>Given a raw financial spreadsheet, the agent generates a multi-sheet workbook:</p>
<ul>
<li><strong>Dashboard</strong>: KPI cards with formulas (<code>=SUM(Data!D:D)</code>, <code>=COUNT(Data!A:A)</code>), color-coded metrics, and a hyperlinked index to all sheets</li>
</ul>
</div>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="567" data-attachment-id="87902" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/dashboard-3/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?fit=867%2C749&amp;ssl=1" data-orig-size="867,749" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="dashboard" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?fit=656%2C567&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?resize=656%2C567&#038;ssl=1" alt="" class="wp-image-87902" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?w=867&amp;ssl=1 867w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?resize=300%2C259&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?resize=768%2C663&amp;ssl=1 768w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><ul>
<li><strong>Data</strong>: The original data with dark blue headers, alternating row colors, auto-filters, data bars on numeric columns, and frozen panes</li>
</ul>
</div>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="573" data-attachment-id="87903" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/data/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?fit=870%2C760&amp;ssl=1" data-orig-size="870,760" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="data" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?fit=656%2C573&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?resize=656%2C573&#038;ssl=1" alt="" class="wp-image-87903" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?w=870&amp;ssl=1 870w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?resize=300%2C262&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?resize=768%2C671&amp;ssl=1 768w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><ul>
<li><strong>Summary</strong>: An executive summary written by the LLM, key findings, concentration risks, trends, anomalies, and actionable recommendations</li>
</ul>
</div>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="559" data-attachment-id="87906" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/summary/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?fit=875%2C745&amp;ssl=1" data-orig-size="875,745" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="summary" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?fit=656%2C559&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?resize=656%2C559&#038;ssl=1" alt="" class="wp-image-87906" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?w=875&amp;ssl=1 875w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?resize=300%2C255&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?resize=768%2C654&amp;ssl=1 768w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><ul>
<li><strong>Analysis sheets</strong>: One per categorical column, each with a SUMIF/COUNTIF/AVERAGEIF table and a bar chart</li>
</ul>
</div>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="575" data-attachment-id="87901" data-permalink="https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/analysis/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?fit=871%2C763&amp;ssl=1" data-orig-size="871,763" data-comments-opened="1" data-image-meta="{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}" data-image-title="analysis" data-image-description="" data-image-caption="" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?fit=656%2C575&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?resize=656%2C575&#038;ssl=1" alt="" class="wp-image-87901" srcset="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?w=871&amp;ssl=1 871w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?resize=300%2C263&amp;ssl=1 300w, https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?resize=768%2C673&amp;ssl=1 768w" sizes="auto, (max-width: 656px) 100vw, 656px" /></a></figure>



<div class="wp-block-jetpack-markdown"><p>The agent also detects the language of the input data and uses the same language for all generated content, sheet names, titles, labels, and the executive summary.</p>
<h2>Monitoring tool execution</h2>
<p>A simple hook tracks how long each tool execution takes. It can be extended to integrate with our application and provide real-time feedback to users about the agent’s progress:</p>
</div>


<div class="wp-block-code">
	<div class="cm-editor">
		<div class="cm-scroller">
			
<pre>
<code class="language-python"><div class="cm-line"><span class="tok-keyword">class</span> <span class="tok-className">ToolProgressHook</span><span class="tok-punctuation">(</span><span class="tok-variableName">HookProvider</span><span class="tok-punctuation">)</span>:</div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">__init__</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_start_time</span>: <span class="tok-variableName">float</span> <span class="tok-operator">=</span> <span class="tok-number">0</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">register_hooks</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">registry</span>: <span class="tok-variableName">HookRegistry</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">registry</span><span class="tok-operator">.</span><span class="tok-propertyName">add_callback</span><span class="tok-punctuation">(</span><span class="tok-variableName">BeforeToolCallEvent</span><span class="tok-punctuation">,</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">on_tool_start</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">registry</span><span class="tok-operator">.</span><span class="tok-propertyName">add_callback</span><span class="tok-punctuation">(</span><span class="tok-variableName">AfterToolCallEvent</span><span class="tok-punctuation">,</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">on_tool_end</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">on_tool_start</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">event</span>: <span class="tok-variableName">BeforeToolCallEvent</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_start_time</span> <span class="tok-operator">=</span> <span class="tok-variableName">time</span><span class="tok-operator">.</span><span class="tok-propertyName">time</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">tool_name</span> <span class="tok-operator">=</span> <span class="tok-variableName">event</span><span class="tok-operator">.</span><span class="tok-propertyName">tool_use</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;name&quot;</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;unknown&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">logger</span><span class="tok-operator">.</span><span class="tok-propertyName">info</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;Tool started: %s&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">tool_name</span><span class="tok-punctuation">)</span></div><div class="cm-line"></div><div class="cm-line">    <span class="tok-keyword">def</span> <span class="tok-variableName tok-definition">on_tool_end</span><span class="tok-punctuation">(</span><span class="tok-variableName">self</span><span class="tok-punctuation">,</span> <span class="tok-variableName">event</span>: <span class="tok-variableName">AfterToolCallEvent</span><span class="tok-punctuation">)</span> -&gt; <span class="tok-keyword">None</span>:</div><div class="cm-line">        <span class="tok-variableName">elapsed</span> <span class="tok-operator">=</span> <span class="tok-variableName">time</span><span class="tok-operator">.</span><span class="tok-propertyName">time</span><span class="tok-punctuation">(</span><span class="tok-punctuation">)</span> <span class="tok-operator">-</span> <span class="tok-variableName">self</span><span class="tok-operator">.</span><span class="tok-propertyName">_start_time</span></div><div class="cm-line">        <span class="tok-variableName">tool_name</span> <span class="tok-operator">=</span> <span class="tok-variableName">event</span><span class="tok-operator">.</span><span class="tok-propertyName">tool_use</span><span class="tok-operator">.</span><span class="tok-propertyName">get</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;name&quot;</span><span class="tok-punctuation">,</span> <span class="tok-string">&quot;unknown&quot;</span><span class="tok-punctuation">)</span></div><div class="cm-line">        <span class="tok-variableName">logger</span><span class="tok-operator">.</span><span class="tok-propertyName">info</span><span class="tok-punctuation">(</span><span class="tok-string">&quot;Tool finished: %s (%.1fs)&quot;</span><span class="tok-punctuation">,</span> <span class="tok-variableName">tool_name</span><span class="tok-punctuation">,</span> <span class="tok-variableName">elapsed</span><span class="tok-punctuation">)</span></div></code></pre>
		</div>
	</div>
</div>


<div class="wp-block-jetpack-markdown"><p>And that’s all. With tools like Strands Agents and AWS Bedrock’s Code Interpreter, we can build AI agents that go beyond text generation, they produce real, functional artifacts. A raw spreadsheet goes in, a professional report comes out. No templates, no manual formatting, just an agent that understands data and knows how to present it.</p>
<p>Full code in my <a href="https://github.com/gonzalo123/xlsx">github</a> account.</p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/02/09/transforming-raw-spreadsheets-into-professional-excel-reports-with-ai-agents-and-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">87898</post-id>	</item>
	</channel>
</rss>
