Lesson 3: Custom Agent Development
Ready to build your own intelligent agents? In this lesson, you'll learn to create custom agents by extending the BaseAgent class, implementing specialized business logic, and using AgentMap's powerful protocol system.
Learning Objectives
By the end of this lesson, you will:
- ✅ Understand BaseAgent architecture and inheritance patterns
- ✅ Implement custom
process()
methods with business logic - ✅ Use AgentMap's scaffolding tools to generate agent templates
- ✅ Integrate custom agents into workflows
- ✅ Apply CapableAgent protocols for service injection
- ✅ Handle errors and logging in custom agents
Overview: What We're Building
We'll create a Smart Expense Validator that:
- Validates expense data using custom business rules
- Categorizes expenses automatically
- Calculates tax implications and budgets
- Flags suspicious or policy-violating expenses
- Generates detailed validation reports
Step 1: Understanding Custom Agent Architecture
BaseAgent Foundation
All AgentMap agents inherit from BaseAgent
and implement the process()
method:
from agentmap.agents.base_agent import BaseAgent
from typing import Dict, Any
class MyCustomAgent(BaseAgent):
"""Custom agent extending BaseAgent functionality."""
def __init__(self, name: str, prompt: str, context: Dict[str, Any] = None, **kwargs):
super().__init__(name, prompt, context, **kwargs)
# Custom initialization logic here
def process(self, inputs: Dict[str, Any]) -> Any:
"""
Custom processing logic - this is where your agent's intelligence lives.
Args:
inputs: Dictionary containing input data from previous agents
Returns:
Any: Processed result that gets passed to next agent
"""
# Your custom logic here
return processed_result
Key Agent Components
- Initialization: Set up agent state and configuration
- Process Method: Core business logic implementation
- Logging: Built-in logging capabilities via
self.log_info()
- Context Access: Configuration via
self.context
- Error Handling: Graceful failure management
Step 2: Download the Lesson Files
Let's get all the files we'll need for this lesson:
Workflow File
Sample Expense Data
Custom Agent Implementation
Step 3: Understanding the Custom Agent Code
Class Structure Analysis
Let's break down the ExpenseValidatorAgent
implementation:
1. Class Declaration and Inheritance
class ExpenseValidatorAgent(BaseAgent):
"""Custom agent that validates expense data against business rules."""
Key Points:
- Inherits from
BaseAgent
to get core functionality - Docstring explains the agent's purpose
- Clear naming convention (
*Agent
suffix)
2. Initialization Method
def __init__(self, name: str, prompt: str, context: Dict[str, Any] = None, **kwargs):
super().__init__(name, prompt, context, **kwargs)
# Initialize validation rules from context
self.max_amount = self.context.get('max_amount', 1000)
self.required_fields = self.context.get('required_fields', ['amount', 'description', 'date'])
self.allowed_categories = self.context.get('allowed_categories', [])
self.log_info(f"ExpenseValidator initialized with max_amount: ${self.max_amount}")
Key Points:
- Calls parent constructor first
- Extracts configuration from context
- Provides sensible defaults
- Uses built-in logging
3. Core Process Method
def process(self, inputs: Dict[str, Any]) -> Any:
"""Validate expense data against business rules."""
try:
expense_data = inputs.get('expense_data', [])
# Main processing logic here
return result
except Exception as e:
error_msg = f"ExpenseValidator process failed: {str(e)}"
self.log_error(error_msg)
return {"error": error_msg}
Key Points:
- Single entry point for agent logic
- Handles inputs from previous agents
- Returns structured output
- Comprehensive error handling
4. Helper Methods
def _validate_single_expense(self, expense: Dict[str, Any], index: int) -> Dict[str, Any]:
def _clean_description(self, description: str) -> str:
def _parse_date(self, date_str: str) -> datetime:
def _generate_validation_summary(self, stats: Dict[str, Any]) -> str:
Key Points:
- Private methods (underscore prefix) for internal logic
- Single responsibility principle
- Reusable, testable components
- Clear input/output contracts
Step 4: Setting Up the Custom Agent
Project Structure
Create this directory structure:
agentmap-learning/
├── lesson3.csv # Workflow definition
├── expense_validator.py # Custom agent code
├── sample_expenses.csv # Test data
└── data/ # Output directory
├── validation_report.txt
└── (other outputs)
Agent Registration
AgentMap automatically discovers custom agents if they follow naming conventions:
- File naming:
{agent_name}.py
- Class naming:
{AgentName}Agent
- Factory function:
create_{agent_name}_agent
For our expense_validator
type in CSV:
- File:
expense_validator.py
- Class:
ExpenseValidatorAgent
- Factory:
create_expense_validator_agent
Running the Workflow
# Make sure all files are in place
ls -la
# lesson3.csv expense_validator.py sample_expenses.csv
# Create data directory
mkdir -p data
# Run the workflow
agentmap run lesson3.csv
Step 5: Understanding the Workflow Execution
Execution Flow
- LoadExpenses: Reads
sample_expenses.csv
using CSVReaderAgent - ValidateExpenses: Our custom agent processes the data
- CategorizeExpenses: AI analyzes and categorizes validated expenses
- GenerateReport: Creates summary report
- SaveReport: Saves final report to file
Custom Agent Integration
In the CSV, our custom agent is referenced like any built-in agent:
ExpenseValidation,ValidateExpenses,Custom validation with business rules,expense_validator,CategorizeExpenses,ErrorHandler,expense_data,validation_results,,"{""max_amount"": 1000, ""required_fields"": [""amount"", ""description"", ""date""], ""allowed_categories"": [""Travel"", ""Office"", ""Meals"", ""Equipment"", ""Software""]}"
Key Configuration:
type
:expense_validator
(matches file name)context
: JSON configuration for validation rulesinput_fields
:expense_data
from previous agentoutput_field
:validation_results
for next agent
Step 6: Examining the Results
Validation Output
After running, check the validation results:
cat data/validation_report.txt
You should see something like:
📊 **EXPENSE VALIDATION REPORT**
[
{
"original_expense": "Uber to client meeting",
"amount": 25.50,
"suggested_category": "Travel",
"confidence": 0.95,
"suspicious": false,
"reason": "Clear business transportation expense"
},
{
"original_expense": "Expensive dinner",
"amount": 300.00,
"suggested_category": "Meals",
"confidence": 0.7,
"suspicious": true,
"reason": "Unusually high meal cost, employee identification unclear"
}
]
✅ **Validation Complete**: All expenses have been processed through custom business rules and AI categorization.
Understanding the Custom Logic
Our agent identified several issues:
- High amounts: Flagged expenses over the limit
- Missing receipts: Noted policy violations
- Suspicious employees: Caught "Unknown" and "Anonymous" entries
- Invalid dates: Detected future dates or bad formats
Step 7: Advanced Custom Agent Patterns
Using CapableAgent Protocols
For agents that need external services, implement capability protocols:
from agentmap.services.protocols import LLMCapableAgent, StorageCapableAgent
class AdvancedExpenseAgent(BaseAgent, LLMCapableAgent, StorageCapableAgent):
"""Agent with multiple service capabilities."""
def configure_llm_service(self, llm_service):
"""Configure LLM service for AI processing."""
self._llm_service = llm_service
def configure_storage_service(self, storage_service):
"""Configure storage service for data persistence."""
self._storage_service = storage_service
def process(self, inputs: Dict[str, Any]) -> Any:
# Use both LLM and storage services
ai_analysis = self._llm_service.call_llm(...)
self._storage_service.write(...)
return result
Error Handling Best Practices
def process(self, inputs: Dict[str, Any]) -> Any:
try:
# Main processing logic
result = self._main_processing(inputs)
self.log_info("Processing completed successfully")
return result
except ValidationError as e:
# Specific business errors
self.log_warning(f"Validation failed: {e}")
return {"error": str(e), "type": "validation"}
except Exception as e:
# Unexpected errors
self.log_error(f"Unexpected error: {e}")
return {"error": "Internal processing error", "type": "system"}
Context-Driven Configuration
def __init__(self, name: str, prompt: str, context: Dict[str, Any] = None, **kwargs):
super().__init__(name, prompt, context, **kwargs)
# Extract complex configuration
self.rules = self._parse_validation_rules(self.context.get('rules', {}))
self.thresholds = self.context.get('thresholds', {})
self.enabled_features = self.context.get('features', [])
def _parse_validation_rules(self, rules_config: Dict) -> Dict:
"""Parse and validate configuration rules."""
# Complex configuration parsing logic
return parsed_rules
Step 8: Exercises and Extensions
Exercise 1: Add More Validation Rules
Extend the validator with additional business rules:
def _validate_single_expense(self, expense: Dict[str, Any], index: int) -> Dict[str, Any]:
# ... existing validation ...
# Add new validations
# 1. Weekend expense validation
if self._is_weekend(result['date_parsed']):
result['validation_flags'].append("Weekend expense requires justification")
# 2. Duplicate detection
if self._is_potential_duplicate(expense):
result['validation_flags'].append("Potential duplicate expense")
# 3. Category-specific validation
category = self._predict_category(description)
if category == "Meals" and amount > 50:
result['validation_flags'].append("High meal expense requires approval")
return result
Exercise 2: Add Statistical Analysis
Include more sophisticated analysis:
def _generate_advanced_stats(self, validation_results: List[Dict]) -> Dict:
"""Generate advanced statistical analysis."""
stats = {
'expense_by_category': {},
'spending_patterns': {},
'anomaly_detection': {},
'compliance_score': 0.0
}
# Category analysis
for result in validation_results:
category = result.get('predicted_category', 'Unknown')
if category not in stats['expense_by_category']:
stats['expense_by_category'][category] = {'count': 0, 'total': 0.0}
stats['expense_by_category'][category]['count'] += 1
stats['expense_by_category'][category]['total'] += result['amount']
return stats
Exercise 3: Create a Report Generator Agent
Build a second custom agent that formats reports:
class ExpenseReportGeneratorAgent(BaseAgent):
"""Generates formatted expense reports from validation data."""
def process(self, inputs: Dict[str, Any]) -> Any:
validation_data = inputs.get('validation_results', {})
# Generate different report formats
reports = {
'executive_summary': self._generate_executive_summary(validation_data),
'detailed_report': self._generate_detailed_report(validation_data),
'compliance_report': self._generate_compliance_report(validation_data)
}
return reports
Key Concepts Learned
1. BaseAgent Architecture
- Inheritance from BaseAgent provides core functionality
process()
method is the main entry point- Context provides configuration flexibility
- Built-in logging and error handling
2. Custom Business Logic
- Implement domain-specific validation rules
- Use helper methods for code organization
- Provide meaningful error messages and flags
- Generate actionable insights and reports
3. Integration Patterns
- Follow naming conventions for automatic discovery
- Use context for configuration
- Structure input/output for workflow compatibility
- Handle errors gracefully
4. Service Integration
- CapableAgent protocols enable service injection
- Multiple capabilities can be combined
- Services are automatically configured by the framework
- Type-safe service access patterns
Best Practices for Custom Agents
1. Code Organization
- Use clear, descriptive class names
- Implement single responsibility principle
- Separate business logic into helper methods
- Provide comprehensive docstrings
2. Error Handling
- Use try-catch blocks appropriately
- Log errors at the right level
- Return structured error information
- Provide recovery strategies when possible
3. Configuration
- Use context for all configuration
- Provide sensible defaults
- Validate configuration on initialization
- Document all configuration options
4. Testing
- Write unit tests for helper methods
- Test with various input scenarios
- Validate error handling paths
- Use mock data for external dependencies
Troubleshooting
Issue: Agent not found
Solution: Check naming conventions:
- File:
{agent_type}.py
- Class:
{AgentType}Agent
- Factory:
create_{agent_type}_agent
Issue: Configuration not loading
Solution: Verify context JSON syntax in CSV:
{"key": "value", "list": ["item1", "item2"]}
Issue: Process method errors
Solution: Add comprehensive error handling:
try:
# processing logic
except Exception as e:
self.log_error(f"Error: {e}")
return {"error": str(e)}
Next Steps
Excellent progress! You've mastered:
- ✅ Custom agent development with BaseAgent inheritance
- ✅ Business logic implementation in process() methods
- ✅ Context-driven configuration and validation
- ✅ Error handling and logging best practices
- ✅ Integration with AgentMap workflows
In Lesson 4, we'll explore the most advanced topic: orchestrating multiple custom agents using intelligent routing and coordination strategies.
Continue to Lesson 4: Multi-Agent Orchestration →
Additional Resources
- Custom Agent Development Guide
- BaseAgent API Reference
- CapableAgent Protocols
- Service Injection Patterns
💡 Pro Tip: Start simple with your custom agents and gradually add complexity. The patterns you learn here scale to sophisticated business logic implementations. Also, consider creating reusable validation libraries that multiple agents can share!