Building a simple agent that manages a to-do list will demonstrate the primary components of an LLM agent in action. You will create your very first LLM agent, observing how it operates from receiving instructions to performing tasks. The structure and flow are emphasized, using a simplified approach for the LLM's decision-making part. This allows concentration on the agent's mechanics.Defining Our To-Do List Agent's TaskOur agent will be a personal assistant for managing a list of tasks. At a basic level, it should be able to:Add a new task to the list.View all current tasks.Remove a task from the list.Let the user know what action it has taken.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.Setting Up the Agent's "Tools" (Task Management Functions)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:We initialize an empty list called 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).The add_task function takes a description and appends it to our list.The view_tasks function formats the list for display. If the list is empty, it says so.The 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.The Agent's "Brain": Interpreting User Commands (Simplified)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:We convert the input to lowercase to make our keyword matching case-insensitive.We use startswith() and direct comparisons to identify the intended action.If an action requires an argument (like add_task needing a task_description), we extract it from the input string.The function returns two things: the actual function to call (e.g., add_task) and a tuple of arguments for that function (e.g., (task_description,)).If the input isn't recognized or is incomplete, it returns a lambda function that, when called, provides helpful feedback or an error message.We've also added an "exit_command" to allow the user to stop the agent.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.The Agent's Operational LoopNow, let's create the main loop for our agent. This loop will:Greet the user.Get input from the user.Pass the input to our process_user_command function.Execute the chosen action.Display the result.Repeat until the user wants to exit.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:The run_todo_agent function starts.It enters an infinite while True loop, prompting the user for input with > .The input is processed by process_user_command.If an "exit_command" is detected, the loop breaks.Otherwise, the returned action_function is called with its action_args (if any).The response from the action function (e.g., "Task added" or the list of tasks) is printed.Running Your Agent: An Example InteractionLet'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.How a Real LLM Fits InIn 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.Summary and Next StepsCongratulations! You've just walked through the creation of a very basic agent. While we simplified the LLM's role, you've seen how to:Define specific actions (tools) for an agent (our Python functions).Create a mechanism (even a simple one) to interpret user intent.Implement an operational loop (observe, "think", act).Execute and observe the agent's behavior.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!