Session 1: Bare Metal Agents - Pure Python Implementation¶
Learning Outcomes¶
By the end of this session, you will be able to: - Implement the core agent architecture patterns using only Python and LLM APIs - Build functional agents that demonstrate all five agentic patterns (Reflection, Tool Use, ReAct, Planning, Multi-Agent) - Understand the separation between model layer (LLM) and application layer (Python logic) - Create agents that integrate with existing enterprise APIs and legacy systems - Debug and customize agent behavior with complete transparency and control
Chapter Overview: Understanding Agent Architecture at the Core¶
Industry Context & Market Significance
While frameworks dominate headlines, enterprise teams increasingly need bare metal implementations for production control. IBM experts note that "most organizations aren't agent-ready" and require custom solutions that integrate with legacy APIs. This session teaches you to build agents from scratch using only Python and LLM APIs, providing the foundation for understanding how all frameworks work internally.
What You'll Learn & Why It Matters
You'll master the core architecture patterns that power every agent framework, implement the five fundamental agentic patterns in pure Python, and understand the separation between model layers and application logic. This knowledge enables you to debug framework issues, build custom enterprise solutions, and make informed architectural decisions for production systems.
How Bare Metal Agents Stand Out
Unlike frameworks that abstract implementation details, bare metal agents offer complete transparency and control. You'll see how "model layer (LLM) → understand natural language and route intent" while "application layer (Python code) → execute tools, manage flow, send responses." This separation allows swapping tools, prompts, or models easily without framework constraints.
Real-World Applications
Companies use bare metal agents for enterprise API integration, legacy system connectivity, and custom security implementations. You'll build agents that can integrate with any existing infrastructure while maintaining full control over data flow and processing logic.
Learning Navigation Hub¶
Total Time Investment: 75 minutes (Core) + 30-105 minutes (Optional)
Learning Path Options¶
- Observer (40 min): Architecture analysis and code structure understanding
- Participant (75 min): Hands-on agent implementation with guided exercises
- Implementer (120 min): Custom agent development with advanced patterns
Session Overview Dashboard¶
Core Learning Track (75 minutes) - REQUIRED¶
Section | Concept Load | Time | Skills |
---|---|---|---|
Agent Architecture | 3 concepts | 20 min | Understanding |
Building First Agent | 4 concepts | 25 min | Implementation |
Tool Integration | 4 concepts | 20 min | Application |
✅ Testing & Validation | 3 concepts | 10 min | Verification |
Optional Advanced Modules¶
⚠️ Advanced Content: These modules contain specialized material for production environments
- Module A: Advanced Agent Patterns (40 min) - Sophisticated reasoning loops & error recovery
- Module B: Performance Optimization (35 min) - Production-grade speed & efficiency patterns
- Module C: Complex State Management (30 min) - Enterprise memory & persistence systems
Code Files: All examples use files in src/session1/
Core Section (Required - 75 minutes)¶
Part 1: Agent Architecture Fundamentals (20 minutes)¶
Cognitive Load: 3 new concepts Learning Mode: Conceptual Understanding
Basic Agent Structure (8 minutes)¶
Every agent needs these core components:
File: src/session1/base_agent.py
This is the foundation class for all agents. Think of it as the blueprint that defines what capabilities every agent should have. The memory list stores past interactions, while the tools dictionary holds functions the agent can call.
Agent Architecture Foundation - BaseAgent Class
The BaseAgent class serves as the architectural blueprint for all agent implementations. This design follows the separation of concerns principle, where each component has a specific responsibility:
class BaseAgent:
"""Foundation class for all agent implementations"""
def __init__(self, model_name="gpt-4"):
self.model_name = model_name # LLM endpoint
self.memory = [] # Conversation context
self.tools = {} # Available functions
Memory System Design: The memory list acts as a sliding window of recent interactions, enabling context-aware responses. In production systems, this would implement sophisticated memory management with compression and relevance scoring.
Tool Registry Pattern: The tools dictionary uses a registry pattern, allowing dynamic addition and removal of capabilities without modifying core agent logic. Each tool follows a standard interface contract.
Agent Execution Loop - The Heart of Agency
def run(self, user_input: str) -> str:
"""Main execution cycle: Input → Reasoning → Action → Response"""
processed_input = self.process_input(user_input)
action_plan = self.decide_action(processed_input)
result = self.execute_action(action_plan)
return self.format_response(result)
Execution Flow Explanation: This four-step process mirrors human decision-making - understand the request, plan the response, take action, and communicate results. Each step can be enhanced with logging, error handling, and performance monitoring for production deployments.
Key Concepts:
- Model Interface: How agents talk to LLMs
- Memory Management: Keeping track of context
- Tool Registry: Available actions the agent can take
Input/Output Handling (7 minutes)¶
Clean interfaces for agent interaction:
Input Processing Pipeline - Structured Data Transformation
Production agents require consistent data structures for reliable processing. These methods transform raw user input into structured formats that enable sophisticated reasoning and logging:
def process_input(self, user_input: str) -> dict:
"""Transform user input into structured agent format"""
return {
"message": user_input,
"timestamp": datetime.now(),
"session_id": self.session_id
}
Structured Input Benefits: Converting strings to dictionaries enables metadata tracking, audit logging, and complex routing logic. Production systems expand this with user authentication, request validation, and security scanning.
Response Formatting System - Consistent Output Interface
def format_output(self, agent_response: str) -> str:
"""Standard response formatting for user display"""
return f"Agent: {agent_response}"
Enterprise Output Considerations: In production, response formatting handles multiple output channels (web, API, mobile), adds response metadata, implements content filtering, and formats structured data for downstream systems.
State Management Basics (5 minutes)¶
Simple but effective state tracking:
State Management Architecture - Agent Memory System
The AgentState class implements a comprehensive memory system that enables contextual decision-making across conversation turns. This pattern separates state from logic, allowing flexible memory strategies:
class AgentState:
"""Comprehensive agent state management system"""
def __init__(self):
self.current_task = None # Active task context
self.conversation_history = [] # Interaction timeline
self.available_tools = [] # Dynamic tool registry
self.confidence_level = 0.0 # Decision confidence
State Design Patterns: This structure follows the State pattern from software engineering, enabling different memory strategies (short-term, long-term, episodic) without changing core agent logic.
Dynamic State Updates - Learning from Actions
def update_state(self, action: str, result: str):
"""Track actions and results for context building"""
self.conversation_history.append({
"action": action, "result": result,
"timestamp": datetime.now()
})
Production State Considerations: Enterprise systems extend this with state persistence, distributed memory, conflict resolution, and state compression for long-running conversations.
Part 2: Building Your First Agent (25 minutes)¶
Cognitive Load: 4 new concepts Learning Mode: Hands-on Implementation
Simple Agent Implementation (10 minutes)¶
Building Your First Production-Ready Agent
File Reference: src/session1/base_agent.py
- Complete SimpleAgent implementation
Agent Initialization - Memory Foundation
The SimpleAgent demonstrates minimal viable architecture for production systems. Starting with essential components ensures maintainability and extensibility:
class SimpleAgent:
"""Minimal viable agent with memory persistence"""
def __init__(self):
self.memory = [] # Conversation state storage
Architecture Decision: This minimal design enables rapid prototyping while maintaining production upgrade paths. Enterprise versions would add configuration management, logging, and health checks at initialization.
Core Reasoning Engine - Context-Aware Processing
The think method implements basic natural language processing with memory integration, forming the foundation for more sophisticated reasoning:
def think(self, input_text: str) -> str:
"""Context-aware reasoning with memory integration"""
# Memory persistence for context
self.memory.append(f"User: {input_text}")
# Intent classification logic
response = (f"I understand you're asking: {input_text}"
if "?" in input_text
else f"I acknowledge: {input_text}")
Intent Recognition Pattern: This simple conditional logic demonstrates the foundation for more complex intent classification. Production systems would implement machine learning-based intent recognition, sentiment analysis, and context extraction.
Memory Management Strategy: Each interaction updates the conversation history, enabling context-aware responses in multi-turn conversations. Enterprise implementations would add memory compression, relevance scoring, and persistence layers.
Agent Execution Interface - Public API Design
The run method provides a clean public interface that abstracts internal complexity from users:
def run(self, input_text: str) -> str:
"""Public interface for agent interaction"""
return self.think(input_text)
Interface Design Pattern: This method acts as a facade, hiding implementation details while providing a stable API. Production systems would add input validation, error handling, rate limiting, and response caching at this layer.
Agent Testing and Validation
# Production-ready testing pattern
agent = SimpleAgent()
response = agent.run("What is machine learning?")
print(f"Agent Response: {response}")
Testing Strategy: This demonstrates basic functional testing. Enterprise testing would include unit tests, integration tests, performance benchmarks, and automated regression testing for different input types and edge cases.
Basic Reasoning Loop (8 minutes)¶
Implementing the core agent thinking process:
Advanced Reasoning Engine - ReAct Pattern Implementation
File Reference: src/session1/react_agent.py
- Complete ReAct implementation
The ReAct (Reasoning + Acting) pattern forms the backbone of modern agent systems, implementing an iterative cycle of thought, action, and observation that mirrors human problem-solving.
ReAct Loop Initialization - Bounded Reasoning Control
def reasoning_loop(self, task: str) -> str:
"""Production ReAct implementation with safety bounds"""
max_steps = 5 # Prevent infinite loops
for step in range(max_steps):
# Generate contextual reasoning
thought = self.generate_thought(task, step)
print(f"Step {step + 1} - Thought: {thought}")
Bounded Reasoning Strategy: The max_steps parameter prevents infinite loops in production, a critical safety feature. Enterprise systems would implement dynamic step limits based on task complexity, user permissions, and resource constraints.
Decision Logic and Termination Control
# Intelligent termination check
if self.is_task_complete(thought):
return self.generate_final_answer(thought)
# Action planning and execution
action = self.decide_action(thought)
print(f"Step {step + 1} - Action: {action}")
Termination Strategy: The completion check prevents unnecessary processing while ensuring task resolution. Production systems would implement sophisticated completion detection using confidence scoring, goal achievement metrics, and resource optimization.
Finally, the agent observes the results and updates its memory for the next iteration:
# Observe: See results
observation = self.execute_action(action)
print(f"Step {step + 1} - Observation: {observation}")
# Update state for next iteration
self.update_memory(thought, action, observation)
return "Task completed after maximum steps"
Error Handling Patterns (4 minutes)¶
Making agents robust:
This wrapper method ensures agents handle errors gracefully, preventing crashes and providing user-friendly error messages when something goes wrong.
def safe_agent_execution(self, user_input: str) -> str:
"""Execute agent with proper error handling"""
try:
return self.run(user_input)
except Exception as e:
error_msg = f"Agent encountered an error: {e}"
self.memory.append(f"Error: {error_msg}")
return "I apologize, but I encountered an issue. Could you try rephrasing your request?"
Testing Your Agent (3 minutes)¶
File: src/session1/test_agents.py
- Complete test suite
def test_simple_agent():
"""Test basic agent functionality"""
agent = SimpleAgent()
# Test question handling
response = agent.run("What is AI?")
assert "asking" in response.lower()
# Test statement handling
response = agent.run("I like programming")
assert "acknowledge" in response.lower()
# Test memory
assert len(agent.memory) == 4 # 2 user inputs + 2 agent responses
print("✅ All tests passed!")
# Run the test
test_simple_agent()
Part 3: Tool Integration Basics (20 minutes)¶
Cognitive Load: 4 new concepts Learning Mode: Application & Integration
Simple Tool Creation (8 minutes)¶
Building tools that agents can use:
File: src/session1/tools.py
- Complete tool implementations
The SimpleTool base class provides a consistent interface for all tools. The name is used by the agent to identify the tool, while the description helps the agent decide when to use it.
⚠️ Security Warning: The calculator example uses eval()
which is dangerous! This implementation includes basic safety checks, but in production you should use a proper math expression parser like sympy or numexpr instead.
class SimpleTool:
def __init__(self, name: str, description: str):
self.name = name
self.description = description
def execute(self, input_data: str) -> str:
"""Override this method in specific tools"""
raise NotImplementedError
This base class defines the interface that all tools must follow. Each tool needs a name and description, plus an execute method.
Now let's implement a specific calculator tool with safety checks:
class CalculatorTool(SimpleTool):
def __init__(self):
super().__init__("calculator", "Performs basic math calculations")
def execute(self, expression: str) -> str:
"""Safely evaluate math expressions"""
try:
# Basic safety check
if any(char in expression for char in ['import', 'exec', 'eval', '__']):
return "Invalid expression for security reasons"
result = eval(expression)
return f"Calculation result: {result}"
except Exception as e:
return f"Math error: {e}"
Here's how to use the calculator tool:
# Usage example
calc = CalculatorTool()
result = calc.execute("2 + 3 * 4")
print(result) # "Calculation result: 14"
Tool Calling Mechanisms (6 minutes)¶
How agents decide when and how to use tools:
File: src/session1/tool_use_agent.py
- See the ToolUseAgent
class
class ToolUseAgent:
def __init__(self):
self.tools = {}
self.memory = []
def register_tool(self, tool: SimpleTool):
"""Add a tool to the agent's toolkit"""
self.tools[tool.name] = tool
The ToolUseAgent starts with basic setup, maintaining a registry of available tools and conversation memory.
Now let's add the decision logic for when to use tools:
def should_use_tool(self, user_input: str) -> tuple[bool, str]:
"""Decide if we need a tool for this input"""
# Simple keyword-based tool selection
if any(word in user_input.lower() for word in ['calculate', 'math', '+', '-', '*', '/']):
return True, 'calculator'
return False, None
Finally, the main execution method that ties everything together:
def run(self, user_input: str) -> str:
"""Execute with tool usage"""
needs_tool, tool_name = self.should_use_tool(user_input)
if needs_tool and tool_name in self.tools:
# Extract the calculation from the input
# (In real implementation, use better parsing)
tool_result = self.tools[tool_name].execute(user_input)
return f"Using {tool_name}: {tool_result}"
else:
return f"I understand your request: {user_input}"
Basic Validation (3 minutes)¶
Ensuring tool outputs are useful:
This validation method helps agents determine if a tool executed successfully. It's part of the agent's quality control system, checking for common failure indicators like error messages or empty responses.
def validate_tool_output(self, tool_name: str, output: str) -> bool:
"""Check if tool output looks reasonable"""
if "error" in output.lower():
return False
if output.strip() == "":
return False
return True
Integration Testing (3 minutes)¶
Testing agents with tools:
This integration test verifies that agents and tools work together correctly, testing the complete flow from user input through tool execution to final response.
def test_tool_integration():
"""Test agent + tool integration"""
agent = ToolUseAgent()
calc_tool = CalculatorTool()
agent.register_tool(calc_tool)
# Test tool usage
response = agent.run("Calculate 15 + 27")
assert "42" in response
# Test non-tool usage
response = agent.run("Hello there")
assert "understand" in response
print("✅ Tool integration tests passed!")
Part 4: Testing & Validation (10 minutes)¶
Cognitive Load: 3 new concepts Learning Mode: Production Readiness
Unit Testing Approaches (4 minutes)¶
File: src/session1/test_agents.py
- Complete test suite
import unittest
class TestAgentFunctionality(unittest.TestCase):
def setUp(self):
"""Set up test fixtures"""
self.agent = SimpleAgent()
self.tool_agent = ToolUseAgent()
self.tool_agent.register_tool(CalculatorTool())
The test class starts with setup, creating agent instances and registering tools for consistent testing.
Now let's add the individual test methods:
def test_basic_responses(self):
"""Test basic agent responses"""
response = self.agent.run("Hello")
self.assertIsInstance(response, str)
self.assertGreater(len(response), 0)
def test_tool_integration(self):
"""Test tool usage"""
response = self.tool_agent.run("What is 5 * 6?")
self.assertIn("30", response)
Finally, the test runner:
Common Troubleshooting (3 minutes)¶
Typical issues and solutions:
def debug_agent_issues(agent, user_input):
"""Helper function to debug agent problems"""
print(f"Input: {user_input}")
print(f"Memory state: {len(agent.memory)} items")
print(f"Available tools: {list(agent.tools.keys())}")
try:
response = agent.run(user_input)
print(f"Response: {response}")
except Exception as e:
print(f"❌ Error: {e}")
print("Check input format and agent state")
Running Everything (3 minutes)¶
File: src/session1/demo_runner.py
- Complete demonstration
def run_agent_demo():
"""Demonstrate all agent capabilities"""
print("=== Bare Metal Agent Demo ===")
# 1. Simple agent
simple_agent = SimpleAgent()
print("Simple Agent Response:", simple_agent.run("What is AI?"))
The demo starts by testing our basic SimpleAgent with a question to see how it responds.
Next, we test tool integration capabilities:
# 2. Tool-enabled agent
tool_agent = ToolUseAgent()
tool_agent.register_tool(CalculatorTool())
print("Tool Agent Response:", tool_agent.run("Calculate 12 * 8"))
Finally, we demonstrate the advanced ReAct reasoning pattern:
# 3. ReAct agent
react_agent = ReActAgent() # From react_agent.py
print("ReAct Agent working on complex task...")
response = react_agent.run("Plan a simple Python project")
print("ReAct Response:", response)
if __name__ == "__main__":
run_agent_demo()
Core Section Validation (5 minutes)¶
Quick Implementation Exercise¶
Files to Run: src/session1/demo_runner.py
Self-Assessment Checklist¶
- I understand basic agent architecture (input/output, memory, tools)
- I can implement a simple reasoning loop
- I can create and integrate basic tools
- I can test agent functionality
- I'm ready to explore framework implementations
Next Session Prerequisites: ✅ Core Section Complete Ready for: Session 2: LangChain Foundations (framework implementation)
Multiple Choice Test - Session 1¶
Test your understanding of bare metal agent implementation and core patterns:
Question 1: What is the primary benefit of implementing agents from scratch before using frameworks?
A) Lower resource usage
B) Easier deployment
C) Better performance
D) Deeper understanding of agent mechanics
Question 2: In the BaseAgent class, what is the purpose of the conversation_history attribute?
A) Maintaining context across interactions
B) Error tracking
C) Performance monitoring
D) Tool execution logs
Question 3: Which method must be implemented by all subclasses of BaseAgent?
A) get_available_tools()
B) process_message()
C) add_tool()
D) _generate_response()
Question 4: How does the Tool abstract base class ensure consistency across different tool implementations?
A) By handling errors automatically
B) By providing default implementations
C) By requiring execute() and _get_parameters() methods
D) By managing tool state
Question 5: What design pattern is demonstrated by the BaseAgent and its subclasses?
A) Strategy Pattern
B) Factory Pattern
C) Template Method Pattern
D) Observer Pattern
Navigation¶
Previous: Session 0 - Introduction to Agent Frameworks & Patterns
Optional Advanced Modules:
- Module A: Advanced Agent Patterns - ⚠️ Advanced: Sophisticated reasoning loops & error recovery
- Module B: Performance Optimization - ⚠️ Advanced: Production-grade speed & efficiency patterns
- Module C: Complex State Management - ⚠️ Advanced: Enterprise memory & persistence systems
Next: Session 2 - LangChain Foundations →