<?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, 23 Mar 2026 13:35:30 +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>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" fetchpriority="high" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo.png?fit=300%2C200&amp;ssl=1" 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="(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" 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-medium-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=300%2C55&amp;ssl=1" 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="(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" 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-medium-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=300%2C144&amp;ssl=1" 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="(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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/logo-1.png?fit=300%2C200&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/eurobeat_README_md_at_main_%C2%B7_gonzalo123_eurobeat.png?fit=300%2C67&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/setup.png?fit=300%2C128&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/03/MPC_Beats.png?fit=300%2C165&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo-2.png?fit=300%2C200&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/forecast_README_md_at_main_%C2%B7_gonzalo123_forecast.png?fit=300%2C145&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/logo.png?fit=300%2C200&amp;ssl=1" 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-medium-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=300%2C45&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/original.png?fit=300%2C187&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/dashboard.png?fit=300%2C259&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/data.png?fit=300%2C262&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/summary.png?fit=300%2C255&amp;ssl=1" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/02/analysis.png?fit=300%2C263&amp;ssl=1" 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>
		<item>
		<title>What if the bug fixed itself? Letting AI agents detect bugs, fix the code, and create PRs proactively.</title>
		<link>https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/</link>
					<comments>https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/#comments</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 13:06:08 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Claude code]]></category>
		<category><![CDATA[cloudwatch]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=87828</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>What if an AI could not only identify errors in your logs but actually fix them and create a pull request? I have done this experiment to do exactly that.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/logo.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="430" data-attachment-id="87830" data-permalink="https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/logo/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/logo.png?fit=1409%2C923&amp;ssl=1" data-orig-size="1409,923" 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-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/logo.png?fit=300%2C197&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/logo.png?fit=656%2C430&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/logo.png?resize=656%2C430&#038;ssl=1" alt="" class="wp-image-87830" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>We can put or application logs in CloudWatch and use AI agents with a worker-coordinator pattern (I’ll share a post explaining this). Today the idea is going one step further. We will detecte errors in our logs, and for certain types of fixable errors, we will let an AI agent fix the code and create a pull request automatically.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="540" height="714" data-attachment-id="87831" data-permalink="https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_.png?fit=540%2C714&amp;ssl=1" data-orig-size="540,714" 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_autofix__What_if_the_bug_fixed_itself__Letting_AI_agents_detect_bugs__fix_the_code__and_create_PRs_proactively_" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_.png?fit=227%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_.png?fit=540%2C714&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/gonzalo123_autofix__what_if_the_bug_fixed_itself__letting_ai_agents_detect_bugs__fix_the_code__and_create_prs_proactively_.png?resize=540%2C714&#038;ssl=1" alt="" class="wp-image-87831" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The core of the system is a tool decorated with <code>@tool</code> from Strands Agents. This makes it available to any AI agent that needs to trigger a fix:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">from strands import tool

@tool
async def register_error_for_fix(error: LogEntry) -&gt; bool:
    """
    Register an error for automatic fixing.
    Clones repo, creates fix branch, uses Claude to fix, creates PR.
    """
    repo = _setup_repo()

    branch_name = _create_fix_branch(repo, error)
    if branch_name is None:
        return True  # Branch already exists, skip

    claude_response = await _invoke_claude_fix(error.message)
    if claude_response is None:
        return False

    pr_info = pr_title_generator(claude_response)
    _commit_and_push(repo, branch_name, pr_info)
    _create_pull_request(branch_name, pr_info)

    return True</pre>



<div class="wp-block-jetpack-markdown"><h2>Step by Step Implementation</h2>
<h3>1. Repository Setup with GitPython</h3>
<p>The tool first clones the repo or pulls the latest changes:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">from git import Repo

def _setup_repo() -&gt; Repo:
    repo_url = f"https://x-access-token:{GITHUB_TOKEN}@github.com/{GITHUB_REPO}.git"

    if (WORK_DIR / ".git").exists():
        repo = Repo(WORK_DIR)
        repo.git.pull(repo_url)
    else:
        repo = Repo.clone_from(repo_url, WORK_DIR)

    return repo</pre>



<div class="wp-block-jetpack-markdown"><h3>2. Branch Creation with Deduplication</h3>
<p>Each fix gets its own branch with a timestamp. If the branch already exists remotely, we skip it to avoid duplicate PRs:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">def _create_fix_branch(repo: Repo, error: LogEntry) -&gt; str | None:
    branch_name = f"autofix/{error.fix_short_name}_{error.timestamp.strftime('%Y%m%d-%H%M%S')}"

    remote_refs = [ref.name for ref in repo.remote().refs]
    if f"origin/{branch_name}" in remote_refs:
        logger.info(f"Branch {branch_name} already exists, skipping")
        return None

    new_branch = repo.create_head(branch_name)
    new_branch.checkout()
    return branch_name</pre>



<div class="wp-block-jetpack-markdown"><h3>3. The Magic: Claude Code SDK</h3>
<p>This is where the actual fix happens. Claude Code SDK allows Claude to read and edit files in the codebase:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">from claude_code_sdk import ClaudeCodeOptions, query

async def _invoke_claude_fix(error_message: str) -&gt; str | None:
    prompt = f"Fix this error in the codebase: {error_message}"

    options = ClaudeCodeOptions(
        cwd=str(WORK_DIR),
        allowed_tools=["Read", "Edit"]  # Safe: no Write, no Bash
    )

    response = None
    async for response in query(prompt=prompt, options=options):
        logger.info(f"Claude response: {response}")

    return response.result if response else None</pre>



<div class="wp-block-jetpack-markdown"><p>Note that we only allow <code>Read</code> and <code>Edit</code> tools &#8211; no <code>Write</code> (creating new files) or <code>Bash</code> (running commands). This keeps the fixes focused and safe.</p>
<h3>4. PR Title Generation with Claude Haiku</h3>
<p>For fast and cheap PR title generation, I use Claude Haiku with structured output:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">from pydantic import BaseModel, Field

class PrTitleModel(BaseModel):
    pr_title: str = Field(..., description="Concise PR title")
    pr_description: str = Field(..., description="Detailed PR description")

def pr_title_generator(response: str) -&gt; PrTitleModel:
    agent = create_agent(
        system_prompt=PR_PROMPT,
        model=Models.CLAUDE_45_HAIKU,
        tools=[]
    )

    result = agent(
        prompt=f"This is response from claude code: {response}\n\n"
               f"Generate a concise title for a GitHub pull request.",
        structured_output_model=PrTitleModel
    )

    return result.structured_output</pre>



<div class="wp-block-jetpack-markdown"><p>The prompt enforces Conventional Commits style:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">PR_PROMPT = """
You are an assistant expert in generating pull request titles for GitHub.
OBJECTIVE:
- Generate concise and descriptive titles for pull requests.
- IMPORTANT: Use Conventional Commits as a style reference.
CRITERIA:
- The title must summarize the main changes or fixes.
- Keep the title under 10 words.</pre>



<div class="wp-block-jetpack-markdown"><h3>5. Commit, Push, and Create PR</h3>
<p>Finally, we commit everything, push to the remote, and create the PR via GitHub API:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">def _commit_and_push(repo: Repo, branch_name: str, pr_info: PrTitleModel) -&gt; None:
    repo.git.add(A=True)
    repo.index.commit(pr_info.pr_title)
    repo.git.push(get_authenticated_repo_url(), branch_name)

def _create_pull_request(branch_name: str, pr_info: PrTitleModel) -&gt; None:
    gh = Github(GITHUB_TOKEN)
    gh_repo = gh.get_repo(GITHUB_REPO)
    gh_repo.create_pull(
        title=pr_info.pr_title,
        body=pr_info.pr_description,
        head=branch_name,
        base="main"
    )</pre>



<div class="wp-block-jetpack-markdown"><h2>The Triage Agent: Deciding What to Fix</h2>
<p>The tool is exposed to a triage agent that analyzes logs and decides when to use it. The agent follows the <strong>ReAct pattern</strong> (Reasoning + Acting), where it explicitly reasons about each error before deciding to act:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">TRIAGE_PROMPT = """
You are a senior DevOps engineer performing triage of production errors.

REGISTRATION CRITERIA:
- The error may be occurring frequently. Register ONLY ONCE.
- The error has a clear stacktrace that indicates the root cause.
- The error can be corrected with a quick fix.

DISCARD CRITERIA:
✗ Single/isolated errors (may be malicious input)
✗ Errors from external services (network, timeouts)
✗ Errors without a clear stacktrace
✗ Errors that require business decisions

Use the ReAct pattern:
Thought: [your analysis of the error]
Action: [register_error_for_fix if criteria met]
Observation: [tool result]
... (repeat for each error type)
Final Answer: [summary of registered errors]</pre>



<div class="wp-block-jetpack-markdown"><p>This pattern forces the agent to reason explicitly before taking action, making decisions more transparent and debuggable.</p>
<p>The agent is given tools and makes the decision autonomously:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">agent = create_agent(
    system_prompt=TRIAGE_PROMPT,
    model=Models.CLAUDE_45,
    tools=[register_error_for_fix]
)

result = agent(prompt=[
    {"text": f"Question: {question}"},
    {"text": f"Log context: {logs_json}"},
])</pre>



<div class="wp-block-jetpack-markdown"><p>To test the system, I created a sample repository with intentional bugs and generated CloudWatch-like logs. The triage agent analyzes the logs, identifies fixable errors, and invokes the <code>register_error_for_fix</code> tool to create PRs automatically.</p>
<p>That’s the code (with the bug):</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">import logging
import traceback

from flask import Flask, jsonify

from lib.logger import setup_logging
from settings import APP, PROCESS, LOG_PATH, ENVIRONMENT

logger = logging.getLogger(__name__)

app = Flask(__name__)

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

for logger_name in ["werkzeug"]:
    logging.getLogger(logger_name).setLevel(logging.CRITICAL)


@app.errorhandler(Exception)
def handle_exception(e):
    logger.error(
        "Unhandled exception: %s",
        e,
        extra={"traceback": traceback.format_exc()},
    )
    return jsonify(error=str(e)), 500


@app.get("/div/&lt;int:a&gt;/&lt;int:b&gt;")
def divide(a: int, b: int):
    return dict(result=a / b)</pre>



<div class="wp-block-jetpack-markdown"><p>As you can see, the <code>/div//</code> endpoint has a bug: it does not handle division by zero properly. We have executed the error and generated logs accordingly. As we have the logs in CloudWatch’s log group /projects/autofix we can execute a command to analyze them:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">pyhon cli.py log --group /projects/autofix --question "Analyze those logs" --start 2026-01-16</pre>



<div class="wp-block-jetpack-markdown"><p>The AI agent will identify the division by zero error, decide it is fixable, and create a PR that modifies the code (using claude code in headless mode) to handle this case properly.</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="337" data-attachment-id="87839" data-permalink="https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/github/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github.png?fit=1658%2C853&amp;ssl=1" data-orig-size="1658,853" 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="github" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github.png?fit=300%2C154&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github.png?fit=656%2C337&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github.png?resize=656%2C337&#038;ssl=1" alt="" class="wp-image-87839" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>And that’s all! The AI agent has autonomously created a PR that fixes the bug. Now we can easily accept or reject the PR after human review. The bug has been fixed!</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github2.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="282" data-attachment-id="87841" data-permalink="https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/github2/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github2.png?fit=2806%2C1208&amp;ssl=1" data-orig-size="2806,1208" 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="github2" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github2.png?fit=300%2C129&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github2.png?fit=656%2C282&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2026/01/github2.png?resize=656%2C282&#038;ssl=1" alt="" class="wp-image-87841" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>This experiment shows that AI agents can go beyond analysis to take action. By giving Claude Code SDK access to a sandboxed environment with limited tools (<code>Read</code>, <code>Edit</code> only), we get a system that can autonomously fix bugs while remaining controllable.</p>
<p>The key is setting clear boundaries: the triage agent decides <em>what</em> to fix based on strict criteria, and the fix agent is constrained to <em>how</em> it can modify code. This separation keeps the system predictable and safe.</p>
<p>Full code in my <a href="https://github.com/gonzalo123/autofix">github</a></p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/01/26/what-if-the-bug-fixed-itself-letting-ai-agents-detect-bugs-fix-the-code-and-create-prs-proactively/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">87828</post-id>	</item>
		<item>
		<title>Using Map-Reduce to process large documents with AI Agents and Python</title>
		<link>https://gonzalo123.com/2026/01/12/using-map-reduce-to-process-large-documents-with-ai-agents-and-python/</link>
					<comments>https://gonzalo123.com/2026/01/12/using-map-reduce-to-process-large-documents-with-ai-agents-and-python/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 12 Jan 2026 13:15:59 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[artificial-intelligence]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<category><![CDATA[technology]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=84114</guid>

					<description><![CDATA[Full code in my github]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>We live in the era of Large Language Models (LLMs) with massive context windows. Claude 3.5 Sonnet offers 200k tokens, and Gemini 1.5 Pro goes up to 2 million. So, why do we still need to worry about document processing strategies? The answer is yes, we do. For example, AWS Bedrock has a strict limit of 4.5MB for documents, regardless of token count. That’s means we can’t just stuff file greater than 4.5MB into a prompt. Today we’ll show you how I built a production-ready document processing agent that handles large files by implementing a <strong>Map-Reduce</strong> pattern using Python, <strong>AWS Bedrock</strong>, and <strong>Strands Agents</strong>.</p>
<p>The core idea is simple: instead of asking the LLM to “read this book and answer” we break the book into chapters, analyze each chapter in parallel, and then synthesize the results.</p>
<p>Here is the high-level flow:</p>
</div>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="495" height="825" data-attachment-id="84116" data-permalink="https://gonzalo123.com/2026/01/12/using-map-reduce-to-process-large-documents-with-ai-agents-and-python/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python.png?fit=495%2C825&amp;ssl=1" data-orig-size="495,825" 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_file_big_ia__Using_Map-Reduce_to_process_large_documents_with_AI_Agents_and_Python" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python.png?fit=180%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python.png?fit=495%2C825&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/gonzalo123_file_big_ia__using_map-reduce_to_process_large_documents_with_ai_agents_and_python.png?resize=495%2C825&#038;ssl=1" alt="" class="wp-image-84116" /></a></figure>
</div>


<div class="wp-block-jetpack-markdown"><p>The heart of the implementation is the <code>DocumentProcessor</code> class. It decides whether to process a file as a whole or split it based on a size threshold. We define a threshold (e.g., 4.3MB) to stay safely within Bedrock’s limits. If the file is larger, we trigger the <code>_process_big</code> method.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code"># src/lib/processor/processor.py

BYTES_THRESHOLD = 4_300_000

async def _process_file(self, file: DocumentFile, question: str, with_callback=True):
    file_bytes = Path(file.path).read_bytes()
    # Strategy pattern: Choose the right processor based on file size
    processor = self._process_big if len(file_bytes) &gt; BYTES_THRESHOLD else self._process
    async for chunk in processor(file_bytes, file, question, with_callback):
        yield chunk</pre>



<div class="wp-block-jetpack-markdown"><p>To increase the performance, we use asyncio to process the file in parallel and we use a semaphore to control the number of workers.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">async def _process_big(self, file_bytes: bytes, file: DocumentFile, question: str, with_callback=True) -&gt; AsyncIterator[str]:
    # ... splitting logic ...
    semaphore = asyncio.Semaphore(self.max_workers)

    # Create async tasks for each chunk
    tasks = [
        self._process_chunk(chunk, i, file_name, question, handler.format, semaphore)
        for i, chunk in enumerate(chunks, 1)
    ]

    # Run in parallel
    results = await asyncio.gather(*tasks)
    
    # Sort results to maintain document order
    results.sort(key=lambda x: x[0])
    responses_from_chunks = [response for _, response in results]</pre>



<div class="wp-block-jetpack-markdown"><p>Each chunk is processed by an isolated agent instance that only sees that specific fragment and the user’s question. Once we have the partial analyses, we consolidate them. This acts as a compression step: we’ve turned raw pages into relevant insights.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">def _consolidate_and_truncate(self, responses: list[str], num_chunks: int) -&gt; str:
    consolidated = "\n\n".join(responses)
    
    if len(consolidated) &gt; MAX_CONTEXT_CHARS:
        # Safety mechanism to ensure we don't overflow the final context
        return consolidated[:MAX_CONTEXT_CHARS] + "\n... [TRUNCATED]"
    return consolidated</pre>



<div class="wp-block-jetpack-markdown"><p>Finally, we feed this consolidated context to the agent for the final answer. In a long-running async process, feedback is critical. I implemented an Observer pattern to decouple the processing logic from the UI/Logging.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code"># src/main.py

class DocumentProcessorEventListener(ProcessingEventListener):
    async def on_chunk_start(self, chunk_number: int, file_name: str):
        logger.info(f"[Worker {chunk_number}] Processing chunk for file {file_name}")

    async def on_chunk_end(self, chunk_number: int, file_name: str, response: str):
        logger.info(f"[Worker {chunk_number}] Completed chunk for file {file_name}")</pre>



<div class="wp-block-jetpack-markdown"><p>By breaking down large tasks, we not only bypass technical limits but often get better results. The model focuses on smaller sections, reducing hallucinations, and the final answer is grounded in a pre-processed summary of facts.</p>
<p>We don’t just send text; we send the raw document bytes. This allows the model (Claude 4.5 Sonnet via Bedrock) to use its native document processing capabilities. Here is how we construct the message payload:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code"># src/lib/processor/processor.py

def _create_document_message(self, file_format: str, file_name: str, file_bytes: bytes, text: str) -&gt; list:
    return [
        {
            "role": "user",
            "content": [
                {
                    "document": {
                        "format": file_format,
                        "name": file_name,
                        "source": {"bytes": file_bytes},
                    },
                },
                {"text": text},
            ],
        },
    ]</pre>



<div class="wp-block-jetpack-markdown"><p>When processing chunks, we don’t want the model to be chatty. We need raw information extraction. We use a “Spartan” system prompt that enforces brevity and objectivity, ensuring the consolidation phase receives high-signal input.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code"># src/lib/processor/prompts.py

SYSTEM_CHUNK_PROMPT = f"""
You are an artificial intelligence assistant specialized in reading and analyzing files.
You have received a chunk of a large file.
...
If the user's question cannot be answered with the information in the current chunk, do not answer it directly.

{SYSTEM_PROMPT_SPARTAN}</pre>



<div class="wp-block-jetpack-markdown"><p>The <code>SYSTEM_PROMPT_SPARTAN</code> (injected above) explicitly forbids conversational filler, ensuring we maximize the token budget for actual data.</p>
<p>The project handles pdf and xlsx files. The rest of the file types are not processed and are given to the LLM as-is.</p>
<p>With this architecture, we can process large files in a production environment. This allows us to easily plug in different interfaces, whether it’s a CLI logger (as shown) or a WebSocket update for a UI frontend like Chainlit.</p>
</div>



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



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2026/01/12/using-map-reduce-to-process-large-documents-with-ai-agents-and-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">84114</post-id>	</item>
		<item>
		<title>Chat with your Data: Building a File-Aware AI Agent with AWS Bedrock and Chainlit</title>
		<link>https://gonzalo123.com/2025/12/09/chat-with-your-data-building-a-file-aware-ai-agent-with-aws-bedrock-and-chainlit/</link>
					<comments>https://gonzalo123.com/2025/12/09/chat-with-your-data-building-a-file-aware-ai-agent-with-aws-bedrock-and-chainlit/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 12:58:16 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[artificial-intelligence]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[Chainlit]]></category>
		<category><![CDATA[chatgpt]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[technology]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=84072</guid>

					<description><![CDATA[The main agent loop handles the conversation. It checks for uploaded files, processes them, and constructs the message payload for the LLM. We also include robust error handling to manage context window limits gracefully. By leveraging the large context window of modern models like Claude 4.5 Sonnet, we can feed entire documents directly into the &#8230; <a href="https://gonzalo123.com/2025/12/09/chat-with-your-data-building-a-file-aware-ai-agent-with-aws-bedrock-and-chainlit/" class="more-link">Continue reading <span class="screen-reader-text">Chat with your Data: Building a File-Aware AI Agent with AWS Bedrock and Chainlit</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>We all know LLMs are powerful, but their true potential is unlocked when they can see your data. While RAG (Retrieval-Augmented Generation) is great for massive knowledge bases, sometimes you just want to drag and drop a file and ask questions about it.</p>
<p>Today we’ll build a “File-Aware” AI agent that can natively understand a wide range of document formats—from PDFs and Excel sheets to Word docs and Markdown files. We’ll use <strong>AWS Bedrock</strong> with <strong>Claude 4.5 Sonnet</strong> for the reasoning engine and <strong>Chainlit</strong> for the conversational UI.</p>
<p>The idea is straightforward: Upload a file, inject it into the model’s context, and let the LLM do the rest. No vector databases, no complex indexing pipelines—just direct context injection for immediate analysis.</p>
<p>The architecture is simple yet effective. We intercept file uploads in the UI, process them into a format the LLM understands, and pass them along with the user’s query.</p>
<pre><code>┌──────────────┐      ┌──────────────┐      ┌────────────────────┐
│   Chainlit   │      │  Orchestrator│      │   AWS Bedrock      │
│      UI      │─────►│    Agent     │─────►│(Claude 4.5 Sonnet) │
└──────┬───────┘      └──────────────┘      └────────────────────┘
       │                      ▲
       │    ┌────────────┐    │
       └───►│ File Proc. │────┘
            │   Logic    │
            └────────────┘
</code></pre>
<p>The tech stack includes:</p>
<ul>
<li><strong>AWS Bedrock</strong> with <strong>Claude 4.5 Sonnet</strong> for high-quality reasoning and large context windows.</li>
<li><strong>Chainlit</strong> for a chat-like interface with native file upload support.</li>
<li><strong>Python</strong> for the backend logic.</li>
</ul>
<p>The core challenge is handling different file types and presenting them to the LLM. We support a variety of formats by mapping them to Bedrock’s expected input types.</p>
</div>



<div class="wp-block-jetpack-markdown"><p>To enable file uploads in Chainlit, you need to configure the <code>[features.spontaneous_file_upload]</code> section in your <code>.chainlit/config.toml</code>. This is where you define which MIME types are accepted.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">[features.spontaneous_file_upload]
    enabled = true
    accept = [
        "application/pdf",
        "text/csv",
        "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "text/html",
        "text/plain",
        "text/markdown",
        "text/x-markdown"
    ]
    max_files = 20
    max_size_mb = 500</pre>



<pre class="wp-block-preformatted">The main agent loop handles the conversation. It checks for uploaded files, processes them, and constructs the message payload for the LLM. We also include robust error handling to manage context window limits gracefully.</pre>



<pre class="wp-block-syntaxhighlighter-code">def get_question_from_message(message: cl.Message):
    content_blocks = None
    if message.elements:
        content_blocks = get_content_blocks_from_message(message)

    if content_blocks:
        content_blocks.append({"text": message.content or "Write a summary of the document"})
        question = content_blocks
    else:
        question = message.content

    return question


def get_content_blocks_from_message(message: cl.Message):
    docs = [f for f in message.elements if f.type == "file" and f.mime in MIME_MAP]
    content_blocks = []

    for doc in docs:
        file = Path(doc.path)
        file_bytes = file.read_bytes()
        shutil.rmtree(file.parent)

        content_blocks.append({
            "document": {
                "name": sanitize_filename(doc.name),
                "format": MIME_MAP[doc.mime],
                "source": {"bytes": file_bytes}
            }
        })

    return content_blocks

@cl.on_message
async def handle_message(message: cl.Message):
    task = asyncio.create_task(process_user_task(
        question=get_question_from_message(message),
        debug=DEBUG))
    cl.user_session.set("task", task)
    try:
        await task
    except asyncio.CancelledError:
        logger.info("User task was cancelled.")</pre>



<div class="wp-block-jetpack-markdown"><p>This pattern allows for <strong>ad-hoc analysis</strong>. You don’t need to pre-ingest data. You can:</p>
<ol>
<li><strong>Analyze Financials:</strong> Upload an Excel sheet and ask for trends.</li>
<li><strong>Review Contracts:</strong> Upload a PDF and ask for clause summaries.</li>
<li><strong>Debug Code:</strong> Upload a source file and ask for a bug fix.</li>
</ol>
</div>



<pre class="wp-block-preformatted">By leveraging the large context window of modern models like Claude 4.5 Sonnet, we can feed entire documents directly into the prompt, providing the model with full visibility without the information loss often associated with RAG chunking.<br><br>And that's all. With tools like Chainlit and powerful APIs like AWS Bedrock, we can create robust, multi-modal assistants that integrate seamlessly into our daily workflows.</pre>



<p class="wp-block-paragraph">Full code in my <a href="https://github.com/gonzalo123/file.ia">github</a> account.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2025/12/09/chat-with-your-data-building-a-file-aware-ai-agent-with-aws-bedrock-and-chainlit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">84072</post-id>	</item>
		<item>
		<title>Building scalable multi-purpose AI agents: Orchestrating Multi-Agent Systems with Strands Agents and Chainlit</title>
		<link>https://gonzalo123.com/2025/11/24/building-scalable-multi-purpose-ai-agents-orchestrating-multi-agent-systems-with-strands-agents-and-chainlit/</link>
					<comments>https://gonzalo123.com/2025/11/24/building-scalable-multi-purpose-ai-agents-orchestrating-multi-agent-systems-with-strands-agents-and-chainlit/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 24 Nov 2025 13:18:21 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[artificial-intelligence]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Bedrock]]></category>
		<category><![CDATA[Chainlit]]></category>
		<category><![CDATA[Claude]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<category><![CDATA[technology]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=84061</guid>

					<description><![CDATA[The orchestrator receives specialized agents as&#160;tools: Each specialized agent follows a consistent pattern. Here&#8217;s the weather agent: Each agent has access to domain-specific tools. For example, the weather agent uses external APIs: The logistics and production agents use synthetic data generators for demonstration: For UI we&#8217;re going to use&#160;Chainlit. The Chainlit integration provides real-time visibility &#8230; <a href="https://gonzalo123.com/2025/11/24/building-scalable-multi-purpose-ai-agents-orchestrating-multi-agent-systems-with-strands-agents-and-chainlit/" class="more-link">Continue reading <span class="screen-reader-text">Building scalable multi-purpose AI agents: Orchestrating Multi-Agent Systems with Strands Agents and Chainlit</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>We can build simple AI agents that handle specific tasks quite easily today. But what about building AI systems that can handle multiple domains effectively? One approach is to create a single monolithic agent that tries to do everything, but this quickly runs into problems of context pollution, maintenance complexity, and scaling limitations. In this article, we’ll show a production-ready pattern for building multi-purpose AI systems using an <strong>orchestrator architecture</strong> that coordinates domain-specific agents.</p>
<p>The idea is simple: <strong>Don’t build one agent to rule them all</strong> instead, create specialized agents that excel in their domains and coordinate them through an intelligent orchestrator. The solution is an <strong>orchestrator agent</strong> that routes requests to <strong>specialized sub-agents</strong>, each with focused expertise and dedicated tools. Think of it as a smart router that understands intent and delegates accordingly.</p>
<p>That’s the core of the <strong>Orchestrator Pattern</strong> for multi-agent systems:</p>
<pre><code>User Query → Orchestrator Agent → Specialized Agent(s) → Orchestrator → Response
</code></pre>
<p>For our example we have three specialized agents:</p>
<ol>
<li><strong>Weather Agent</strong>: Expert in meteorological data and weather patterns. It uses external weather APIs to fetch historical and current weather data.</li>
<li><strong>Logistics Agent</strong>: Specialist in supply chain and shipping operations. Fake logistics data is generated to simulate shipment tracking, route optimization, and delivery performance analysis.</li>
<li><strong>Production Agent</strong>: Focused on manufacturing operations and production metrics. Also, fake production data is generated to analyze production KPIs.</li>
</ol>
<p>That’s the architecture in a nutshell:</p>
<pre><code>┌─────────────────────────────────────────────┐
│          Orchestrator Agent                 │
│  (Routes &amp;amp; Synthesizes)                 │
└────────┬─────────┬─────────┬────────────────┘
         │         │         │
    ┌────▼────┐ ┌──▼─────┐ ┌─▼─────────┐
    │ Weather │ │Logistic│ │Production │
    │  Agent  │ │ Agent  │ │  Agent    │
    └────┬────┘ └──┬─────┘ └┬──────────┘
         │         │        │
    ┌────▼────┐ ┌──▼─────┐ ┌▼──────────┐
    │External │ │Database│ │ Database  │
    │   API   │ │ Tools  │ │  Tools    │
    └─────────┘ └────────┘ └───────────┘
</code></pre>
<p>The tech stack includes:</p>
<ul>
<li><strong>AWS Bedrock</strong> with Claude 4.5 Sonnet for agent reasoning</li>
<li><strong>Strands Agents</strong> framework for agent orchestration</li>
<li><strong>Chainlit</strong> for the conversational UI</li>
<li><strong>FastAPI</strong> for the async backend</li>
<li><strong>PostgreSQL</strong> for storing conversation history and domain data</li>
</ul>
<p>The orchestrator’s job is simple but critical: understand the user’s intent and route to the right specialist(s).</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">MAIN_SYSTEM_PROMPT = """You are an intelligent orchestrator agent 
responsible for routing user requests to specialized sub-agents 
based on their domain expertise.

## Available Specialized Agents

### 1. Production Agent
**Domain**: Manufacturing operations, production metrics, quality control
**Handles**: Production KPIs, machine performance, downtime analysis

### 2. Logistics Agent
**Domain**: Supply chain, shipping, transportation operations
**Handles**: Shipment tracking, route optimization, delivery performance

### 3. Weather Agent
**Domain**: Meteorological data and weather patterns
**Handles**: Historical weather, atmospheric conditions, climate trends

## Your Decision Process
1. Analyze the request for key terms and domains
2. Determine scope (single vs multi-domain)
3. Route to appropriate agent(s)
4. Synthesize results when multiple agents are involved
"""</pre>



<p class="wp-block-paragraph">The orchestrator receives specialized agents as&nbsp;<strong>tools</strong>:</p>



<pre class="wp-block-syntaxhighlighter-code">def get_orchestrator_tools() -&gt; List[Any]:
    from tools.logistics.agent import logistics_assistant
    from tools.production.agent import production_assistant
    from tools.weather.agent import weather_assistant

    tools = [
        calculator,
        think,
        current_time,
        AgentCoreCodeInterpreter(region=AWS_REGION).code_interpreter,
        logistics_assistant,  # Specialized agent as tool
        production_assistant,  # Specialized agent as tool
        weather_assistant     # Specialized agent as tool
    ]
    return tools</pre>



<p class="wp-block-paragraph">Each specialized agent follows a consistent pattern. Here&#8217;s the weather agent:</p>



<pre class="wp-block-syntaxhighlighter-code">@tool
@stream_to_step("weather_assistant")
async def weather_assistant(query: str):
    """
    A research assistant specialized in weather topics with streaming support.
    """
    try:
        tools = [
            calculator,
            think,
            current_time,
            AgentCoreCodeInterpreter(region=AWS_REGION).code_interpreter
        ]
        # Domain-specific tools
        tools += WeatherTools(latitude=MY_LATITUDE, longitude=MY_LONGITUDE).get_tools()

        research_agent = get_agent(
            system_prompt=WEATHER_ASSISTANT_PROMPT,
            tools=tools
        )

        async for token in research_agent.stream_async(query):
            yield token

    except Exception as e:
        yield f"Error in research assistant: {str(e)}"</pre>



<p class="wp-block-paragraph">Each agent has access to domain-specific tools. For example, the weather agent uses external APIs:</p>



<pre class="wp-block-syntaxhighlighter-code">class WeatherTools:
    def __init__(self, latitude: float, longitude: float):
        self.latitude = latitude
        self.longitude = longitude

    def get_tools(self) -&gt; List[tool]:
        @tool
        def get_hourly_weather_data(from_date: date, to_date: date) -&gt; MeteoData:
            """Get hourly weather data for a specific date range."""
            url = (f"https://api.open-meteo.com/v1/forecast?"
                   f"latitude={self.latitude}&amp;longitude={self.longitude}&amp;"
                   f"hourly=temperature_2m,relative_humidity_2m...")
            response = requests.get(url)
            return parse_weather_response(response.json())
        
        return [get_hourly_weather_data]</pre>



<p class="wp-block-paragraph">The logistics and production agents use synthetic data generators for demonstration:</p>



<pre class="wp-block-syntaxhighlighter-code">class LogisticsTools:
    def get_tools(self) -&gt; List[tool]:
        @tool
        def get_logistics_data(
            from_date: date,
            to_date: date,
            origins: Optional[List[str]] = None,
            destinations: Optional[List[str]] = None,
        ) -&gt; LogisticsDataset:
            """Generate synthetic logistics shipment data."""
            # Generate realistic shipment data with delays, costs, routes
            records = generate_synthetic_shipments(...)
            return LogisticsDataset(records=records, aggregates=...)
        
        return [get_logistics_data]</pre>



<p class="wp-block-paragraph">For UI we&#8217;re going to use&nbsp;<a href="https://chainlit.io/">Chainlit</a>. The Chainlit integration provides real-time visibility into agent execution:</p>



<pre class="wp-block-syntaxhighlighter-code">class LoggingHooks(HookProvider):
    async def before_tool(self, event: BeforeToolCallEvent) -&gt; None:
        step = cl.Step(name=f"{event.tool_use['name']}", type="tool")
        await step.send()
        cl.user_session.set(f"step_{event.tool_use['name']}", step)

    async def after_tool(self, event: AfterToolCallEvent) -&gt; None:
        step = cl.user_session.get(f"step_{event.tool_use['name']}")
        if step:
            await step.update()

@cl.on_message
async def handle_message(message: cl.Message):
    agent = cl.user_session.get("agent")
    message_history = cl.user_session.get("message_history")
    message_history.append({"role": "user", "content": message.content})
    
    response = await agent.run_async(message.content)
    await cl.Message(content=response).send()</pre>



<p class="wp-block-paragraph">This creates a transparent experience where users see:</p>



<ul class="wp-block-list">
<li>Which agent is handling their request</li>



<li>What tools are being invoked</li>



<li>Real-time streaming of responses</li>
</ul>



<p class="wp-block-paragraph">Now we can handle a variety of user queries: For example:</p>



<p class="wp-block-paragraph"><strong>User</strong>: &#8220;What was the average temperature last week?&#8221;</p>



<div class="wp-block-jetpack-markdown"><p>Flow:</p>
<ol>
<li>Orchestrator identifies weather domain</li>
<li>Routes to weather_assistant</li>
<li>Weather agent calls get_hourly_weather_data</li>
<li>Analyzes and returns formatted response</li>
</ol>
</div>



<p class="wp-block-paragraph">Or multi-domain queries:</p>



<p class="wp-block-paragraph"><strong>User</strong>: &#8220;Did weather conditions affect our shipment delays yesterday?&#8221;</p>



<div class="wp-block-jetpack-markdown"><p>Flow:</p>
<ol>
<li>Orchestrator identifies weather + logistics domains</li>
<li>Routes to weather_assistant for climate data</li>
<li>Routes to logistics_assistant for shipment data</li>
<li>Synthesizes correlation analysis</li>
<li>Returns unified insight</li>
</ol>
</div>



<p class="wp-block-paragraph">And complex analytics:</p>



<p class="wp-block-paragraph"><strong>User</strong>: &#8220;Analyze production efficiency trends and correlate with weather and logistics performance based in yesterday&#8217;s data.&#8221;</p>



<div class="wp-block-jetpack-markdown"><p>Flow:</p>
<ol>
<li>Orchestrator coordinates all three agents</li>
<li>Production agent retrieves manufacturing KPIs</li>
<li>Weather agent provides environmental data</li>
<li>Logistics agent supplies delivery metrics</li>
<li>Orchestrator synthesizes multi-domain analysis</li>
</ol>
</div>



<p class="wp-block-paragraph">This architecture scales naturally in multiple dimensions. We can easily add new specialized agents without disrupting existing functionality. WE only need to create the new agent and register it as a tool with the orchestratortrator prompt with new domain description. That&#8217;s it.</p>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/chainlit_screenshot.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="823" data-attachment-id="84067" data-permalink="https://gonzalo123.com/2025/11/24/building-scalable-multi-purpose-ai-agents-orchestrating-multi-agent-systems-with-strands-agents-and-chainlit/chainlit_screenshot/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/chainlit_screenshot.png?fit=753%2C945&amp;ssl=1" data-orig-size="753,945" 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="chainlit_screenshot" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/chainlit_screenshot.png?fit=239%2C300&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/chainlit_screenshot.png?fit=656%2C823&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/11/chainlit_screenshot.png?resize=656%2C823&#038;ssl=1" alt="" class="wp-image-84067" /></a></figure>



<p class="wp-block-paragraph">The orchestrator pattern transforms multi-domain AI from a monolithic challenge into a composable architecture. Each agent focuses on what it does best, while the orchestrator provides intelligent coordination.</p>



<p class="wp-block-paragraph">Full code in my <a href="https://github.com/gonzalo123/multi.ia">github</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2025/11/24/building-scalable-multi-purpose-ai-agents-orchestrating-multi-agent-systems-with-strands-agents-and-chainlit/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">84061</post-id>	</item>
		<item>
		<title>Implementing a Kafka Producer and Consumer in Python</title>
		<link>https://gonzalo123.com/2025/11/10/implementing-a-kafka-producer-and-consumer-in-python/</link>
					<comments>https://gonzalo123.com/2025/11/10/implementing-a-kafka-producer-and-consumer-in-python/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 10 Nov 2025 13:18:18 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[kafka]]></category>
		<category><![CDATA[Queue]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=83229</guid>

					<description><![CDATA[Today we&#8217;re going to play with Kafka. We&#8217;ll implement a simple producer and consumer in Python using the&#160;kafka-python&#160;library. The project consists of two main components: First tne producer. It uses a dedicated class to send messages to a Kafka topic. One consumer. It Listens to a Kafka topic, processes messages received, and commits their offsets. &#8230; <a href="https://gonzalo123.com/2025/11/10/implementing-a-kafka-producer-and-consumer-in-python/" class="more-link">Continue reading <span class="screen-reader-text">Implementing a Kafka Producer and Consumer in Python</span></a>]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Today we&#8217;re going to play with Kafka. We&#8217;ll implement a simple producer and consumer in Python using the&nbsp;<code>kafka-python</code>&nbsp;library. The project consists of two main components: First tne producer. It uses a dedicated class to send messages to a Kafka topic. One consumer. It Listens to a Kafka topic, processes messages received, and commits their offsets. Communication with Kafka is handled by a helper module that encapsulates producer and consumer configurations. The setup uses Docker Compose to manage the Kafka broker and supporting services such as Zookeeper.</p>



<p class="wp-block-paragraph">Below is a simplified Producer class and corresponding function:</p>



<pre class="wp-block-syntaxhighlighter-code">import json
import logging

from jsonencoder import DefaultEncoder
from kafka import KafkaProducer

from settings import KAFKA_BOOTSTRAP_SERVERS

logger = logging.getLogger(__name__)


def get_producer():
    return KafkaProducer(
        bootstrap_servers=KAFKA_BOOTSTRAP_SERVERS,
        value_serializer=lambda data: json.dumps(data, cls=DefaultEncoder).encode('utf-8')
    )


class Producer:
    def __init__(self):
        self.producer = get_producer()

    def send(self, topic: str, message: any):
        try:
            self.producer.send(topic, value=message)
            self.producer.flush()
            logger.info(f"Message sent to topic: {topic}: {message}")
        except Exception as e:
            logger.error(f"Error sending message to {topic}: {str(e)}")
            raise
        finally:
            if self.producer:
                self.producer.close()


def send_message(topic: str, message: any):
    producer = Producer()
    producer.send(topic, message)</pre>



<p class="wp-block-paragraph">We&#8217;re using click to build the command line interface.</p>



<pre class="wp-block-syntaxhighlighter-code">import click
from datetime import datetime
from lib.kafka_broker import send_message


@click.command()
@click.option('--topic', required=True, help='topic')
@click.option('--message', required=True, help='message')
def run(topic, message):
    send_message(topic, dict(
        timestamp=datetime.now().isoformat(),
        body=message
    ))</pre>



<p class="wp-block-paragraph">The consumer processes messages by consuming them from a Kafka topic. When a message is received, it gets logged and the consumer commits the offsets, ensuring that no message is processed more than once. The consumer functionality is implemented in a callback that is passed as a parameter to the topic consumption function.</p>



<p class="wp-block-paragraph">Below is the consumer’s function definition and command setup:</p>



<pre class="wp-block-syntaxhighlighter-code">import logging
import click
from kafka import KafkaConsumer
from kafka.protocol.message import Message
from lib.kafka_broker import consume_topic

logger = logging.getLogger(__name__)

def process_message(message: Message, consumer: KafkaConsumer) -&gt; None:
    logger.info(f"received message: {message.value}")
    consumer.commit()

@click.command()
@click.option('--topic', required=True, help='topic')
def run(topic):
    consume_topic(topic, process_message)</pre>



<p class="wp-block-paragraph">The consume_topic function (from lib/kafka_broker.py) configures the Kafka consumer to listen to a specific topic. On receipt of each message, the process_mensaje callback handles the message by logging information and committing the consumer’s current offset.</p>



<pre class="wp-block-syntaxhighlighter-code">import json
import logging
from typing import Protocol

from kafka import KafkaConsumer
from kafka.protocol.message import Message

from settings import KAFKA_BOOTSTRAP_SERVERS

logger = logging.getLogger(__name__)

EARLIEST = 'earliest'  # Automatically reset the offset to the earliest offset.
LATEST = 'latest'  # Automatically reset the offset to the latest offset.
NONE = 'none'  # You must set the partition and index manually.


def get_consumer(topic, *,
                 auto_commit=False,
                 group_id=None,
                 auto_offset_reset=EARLIEST) -&gt; KafkaConsumer:
    return KafkaConsumer(
        topic,
        bootstrap_servers=KAFKA_BOOTSTRAP_SERVERS,
        auto_offset_reset=auto_offset_reset,
        enable_auto_commit=auto_commit,
        group_id=group_id,
        value_deserializer=lambda data: json.loads(data.decode('utf-8'))
    )


class MessageProcessorProtocol(Protocol):
    def __call__(self, message: Message, consumer: KafkaConsumer) -&gt; None:
        ...


def consume_topic(topic, callback: MessageProcessorProtocol, stop_event=None):
    logger.info(f"Listening to topic: {topic}")
    consumer = get_consumer(topic, group_id=topic)
    try:
        while stop_event is None or not stop_event.is_set():
            messages = consumer.poll(timeout_ms=1000)
            for tp, msgs in messages.items():
                for mensaje in msgs:
                    logger.info(f"Received message: {mensaje.value} "
                                f"Partition: {mensaje.partition}, "
                                f"Offset: {mensaje.offset}")
                    callback(mensaje, consumer)
    finally:
        consumer. Close()
</pre>



<p class="wp-block-paragraph">The project relies on Docker Compose to run the required Kafka and Zookeeper containers. This setup allows the application to interact with a local Kafka broker without needing complex installation processes. A simplified excerpt of the docker-compose.yml file is shown below:</p>



<pre class="wp-block-syntaxhighlighter-code"></pre>



<pre class="wp-block-syntaxhighlighter-code">version: '3'

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    networks:
      - kafka-net

  kafka:
    image: confluentinc/cp-kafka:latest
    container_name: kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
      - "29092:29092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
    networks:
      - kafka-net

networks:
  kafka-net:
    driver: bridge</pre>



<p class="wp-block-paragraph">And that&#8217;s all. Source code in my <a href="https://github.com/gonzalo123/kafka">github</a> account.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2025/11/10/implementing-a-kafka-producer-and-consumer-in-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">83229</post-id>	</item>
		<item>
		<title>Building an AI Frontend with Chainlit and OAuth2 Authentication</title>
		<link>https://gonzalo123.com/2025/09/29/building-an-ai-frontend-with-chainlit-and-oauth2-authentication/</link>
					<comments>https://gonzalo123.com/2025/09/29/building-an-ai-frontend-with-chainlit-and-oauth2-authentication/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 29 Sep 2025 11:33:33 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[agentic-ai]]></category>
		<category><![CDATA[agents]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[Chainlit]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[OAuth2]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=83817</guid>

					<description><![CDATA[Chainlit processes the JWT token via a custom header authentication callback: Full code in my github account]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-jetpack-markdown"><p>Today we’ll explore how to build a secure AI frontend using <a href="https://docs.chainlit.io/get-started/overview">Chainlit</a>. Chainlit is Python framework that allows us to create interactive AI applications. In this example we are going to reuse the weather tool created in a previous <a href="https://gonzalo123.com/2025/07/14/building-production-ready-ai-agents-with-strands-agents-and-python/">post</a>. Also, we will implement OAuth2 authentication with a Nginx as a reverse proxy.</p>
<p>The project consists of four main components:</p>
<ol>
<li><strong>Nginx Reverse Proxy</strong>: Handles authentication via <code>auth_request</code> and routes traffic</li>
<li><strong>Fake OAuth Server</strong>: Simple Flask app that simulates OAuth2 authentication</li>
<li><strong>Chainlit Application</strong>: The main chat interface with AI capabilities</li>
<li><strong>Strands AI Agent</strong>: Weather-focused AI assistant with custom tools</li>
</ol>
<p>The Nginx configuration implements OAuth2 authentication using the <code>auth_request</code> module:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">server {
    listen 8000;

    location / {
        auth_request /oauth2/auth;
        
        auth_request_set $user_jwt $upstream_http_x_user_jwt;
        add_header X-Debug-User-JWT $user_jwt always;
        
        error_page 401 = @error401;
        try_files $uri @proxy_to_app;
    }

    location = /oauth2/auth {
        internal;
        proxy_pass http://oauth2/oauth2/auth;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
        proxy_set_header X-Original-Remote-Addr $remote_addr;
        proxy_set_header X-Original-Host $host;
    }

    location @proxy_to_app {
        proxy_set_header X-User-JWT $user_jwt;
        proxy_pass http://chainlit;
    }
}</pre>



<div class="wp-block-jetpack-markdown"><p><strong>Key Features:</strong></p>
<ul>
<li>Every request to <code>/</code> triggers an authentication check via <code>/oauth2/auth</code></li>
<li>JWT token is extracted from the OAuth response and forwarded to Chainlit</li>
<li>Unauthenticated users are redirected to the OAuth sign-in page</li>
<li>The JWT token is passed to Chainlit via the <code>X-User-JWT</code> header</li>
</ul>
<p>A simple Flask application simulates an OAuth2 provider for demonstration purposes. In a production environment, you would replace this with a real OAuth2 provider or implemente the whole OAuth2 flow.</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">@app.get(f"/oauth2/auth")
def auth():
    now = datetime.now()
    response = make_response(jsonify(dict(error='OK')), 200)
    expiration = now + JWT_EXPIRATION_TIMEDELTA
    user = 'gonzalo'
    display_name = 'Gonzalo'
    response.headers['X-User-JWT'] = str(jwt.encode(dict(
        user=user,
        display_name=display_name,
        exp=int(expiration.timestamp())
    ), SECRET, algorithm=JWT_ALGORITHM))
    logger.info("Fake OAuth authentication successful")
    return response</pre>



<p class="wp-block-paragraph">Chainlit processes the JWT token via a custom header authentication callback:</p>



<pre class="wp-block-syntaxhighlighter-code">@cl.header_auth_callback
def header_auth_callback(headers: Dict) -&gt; Optional[cl.User]:
    if headers.get("x-user-jwt"):
        jwt_token = headers.get("x-user-jwt")
        try:
            decoded_payload = jwt.decode(jwt_token, SECRET, algorithms=[JWT_ALGORITHM])
            return cl.User(
                identifier=decoded_payload['user'],
                display_name=decoded_payload['display_name'],
                metadata={"role": 'user', "provider": "header"})
        except jwt.ExpiredSignatureError:
            cl.logger.error("Token has expired.")
            return None
    else:
        return None</pre>



<div class="wp-block-jetpack-markdown"><p>This callback:</p>
<ul>
<li>Extracts the JWT from the <code>x-user-jwt</code> header</li>
<li>Validates the token signature and expiration</li>
<li>Creates a Chainlit <code>User</code> object with the decoded information</li>
<li>Handles token expiration gracefully</li>
</ul>
<p>The application uses <strong>Strands agents</strong> with both base tools and custom weather tools:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">agent = get_agent(
    system_prompt=PROMPT_GENERAL,
    base_tools=get_all_base_tools(),
    custom_tools=get_all_custom_tools()
)</pre>



<div class="wp-block-jetpack-markdown"><p><strong>Base Tools Include:</strong></p>
<ul>
<li>Calculator</li>
<li>Browser access</li>
<li>Current time</li>
<li>Batch processing</li>
<li>Think (reasoning tool)</li>
</ul>
<p>The weather functionality is implemented using custom Strands tools that fetch meteorological data:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">class WeatherTools:
    def __init__(self, latitude: float, longitude: float):
        self.latitude = latitude
        self.longitude = longitude

    def get_tools(self, tools=None) -&gt; List[tool]:
        @tool
        def get_hourly_weather_data(from_date: date, to_date: date) -&gt; MeteoData:
            """
            Get hourly weather data for a specific date range in my city.
            
            Returns:
                MeteoData: Object containing weather readings for temperature, 
                          humidity, precipitation, etc.
            """
            # Implementation details...</pre>



<div class="wp-block-jetpack-markdown"><p>The weather tools provide:</p>
<ul>
<li><strong>Hourly weather data</strong> for specific date ranges</li>
<li><strong>Temperature readings</strong> (actual and apparent)</li>
<li><strong>Humidity and precipitation data</strong></li>
<li><strong>Surface pressure measurements</strong></li>
<li><strong>Evapotranspiration data</strong></li>
</ul>
<p>The Chainlit interface provides several starter prompts to help users interact with the weather agent:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">@cl.set_starters
async def set_starters():
    return [
        cl.Starter(label="Is going to rain today?", message="Is going to rain today?"),
        cl.Starter(label="tomorrow's weather", message="What will the weather be like tomorrow?"),
        cl.Starter(label="Next 7 days weather", message="Make a weather forecast for the next 7 days."),
    ]</pre>



<div class="wp-block-jetpack-markdown"><p>Chainlit also supports message history management, allowing users to see their previous interactions:</p>
</div>



<pre class="wp-block-syntaxhighlighter-code">@cl.on_message
async def handle_message(message: cl.Message):
    message_history = cl.user_session.get("message_history")
    message_history.append({"role": "user", "content": message.content})
    
    msg = cl.Message(content="")
    await msg.send()
    
    app_user = cl.user_session.get("user")
    question = f"user: {app_user.display_name} Content: {message.content}"
    
    async for event in agent.stream_async(question):
        if "data" in event:
            await msg.stream_token(str(event["data"]))
        elif "message" in event:
            await msg.stream_token("\n")
            message_history.append(event["message"])
    
    await msg.update()</pre>



<div class="wp-block-jetpack-markdown"><p>And that’s all. Thanks to Chainlit, we can build AI frontends and integrate them with OAuth2 authentication in a secure and efficient way. The combination of Chainlit’s interactive capabilities and Nginx’s robust authentication features provides a solid foundation for building AI applications that require user authentication.</p>
</div>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/08/img2.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="656" height="504" data-attachment-id="83823" data-permalink="https://gonzalo123.com/2025/09/29/building-an-ai-frontend-with-chainlit-and-oauth2-authentication/img2-2/" data-orig-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/08/img2.png?fit=915%2C703&amp;ssl=1" data-orig-size="915,703" 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="img2" data-image-description="" data-image-caption="" data-medium-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/08/img2.png?fit=300%2C230&amp;ssl=1" data-large-file="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/08/img2.png?fit=656%2C504&amp;ssl=1" src="https://i0.wp.com/gonzalo123.com/wp-content/uploads/2025/08/img2.png?resize=656%2C504&#038;ssl=1" alt="" class="wp-image-83823" /></a></figure>



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



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://gonzalo123.com/2025/09/29/building-an-ai-frontend-with-chainlit-and-oauth2-authentication/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">83817</post-id>	</item>
	</channel>
</rss>