Search Docsā¦
Search Docsā¦
Tutorials
Build your first AI Agent evaluation and test using Python
This hands-on tutorial will guide you through building a complete AI agent application using CoAgent's Python client. You'll create a recipe generator agent with context switching, logging integration, and error handling.
What You'll Build
By the end of this tutorial, you'll have:
- A recipe generator agent that adapts to different cooking styles 
- Context-aware responses based on ingredients and dietary preferences 
- Integrated logging and monitoring 
- Error handling and recovery mechanisms 
- A complete Python application ready for production use 
Prerequisites
- CoAgent running locally ( - docker-compose up)
- Python 3.8+ installed 
- Basic knowledge of Python programming 
- Familiarity with virtual environments (recommended) 
Tutorial Overview
Phase 1: Environment Setup Phase 2: Basic Agent Creation Phase 3: Context-Aware Responses Phase 4: Logging Integration Phase 5: Error Handling & Recovery Phase 6: Advanced Features Phase 7: Testing & Validation
Phase 1: Environment Setup
1.1 Create Project Directory
mkdir recipe-agent-tutorial cd
1.2 Set Up Virtual Environment
# Create virtual environment python -m venv venv # Activate virtual environment # On macOS/Linux: source venv/bin/activate # On Windows: # venv\Scripts\activate
1.3 Install Dependencies
Navigate to your CoAgent installation and install the Python client:
# Assuming CoAgent is cloned at /path/to/coagent cd /path/to/coagent/langchain pip install -r requirements.txt pip install -e . # Install coagent package in development mode # Return to your project directory cd
Create a requirements.txt for your project:
cat > requirements.txt << EOF # Core dependencies langchain-ollama>=0.1.0 langchain>=0.1.0 pydantic>=2.0.0 requests>=2.28.0 # Development dependencies pytest>=7.0.0 python-dotenv>=1.0.0 rich>=13.0.0 # For pretty console output EOF pip install -r
1.4 Verify CoAgent Connection
Create a quick connection test:
# test_connection.py import requests def test_coagent_connection(): try: response = requests.get("http://localhost:3000/health") if response.status_code == 200: print("ā CoAgent is running and accessible") return True else: print(f"ā CoAgent health check failed: {response.status_code}") return False except requests.ConnectionError: print("ā Cannot connect to CoAgent. Is it running on localhost:3000?") return False if __name__ == "__main__": test_coagent_connection()
Run the test:
Phase 2: Basic Agent Creation
2.1 Create the Base Recipe Agent
Create recipe_agent.py:
#!/usr/bin/env python3 """ Recipe Generator Agent - CoAgent Python Client Tutorial A context-aware recipe generator that provides personalized cooking suggestions. """ from coagent import Coagent from coagent_types import CoagentConfig, CoagentContext, LoggerConfig from typing import List, Optional import json import os from rich.console import Console from rich.table import Table console = Console() class RecipeAgent: """A recipe generator agent with context-switching capabilities.""" def __init__(self, model_name: str = "llama3.1:8b", enable_logging: bool = True): """Initialize the recipe agent with CoAgent configuration.""" # Configure logging logger_config = None if enable_logging: logger_config = LoggerConfig( base_url="http://localhost:3000", enabled=True, timeout=30 ) # Create agent configuration config = CoagentConfig( model_name=model_name, logger_config=logger_config, contexts=self._create_contexts() ) # Initialize the CoAgent self.agent = Coagent(config) console.print(f"ā Recipe Agent initialized with model: {model_name}") def _create_contexts(self) -> List[CoagentContext]: """Create specialized cooking contexts for different scenarios.""" return [ CoagentContext( name="quick_meals", description="Fast and easy recipes for busy schedules", prompt="""You are a quick meal specialist. Focus on recipes that can be prepared in 30 minutes or less using common ingredients. Prioritize simple techniques and minimal cleanup. Always include prep and cook times.""" ), CoagentContext( name="healthy_cooking", description="Nutritious and balanced meal suggestions", prompt="""You are a nutrition-focused chef. Create healthy, balanced recipes that are both nutritious and delicious. Include nutritional benefits, suggest ingredient substitutions for dietary restrictions, and focus on whole foods.""" ), CoagentContext( name="comfort_food", description="Hearty, satisfying comfort food recipes", prompt="""You are a comfort food specialist. Create hearty, satisfying recipes that bring warmth and happiness. Focus on traditional techniques, rich flavors, and recipes that feed the soul. Include stories or tips that make cooking enjoyable.""" ), CoagentContext( name="international_cuisine", description="Authentic recipes from around the world", prompt="""You are a world cuisine expert. Provide authentic international recipes with cultural context. Explain unique ingredients, traditional cooking methods, and the cultural significance of dishes. Help users explore global flavors.""" ), CoagentContext( name="baking_pastry", description="Baking and pastry recipes with precise techniques", prompt="""You are a professional baker and pastry chef. Provide precise, detailed baking recipes with exact measurements and techniques. Explain the science behind baking, troubleshoot common issues, and emphasize precision and timing.""" ) ] def generate_recipe(self, ingredients: List[str], context: Optional[str] = None, dietary_restrictions: Optional[List[str]] = None, servings: int = 4) -> dict: """Generate a recipe based on ingredients and preferences.""" # Build the prompt prompt_parts = [ f"Create a recipe using these ingredients: {', '.join(ingredients)}", f"The recipe should serve {servings} people." ] if dietary_restrictions: prompt_parts.append(f"Dietary restrictions: {', '.join(dietary_restrictions)}") prompt_parts.append("Please provide the recipe in a structured format with ingredients, instructions, and cooking tips.") prompt = " ".join(prompt_parts) try: # Process the prompt with optional context response = self.agent.process_prompt(prompt, context or "") # Parse the response return { "success": True, "recipe": response.response, "context_used": response.meta.get("context_name", "auto-selected"), "llm_calls": len(response.llm_calls), "run_id": response.meta.get("run_id") } except Exception as e: console.print(f"ā Error generating recipe: {str(e)}") return { "success": False, "error": str(e), "recipe": None } def suggest_context(self, ingredients: List[str], preferences: dict) -> str: """Suggest the best context based on ingredients and user preferences.""" # Simple heuristics for context selection if preferences.get("time_limit", 60) <= 30: return "quick_meals" elif any(word in " ".join(ingredients).lower() for word in ["flour", "sugar", "butter", "eggs"]): return "baking_pastry" elif preferences.get("healthy", False): return "healthy_cooking" elif preferences.get("comfort", False): return "comfort_food" elif preferences.get("international", False): return "international_cuisine" else: return "" # Auto-select def display_recipe(self, result: dict): """Display the recipe result in a formatted way.""" if not result["success"]: console.print(f"[red]Failed to generate recipe: {result.get('error', 'Unknown error')}[/red]") return # Create a table for recipe metadata table = Table(title="Recipe Generation Results") table.add_column("Attribute", style="cyan") table.add_column("Value", style="green") table.add_row("Context Used", result["context_used"]) table.add_row("LLM Calls Made", str(result["llm_calls"])) if result.get("run_id"): table.add_row("Run ID", result["run_id"]) console.print(table) console.print("\n[bold yellow]Generated Recipe:[/bold yellow]") console.print(result["recipe"]) # Test the basic functionality if __name__ == "__main__": # Create the agent agent = RecipeAgent() # Test with sample ingredients ingredients = ["chicken", "broccoli", "rice"] preferences = {"healthy": True} # Suggest context suggested_context = agent.suggest_context(ingredients, preferences) console.print(f"Suggested context: {suggested_context}") # Generate recipe result = agent.generate_recipe( ingredients=ingredients, context=suggested_context, dietary_restrictions=["gluten-free"] ) # Display result agent.display_recipe(result)
2.2 Test the Basic Agent
Run your basic agent:
You should see output similar to:
ā Recipe Agent initialized with model: llama3.1:8b Suggested context: healthy_cooking āāāāāāāāāāāāāāāāāā³āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā Attribute ā Value ā ā”āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā© ā Context Used ā healthy_cooking ā ā LLM Calls Made ā 1 ā āāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā Generated Recipe: [Recipe content here
Phase 3: Context-Aware Responses
3.1 Enhance Context Selection
Update recipe_agent.py to include more sophisticated context selection:
def analyze_ingredients_and_preferences(self, ingredients: List[str], preferences: dict) -> dict: """Analyze ingredients and preferences to suggest optimal context.""" analysis = { "ingredient_categories": [], "cooking_complexity": "medium", "estimated_time": 45, "suggested_contexts": [] } # Categorize ingredients protein_keywords = ["chicken", "beef", "pork", "fish", "tofu", "beans"] carb_keywords = ["rice", "pasta", "bread", "potatoes", "quinoa"] vegetable_keywords = ["broccoli", "spinach", "carrots", "onions"] baking_keywords = ["flour", "sugar", "butter", "eggs", "milk"] ingredients_lower = [ing.lower() for ing in ingredients] if any(kw in " ".join(ingredients_lower) for kw in protein_keywords): analysis["ingredient_categories"].append("protein") if any(kw in " ".join(ingredients_lower) for kw in carb_keywords): analysis["ingredient_categories"].append("carbohydrates") if any(kw in " ".join(ingredients_lower) for kw in vegetable_keywords): analysis["ingredient_categories"].append("vegetables") if any(kw in " ".join(ingredients_lower) for kw in baking_keywords): analysis["ingredient_categories"].append("baking") analysis["suggested_contexts"].append("baking_pastry") # Time-based context selection if preferences.get("time_limit", 60) <= 20: analysis["suggested_contexts"].append("quick_meals") analysis["estimated_time"] = 20 elif preferences.get("time_limit", 60) <= 40: analysis["estimated_time"] = 30 # Health-based selection if preferences.get("healthy", False) or len(analysis["ingredient_categories"]) >= 2: analysis["suggested_contexts"].append("healthy_cooking") # Comfort food indicators if preferences.get("comfort", False) or any(word in " ".join(ingredients_lower) for word in ["cheese", "cream", "butter"]): analysis["suggested_contexts"].append("comfort_food") # International cuisine if preferences.get("cuisine_style") in ["italian", "asian", "mexican", "indian"]: analysis["suggested_contexts"].append("international_cuisine") return analysis def get_context_with_analysis(self, ingredients: List[str], preferences: dict) -> tuple[str, dict]: """Get the best context along with analysis information.""" analysis = self.analyze_ingredients_and_preferences(ingredients, preferences) if analysis["suggested_contexts"]: # Use the first suggested context selected_context = analysis["suggested_contexts"][0] else: # Fallback to general context selection selected_context = self.suggest_context(ingredients, preferences) return selected_context, analysis
3.2 Add Recipe Refinement
Add a method to refine recipes based on feedback:
def refine_recipe(self, original_recipe: str, refinement_request: str, context: str) -> dict: """Refine an existing recipe based on user feedback.""" prompt = f""" Here's a recipe I generated earlier: {original_recipe} The user wants to refine it with this request: {refinement_request} Please provide an improved version of the recipe that addresses their request while maintaining the overall structure and quality. """ try: response = self.agent.process_prompt(prompt, context) return { "success": True, "refined_recipe": response.response, "context_used": response.meta.get("context_name", context), "refinement_applied": refinement_request } except Exception as e: return { "success": False, "error": str(e) }
Phase 4: Logging Integration
4.1 Create Interactive Recipe Session
Create interactive_recipe_session.py:
#!/usr/bin/env python3 """ Interactive Recipe Session - Demonstrates logging and monitoring integration """ from recipe_agent import RecipeAgent from rich.console import Console from rich.prompt import Prompt, Confirm from rich.panel import Panel import time import uuid console = Console() class RecipeSession: """Interactive recipe generation session with full logging.""" def __init__(self): self.agent = RecipeAgent(enable_logging=True) self.session_id = str(uuid.uuid4()) self.recipes_generated = [] def start_session(self): """Start an interactive recipe generation session.""" console.print(Panel.fit( "[bold blue]Welcome to CoAgent Recipe Generator![/bold blue]\n" f"Session ID: {self.session_id}\n" "Let's create some amazing recipes together!", border_style="blue" )) while True: try: # Get user input ingredients = self._get_ingredients() if not ingredients: break preferences = self._get_preferences() # Generate recipe with timing start_time = time.time() context, analysis = self.agent.get_context_with_analysis(ingredients, preferences) console.print(f"\nš¤ Using context: [cyan]{context}[/cyan]") console.print(f"š Analysis: {analysis}") result = self.agent.generate_recipe( ingredients=ingredients, context=context, dietary_restrictions=preferences.get("dietary_restrictions", []), servings=preferences.get("servings", 4) ) generation_time = time.time() - start_time # Store recipe recipe_data = { **result, "ingredients": ingredients, "preferences": preferences, "analysis": analysis, "generation_time": generation_time, "session_id": self.session_id } self.recipes_generated.append(recipe_data) # Display results self.agent.display_recipe(result) console.print(f"ā±ļø Generation time: {generation_time:.2f}s") # Offer refinement if result["success"]: if Confirm.ask("\nWould you like to refine this recipe?"): self._refine_recipe_interactive(result["recipe"], context) # Continue? if not Confirm.ask("\nGenerate another recipe?"): break except KeyboardInterrupt: console.print("\nš Session interrupted by user") break except Exception as e: console.print(f"[red]ā Unexpected error: {str(e)}[/red]") if not Confirm.ask("Continue despite error?"): break self._session_summary() def _get_ingredients(self) -> list: """Get ingredients from user input.""" ingredients_input = Prompt.ask( "\nš„ Enter ingredients (comma-separated), or 'quit' to exit" ) if ingredients_input.lower() in ['quit', 'exit', 'q']: return [] return [ing.strip() for ing in ingredients_input.split(',') if ing.strip()] def _get_preferences(self) -> dict: """Get cooking preferences from user.""" preferences = {} # Time preference time_input = Prompt.ask( "ā° Time limit in minutes (press Enter for no limit)", default="" ) if time_input: try: preferences["time_limit"] = int(time_input) except ValueError: console.print("[yellow]Invalid time input, using no limit[/yellow]") # Dietary restrictions dietary = Prompt.ask( "š„ Dietary restrictions (comma-separated, or Enter for none)", default="" ) if dietary: preferences["dietary_restrictions"] = [d.strip() for d in dietary.split(',')] # Servings servings = Prompt.ask("š„ Number of servings", default="4") try: preferences["servings"] = int(servings) except ValueError: preferences["servings"] = 4 # Style preferences preferences["healthy"] = Confirm.ask("š„¦ Focus on healthy options?", default=False) preferences["comfort"] = Confirm.ask("š² Want comfort food?", default=False) cuisine = Prompt.ask( "š Preferred cuisine style (italian, asian, mexican, indian, or Enter for any)", default="" ) if cuisine: preferences["cuisine_style"] = cuisine.lower() return preferences def _refine_recipe_interactive(self, original_recipe: str, context: str): """Interactive recipe refinement.""" refinement = Prompt.ask("\n⨠How would you like to refine the recipe?") console.print("š Refining recipe...") result = self.agent.refine_recipe(original_recipe, refinement, context) if result["success"]: console.print("\n[bold green]Refined Recipe:[/bold green]") console.print(result["refined_recipe"]) else: console.print(f"[red]ā Refinement failed: {result.get('error')}[/red]") def _session_summary(self): """Display session summary with monitoring insights.""" console.print(Panel.fit( f"[bold green]Session Complete![/bold green]\n" f"Session ID: {self.session_id}\n" f"Recipes Generated: {len(self.recipes_generated)}\n" f"Total Generation Time: {sum(r['generation_time'] for r in self.recipes_generated):.2f}s\n" f"Average Time per Recipe: {sum(r['generation_time'] for r in self.recipes_generated) / len(self.recipes_generated):.2f}s" if self.recipes_generated else "No recipes generated", border_style="green" )) # Log session summary if self.recipes_generated: successful_recipes = [r for r in self.recipes_generated if r['success']] console.print(f"\nā Success Rate: {len(successful_recipes)}/{len(self.recipes_generated)} ({len(successful_recipes)/len(self.recipes_generated)*100:.1f}%)") # Show contexts used contexts_used = [r.get('context_used', 'unknown') for r in successful_recipes] context_counts = {} for ctx in contexts_used: context_counts[ctx] = context_counts.get(ctx, 0) + 1 console.print("\nš Contexts Used:") for ctx, count in context_counts.items(): console.print(f" ⢠{ctx}: {count}") if __name__ == "__main__": session = RecipeSession() session.start_session()
4.2 Monitor the Session
Run the interactive session:
While it's running, you can monitor the activity in the CoAgent monitoring dashboard:
- Open - http://localhost:3000/monitoringin your browser
- Watch real-time logs appear as you interact with the agent 
- View performance metrics and response times 
Phase 5: Error Handling & Recovery
5.1 Add Robust Error Handling
Update recipe_agent.py with comprehensive error handling:
import logging from typing import Union import time # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class RecipeAgentError(Exception): """Custom exception for recipe agent errors.""" pass class RecipeAgent: # ... (existing code) ... def __init__(self, model_name: str = "llama3.1:8b", enable_logging: bool = True, max_retries: int = 3, timeout: int = 30): """Initialize with error handling configuration.""" self.max_retries = max_retries self.timeout = timeout # ... (rest of existing init code) ... def generate_recipe_with_retry(self, ingredients: List[str], context: Optional[str] = None, dietary_restrictions: Optional[List[str]] = None, servings: int = 4) -> dict: """Generate recipe with retry logic and error recovery.""" last_error = None for attempt in range(self.max_retries): try: logger.info(f"Recipe generation attempt {attempt + 1}/{self.max_retries}") result = self.generate_recipe( ingredients=ingredients, context=context, dietary_restrictions=dietary_restrictions, servings=servings ) if result["success"]: logger.info("Recipe generated successfully") return result else: last_error = result.get("error", "Unknown error") logger.warning(f"Recipe generation failed: {last_error}") except Exception as e: last_error = str(e) logger.error(f"Exception in attempt {attempt + 1}: {last_error}") # Wait before retry (exponential backoff) if attempt < self.max_retries - 1: wait_time = 2 ** attempt logger.info(f"Waiting {wait_time}s before retry...") time.sleep(wait_time) # All attempts failed error_msg = f"Failed to generate recipe after {self.max_retries} attempts. Last error: {last_error}" logger.error(error_msg) return { "success": False, "error": error_msg, "attempts_made": self.max_retries, "recipe": None } def validate_inputs(self, ingredients: List[str], servings: int) -> Union[bool, str]: """Validate user inputs before processing.""" if not ingredients: return "No ingredients provided" if len(ingredients) > 20: return "Too many ingredients (max 20)" if servings <= 0 or servings > 50: return "Invalid serving size (must be 1-50)" # Check for obviously invalid ingredients invalid_chars = set('0123456789!@#$%^&*()+={}[]|\\:";\'<>?/~`') for ingredient in ingredients: if len(ingredient) > 100: return f"Ingredient name too long: {ingredient[:50]}..." if any(char in ingredient for char in invalid_chars): return f"Invalid ingredient format: {ingredient}" return True def health_check(self) -> dict: """Check if the agent is functioning properly.""" try: # Simple test prompt start_time = time.time() result = self.agent.process_prompt("Say hello", "") response_time = time.time() - start_time return { "healthy": True, "response_time": response_time, "model_accessible": True, "logging_enabled": self.agent.logger_enabled } except Exception as e: return { "healthy": False, "error": str(e), "response_time": None, "model_accessible": False }
5.2 Create Error Recovery Demo
Create error_recovery_demo.py:
#!/usr/bin/env python3 """ Error Recovery Demo - Shows robust error handling in action """ from recipe_agent import RecipeAgent from rich.console import Console import time console = Console() def test_error_scenarios(): """Test various error scenarios and recovery mechanisms.""" console.print("[bold blue]Testing Error Recovery Scenarios[/bold blue]\n") agent = RecipeAgent(max_retries=3, timeout=10) # Test 1: Invalid inputs console.print("[yellow]Test 1: Invalid Inputs[/yellow]") invalid_tests = [ ([], 4, "Empty ingredients list"), (["chicken"], 0, "Invalid serving size"), (["ingredient_with_numbers123", "normal_ingredient"], 4, "Invalid ingredient format"), (["x" * 150], 4, "Ingredient name too long") ] for ingredients, servings, description in invalid_tests: validation_result = agent.validate_inputs(ingredients, servings) if validation_result is True: console.print(f" ā {description}: Validation passed") else: console.print(f" ā {description}: {validation_result}") # Test 2: Health check console.print(f"\n[yellow]Test 2: Agent Health Check[/yellow]") health = agent.health_check() if health["healthy"]: console.print(f" ā Agent healthy (response time: {health['response_time']:.2f}s)") else: console.print(f" ā Agent unhealthy: {health['error']}") # Test 3: Normal operation with monitoring console.print(f"\n[yellow]Test 3: Normal Operation with Error Monitoring[/yellow]") test_cases = [ (["chicken", "rice"], {"healthy": True}, "Simple healthy recipe"), (["chocolate", "flour", "butter"], {"comfort": True}, "Comfort food baking"), (["tofu", "vegetables"], {"time_limit": 15}, "Quick vegetarian meal") ] for ingredients, preferences, description in test_cases: console.print(f" š§Ŗ Testing: {description}") try: start_time = time.time() result = agent.generate_recipe_with_retry( ingredients=ingredients, dietary_restrictions=preferences.get("dietary_restrictions") ) elapsed = time.time() - start_time if result["success"]: console.print(f" ā Success in {elapsed:.2f}s") console.print(f" š Context: {result.get('context_used', 'unknown')}") else: console.print(f" ā Failed: {result.get('error', 'Unknown error')}") console.print(f" š Attempts made: {result.get('attempts_made', 'unknown')}") except Exception as e: console.print(f" š„ Unexpected error: {str(e)}") # Brief pause between tests time.sleep(1) console.print("\n[green]Error recovery testing complete![/green]") if __name__ == "__main__": test_error_scenarios()
Phase 6: Advanced Features
6.1 Add Recipe Persistence
Create recipe_storage.py:
#!/usr/bin/env python3 """ Recipe Storage - Persistent storage for generated recipes """ import json import sqlite3 from datetime import datetime from typing import List, Dict, Optional import uuid import os class RecipeStorage: """SQLite-based storage for recipe data and session history.""" def __init__(self, db_path: str = "recipes.db"): self.db_path = db_path self.init_database() def init_database(self): """Initialize the SQLite database with required tables.""" with sqlite3.connect(self.db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS recipes ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, ingredients TEXT NOT NULL, recipe_text TEXT NOT NULL, context_used TEXT, preferences TEXT, generation_time REAL, success INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, rating INTEGER, notes TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, total_recipes INTEGER DEFAULT 0, successful_recipes INTEGER DEFAULT 0, total_time REAL DEFAULT 0, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ended_at TIMESTAMP, user_notes TEXT ) """) conn.commit() def save_recipe(self, recipe_data: Dict) -> str: """Save a recipe to the database.""" recipe_id = str(uuid.uuid4()) with sqlite3.connect(self.db_path) as conn: conn.execute(""" INSERT INTO recipes (id, session_id, ingredients, recipe_text, context_used, preferences, generation_time, success) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( recipe_id, recipe_data.get("session_id"), json.dumps(recipe_data.get("ingredients", [])), recipe_data.get("recipe", ""), recipe_data.get("context_used"), json.dumps(recipe_data.get("preferences", {})), recipe_data.get("generation_time", 0), 1 if recipe_data.get("success", False) else 0 )) conn.commit() return recipe_id def get_recipe(self, recipe_id: str) -> Optional[Dict]: """Retrieve a recipe by ID.""" with sqlite3.connect(self.db_path) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM recipes WHERE id = ?", (recipe_id,)) row = cursor.fetchone() if row: return dict(row) return None def search_recipes(self, ingredient: str = None, context: str = None, limit: int = 10) -> List[Dict]: """Search recipes by ingredient or context.""" query = "SELECT * FROM recipes WHERE success = 1" params = [] if ingredient: query += " AND ingredients LIKE ?" params.append(f"%{ingredient}%") if context: query += " AND context_used = ?" params.append(context) query += " ORDER BY created_at DESC LIMIT ?" params.append(limit) with sqlite3.connect(self.db_path) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(query, params) return [dict(row) for row in cursor.fetchall()] def rate_recipe(self, recipe_id: str, rating: int, notes: str = ""): """Rate a recipe and add notes.""" with sqlite3.connect(self.db_path) as conn: conn.execute(""" UPDATE recipes SET rating = ?, notes = ? WHERE id = ? """, (rating, notes, recipe_id)) conn.commit() def get_session_stats(self, session_id: str) -> Dict: """Get statistics for a session.""" with sqlite3.connect(self.db_path) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT COUNT(*) as total_recipes, SUM(success) as successful_recipes, SUM(generation_time) as total_time, AVG(generation_time) as avg_time, AVG(rating) as avg_rating FROM recipes WHERE session_id = ? """, (session_id,)) stats = dict(cursor.fetchone()) # Get most used context cursor.execute(""" SELECT context_used, COUNT(*) as count FROM recipes WHERE session_id = ? AND success = 1 GROUP BY context_used ORDER BY count DESC LIMIT 1 """, (session_id,)) context_row = cursor.fetchone() if context_row: stats["most_used_context"] = dict(context_row) return stats
6.2 Enhanced Recipe Agent with Persistence
Update recipe_agent.py to include storage:
from recipe_storage import RecipeStorage class RecipeAgent: # ... (existing code) ... def __init__(self, model_name: str = "llama3.1:8b", enable_logging: bool = True, max_retries: int = 3, timeout: int = 30, enable_storage: bool = True): # ... (existing initialization) ... # Initialize storage if enabled self.storage = RecipeStorage() if enable_storage else None if self.storage: console.print("š¾ Recipe storage enabled") def generate_recipe_with_storage(self, ingredients: List[str], session_id: str, context: Optional[str] = None, dietary_restrictions: Optional[List[str]] = None, servings: int = 4) -> dict: """Generate recipe and automatically save to storage.""" result = self.generate_recipe_with_retry( ingredients=ingredients, context=context, dietary_restrictions=dietary_restrictions, servings=servings ) # Add session and storage info result.update({ "session_id": session_id, "ingredients": ingredients, "preferences": { "dietary_restrictions": dietary_restrictions or [], "servings": servings } }) # Save to storage if enabled and successful if self.storage and result.get("success", False): recipe_id = self.storage.save_recipe(result) result["recipe_id"] = recipe_id console.print(f"š¾ Recipe saved with ID: {recipe_id}") return result def find_similar_recipes(self, ingredients: List[str], limit: int = 5) -> List[Dict]: """Find similar recipes based on ingredients.""" if not self.storage: return [] # Search for recipes containing any of the ingredients similar_recipes = [] for ingredient in ingredients: recipes = self.storage.search_recipes(ingredient=ingredient, limit=limit) similar_recipes.extend(recipes) # Remove duplicates and sort by creation date seen_ids = set() unique_recipes = [] for recipe in similar_recipes: if recipe["id"] not in seen_ids: unique_recipes.append(recipe) seen_ids.add(recipe["id"]) return unique_recipes[:limit]
Phase 7: Testing & Validation
7.1 Create Comprehensive Test Suite
Create test_recipe_agent.py:
#!/usr/bin/env python3 """ Test Suite for Recipe Agent - Comprehensive testing of all features """ import pytest import tempfile import os from recipe_agent import RecipeAgent from recipe_storage import RecipeStorage import time class TestRecipeAgent: """Test suite for the RecipeAgent class.""" def setup_method(self): """Set up test environment before each test.""" self.agent = RecipeAgent(enable_logging=False) # Disable logging for tests self.test_ingredients = ["chicken", "broccoli", "rice"] self.test_preferences = {"healthy": True, "servings": 4} def test_agent_initialization(self): """Test that the agent initializes correctly.""" assert self.agent is not None assert hasattr(self.agent, 'agent') assert len(self.agent._create_contexts()) > 0 def test_input_validation(self): """Test input validation functionality.""" # Valid inputs assert self.agent.validate_inputs(self.test_ingredients, 4) is True # Invalid inputs assert self.agent.validate_inputs([], 4) != True # Empty ingredients assert self.agent.validate_inputs(self.test_ingredients, 0) != True # Invalid servings assert self.agent.validate_inputs(["ingredient123"], 4) != True # Invalid format def test_context_suggestion(self): """Test context suggestion logic.""" # Health-focused health_context = self.agent.suggest_context( ["vegetables", "quinoa"], {"healthy": True} ) assert health_context == "healthy_cooking" # Quick meals quick_context = self.agent.suggest_context( ["pasta"], {"time_limit": 15} ) assert quick_context == "quick_meals" # Baking baking_context = self.agent.suggest_context( ["flour", "sugar", "butter"], {} ) assert baking_context == "baking_pastry" def test_health_check(self): """Test agent health check functionality.""" health = self.agent.health_check() # Should have required keys required_keys = ["healthy", "response_time", "model_accessible"] for key in required_keys: assert key in health @pytest.mark.integration def test_recipe_generation(self): """Integration test for recipe generation.""" result = self.agent.generate_recipe( ingredients=self.test_ingredients, context="healthy_cooking" ) # Should have required keys assert "success" in result assert "recipe" in result or "error" in result if result["success"]: assert result["recipe"] is not None assert len(result["recipe"]) > 0 @pytest.mark.integration def test_recipe_generation_with_retry(self): """Test recipe generation with retry logic.""" result = self.agent.generate_recipe_with_retry( ingredients=self.test_ingredients ) assert "success" in result if not result["success"]: assert "attempts_made" in result class TestRecipeStorage: """Test suite for recipe storage functionality.""" def setup_method(self): """Set up test database before each test.""" self.temp_db = tempfile.NamedTemporaryFile(delete=False, suffix='.db') self.temp_db.close() self.storage = RecipeStorage(db_path=self.temp_db.name) self.test_recipe_data = { "session_id": "test-session-123", "ingredients": ["chicken", "rice"], "recipe": "Test recipe content...", "context_used": "healthy_cooking", "preferences": {"healthy": True}, "generation_time": 2.5, "success": True } def teardown_method(self): """Clean up test database after each test.""" os.unlink(self.temp_db.name) def test_save_and_retrieve_recipe(self): """Test saving and retrieving recipes.""" # Save recipe recipe_id = self.storage.save_recipe(self.test_recipe_data) assert recipe_id is not None # Retrieve recipe retrieved = self.storage.get_recipe(recipe_id) assert retrieved is not None assert retrieved["ingredients"] == '["chicken", "rice"]' # JSON serialized assert retrieved["success"] == 1 def test_search_recipes(self): """Test recipe search functionality.""" # Save test recipe recipe_id = self.storage.save_recipe(self.test_recipe_data) # Search by ingredient results = self.storage.search_recipes(ingredient="chicken") assert len(results) >= 1 assert any(r["id"] == recipe_id for r in results) # Search by context results = self.storage.search_recipes(context="healthy_cooking") assert len(results) >= 1 assert any(r["id"] == recipe_id for r in results) def test_recipe_rating(self): """Test recipe rating functionality.""" recipe_id = self.storage.save_recipe(self.test_recipe_data) # Rate the recipe self.storage.rate_recipe(recipe_id, 5, "Excellent!") # Verify rating recipe = self.storage.get_recipe(recipe_id) assert recipe["rating"] == 5 assert recipe["notes"] == "Excellent!" def run_performance_test(): """Run a performance test to measure response times.""" print("\nš Running Performance Tests...") agent = RecipeAgent(enable_logging=True) test_cases = [ (["pasta", "tomatoes"], "quick_meals"), (["chicken", "vegetables"], "healthy_cooking"), (["flour", "chocolate"], "baking_pastry"), (["beef", "potatoes"], "comfort_food") ] total_time = 0 successful_generations = 0 for ingredients, context in test_cases: start_time = time.time() result = agent.generate_recipe(ingredients=ingredients, context=context) elapsed = time.time() - start_time total_time += elapsed if result["success"]: successful_generations += 1 print(f" ⢠{context}: {elapsed:.2f}s ({'ā ' if result['success'] else 'ā'})") print(f"\nš Performance Summary:") print(f" ⢠Total time: {total_time:.2f}s") print(f" ⢠Average time: {total_time/len(test_cases):.2f}s") print(f" ⢠Success rate: {successful_generations}/{len(test_cases)} ({successful_generations/len(test_cases)*100:.1f}%)") if __name__ == "__main__": # Run basic tests print("š§Ŗ Running Recipe Agent Tests...") # You can run pytest if installed, or run basic tests manually try: import pytest pytest.main([__file__, "-v"]) except ImportError: print("pytest not available, running manual tests...") # Manual test execution test_agent = TestRecipeAgent() test_agent.setup_method() try: test_agent.test_agent_initialization() print("ā Agent initialization test passed") except Exception as e: print(f"ā Agent initialization test failed: {e}") try: test_agent.test_input_validation() print("ā Input validation test passed") except Exception as e: print(f"ā Input validation test failed: {e}") try: test_agent.test_context_suggestion() print("ā Context suggestion test passed") except Exception as e: print(f"ā Context suggestion test failed: {e}") # Run performance test run_performance_test()
7.2 Create Final Demo Application
Create final_demo.py:
#!/usr/bin/env python3 """ Final Demo - Complete Recipe Agent with all features """ from recipe_agent import RecipeAgent from recipe_storage import RecipeStorage from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.prompt import Prompt, Confirm import uuid import time console = Console() class RecipeAgentDemo: """Complete demo showcasing all Recipe Agent features.""" def __init__(self): self.agent = RecipeAgent(enable_storage=True, enable_logging=True) self.session_id = str(uuid.uuid4()) console.print(Panel.fit( f"[bold blue]CoAgent Recipe Generator - Complete Demo[/bold blue]\n" f"Session ID: {self.session_id[:8]}...\n" "Features: Context Switching ⢠Logging ⢠Storage ⢠Error Recovery", border_style="blue" )) def run_demo(self): """Run the complete feature demonstration.""" while True: console.print("\n[bold cyan]Choose an option:[/bold cyan]") console.print("1. Generate new recipe") console.print("2. Find similar recipes") console.print("3. Rate a previous recipe") console.print("4. View session statistics") console.print("5. Run performance test") console.print("6. Exit") choice = Prompt.ask("Enter your choice", choices=["1", "2", "3", "4", "5", "6"]) if choice == "1": self._generate_recipe_workflow() elif choice == "2": self._find_similar_recipes() elif choice == "3": self._rate_recipe() elif choice == "4": self._show_session_stats() elif choice == "5": self._run_performance_test() else: break console.print("\n[green]Thanks for trying the CoAgent Recipe Generator![/green]") def _generate_recipe_workflow(self): """Complete recipe generation workflow.""" # Get ingredients ingredients_input = Prompt.ask("š„ Enter ingredients (comma-separated)") ingredients = [ing.strip() for ing in ingredients_input.split(',') if ing.strip()] # Validate inputs validation = self.agent.validate_inputs(ingredients, 4) if validation is not True: console.print(f"[red]ā {validation}[/red]") return # Get preferences preferences = {} if Confirm.ask("Configure preferences?", default=False): time_limit = Prompt.ask("Time limit (minutes, or Enter for none)", default="") if time_limit: preferences["time_limit"] = int(time_limit) preferences["healthy"] = Confirm.ask("Focus on healthy?", default=False) preferences["comfort"] = Confirm.ask("Want comfort food?", default=False) # Context analysis context, analysis = self.agent.get_context_with_analysis(ingredients, preferences) console.print(f"\nš¤ Context Analysis:") console.print(f" ⢠Selected context: [cyan]{context}[/cyan]") console.print(f" ⢠Ingredient categories: {analysis.get('ingredient_categories', [])}") console.print(f" ⢠Estimated time: {analysis.get('estimated_time', 'unknown')} minutes") # Generate recipe console.print("\nš³ Generating recipe...") start_time = time.time() result = self.agent.generate_recipe_with_storage( ingredients=ingredients, session_id=self.session_id, context=context, dietary_restrictions=preferences.get("dietary_restrictions") ) generation_time = time.time() - start_time # Display results if result["success"]: console.print(f"\n[bold green]Recipe Generated Successfully![/bold green]") console.print(f"ā±ļø Generation time: {generation_time:.2f}s") console.print(f"š Recipe ID: {result.get('recipe_id', 'Not saved')}") console.print("\n[bold yellow]Recipe:[/bold yellow]") console.print(Panel(result["recipe"], border_style="yellow")) # Offer refinement if Confirm.ask("Refine this recipe?", default=False): refinement = Prompt.ask("How would you like to refine it?") refined = self.agent.refine_recipe(result["recipe"], refinement, context) if refined["success"]: console.print("\n[bold green]Refined Recipe:[/bold green]") console.print(Panel(refined["refined_recipe"], border_style="green")) else: console.print(f"[red]ā Recipe generation failed: {result.get('error')}[/red]") def _find_similar_recipes(self): """Find and display similar recipes.""" ingredient = Prompt.ask("š Search for recipes containing which ingredient?") similar_recipes = self.agent.find_similar_recipes([ingredient], limit=5) if similar_recipes: table = Table(title=f"Recipes containing '{ingredient}'") table.add_column("ID", style="cyan") table.add_column("Ingredients", style="green") table.add_column("Context", style="yellow") table.add_column("Rating", style="magenta") table.add_column("Created", style="blue") for recipe in similar_recipes: ingredients = eval(recipe["ingredients"]) if recipe["ingredients"] else [] rating = f"ā {recipe['rating']}" if recipe["rating"] else "No rating" created = recipe["created_at"][:10] if recipe["created_at"] else "Unknown" table.add_row( recipe["id"][:8] + "...", ", ".join(ingredients[:3]) + ("..." if len(ingredients) > 3 else ""), recipe["context_used"] or "Unknown", rating, created ) console.print(table) else: console.print(f"[yellow]No recipes found containing '{ingredient}'[/yellow]") def _rate_recipe(self): """Rate a previously generated recipe.""" recipe_id = Prompt.ask("š Enter recipe ID to rate") if self.agent.storage: recipe = self.agent.storage.get_recipe(recipe_id) if recipe: # Show recipe summary ingredients = eval(recipe["ingredients"]) if recipe["ingredients"] else [] console.print(f"\nš Recipe: {', '.join(ingredients)}") console.print(f"š Created: {recipe['created_at']}") # Get rating rating = int(Prompt.ask("ā Rate this recipe (1-5)", choices=["1", "2", "3", "4", "5"])) notes = Prompt.ask("š Notes (optional)", default="") self.agent.storage.rate_recipe(recipe_id, rating, notes) console.print("[green]ā Rating saved![/green]") else: console.print("[red]ā Recipe not found[/red]") else: console.print("[red]ā Storage not enabled[/red]") def _show_session_stats(self): """Display session statistics.""" if self.agent.storage: stats = self.agent.storage.get_session_stats(self.session_id) table = Table(title="Session Statistics") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") table.add_row("Total Recipes", str(stats.get("total_recipes", 0))) table.add_row("Successful", str(stats.get("successful_recipes", 0))) table.add_row("Success Rate", f"{stats.get('successful_recipes', 0) / max(1, stats.get('total_recipes', 1)) * 100:.1f}%") table.add_row("Total Time", f"{stats.get('total_time', 0):.2f}s") table.add_row("Avg Time", f"{stats.get('avg_time', 0):.2f}s") if stats.get("avg_rating"): table.add_row("Avg Rating", f"ā {stats['avg_rating']:.1f}") if stats.get("most_used_context"): context_info = stats["most_used_context"] table.add_row("Most Used Context", f"{context_info['context_used']} ({context_info['count']}x)") console.print(table) else: console.print("[red]ā Storage not enabled[/red]") def _run_performance_test(self): """Run a quick performance test.""" console.print("š Running performance test...") test_cases = [ (["pasta", "tomatoes"], "Italian pasta"), (["chicken", "vegetables"], "Healthy meal"), (["flour", "chocolate"], "Dessert") ] total_time = 0 successful = 0 for ingredients, description in test_cases: start = time.time() result = self.agent.generate_recipe(ingredients=ingredients) elapsed = time.time() - start total_time += elapsed if result["success"]: successful += 1 console.print(f" ⢠{description}: {elapsed:.2f}s ({'ā ' if result['success'] else 'ā'})") console.print(f"\nš Performance: {successful}/{len(test_cases)} successful, avg {total_time/len(test_cases):.2f}s") if __name__ == "__main__": demo = RecipeAgentDemo() demo.run_demo()
7.3 Run the Complete Tutorial
# Test the complete application python final_demo.py # Run the test suite python test_recipe_agent.py # Try the interactive session
Monitoring Your Application
While using your recipe agent, monitor its performance:
- CoAgent Dashboard: Visit - http://localhost:3000/monitoring
- View Logs: Check the Runs section for detailed execution logs 
- Performance Metrics: Monitor response times and success rates 
- Cost Tracking: Watch token usage and estimated costs 
Summary
š Congratulations! You've built a complete AI agent application with:
- ā Context-Aware Responses - Adaptive behavior based on cooking scenarios 
- ā Robust Error Handling - Retry logic and input validation 
- ā Integrated Logging - Full observability with CoAgent monitoring 
- ā Persistent Storage - SQLite database for recipe history 
- ā Performance Testing - Comprehensive test suite 
- ā Interactive Interface - Rich command-line experience 
Next Steps
- Multi-Agent Testing Tutorial - Learn to test and compare different agent configurations 
- Agent Configuration Guide - Advanced agent setup patterns 
- Monitoring Guide - Deep dive into observability features 
- REST API Reference - Build custom integrations