<?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, 11 May 2026 12:24:03 +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>AI-Powered CloudWatch Logs Analysis with Python and Strands Agents</title>
		<link>https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/</link>
					<comments>https://gonzalo123.com/2026/05/11/ai-powered-cloudwatch-logs-analysis-with-python-and-strands-agents/#respond</comments>
		
		<dc:creator><![CDATA[Gonzalo Ayuso]]></dc:creator>
		<pubDate>Mon, 11 May 2026 12:24:03 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[cloudwatch]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[StrandsAgents]]></category>
		<guid isPermaLink="false">https://gonzalo123.com/?p=87732</guid>

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


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


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


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

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


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


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


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


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


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


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


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


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


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


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


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

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

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

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

[Agent analyzes logs and provides intelligent summary]

&amp;gt; Were there any timeouts?

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


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

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

    return rows
</pre></div>


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



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


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

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

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

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

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


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


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

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


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


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

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

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


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



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


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


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


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

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

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


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



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


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

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

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

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

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


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


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

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

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

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

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


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


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

</pre></div>

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

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


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



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


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

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


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


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


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

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


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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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



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

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


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


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


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


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


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


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


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


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


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


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


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


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



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

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


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

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

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

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


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

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

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

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

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

        return base_message
</pre></div>


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



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



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


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

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

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


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


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

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

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

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


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


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


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


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


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


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


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



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



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


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

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

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


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


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

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

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

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

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


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



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


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


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



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

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


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


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



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



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



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



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



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



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



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



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

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


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


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


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


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


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


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


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


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


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


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


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


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

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

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


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


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



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



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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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


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


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



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

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


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


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



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



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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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



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



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



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



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


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


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


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


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


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


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


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


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


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


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



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



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



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



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



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



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



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



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



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



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


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


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


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

@tool
async def register_error_for_fix(error: LogEntry) -&gt; bool:
    &quot;&quot;&quot;
    Register an error for automatic fixing.
    Clones repo, creates fix branch, uses Claude to fix, creates PR.
    &quot;&quot;&quot;
    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>


<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>


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

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

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

    return repo
</pre></div>


<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>


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

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

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


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
from claude_code_sdk import ClaudeCodeOptions, query

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

    options = ClaudeCodeOptions(
        cwd=str(WORK_DIR),
        allowed_tools=&#x5B;&quot;Read&quot;, &quot;Edit&quot;]  # Safe: no Write, no Bash
    )

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

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


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
from pydantic import BaseModel, Field

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

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

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

    return result.structured_output
</pre></div>


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


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
PR_PROMPT = &quot;&quot;&quot;
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>


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
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=&quot;main&quot;
    )
</pre></div>


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
TRIAGE_PROMPT = &quot;&quot;&quot;
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: &#x5B;your analysis of the error]
Action: &#x5B;register_error_for_fix if criteria met]
Observation: &#x5B;tool result]
... (repeat for each error type)
Final Answer: &#x5B;summary of registered errors]
</pre></div>


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
agent = create_agent(
    system_prompt=TRIAGE_PROMPT,
    model=Models.CLAUDE_45,
    tools=&#x5B;register_error_for_fix]
)

result = agent(prompt=&#x5B;
    {&quot;text&quot;: f&quot;Question: {question}&quot;},
    {&quot;text&quot;: f&quot;Log context: {logs_json}&quot;},
])
</pre></div>


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
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 &#x5B;&quot;werkzeug&quot;]:
    logging.getLogger(logger_name).setLevel(logging.CRITICAL)


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


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


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: bash; title: ; notranslate">
pyhon cli.py log --group /projects/autofix --question &quot;Analyze those logs&quot; --start 2026-01-16
</pre></div>


<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-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-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-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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# 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>


<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>


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

    # Create async tasks for each chunk
    tasks = &#x5B;
        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&#x5B;0])
    responses_from_chunks = &#x5B;response for _, response in results]
</pre></div>


<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>


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


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# src/main.py

class DocumentProcessorEventListener(ProcessingEventListener):
    async def on_chunk_start(self, chunk_number: int, file_name: str):
        logger.info(f&quot;&#x5B;Worker {chunk_number}] Processing chunk for file {file_name}&quot;)

    async def on_chunk_end(self, chunk_number: int, file_name: str, response: str):
        logger.info(f&quot;&#x5B;Worker {chunk_number}] Completed chunk for file {file_name}&quot;)
</pre></div>


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# src/lib/processor/processor.py

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


<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>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: python; title: ; notranslate">
# src/lib/processor/prompts.py

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

{SYSTEM_PROMPT_SPARTAN}
</pre></div>


<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>
	</channel>
</rss>
