Now, that you know how to build a simple AI agent with LiteLLM, CrewAI and Generative AI Hub, let's work on making the code prettier and easier to understand. CrewAI recommends to keep the AI agent definition in a separate YAML file.
"Using YAML configuration provides a cleaner, more maintainable way to define agents. We strongly recommend using this approach in your CrewAI projects." - CrewAI
👉 Create a new folder /project/Python/starter-project/config.
👉 Create a new file /project/Python/starter-project/config/agents.yaml
👉 Create a new file /project/Python/starter-project/config/tasks.yaml
👉 Create another new file in the starter project root /project/Python/starter-project/investigator_crew.py
Instead of defining agents and tasks directly in Python code, we'll move them to YAML configuration files. This makes the code cleaner and easier to maintain.
👉 Open /project/Python/starter-project/config/agents.yaml and add:
# agents.yaml
appraiser_agent:
role: >
Loss Appraiser
goal: >
Predict the missing values of stolen items using the RPT-1 model via the call_rpt1 tool.
Use the payload data from inputs: {payload}
backstory: >
You are an expert insurance appraiser specializing in fine art valuation and theft assessment.
llm: sap/gpt-4o💡 What's happening here? We're taking the agent definition from your
basic_agent.pyfile (therole,goal,backstory, andllmparameters) and moving them to this YAML file. The>symbol in YAML allows multi-line text.
👉 Open /project/Python/starter-project/config/tasks.yaml and add:
# tasks.yaml
appraise_loss_task:
description: >
Analyze the theft crime scene and predict the missing values of stolen items using the RPT-1 model via the call_rpt1 tool.
Use the payload data provided in the inputs: {payload}
expected_output: >
JSON with predicted values for the stolen items.
agent: appraiser_agent💡 Note: The
agentfield references the agent key fromagents.yaml(in this case,appraiser_agent).
Now that we've moved the agent and task definitions to YAML files, we'll be moving all the remaining code to a new file structure.
💡 What's happening? In the next steps, you'll create
investigator_crew.pywhich will contain all the code currently inbasic_agent.py(imports, environment loading, RPT1Client initialization, and the @tool function) plus the new crew structure. Once that's done,basic_agent.pywill no longer be needed.
Now we will move the crew to their own file as well. It will make the code more readable and easier to maintain, especially with multiple agents working together.
👉 Navigate to /project/Python/starter-project/investigator_crew.py.
👉 Paste the code snippets below step by step.
from crewai import Agent, Crew, Task, Process
from crewai.project import CrewBase, agent, task, crew
from crewai.tools import tool
from dotenv import load_dotenv
from rpt_client import RPT1Clientimport os
from pathlib import Path
from dotenv import load_dotenv
# Load .env from the same directory as this script
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)
# Initialize RPT1 client after loading environment variables
rpt1_client = RPT1Client()
@tool("call_rpt1")
def call_rpt1(payload: dict) -> str:
"""Call RPT-1 model to predict missing values in the payload.
Args:
payload: A dictionary containing the stolen items data with prediction placeholders.
This should be the exact payload provided in the task inputs.
Returns:
JSON string with predicted insurance values and item categories.
"""
try:
response = rpt1_client.post_request(json_payload=payload)
if response.status_code == 200:
import json
return json.dumps(response.json(), indent=2)
else:
return f"Error {response.status_code}: {response.text}"
except Exception as e:
return f"Error calling RPT-1: {str(e)}"👉 Delete /project/Python/starter-project/basic_agent.py entirely.
💡 Why? All functionality from
basic_agent.pyhas been moved toinvestigator_crew.py. Keeping the old file would cause confusion and potential import conflicts.
@CrewBase
class InvestigatorCrew():
"""InvestigatorCrew crew"""
agents_config = "config/agents.yaml"
tasks_config = 'config/tasks.yaml'
@agent
def appraiser_agent(self) -> Agent:
return Agent(
config=self.agents_config['appraiser_agent'],
verbose=True,
tools=[call_rpt1]
)
@task
def appraise_loss_task(self) -> Task:
return Task(
config=self.tasks_config['appraise_loss_task']
)
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents, # Automatically collected by the @agent decorator
tasks=self.tasks, # Automatically collected by the @task decorator.
process=Process.sequential,
verbose=True,
)💡 Understanding the code:
The @CrewBase Decorator:
- Tells CrewAI this class defines a crew structure
- Enables automatic discovery of agents and tasks via decorators
- Loads YAML configurations from the paths you specify
Configuration Loading:
agents_config = "config/agents.yaml"- Loads all agent definitions from the YAML filetasks_config = 'config/tasks.yaml'- Loads all task definitions from the YAML file- These become dictionaries accessible via
self.agents_config['key']andself.tasks_config['key']The @agent Decorator:
- Each method decorated with
@agentdefines one agentconfig=self.agents_config['appraiser_agent']pulls therole,goal,backstory, andllmfrom your YAMLtools=[call_rpt1]gives this agent access to the RPT-1 tool- All decorated agents are automatically collected into
self.agentsThe @task Decorator:
- Each method decorated with
@taskdefines one taskconfig=self.tasks_config['appraise_loss_task']pulls thedescription,expected_output, andagentfrom your YAML- All decorated tasks are automatically collected into
self.tasksThe @crew Decorator:
- Assembles everything into a crew
agents=self.agentsuses all agents you defined with@agenttasks=self.tasksuses all tasks you defined with@taskprocess=Process.sequentialmeans tasks run one after another in orderverbose=Trueprints detailed execution logs
⚠️ Important: The keys in your YAML files (appraiser_agent,appraise_loss_task) must match exactly what you reference in the code.
👉 Create a new file /project/Python/starter-project/main.py.
This file will contain the main entry point for your agent orchestration application.
from investigator_crew import InvestigatorCrew
from payload import payload
def main():
result = InvestigatorCrew().crew().kickoff(inputs={
'payload': payload,
'suspect_names': "Sophie Dubois, Marcus Chen, Viktor Petrov"
})
print("\n📘 Result:\n", result)
if __name__ == "__main__":
main()💡 Note: We're importing the
payloadfrom thepayload.pyfile you created in Exercise 03. Thesuspect_namesinput is required by the evidence analyst agent's goal, which uses it as a template variable{suspect_names}.
We also have a lot of evidence in our evidence database. You can check the documents that are part of the evidence here.
To analyze the evidence and find all the information on our three suspects:
- Sophie Dubois (the night manager who was on duty)
- Marcus Chen (the security technician who was recently fired)
- Viktor Petrov (a shadowy figure whose name has surfaced in connection with the crime)
You will now build an Criminal Evidence Analyst. This new agent will try to find any inconsistencies in the alibis and motives of the suspects.
👉 Navigate to /project/Python/starter-project/config/agents.yaml
👉 Add the configuration for the Criminal Evidence Analyst below your other agent.
evidence_analyst_agent:
role: >
Criminal Evidence Analyst
goal: >
Retrieve and analyze evidence ONLY via the call_grounding_service tool.
Search for each suspect by name: {suspect_names}. Do NOT fabricate any evidence or alibis.
Report only what the tool returns.
backstory: >
You are a methodical evidence analyst who bases conclusions strictly on retrieved documents. You never assume facts.
llm: sap/anthropic--claude-4.5-opus👉 Navigate to /project/Python/starter-project/config/tasks.yaml
👉 Add the configuration for the analyze evidence task for your new Evidence Analyst Agent.
analyze_evidence_task:
description: >
Analyze the evidence of the theft that you can access via the grounding tool.
Provide any insights that can help in the investigation especially regarding alibis.
Check the evidence iteratively for all three suspect names and provide an analysis for each of them.
expected_output: >
A detailed analysis of the evidence for each suspect, including any insights that can help in the investigation.
agent: evidence_analyst_agent👉 Navigate to /project/Python/starter-project/investigator_crew.py
👉 Inside the InvestigatorCrew class, add the new agent and task methods after the existing appraise_loss_task method and before the @crew method:
@agent
def evidence_analyst_agent(self) -> Agent:
return Agent(
config=self.agents_config['evidence_analyst_agent'],
verbose=True,
tools=[call_rpt1]
)
@task
def analyze_evidence_task(self) -> Task:
return Task(
config=self.tasks_config['analyze_evidence_task']
)💡 Where to place this code: Add these methods inside the
InvestigatorCrewclass, after yourappraise_loss_task()method. The order should be:
appraiser_agent()methodappraise_loss_task()method- 👈 Add new
evidence_analyst_agent()here- 👈 Add new
analyze_evidence_task()herecrew()method (keep at the end)
👉 Run your crew to test it.
python project/Python/starter-project/main.py💡 Note: The Evidence Analyst currently uses
call_rpt1as a placeholder tool to make the code functional. This isn't the right tool for evidence analysis. You'll replace it with thecall_grounding_servicetool in Exercise 05 to give the agent proper access to evidence documents.
You've transformed your single-agent system into a multi-agent crew with specialized roles working together. Here's the complete architecture you built:
1. Configuration-Driven Design
- Moved agent definitions to
agents.yaml- separating "what" agents do from "how" they do it - Moved task definitions to
tasks.yaml- defining clear responsibilities and expected outcomes - This YAML-first approach makes agents easy to modify without touching Python code
2. Specialized Agent Roles You now have two agents working in parallel domains:
- Loss Appraiser Agent - Expert in art valuation, uses the RPT-1 tool to predict insurance values
- Criminal Evidence Analyst - Methodical investigator, searches evidence for suspect information
3. Task Orchestration
- appraise_loss_task - Analyzes stolen items and predicts missing values
- analyze_evidence_task - Investigates suspects' alibis and motives
- Tasks are linked to agents via YAML configuration, creating clear accountability
4. Sequential Processing
With Process.sequential, CrewAI executes tasks in order:
- Appraiser runs first, calculating financial losses
- Evidence analyst runs second, examining suspects
- Each agent can build on previous results (though not implemented yet)
When you run InvestigatorCrew().crew().kickoff(inputs={...}):
- Initialization: CrewAI loads
agents.yamlandtasks.yaml, creating agent and task objects - Input Interpolation: Template variables like
{suspect_names}and{payload}are replaced with actual values - Task Assignment: Each task is matched to its assigned agent
- Sequential Execution:
- Loss Appraiser receives the payload and calls the RPT-1 tool
- Evidence Analyst receives suspect names (but lacks the grounding tool - Exercise 05!)
- Result Aggregation: All task outputs are combined into the final result
Benefits of Multi-Agent Systems:
-
Specialization - Each agent is an expert in one domain (valuation vs. investigation)
- Different LLMs can be assigned (GPT-4 for appraiser, Claude for analyst)
- Each agent has only the tools it needs (principle of least privilege)
-
Scalability - Adding new agents is straightforward
- Create new YAML entries for agent + task
- Add
@agentand@taskmethods to the crew - No need to modify existing agents
-
Collaboration - Agents can build upon each other's work
- Sequential processing allows later agents to use earlier results
contextparameter (coming in Exercise 06) enables explicit data sharing
-
Maintainability - Clear separation of concerns
- Business logic (goals, roles) lives in YAML
- Tool integration lives in Python
- Orchestration logic lives in the crew class
Real-World Applications:
- Customer service: Routing agent → Specialist agents → Escalation agent
- Research: Data collection agent → Analysis agent → Report generation agent
- DevOps: Monitoring agent → Diagnosis agent → Remediation agent
Notice each agent has different tools:
- Loss Appraiser uses
call_rpt1- a structured prediction model - Evidence Analyst uses
call_rpt1(temporarily) but needscall_grounding_service(Exercise 05)
This demonstrates tool specialization: agents only get tools relevant to their role.
- YAML Configuration separates agent definitions from implementation, making systems easier to maintain and modify
- Decorators (
@agent,@task,@crew) provide a clean, Pythonic way to define multi-agent systems - Agent Specialization with different roles, tools, and LLMs creates more robust and accurate systems
- Sequential Processing orchestrates agents in a specific order, allowing later agents to build on earlier results
- Input Variables like
{payload}and{suspect_names}make agents reusable with different data - Separation of Concerns keeps configuration (YAML), tools (Python functions), and orchestration (crew class) separate
What's Next? Your Evidence Analyst can't access actual evidence yet—it's using the wrong tool! In Exercise 05, you'll integrate the Grounding Service to give it real document access, completing the investigation system.
In the following exercises, you will:
- ✅ Set up your development space
- ✅ Build a basic agent
- ✅ Add custom tools (RPT-1 model integration)
- ✅ Build a multi-agent system (this exercise)
- 📌 Add the Grounding Service - Give your Evidence Analyst access to real documents
- 📌 Solve the crime - Add a Lead Detective Agent to combine findings and crack the case
Issue: YAML configuration not found or file path errors
- Solution: Ensure you've created all YAML files in
/project/Python/starter-project/config/directory and verify the file paths in your Python code match exactly.
Issue: Agent not found in configuration or missing agent reference
- Solution: Verify that the agent name in your
agents.yamlmatches the key referenced in the@agentmethod and that the agent is added to the agents list in the crew.
Issue: Tool not available to agents
- Solution: Verify that you're passing
tools=[call_rpt1]to each agent that needs access to the tool in the@agentmethod.
Issue: AttributeError: 'InvestigatorCrew' object has no attribute 'crew'
- Solution: This means the
@crewdecorated method is not properly indented inside theInvestigatorCrewclass. All methods decorated with@agent,@task, and@crewmust be indented with 4 spaces inside the class. Clear Python cache withrm -rf __pycache__and run again.
Issue: KeyError: "Template variable 'suspect_names' not found in inputs dictionary"
- Solution: The evidence analyst agent's goal uses
{suspect_names}as a template variable. Make sure you're passing'suspect_names': "Sophie Dubois, Marcus Chen, Viktor Petrov"in theinputsdictionary when callingkickoff()inmain.py.