
Building a Real Python Agent (Without Hype) for Instructors
- Mark Kendall
- Feb 11
- 4 min read
Building a Real Python Agent (Without Hype)
Part Two — Modular Architecture & System Evolution
In Part One, we built a working CLI-based Python agent.
It:
Read a code file
Called an LLM
Produced structured feedback
Maintained in-memory state
Used clean separation of concerns
That was foundational.
In Part Two, we move from:
“Building an agent”
to
“Proving it is a system.”
The Goal of Part Two
You are no longer trying to make it work.
You are trying to prove:
It is modular
It is composable
It is replaceable
It is evolvable
It respects architectural boundaries
This is where engineers become architects.
The Core Principle
If you cannot replace a component without rewriting everything else,
you built a script.
If you can swap a module cleanly,
you built a system.
Part Two is about forcing that proof.
Architecture Review
From Part One, we had:
Agent (Orchestrator)
Memory (State)
LLM Client (Execution)
Tool (Reasoning Strategy)
CLI (Operational Interface)
Each of these was isolated intentionally.
Now we test the integrity of those boundaries.
Exercise 1 — Replace the Memory Layer
In Part One:
class SessionMemory:
def init(self):
self.history = []
def add(self, role: str, content: str):
self.history.append({"role": role, "content": content})
def get(self):
return self.history
This is in-memory only.
Now replace it with persistent storage.
Example: SQLite-backed memory.
# persistent_memory.py
import sqlite3
class PersistentMemory:
def init(self, db_path="memory.db"):
self.conn = sqlite3.connect(db_path)
self._create_table()
def createtable(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT,
content TEXT
)
""")
def add(self, role: str, content: str):
self.conn.execute(
"INSERT INTO history (role, content) VALUES (?, ?)",
(role, content)
)
self.conn.commit()
def get(self):
cursor = self.conn.execute("SELECT role, content FROM history")
return [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
Then in agent.py, change only:
from persistent_memory import PersistentMemory
self.memory = PersistentMemory()
If nothing else breaks,
your architecture is sound.
Exercise 2 — Replace the Tool
Originally:
class CodeReviewTool:
Now build a Security Audit Tool.
class SecurityAuditTool:
@staticmethod
def build_prompt(code: str) -> str:
return f"""
You are a senior security engineer.
Analyze the following code and provide:
1. Security vulnerabilities
2. Injection risks
3. Data exposure risks
4. Missing validation
5. Logging weaknesses
Code:
{code}
"""
In your agent:
from tools import SecurityAuditTool
prompt = SecurityAuditTool.build_prompt(code)
Nothing else changes.
You just proved tool abstraction works.
Exercise 3 — Swap the Interface
Remove CLI entirely.
Wrap the agent with FastAPI.
# api.py
from fastapi import FastAPI
from agent import DevAssistAgent
app = FastAPI()
agent = DevAssistAgent()
@app.post("/analyze")
def analyze(payload: dict):
code = payload.get("code")
result = agent.analyze_code(code)
return {"analysis": result}
You just moved from CLI utility to microservice.
The core logic did not change.
That is composability.
Exercise 4 — Add Retry Logic
Inside llm.py:
import time
def chat(self, messages):
retries = 3
for attempt in range(retries):
try:
response = self.client.chat.completions.create(
model=Config.MODEL,
messages=messages,
temperature=0.2
)
return response.choices[0].message.content
except Exception:
if attempt < retries - 1:
time.sleep(2 ** attempt)
else:
raise
Now you are thinking production.
Why This Matters
These exercises teach:
Layering
Abstraction boundaries
Dependency direction
System composability
Infrastructure replaceability
You are no longer just “using AI.”
You are engineering around it.
The Curriculum Structure
Beginner Track (Part One)
Build CLI agent
Understand components
See orchestration
Intermediate Track (Part Two)
Swap memory
Swap tool
Swap interface
Add resiliency
Add persistence
Advanced Track (Optional)
Add Redis memory
Add async execution
Add OpenTelemetry tracing
Add usage tracking
Add CI integration
Add guardrails
Add multi-tool selection logic
Each level forces architectural maturity.
The Real Teaching Objective
Students must understand:
This is not about LLM prompts.
This is about system design.
If they can:
Replace storage
Replace execution layer
Replace reasoning tool
Replace interface
without rewriting the system,
then they understand:
Architecture.
A Structured Instructor Recommendation
If you are using this as part of a class or training program:
Require students to build Part One exactly as written.
For final evaluation, require them to replace one module.
They must demonstrate:
No cascading rewrites
Clean integration
Preserved boundaries
If they succeed:
They built a system.
If they fail:
They built a script.
That distinction is the entire point of this exercise.
Layering Philosophy
This is Learn • Teach • Master in practice:
Learn — Build it exactly as designed.
Teach — Explain why the layers exist.
Master — Replace one without breaking the others.
That is skill compounding.
Final Thought
Most AI tutorials teach you how to call an API.
Very few teach you how to design around it.
If your agent collapses when one component changes,
it was never architecture.
Build foundations.
Modularize.
Swap components.
Prove composability.
That is how engineers evolve into architects.
— LearnTeachMaster
Comments