Lesson Introduction

Welcome! In this lesson, we’ll enhance our Todo API agent to support all CRUD operations: Create, Read, Update, and Delete. These are the core actions behind any data-driven app, from simple to-do lists to complex systems. By the end, you’ll know how to implement and orchestrate these operations in a Python agent that communicates with a RESTful API.

Think of building a digital assistant for daily tasks. To be useful, it must add, show, update, and remove tasks. CRUD operations make this possible. Our goal is to make our Todo agent handle all these actions smoothly.

Note: In this lesson, we focus on implementing the client side of the Todo API agent — that is, the code that sends requests to an API to perform CRUD operations. The lesson does not cover the implementation of the API server that receives and responds to these requests.

Overview of CRUD Operations and Data Modeling

CRUD stands for Create, Read, Update, and Delete — the four basic ways to manage data.

  • Create: Add a new item (e.g., a new task).
  • Read: Retrieve items (e.g., list all tasks).
  • Update: Change an item (e.g., mark a task as done).
  • Delete: Remove an item (e.g., delete a finished task).

In web apps, these map to HTTP methods:

  • POST for Create
  • GET for Read
  • PUT for Update
  • DELETE for Delete

For example, adding a new task in a to-do app sends a POST request. Viewing tasks uses GET. Editing uses PUT, and deleting uses DELETE.

To manage tasks, we use data models. Our agent uses Pydantic models to define a task and the required arguments.

Here’s the task model:

from pydantic import BaseModel

class TodoItem(BaseModel):
    id: int                # Unique task identifier
    title: str             # Task name
    done: bool             # Completion status (True if done, False otherwise)
    description: str       # Task details
  • id: Unique task identifier.
  • title: Task name.
  • done: Completion status.
  • description: Task details.

We use an Enum to specify the action:

from enum import Enum
from typing import Optional

class Action(str, Enum):
    GET = "get"            # Read operation
    POST = "post"          # Create operation
    PUT = "put"            # Update operation
    DELETE = "delete"      # Delete operation

We wrap the action and task together:

class TodoItemArgs(BaseModel):
    action: Action                 # The CRUD action to perform
    task: Optional[TodoItem] = None  # The task to operate on (if needed)

This structure lets us clearly state what to do and with which task.

Implementing CRUD in the TodoAPI Class: Read and Create

The TodoAPI class handles API communication. Each CRUD operation is a class method.

Read (GET):

The get_tasks method is the same as in the previous lesson—a quick revision:

@classmethod
def get_tasks(cls):
    # Send a GET request to fetch all tasks
    response = requests.get(cls.BASE_URL)
    response.raise_for_status()  # Raise an error if the request failed
    # Convert each item in the response to a TodoItem object
    return [TodoItem(**item) for item in response.json()]

# Output: [TodoItem(id=1, title='Learn Python', done=False, description='Complete the Python course'), ...]
  • Sends GET to fetch all tasks.
  • Converts the response to TodoItem objects.

Create (POST):

@classmethod
def create_task(cls, task: TodoItem):
    # Send a POST request with the new task data as JSON
    response = requests.post(cls.BASE_URL, json=task.dict())
    response.raise_for_status()  # Raise an error if the request failed
    # Return the created task as a TodoItem object
    return TodoItem(**response.json())

# Output: TodoItem(id=2, title='Learn Python', done=False, description='Complete the Python course')
  • Sends POST with new task data.
  • Returns the created task.
Implementing CRUD in the TodoAPI Class: Update and Delete

Update (PUT):

@classmethod
def update_task(cls, task: TodoItem):
    # Send a PUT request to update the task by its ID
    response = requests.put(f"{cls.BASE_URL}/{task.id}", json=task.dict())
    response.raise_for_status()  # Raise an error if the request failed
    # Return the updated task as a TodoItem object
    return TodoItem(**response.json())

# Output: TodoItem(id=2, title='Learn Python', done=True, description='Complete the Python course')
  • Sends PUT to update a task by ID.

Delete (DELETE):

@classmethod
def delete_task(cls, task: TodoItem):
    # Send a DELETE request to remove the task by its ID
    response = requests.delete(f"{cls.BASE_URL}/{task.id}")
    response.raise_for_status()  # Raise an error if the request failed
    # Return a confirmation message
    return {"message": "Task deleted successfully"}

# Output: {'message': 'Task deleted successfully'}
  • Sends DELETE to remove a task by ID.

Dispatching Actions:

The handle_request method selects the appropriate CRUD method:

@classmethod
def handle_request(cls, action, task):
    # Dispatch the action to the correct CRUD method
    if action == Action.GET:
        tasks = cls.get_tasks()
        return [t.dict() for t in tasks]  # Return a list of tasks as dictionaries
    elif action == Action.POST:
        return cls.create_task(task).dict()  # Return the created task as a dictionary
    elif action == Action.PUT:
        return cls.update_task(task).dict()  # Return the updated task as a dictionary
    elif action == Action.DELETE:
        return cls.delete_task(task)         # Return the delete confirmation message
    else:
        raise ValueError(f"Unknown action: {action}")

# Output (for GET): [{'id': 1, 'title': 'Learn Python', 'done': False, 'description': 'Complete the Python course'}, ...]
# Output (for POST): {'id': 2, 'title': 'Learn Python', 'done': False, 'description': 'Complete the Python course'}
# Output (for PUT): {'id': 2, 'title': 'Learn Python', 'done': True, 'description': 'Complete the Python course'}
# Output (for DELETE): {'message': 'Task deleted successfully'}

This keeps the code organized and easy to extend.

Integrating CRUD with the Agent

Now, let’s connect these CRUD operations to our agent so it can process natural language commands.

We wrap the API logic in a FunctionTool:

todos_api_tool = FunctionTool(
    name="todos_api",                        # Tool’s name
    description="A tool for interacting with the Todo API",  # Tool description
    params_json_schema=model_json_schema,    # Expected input schema
    on_invoke_tool=run_function              # Function to call when the tool is invoked
)
# Output: <FunctionTool object configured for Todo API>
  • name: Tool’s name.
  • description: What it does.
  • params_json_schema: Expected input.
  • on_invoke_tool: Function to call.

The agent is created with clear instructions:

TODO_AGENT = Agent(
    name="Todo Manager",
    instructions=(
        "You are a Todo API agent. "
        "You can create, read, update, and delete tasks using the Todo API. "
        "Use the todos_api tool to interact with the API. "
        "For GET and PUT actions: always first list all tasks (use GET), then proceed with the requested operation (for PUT, update the task after listing). "
        "For DELETE action: you must first list all the tasks, identify the ID of the task you need to remove and then use the tool to delete the task."
    ),
    tools=[todos_api_tool]
)
# Output: <Agent object named 'Todo Manager'>

This setup ensures the agent knows how to handle each request and can guide users, even with partial information.

Example Interactions

Let’s see this in action. You can interact with your agent using natural language.

List all tasks:

query = "Hey list all my tasks"
response = ask_agent(query)
print(response)  # [{"id": 1, "title": "Learn Python", "done": False, "description": "Complete the Python course"}, ...]

Create a new task:

query = "Create a new task with title 'Learn Python' and description 'Complete the Python course'"
response = ask_agent(query)
print(response)  # {"id": 2, "title": "Learn Python", "done": False, "description": "Complete the Python course"}

Update a task:

query = "Update the python learning task to mark it as done"
response = ask_agent(query)
print(response)  # {"id": 2, "title": "Learn Python", "done": True, "description": "Complete the Python course"}

Delete a task:

query = "Delete the python learning task"
response = ask_agent(query)
print(response)  # {"message": "Task deleted successfully"}

The agent translates your request into the correct CRUD operation and interacts with the API. This makes managing tasks as easy as typing or speaking commands.

Lesson Summary and Practice Introduction

You’ve learned how to enhance a Todo API agent to support all CRUD operations. We covered CRUD theory, data modeling with Pydantic, implementing each operation, and connecting everything to an agent that understands natural language.

Now, it’s your turn. In the next section, you’ll practice implementing and testing CRUD operations with the Todo API agent. This hands-on work will help you master these concepts and prepare you for more advanced agent development. Good luck!

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