Chapter 3: Autonomous Agent Demo

Python Engineering for Agentic Systems

Overview

This project demonstrates six key standardization patterns for building reliable, maintainable autonomous AI agents. It showcases best practices for production-ready agent systems through a working RAG (Retrieval-Augmented Generation) implementation.

🔒 Strong Typing

Pydantic models with runtime validation and mypy strict type checking

⚠️ Error Handling

Result<T, E> pattern for explicit error states without exceptions

📊 Structured Logging

JSON logs with correlation IDs for distributed tracing

⚡ Async Orchestration

asyncio-based concurrent operations for better performance

📝 Versioned Prompts

Declarative prompt management with A/B testing support

🔌 Plugin Registry

Protocol-based tool registry for extensible architecture

System Architecture

The system follows a clean, modular architecture with clear separation of concerns:

CLI Entry Point Agent Core run_agent() | plan() Prompt Store Versioned Prompts Tool Registry Plugin System Result Handler Error Management JSON Logger Correlation IDs Passages (get_passages) Web Search (get_web) Decision (decide) Pydantic Models Passage | Decision

Data Flow

  1. CLI Entry: User query enters through the CLI interface
  2. Agent Orchestration: Agent core loads prompts and orchestrates async operations
  3. Parallel Retrieval: Multiple data sources queried concurrently using asyncio
  4. Decision Making: Results validated through Pydantic models and processed
  5. Structured Output: JSON logs with correlation IDs track the entire flow

Standardization Patterns

1. Strong Typing & Validation

class Passage(BaseModel):
    id: str
    text: str
    score: float = Field(ge=0, le=1)  # Runtime validation

@runtime_checkable
class Tool(Protocol):
    def execute(self, **kwargs) -> dict: ...

Benefits: Catch errors at design time, self-documenting APIs, runtime validation prevents invalid data propagation.

2. Result-Based Error Handling

@dataclass
class Result(Generic[T, E]):
    ok: Optional[T] = None
    err: Optional[E] = None

    @property
    def is_ok(self) -> bool: return self.err is None

# Usage
result = decide(passages)
if result.is_err:
    log.info("plan_failed", extra={"error": result.err})
    return {"error": result.err}

Benefits: Explicit error states, no silent failures, forces error handling at call sites.

3. Structured JSON Logging

class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "level": record.levelname,
            "msg": record.getMessage(),
            "logger": record.name,
            "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "correlation_id": getattr(record, "correlation_id", None),
        }
        return json.dumps(payload)

Benefits: Machine-readable logs, easy to parse and analyze, correlation IDs enable distributed tracing.

4. Async Orchestration

async def plan(q: str) -> Result[Decision, str]:
    log.info("plan_start", extra={"query": q})
    p_task = asyncio.create_task(get_passages(q))
    w_task = asyncio.create_task(get_web(q))
    passages_a, passages_b = await asyncio.gather(p_task, w_task)
    # Process results...

Benefits: Concurrent I/O operations, better resource utilization, cleaner than threading.

5. Versioned Prompt Management

{
  "name": "summarize",
  "version": "v1",
  "system": "You are a precise technical assistant...",
  "user_template": "Summarize the following text in at most ${max_bullets} bullet points.",
  "constraints": {
    "max_tokens": 400
  }
}

Benefits: Prompts versioned in git, A/B testing via environment variables, audit trail for prompt changes.

6. Plugin Registry Pattern

registry = Registry()
registry.register("search", SearchTool())

# Anywhere in code:
result = registry.use("search", query="RAG systems")

# Add new tools without modifying agent code
registry.register("summarizer", SummarizerTool())

Benefits: Decoupled architecture, easy to add/remove tools, testable in isolation.

Core Components

cli.py

Entry point for the agent-demo command. Sets up environment and runs the agent with a sample query.

agent-demo

agent.py

Core agent logic with async orchestration. Manages retrieval, decision-making, and prompt rendering.

run_agent() | plan()

prompt_store.py

Loads and renders versioned prompts from JSON files with template substitution.

PromptStore.load()

registry.py

Protocol-based plugin system for registering and using tools dynamically.

Registry.register()

result.py

Generic Result<T, E> type for explicit error handling without exceptions.

Result[Decision, str]

models.py

Pydantic models with runtime validation for Passages and Decisions.

Passage | Decision

logging.py

Custom JSON formatter with correlation IDs for distributed tracing.

JsonFormatter

demo.py

Bootstrap script that creates venv, installs package, and runs the demo.

python demo.py

Getting Started

Quick Start

# Clone the repository
git clone https://github.com/ranjanarajendran/engineering-autonomous-ai.git
cd engineering-autonomous-ai/chapter3-agentic-standardization-demo

# Run the one-command demo
python demo.py

# Output: JSON with prompt details, decision, and citations

Run Tests

python -m venv .venv && source .venv/bin/activate
pip install -e .
pip install pytest
pytest -q

Type Checking

pip install mypy
mypy --strict src/autonomous_agent_demo

Switch Prompt Versions

export PROMPT_VARIANT=v2
python demo.py

Why Standardization Matters

Without these patterns, autonomous agents suffer from:

This demo shows how adopting industry-standard patterns creates agents that are observable, testable, and maintainable at scale.