Building Python Agents That Actually Fix Themselves
6 mins read

Building Python Agents That Actually Fix Themselves

Actually, I should clarify — I spent three hours last Tuesday fixing a daily cron job because a vendor changed a single <div class="wrapper"> to <div class="container-fluid"> on their pricing page. Three hours of my life, gone, tracing a generic NoneType object has no attribute 'text' error through a massive BeautifulSoup script.

Traditional Python automation is incredibly brittle. You write a script, it does exactly what you tell it to do, and the second the environment shifts by a single pixel, the whole thing crashes. But I ripped out the static scrapers and replaced them with an agentic loop. Instead of giving Python a rigid set of steps, I gave it a goal, a set of tools, and an LLM to figure out the routing. It cut my weekly triage time from about 3 hours to maybe 12 minutes.

And here is how you actually build one of these from scratch without getting buried in heavy framework abstractions.

The Core Agent Architecture

artificial intelligence automation - The Future of Process Automation will be Driven by Artificial ...
artificial intelligence automation – The Future of Process Automation will be Driven by Artificial …

Most people overcomplicate agents. At its core, an agent is just a while loop. It observes a state, decides if it needs to use a tool, uses the tool, and checks if it reached the goal. If yes, it stops. If no, it loops.

I run this on a t4g.small AWS instance using Python 3.12.4. You don’t need a massive machine for the orchestration part—all the heavy lifting happens on the API side. We’ll use the standard openai package and pydantic to force structured data outputs.

import json
import os
from typing import List, Optional
from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

class ExtractedData(BaseModel):
    company_name: str
    pricing_tiers: List[str]
    has_enterprise_plan: bool
    confidence_score: float = Field(ge=0.0, le=1.0)

class SimpleAgent:
    def __init__(self, system_prompt: str):
        self.system_prompt = system_prompt
        self.messages = [{"role": "system", "content": system_prompt}]
        
    def add_user_message(self, content: str):
        self.messages.append({"role": "user", "content": content})
        
    def run_extraction(self) -> Optional[ExtractedData]:
        # The agent loop
        max_retries = 3
        attempts = 0
        
        while attempts < max_retries:
            try:
                response = client.beta.chat.completions.parse(
                    model="gpt-4o",
                    messages=self.messages,
                    response_format=ExtractedData,
                    temperature=0.1
                )
                
                parsed_result = response.choices[0].message.parsed
                
                # If confidence is too low, we force it to try again
                if parsed_result.confidence_score < 0.7:
                    self.messages.append({
                        "role": "user", 
                        "content": f"Confidence too low ({parsed_result.confidence_score}). Re-evaluate the source text and try again."
                    })
                    attempts += 1
                    continue
                    
                return parsed_result
                
            except Exception as e:
                print(f"Extraction failed: {str(e)}")
                attempts += 1
                
        return None

Notice the confidence score check. That right there is the difference between a basic API call and an agentic workflow. The script evaluates its own output and decides whether to accept it or kick it back for revision.

Processing Messy Inputs

Data in the real world is garbage. You’re rarely handing your agent a clean JSON payload. Usually, it’s raw HTML, a badly formatted CSV, or an email thread. But I use a lightweight processing class to strip the noise before passing it to the LLM.

artificial intelligence automation - Artificial Intelligence Vs Automation PowerPoint and Google Slides ...
artificial intelligence automation – Artificial Intelligence Vs Automation PowerPoint and Google Slides …
import re
from bs4 import BeautifulSoup

class DataProcessor:
    @staticmethod
    def clean_html_payload(raw_html: str) -> str:
        """Strips scripts, styles, and excessive whitespace."""
        soup = BeautifulSoup(raw_html, 'html.parser')
        
        # Kill JavaScript and CSS
        for script in soup(["script", "style", "meta", "noscript"]):
            script.decompose()
            
        text = soup.get_text(separator=' ')
        
        # Collapse multiple spaces and newlines
        text = re.sub(r'\s+', ' ', text).strip()
        
        # Chunk it if it's still too massive
        # 12000 chars is roughly 3000 tokens depending on the text
        return text[:12000] 

    @staticmethod
    def process_batch(html_list: List[str]) -> List[str]:
        return [DataProcessor.clean_html_payload(doc) for doc in html_list]

Combine these two, and you have an automation pipeline that doesn’t care if a developer changes their CSS classes. The agent reads the text, understands the semantic meaning, and maps it to your Pydantic schema anyway.

The Infinite Loop Trap (And How I Burned $14)

The docs don’t warn you about this, but you need to be extremely careful with autonomous retry logic. I made a typo in an API endpoint URL, and the agent got trapped in an infinite retry loop that racked up a $14 bill in four minutes before I caught it.

Always implement a hard stop. Here is the circuit breaker pattern I use now for any agent that has access to external tools.

import time

class CircuitBreaker:
    def __init__(self, max_failures: int = 3, reset_window: int = 60):
        self.max_failures = max_failures
        self.reset_window = reset_window
        self.failures = 0
        self.last_failure_time = 0
        
    def record_failure(self):
        self.failures += 1
        self.last_failure_time = time.time()
        
    def is_open(self) -> bool:
        if self.failures >= self.max_failures:
            # Check if the window has expired
            if time.time() - self.last_failure_time > self.reset_window:
                self.failures = 0 # Reset
                return False
            return True
        return False

# Usage inside your agent tool execution:
breaker = CircuitBreaker()

def execute_tool(tool_name, params):
    if breaker.is_open():
        raise Exception("Circuit breaker open. Halting agent execution to prevent loop.")
        
    try:
        # tool execution logic here
        pass
    except Exception:
        breaker.record_failure()
        raise

Where This Is Going

Writing raw regex for data extraction is a dying practice. But the trade-off right now is latency. A BeautifulSoup script parses a page in 40 milliseconds. My agent takes about 2.5 seconds to do the same job. But I’ll gladly trade two seconds of compute time to get my Tuesday mornings back.

Leave a Reply

Your email address will not be published. Required fields are marked *