Alright, let's put theory into practice and build your very first LLM agent. In this hands-on exercise, we'll create a simple agent that helps manage a to-do list. This will allow you to see the core components of an agent in action, from receiving instructions to performing tasks. We'll focus on the structure and flow, using a simplified approach for the LLM's decision-making part for now, so you can concentrate on the agent's mechanics.
Our agent will be a personal assistant for managing a list of tasks. At a basic level, it should be able to:
We'll keep the to-do list in memory for this example, meaning it will reset if you stop and restart the program. In later chapters, we'll explore how agents can remember things more permanently.
First, let's create the basic functions that our agent will use to manage the to-do list. Think of these as the agent's specific skills or tools. We'll use a simple Python list to store our tasks.
# This list will store our tasks
todo_list = []
def add_task(task_description):
"""Adds a task to the to-do list."""
todo_list.append({"task": task_description, "done": False})
return f"Okay, I've added '{task_description}' to your list."
def view_tasks():
"""Displays all tasks in the to-do list."""
if not todo_list:
return "Your to-do list is empty!"
response = "Here are your tasks:\n"
for index, item in enumerate(todo_list):
# We'll display tasks with a number for easier removal later
response += f"{index + 1}. {item['task']}\n"
return response.strip() # Remove trailing newline
def remove_task(task_number_str):
"""Removes a task from the list by its number."""
try:
task_number = int(task_number_str)
if 1 <= task_number <= len(todo_list):
removed_task = todo_list.pop(task_number - 1)
return f"Okay, I've removed '{removed_task['task']}'."
else:
return "Sorry, I couldn't find a task with that number. Try 'view tasks' to see the numbers."
except ValueError:
# This handles cases where the input isn't a number
return "Please tell me the number of the task you want to remove."
except IndexError:
# This is a fallback, though the check above should catch it
return "That task number is out of range."
In this code:
todo_list
. Each item in the list will be a dictionary containing the task description and a status (which we're not fully using yet, but it's good practice).add_task
function takes a description and appends it to our list.view_tasks
function formats the list for display. If the list is empty, it says so.remove_task
function takes a string (which we expect to be a number), converts it to an integer, and tries to remove the task at that position (adjusting for 0-based indexing). It includes some basic error handling if the number is invalid or not found.This is where the Large Language Model (LLM) would typically play its central role. The LLM would analyze the user's natural language input and decide which action (or tool) to use and what information to pass to that action.
For our first agent, we'll simulate this "brain" with a very simple function. It will look for keywords in the user's input to decide what to do. This is a significant simplification, but it helps us focus on the agent's overall structure. Later, you'll learn how to integrate a real LLM here.
def process_user_command(user_input):
"""
Simulates an LLM understanding user input and deciding which action to take.
Returns the function to call and any necessary arguments.
"""
user_input_lower = user_input.lower()
if user_input_lower.startswith("add "):
# Extract the task description after "add "
task_description = user_input[len("add "):].strip()
if task_description:
return add_task, (task_description,)
else:
return lambda: "Please tell me what task to add.", () # Returns a function that returns a string
elif user_input_lower == "view tasks" or user_input_lower == "show tasks" or user_input_lower == "list tasks":
return view_tasks, () # No arguments needed for view_tasks
elif user_input_lower.startswith("remove "):
task_identifier = user_input[len("remove "):].strip()
if task_identifier:
return remove_task, (task_identifier,)
else:
return lambda: "Please tell me which task to remove (e.g., 'remove 1').", ()
elif user_input_lower in ["exit", "quit", "bye"]:
return "exit_command", None
else:
# If the command isn't recognized
unrecognized_response = (
"Sorry, I didn't understand that. You can:\n"
"- Add a task: 'add buy milk'\n"
"- View tasks: 'view tasks'\n"
"- Remove a task: 'remove 1' (use the number from 'view tasks')\n"
"- Exit: 'exit' or 'quit'"
)
return lambda: unrecognized_response, ()
In process_user_command
:
startswith()
and direct comparisons to identify the intended action.add_task
needing a task_description
), we extract it from the input string.add_task
) and a tuple of arguments for that function (e.g., (task_description,)
).This process_user_command
function is a placeholder for the more sophisticated reasoning an LLM would provide. A real LLM, guided by a well-crafted prompt, would be much more flexible in understanding varied phrasing of these commands.
Now, let's create the main loop for our agent. This loop will:
process_user_command
function.This is a simplified version of the "Observation, Thought, Action" cycle we discussed in Chapter 2.
def run_todo_agent():
"""Runs the main loop for the to-do list agent."""
print("Hi! I'm your To-Do List Agent. How can I help you today?")
print("You can 'add [task]', 'view tasks', 'remove [task number]', or 'exit'.")
while True:
try:
user_input = input("> ")
action_function, action_args = process_user_command(user_input)
if action_function == "exit_command":
print("Goodbye!")
break
if action_function:
if action_args:
response = action_function(*action_args)
else:
response = action_function()
print(response)
else:
# This case should ideally be handled within process_user_command
# by returning a lambda for unrecognized commands.
print("I'm not sure how to handle that. Please try again.")
except Exception as e:
# Basic error catching for unexpected issues during the loop
print(f"An unexpected error occurred: {e}")
print("Let's try that again.")
# To start the agent:
if __name__ == "__main__":
run_todo_agent()
When you run this Python script:
run_todo_agent
function starts.while True
loop, prompting the user for input with >
.process_user_command
.action_function
is called with its action_args
(if any).response
from the action function (e.g., "Task added" or the list of tasks) is printed.Let's imagine running this script. Here's how an interaction might look:
Hi! I'm your To-Do List Agent. How can I help you today?
You can 'add [task]', 'view tasks', 'remove [task number]', or 'exit'.
> add Buy groceries
Okay, I've added 'Buy groceries' to your list.
> add Call the plumber
Okay, I've added 'Call the plumber' to your list.
> view tasks
Here are your tasks:
1. Buy groceries
2. Call the plumber
> remove 1
Okay, I've removed 'Buy groceries'.
> view tasks
Here are your tasks:
1. Call the plumber
> remove Call the plumber
Please tell me the number of the task you want to remove.
> remove 1
Okay, I've removed 'Call the plumber'.
> view tasks
Your to-do list is empty!
> something random
Sorry, I didn't understand that. You can:
- Add a task: 'add buy milk'
- View tasks: 'view tasks'
- Remove a task: 'remove 1' (use the number from 'view tasks')
- Exit: 'exit' or 'quit'
> exit
Goodbye!
This interaction demonstrates the agent adding tasks, viewing them, and removing them by number. It also shows how our simple command processor handles unrecognized input and provides help. You'll notice that trying to remove a task by name ("remove Call the plumber") doesn't work with our current remove_task
function, which expects a number. This is a small example of an "initial problem" you might encounter and refine. A more advanced agent using an LLM might be able to infer the user's intent better or ask for clarification.
In our current process_user_command
function, we used simple if/elif
statements based on keywords. A true LLM-powered agent would handle this differently:
System Prompt: You would have a "system prompt" or a set of initial instructions for the LLM. For our to-do agent, it might be something like:
"You are a helpful to-do list assistant. Your available actions are: add_task(description)
, view_tasks()
, remove_task(task_number)
. When the user asks to do something, identify the correct action and any parameters. If the user asks to remove a task by name, first use view_tasks
to find its number, then use remove_task(task_number)
."
LLM Call: The process_user_command
function would send the user's input (e.g., "add pick up dry cleaning") and this system prompt to an LLM API.
LLM Response: The LLM would ideally respond with a structured output, perhaps in JSON format, like:
{"action": "add_task", "parameters": {"description": "pick up dry cleaning"}}
or for viewing tasks:
{"action": "view_tasks", "parameters": {}}
Action Dispatch: Your Python code would then parse this JSON and call the corresponding Python function (add_task
or view_tasks
) with the extracted parameters.
This approach makes the agent much more flexible with natural language and capable of more complex reasoning, which we'll explore as we move forward.
Congratulations! You've just walked through the creation of a very basic agent. While we simplified the LLM's role, you've seen how to:
This to-do list agent forms a solid foundation. As you progress through this course, you'll learn how to replace the simplified process_user_command
with actual LLM calls, give your agents more sophisticated tools, enable them to remember information across sessions, and help them plan more complex sequences of actions. For now, try modifying this agent: Can you add a feature to mark tasks as "done" instead of just removing them? Or perhaps allow tasks to be edited? Experimenting is a great way to learn!
Was this section helpful?
© 2025 ApX Machine Learning