Lesson Introduction

Welcome! Today, we’ll learn how to design a Todo API agent — a software agent that interacts with a Todo API for you. In modern systems, especially those using multi-agent setups, it’s common to delegate tasks to agents. For example, you might want an agent to manage your to-do list by reading tasks from an external service.

The goal: understand how to structure the code, define data models, and ensure all API interactions go through the agent. By the end, you’ll know how to build an agent that reads tasks from a Todo API and responds to user prompts in natural language.

Understanding the Todo API Agent Structure

Let’s break down the main components of our Todo API agent. In a multi-agent system, each agent handles a specific domain. Here, our agent manages to-do tasks.

Key components:

  • Agent: Receives user prompts and decides what to do.
  • Tool: Wraps specific functionality (like calling an API) for the agent.
  • API Interaction: Handles communication with the external Todo API.

Why this structure? Think of the agent as a team member who knows how to use certain tools (like the Todo API) to get things done. This makes the system modular and easier to maintain.

Here’s how a user interacts with the agent:

from todo_agent import ask_agent

def main():
    query = "Hey list all my tasks"
    response = ask_agent(query)
    print(response)  # [{"id": 1, "title": "Buy milk", "done": False, "description": "Get 2 liters of milk"}, {"id": 2, "title": "Read book", "done": True, "description": "Finish reading 'Atomic Habits'"}]
    
if __name__ == '__main__':
    main()

Notice: the user never calls the API directly. They interact with the agent, which uses its tools to fulfill the request.

For context, here is a simple implementation of ask_agent function we can use:

def ask_agent(prompt):
    # Sends the prompt to the agent and returns the agent's final output
    result = Runner.run_sync(TODO_AGENT, prompt)
    return result.final_output
Defining Actions and Data Models

To make our agent reliable, we define clear data models and possible actions. Pydantic models and enums help here. Enums define allowed actions. We only support reading tasks for now:

from enum import Enum

class Action(Enum):
    GET = "get"  # Only the 'get' action is supported for now

This makes supported actions explicit. To add more actions later, just extend the enum.

Pydantic’s BaseModel defines the structure of a to-do item:

from pydantic import BaseModel

class TodoItem(BaseModel):
    id: int  # Unique identifier for the task
    title: str  # Title of the task
    done: bool  # Whether the task is completed
    description: str  # Description of the task

This ensures every task has a consistent structure, making validation and serialization easy.

This model defines the arguments needed for an action:

class TodoItemArgs(BaseModel):
    action: Action  # The action to perform (currently only 'get')

Using these models ensures the agent only receives valid, well-structured data.

Implementing the TodoAPI Class

The TodoAPI class contains all logic for interacting with the external Todo API. This keeps API logic separate from the agent’s decisions.

import requests
import json

class TodoAPI:
    BASE_URL = "http://127.0.0.1:8000/todos"  # Base URL for the Todo API
    
    @classmethod
    def get_tasks(cls):
        # Fetches all tasks from the Todo API
        response = requests.get(cls.BASE_URL)
        response.raise_for_status()  # Raises an error if the request failed
        # Converts each item in the response to a TodoItem object
        return [TodoItem(**item) for item in response.json()]

    @classmethod
    def handle_request(cls, action):
        # Handles the requested action
        if action == Action.GET:
            tasks = cls.get_tasks()
            # Convert each TodoItem to a dictionary for serialization
            tasks_dicts = [task.dict() for task in tasks]
            return tasks_dicts
        else:
            # Raise an error if the action is not supported
            raise ValueError(f"Unknown action: {action}")

async def run_function(ctx: RunContextWrapper[Any], args: str) -> str:
    # Parses the arguments from JSON and calls the API handler
    parsed = TodoItemArgs.model_validate_json(args)
    # Returns the tasks as a JSON string
    return json.dumps({"tasks": TodoAPI.handle_request(parsed.action)})
  • get_tasks: Fetches all tasks from the API and returns them as TodoItem objects.
  • handle_request: Decides what to do based on the action. For now, only GET is supported.
  • run_function: Parses the arguments, calls the API handler, and returns the result as JSON.

This design makes it easy to add more actions later.

Integrating the API with the Agent: Creating the Tool

Now, let’s connect the API logic to the agent using a tool. The tool bridges the agent and the API. We use a FunctionTool to wrap the API:

from agents import FunctionTool

todos_api_tool = FunctionTool(
    name="todos_api",  # Name of the tool
    description="A tool for interacting with the Todo API",  # What the tool does
    params_json_schema = {
        **TodoItemArgs.model_json_schema(),  # Expected input structure from the Pydantic model
        "additionalProperties": False
    },
    on_invoke_tool=run_function  # Function to call when the tool is used
)
  • name: The tool’s name.
  • description: What the tool does.
  • params_json_schema: The expected input structure, from our Pydantic model.
  • on_invoke_tool: The function to call when the tool is used.
Integrating the API with the Agent: Defining the Agent

The agent is created with a name, instructions, and a list of tools:

from agents import Agent

TODO_AGENT = Agent(
    name="Todo Manager",  # Name of the agent
    instructions=(
        "You are a Todo API agent. "
        "You can read tasks using the Todo API."
        "Use the todos_api tool to interact with the API."
    ),  # Instructions for the agent's behavior
    tools=[todos_api_tool]  # List of tools the agent can use
)

Instructions tell the agent how to behave and which tool to use.

Using the Agent in Practice

As a user, you should never call the API directly. Always use the agent interface.

Here’s how to ask the agent to list your tasks:

from todo_agent import ask_agent

def main():
    query = "Hey list all my tasks"
    response = ask_agent(query)
    print(response)  # [{"id": 1, "title": "Buy milk", "done": False, "description": "Get 2 liters of milk"}, {"id": 2, "title": "Read book", "done": True, "description": "Finish reading 'Atomic Habits'"}]
    
if __name__ == '__main__':
    main()
  • The user gives a natural language prompt.
  • ask_agent sends this prompt to the agent.
  • The agent uses its todos_api tool to fetch tasks.
  • The agent returns the result.

This keeps your code clean and ensures all API interactions are managed by the agent, making it easier to add features and handle errors.

Lesson Summary

You learned how to design a Todo API agent that interacts with an external API through a clear interface. We covered structuring code with agents, tools, and data models, and encapsulating API logic in a class. Most importantly, all API interactions should go through the agent, not directly from your code.

Now it’s your turn! Next, you’ll get hands-on experience building and using your own Todo API agent. You’ll practice defining models, implementing API logic, and interacting with the agent using natural language prompts. This will help solidify your understanding and prepare you for more advanced multi-agent designs.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal