Build an AI Agent with Tool Calling
Build a Python AI agent that autonomously calls tools to answer questions.
Introduction
30 Second Summary
When you ask ChatGPT to calculate something or check today's date, it doesn't just guess. It calls external tools behind the scenes, gets real results, and weaves them into its answer. That pattern is called "tool calling," and it's the foundation of every AI agent.
In this project, you will build a Research Assistant AI Agent from scratch in Python. Your agent will autonomously decide which tools to use, execute them, and chain results together to answer complex questions.
What You'll Build
A Python-based AI agent that uses Groq's free API to reason about questions, call tools (a calculator, a datetime tool, and a word lookup), and return accurate answers through an interactive terminal chat.
By the end of this project, you'll have:
- A working agent loop that sends queries to the AI, detects tool call requests, executes the right function, and feeds results back.
- Tool functions you write yourself (a calculator, a datetime tool, and a word lookup) with JSON schema definitions the AI can understand.
- An interactive chat interface in your terminal where you can talk to your agent in real time.
- Secret Mission: Add a multi-step "research" tool that chains multiple tool calls together to answer complex questions requiring multiple steps.
Are there any prerequisites?
You should be comfortable with basic Python syntax (variables, functions, loops, and conditionals). No advanced knowledge is needed. You'll create a free Groq account during setup.
Talk to the AI
Your environment is ready. Now it's time to write the code that actually sends a message to the AI and gets a response back.
You'll create two Python files: an agent module that connects to Groq's API, and a main script that lets you chat with the AI from your terminal.
In this step, get ready to:
- Create an agent module that connects to the Groq API.
- Create a main script with an interactive chat loop.
- Run the script and have your first conversation with the AI.
Create the agent module
The agent module handles the connection to Groq's LLM. It creates a client, sets up the conversation, and provides a function to send messages and receive responses.
- In VS Code, create a new file called agent.py inside your ai-agent folder.
- Paste the following code into agent.py:
import os
from dotenv import load_dotenv
from groq import Groq
load_dotenv()
def create_agent():
"""Create and return the Groq client and initial messages."""
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
messages = [
{
"role": "system",
"content": (
"You are a helpful AI assistant with access to tools. "
"Use tools when they would help answer the user's question accurately. "
"For math questions, always use the calculate tool instead of doing mental math. "
"For date/time questions, use the get_datetime tool. "
"For word definitions, use the word_lookup tool. "
"After receiving tool results, provide a clear, friendly answer to the user."
),
}
]
return client, messages
def chat(client, messages, user_message):
"""Send a message and get a response, handling any tool calls."""
# Add the user's message to the conversation
messages.append({"role": "user", "content": user_message})
# Send to the AI
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
)
response_message = response.choices[0].message
messages.append(response_message)
return response_message.content
What does this code do?
This module has two functions that work together:
- create_agent() loads your API key from .env, creates a Groq client, and sets up a system message that tells the AI how to behave.
- chat() takes a user message, adds it to the conversation history, sends everything to the llama-3.3-70b-versatile model, and returns the AI's response.
What is a system message?
The system message is like giving the AI a job description before the conversation starts. It shapes how the AI responds to everything the user says.
Right now the system message mentions tools, but the AI will still respond normally to general questions. You'll add actual tool capabilities in the next steps.
- Save agent.py.
Create the main script
The main script is the entry point you'll run to start your agent. It handles user input, sends messages to the agent, and prints responses in a loop.
- Create a new file called main.py inside your ai-agent folder.
- Paste the following code into main.py:
from agent import create_agent, chat
def main():
print("=" * 50)
print(" AI Agent with Tool Calling")
print(" Powered by Groq (llama-3.3-70b-versatile)")
print("=" * 50)
print()
print("Commands:")
print(" Type a question to chat with the agent")
print(" Type 'reset' to clear conversation history")
print(" Type 'quit' to exit")
print()
client, messages = create_agent()
while True:
try:
user_input = input("You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("Goodbye!")
break
if user_input.lower() == "reset":
client, messages = create_agent()
print(" [Agent] Conversation reset.")
continue
try:
response = chat(client, messages, user_input)
print(f"\nAgent: {response}\n")
except Exception as e:
print(f"\n[Error] {e}\n")
if __name__ == "__main__":
main()
What does this code do?
- from agent import create_agent, chat pulls in the two functions you just wrote in agent.py.
- The while True loop keeps asking for input until you type quit or press Ctrl+C.
- The reset command clears the conversation history so you can start fresh without restarting the script.
- The try/except block catches errors so the chat keeps running even if something goes wrong with a single message.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Make sure both agent.py and main.py are saved before moving on.
ⓧ I'd like to double check the full code
Here's the full main.py for comparison:
from agent import create_agent, chat
def main():
print("=" * 50)
print(" AI Agent with Tool Calling")
print(" Powered by Groq (llama-3.3-70b-versatile)")
print("=" * 50)
print()
print("Commands:")
print(" Type a question to chat with the agent")
print(" Type 'reset' to clear conversation history")
print(" Type 'quit' to exit")
print()
client, messages = create_agent()
while True:
try:
user_input = input("You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() == "quit":
print("Goodbye!")
break
if user_input.lower() == "reset":
client, messages = create_agent()
print(" [Agent] Conversation reset.")
continue
try:
response = chat(client, messages, user_input)
print(f"\nAgent: {response}\n")
except Exception as e:
print(f"\n[Error] {e}\n")
if __name__ == "__main__":
main()
Run the script and chat with the AI
Time to see your agent in action. You'll run the script and ask it a question to confirm the API connection works.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Make sure you're inside your ai-agent folder. If not, navigate there by running:
cd ~/Desktop/ai-agent
- Start your agent by running:
python main.py
You should see a welcome banner with instructions. The script is now waiting for your input at the You: prompt.
- Type a question like Why is the sky blue? and press Enter.
After a moment, the AI responds with an answer. You now have a working connection to Groq's llama-3.3-70b-versatile model.
- Type quit and press Enter to exit the chat.
Seeing an error when you run the script?
- If you see ModuleNotFoundError: No module named 'groq', the packages aren't installed. Run pip install groq python-dotenv and try again.
- If you see an authentication error, double check that your .env file contains GROQ_API_KEY= followed by your actual key (no quotes, no spaces around the =).
- If you see ModuleNotFoundError: No module named 'agent', make sure agent.py is in the same folder as main.py and that your terminal is inside the ai-agent folder.
Help me debug my AI agent script
Your AI can answer general questions, but it's just a chatbot right now. In the next step, you'll write your first tool function and teach the AI to use it.
Write Your First Tool Function
In the previous step, you set up your project and confirmed that you can talk to the AI. Right now, the AI can only respond with text. Ask it a math question like "What is 15% of 847 plus the square root of 144?" and it will guess the answer (often incorrectly).
Tools are what turn a chatbot into an agent. In this step, you'll write a calculator function that the AI can call to get precise answers. You'll also learn how to describe that tool to the AI using a JSON schema.
In this step, get ready to:
- Write a calculator function that safely evaluates math expressions.
- Test the function directly in Python to confirm it works.
- Create a JSON tool definition that describes your calculator to the AI.
Write the calculate function
Your agent's first tool is a calculator. Instead of letting the AI guess at math, this function will evaluate expressions precisely and return the result as structured data.
- In VS Code, create a new file called tools.py inside your ai-agent folder.
- Add the imports and the calculate function by pasting this code:
import json
import math
from datetime import datetime
# --- Tool Implementations ---
def calculate(expression: str) -> str:
"""Safely evaluate a mathematical expression."""
try:
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "pow": pow, "sqrt": math.sqrt,
"pi": math.pi, "e": math.e, "log": math.log,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
}
result = eval(expression, {"__builtins__": {}}, allowed_names)
return json.dumps({"result": str(result), "expression": expression})
except Exception as e:
return json.dumps({"error": str(e), "expression": expression})
What does this code do?
The calculate function takes a math expression as a string (like "2 + 2") and returns a JSON string with the result.
- allowed_names is a Python dictionary that maps safe function names (like sqrt and abs) to their actual Python functions. This restricts what eval() can access.
- eval(expression, {"__builtins__": {}}, allowed_names) evaluates the math expression but blocks access to dangerous built-in functions. Only the functions listed in allowed_names are available.
- json.dumps() converts the Python dictionary into a JSON string. The AI expects tool results as text, so we serialize the output.
- The try/except block catches invalid expressions gracefully instead of crashing the program.
What is a Python dictionary?
A dictionary is a collection of key-value pairs. Think of it like a real dictionary where you look up a word (the key) to find its meaning (the value). In allowed_names, the keys are strings like "sqrt" and the values are the actual Python functions they point to.
- Save tools.py.
Seeing red underlines in VS Code?
If VS Code shows a warning about the datetime import being unused, that's expected. We're importing it now because you'll use it in a later step. The warning will go away then.
If you see a syntax error underline, make sure your curly braces and quotes match exactly. Dictionaries need a colon between each key and value, and a comma after each pair.
Help me fix a syntax error in my calculate function.
Test the calculate function directly
Before connecting this function to the AI, you should confirm it works on its own. Testing in isolation makes debugging much easier.
- In your terminal, make sure you're inside the ai-agent folder, then run this command to test the function:
python3 -c "from tools import calculate; print(calculate('2 + 2')); print(calculate('sqrt(144)')); print(calculate('15 * 847 / 100'))"
- You should see three JSON strings printed, each with a "result" key showing 4, 12.0, and 127.05.
What should I see?
Your output should look something like this:
{"result": "4", "expression": "2 + 2"}
{"result": "12.0", "expression": "sqrt(144)"}
{"result": "127.05", "expression": "15 * 847 / 100"}
Each result is a JSON string containing the answer and the original expression. This structured format makes it easy for the AI to read the result and include it in its response.
Getting a ModuleNotFoundError?
Make sure your terminal is inside the ai-agent folder (the same folder where tools.py lives). Python needs to be in the same directory to import the file.
If you see a SyntaxError instead, double-check that you saved tools.py after pasting the code.
Help me fix an import error with my tools.py file.
Create the tool definition and registry
Your function works, but the AI doesn't know it exists yet. To tell the AI about your tool, you need to describe it in a specific JSON format called a tool definition. This is how the AI knows what the tool does, what parameters it accepts, and when to use it.
- In tools.py, add the tool definition and registry below your calculate function by pasting this code:
# --- Tool Definitions (JSON Schema for the LLM) ---
TOOL_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports basic arithmetic (+, -, *, /), powers (**), sqrt(), round(), abs(), min(), max(), log(), sin(), cos(), tan(), pi, and e.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The mathematical expression to evaluate, e.g. '15 * 847 / 100 + sqrt(144)'",
}
},
"required": ["expression"],
},
},
},
]
# --- Tool Registry ---
TOOL_REGISTRY = {
"calculate": calculate,
}
What does this code do?
This code has two parts that work together:
- TOOL_DEFINITIONS is a list of JSON schemas. Each schema tells the AI: "Here's a tool called calculate, here's what it does, and here's what parameters it needs." The AI reads this description to decide when to use the tool.
- TOOL_REGISTRY is a dictionary that maps tool names (strings) to actual Python functions. When the AI says "call the calculate tool", your code looks up "calculate" in this dictionary to find the function to run.
Why does the AI need a JSON schema?
The AI is a LLM that processes text. It cannot look at your Python code directly. The JSON schema is a structured description written in a format the AI understands. It's like giving someone a menu instead of access to the kitchen.
The better your description, the better the AI will be at choosing when to use your tool. A vague description leads to the AI using it at the wrong time (or not at all).
- Save tools.py.
- Confirm the file still works by running this in your terminal:
python3 -c "from tools import TOOL_DEFINITIONS, TOOL_REGISTRY; print(len(TOOL_DEFINITIONS), 'tool(s) defined'); print(list(TOOL_REGISTRY.keys()))"
- You should see 1 tool(s) defined and ['calculate'] printed in the terminal.
Getting a NameError for TOOL_DEFINITIONS?
Make sure the TOOL_DEFINITIONS list and TOOL_REGISTRY dictionary are outside the calculate function (not indented inside it). They should be at the top level of the file.
Also check that you didn't accidentally delete the calculate function when pasting. The registry references it by name, so it must exist above the TOOL_REGISTRY line.
Help me fix a NameError in my tools.py file.
✔️ Awesome, I've got everything!
Great. Double check you've saved tools.py and move on to the next step.
ⓧ I'd like to double check the full code
Here's the complete tools.py file so far. Make sure yours matches:
import json
import math
from datetime import datetime
# --- Tool Implementations ---
def calculate(expression: str) -> str:
"""Safely evaluate a mathematical expression."""
try:
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "pow": pow, "sqrt": math.sqrt,
"pi": math.pi, "e": math.e, "log": math.log,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
}
result = eval(expression, {"__builtins__": {}}, allowed_names)
return json.dumps({"result": str(result), "expression": expression})
except Exception as e:
return json.dumps({"error": str(e), "expression": expression})
# --- Tool Definitions (JSON Schema for the LLM) ---
TOOL_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports basic arithmetic (+, -, *, /), powers (**), sqrt(), round(), abs(), min(), max(), log(), sin(), cos(), tan(), pi, and e.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The mathematical expression to evaluate, e.g. '15 * 847 / 100 + sqrt(144)'",
}
},
"required": ["expression"],
},
},
},
]
# --- Tool Registry ---
TOOL_REGISTRY = {
"calculate": calculate,
}
Your calculator tool is built, tested, and described. In the next step, you'll wire it up to the AI so it actually knows the calculator exists and can request to use it.
Tell the AI About Your Tool
You have a working calculator function and a tool definition that describes it. Now you need to connect them to the AI so it knows the calculator exists.
In this step, you'll create a new file called agent.py that handles communication with the Groq API. You'll pass your tool definition directly into the API call and observe something interesting: when you ask a math question, the AI won't answer directly. Instead, it asks YOU to run the tool.
In this step, get ready to:
- Create agent.py with a function that initializes the AI client and a system prompt.
- Pass the tool definition to the API call so the AI knows about your calculator.
- Observe the AI responding with a tool call request instead of a direct answer.
Create the agent file with a system prompt
The agent is the code that sits between you and the AI. It sends your messages to the AI, and it will eventually handle tool calls too.
- In VS Code, create a new file called agent.py inside your ai-agent folder.
- Add the following code to set up the AI client and system prompt:
import json
import os
from dotenv import load_dotenv
from groq import Groq
from tools import TOOL_DEFINITIONS, TOOL_REGISTRY
load_dotenv()
def create_agent():
"""Create and return the Groq client and initial messages."""
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
messages = [
{
"role": "system",
"content": (
"You are a helpful AI assistant with access to tools. "
"Use tools when they would help answer the user's question accurately. "
"For math questions, always use the calculate tool instead of doing mental math. "
"For date/time questions, use the get_datetime tool. "
"For word definitions, use the word_lookup tool. "
"After receiving tool results, provide a clear, friendly answer to the user."
),
}
]
return client, messages
What does this code do?
load_dotenv() reads your .env file so the API key is available as an environment variable.
create_agent() does two things: it creates a Groq client (your connection to the AI), and it sets up a system message that tells the AI how to behave. The system prompt explicitly instructs the AI to use tools for math, date, and word questions instead of guessing.
- Save agent.py.
Add the chat function with tool definitions
The key moment is here. When you call the Groq API, you can pass a tools parameter containing your tool definitions. This tells the AI what tools are available and how to call them.
- In agent.py, add the following chat() function below create_agent():
def chat(client, messages, user_message):
"""Send a message and get a response, handling any tool calls."""
# Add the user's message to the conversation
messages.append({"role": "user", "content": user_message})
# Send to the AI with our tool definitions
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
)
# Get the AI's response
response_message = response.choices[0].message
messages.append(response_message)
return response_message.content
What does this code do?
- messages.append() adds the user's message to the conversation history so the AI has context.
- tools=TOOL_DEFINITIONS passes your calculator's JSON schema to the API. This is how the AI learns what tools it can use.
- tool_choice="auto" lets the AI decide whether to use a tool or respond directly. It will pick the tool when a question matches a tool's description.
- response.choices[0].message extracts the AI's response from the API result.
- Save agent.py.
Seeing a red underline on the import?
If VS Code shows a warning on from tools import TOOL_DEFINITIONS, TOOL_REGISTRY, make sure tools.py is saved and is in the same folder as agent.py.
Help me fix import errors in my AI agent project.
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py before moving on.
ⓧ I'd like to double check the full code
Here's the complete agent.py file so far:
import json
import os
from dotenv import load_dotenv
from groq import Groq
from tools import TOOL_DEFINITIONS, TOOL_REGISTRY
load_dotenv()
def create_agent():
"""Create and return the Groq client and initial messages."""
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
messages = [
{
"role": "system",
"content": (
"You are a helpful AI assistant with access to tools. "
"Use tools when they would help answer the user's question accurately. "
"For math questions, always use the calculate tool instead of doing mental math. "
"For date/time questions, use the get_datetime tool. "
"For word definitions, use the word_lookup tool. "
"After receiving tool results, provide a clear, friendly answer to the user."
),
}
]
return client, messages
def chat(client, messages, user_message):
"""Send a message and get a response, handling any tool calls."""
messages.append({"role": "user", "content": user_message})
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
)
response_message = response.choices[0].message
messages.append(response_message)
return response_message.content
Run the agent and observe the tool call response
Here's where it gets interesting. Your agent can now talk to the AI and the AI knows your calculator exists. But what happens when you ask a math question?
- Run your agent by entering this command in your terminal:
python main.py
- When you see the You: prompt, type a math question like:
What is 15 + 27?
You should see Agent: None printed in the terminal. That might look like a bug, but it's actually a success.
- Type quit to exit the agent.
Why did the AI respond with None?
When the AI receives a math question, it sees the calculator tool in its available tools list. Instead of answering the question directly (and potentially getting the math wrong), it responds with a tool call request. This means the content field of the response is empty (None), and the actual response is in a tool_calls field instead.
The AI is saying: "I want to call the calculate function with this expression. Please run it for me and send back the result." The AI never runs tools itself. Your code is responsible for executing the tool and feeding the result back.
What is a tool call?
A tool call is a structured object the AI returns containing the function name it wants to call and the arguments it wants to pass. For your math question, the AI's response includes something like: function.name = "calculate" and function.arguments = {"expression": "15 + 27"}.
In the next step, you'll write the code that detects this tool call, executes the function, and sends the result back to the AI so it can give a final answer.
Seeing an error instead of None?
If you see an authentication error, double check that your .env file contains your real Groq API key (not the placeholder text) and that .env is in the same folder as agent.py.
If you see a module not found error, make sure you installed groq and python-dotenv in the earlier step and that your terminal is in the ai-agent folder.
Help me debug my AI agent connection.
Your AI now knows the calculator tool exists and is already trying to use it. Next up, you'll complete the agent loop so the AI can actually execute the tool and get a real answer back.
Complete the Agent Loop
In the last step, you told the AI about your calculator tool and watched it respond with a tool call request. The AI asked you to run the calculator, but your code ignored that request and returned None instead of an answer.
Now you'll close that gap. You'll write the code that detects when the AI wants a tool, runs the correct function, feeds the result back, and keeps looping until the AI delivers a final answer. This is the agent loop, the core pattern that powers every production AI agent.
In this step, get ready to:
- Write a function that executes tool calls from the AI.
- Add a while loop so the agent keeps reasoning until it has a final answer.
- Test the full cycle: question, tool call, execution, and final answer.
Write the execute_tool_call function
When the AI wants to use a tool, it sends back a structured object containing the function name and arguments. Your code needs to receive that object, find the matching Python function, run it with the right arguments, and return the result as a string.
- In agent.py, find the chat() function definition.
- Add the following execute_tool_call function directly above the chat() function (but below create_agent()):
def execute_tool_call(tool_call):
"""Execute a single tool call and return the result."""
function_name = tool_call.function.name
# Try to parse the arguments the AI sent
try:
function_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
return json.dumps({"error": f"Invalid arguments for {function_name}"})
# Check if we have this tool
if function_name not in TOOL_REGISTRY:
return json.dumps({"error": f"Unknown tool: {function_name}"})
# Get the function and call it
tool_function = TOOL_REGISTRY[function_name]
try:
result = tool_function(**function_args)
return result
except Exception as e:
return json.dumps({"error": f"Tool execution failed: {str(e)}"})
What does this code do?
This function is the bridge between what the AI asks for and what actually runs on your machine.
- tool_call.function.name extracts which tool the AI wants (e.g. "calculate").
- json.loads(tool_call.function.arguments) parses the JSON string of arguments into a Python dictionary.
- TOOL_REGISTRY[function_name] looks up the actual Python function by name.
- tool_function(**function_args) calls the function with the AI's arguments unpacked as keyword arguments.
- Save agent.py.
Seeing a red underline on TOOL_REGISTRY?
Make sure from tools import TOOL_DEFINITIONS, TOOL_REGISTRY is at the top of your file. This import was added in the previous step.
Help me debug my import error in agent.py
Add the while loop to the chat function
Right now, your chat() function sends a message and immediately returns whatever the AI says. If the AI asks for a tool, you get None back. The while loop fixes this by checking whether the AI's response contains tool calls. If it does, the loop executes the tools, sends results back, and asks the AI again. It keeps going until the AI gives a final text answer.
- In agent.py, find this line inside your chat() function:
return response_message.content
- Replace that line with the following while loop:
# Keep looping while the AI wants to call tools
while response_message.tool_calls:
print(f" [Agent] Calling {len(response_message.tool_calls)} tool(s)...")
# Execute each tool the AI requested
for tool_call in response_message.tool_calls:
print(f" -> {tool_call.function.name}({tool_call.function.arguments})")
result = execute_tool_call(tool_call)
print(f" <- {result}")
# Send the result back to the AI
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": result,
})
# Get the AI's next response (it might call more tools or give a final answer)
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
)
response_message = response.choices[0].message
messages.append(response_message)
return response_message.content
What does this while loop do?
This is the agent loop. It keeps cycling until the AI stops requesting tools.
- while response_message.tool_calls: checks if the AI's response contains any tool call requests.
- The for loop runs each requested tool and appends a role: "tool" message with the result.
- After all tools finish, it calls the AI again. The AI might request more tools or give a final text answer.
- Once response_message.tool_calls is empty, the loop exits and the function returns the AI's final answer.
Why a while loop instead of a single if statement?
Some questions require multiple tool calls in sequence. For example, the AI might call the calculator once, see the result, and then decide it needs to call it again with a different expression. The while loop handles this multi-step reasoning automatically.
- Save agent.py.
Getting an IndentationError?
The while loop must be indented one level inside the chat() function (4 spaces in from def chat). Make sure you fully deleted the old return response_message.content line before pasting the new block.
Help me fix my indentation error in agent.py
Test the full agent loop
Your agent is now complete enough to handle math questions end-to-end. When you ask a math question, you should see the AI request the calculator tool, your code execute it, and the AI deliver a final answer using the result.
- Run your agent by entering this command in the terminal:
python main.py
- When you see the You: prompt, type this question:
What is 15% of 847 plus the square root of 144?
You should see output showing the full cycle. The agent prints which tool it calls, the arguments, the result, and then delivers a final answer.
What should I see?
Your terminal output should look something like this:
You: What is 15% of 847 plus the square root of 144?
[Agent] Calling 1 tool(s)...
-> calculate({"expression": "15 * 847 / 100 + sqrt(144)"})
<- {"result": "139.05", "expression": "15 * 847 / 100 + sqrt(144)"}
Agent: 15% of 847 plus the square root of 144 is 139.05.
The exact wording of the Agent's final answer may vary. The important thing is that you see the tool call happening (the -> and <- lines) and a correct numerical answer at the end.
- Type quit to exit the agent.
Still seeing None instead of an answer?
This means the while loop isn't running. Check that you fully replaced the old return response_message.content line. If both the old return and the new while loop exist, the function returns before reaching the loop.
Also confirm that execute_tool_call is defined above the chat() function. Python reads top-to-bottom, so chat() must be able to see execute_tool_call above it.
Help me debug why my agent is returning None instead of an answer
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py and your agent answered a math question correctly.
ⓧ I'd like to double check the full code
Here is the complete agent.py file as it should look at the end of this step:
import json
import os
from dotenv import load_dotenv
from groq import Groq
from tools import TOOL_DEFINITIONS, TOOL_REGISTRY
load_dotenv()
def create_agent():
"""Create and return the Groq client and initial messages."""
client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
messages = [
{
"role": "system",
"content": (
"You are a helpful AI assistant with access to tools. "
"Use tools when they would help answer the user's question accurately. "
"For math questions, always use the calculate tool instead of doing mental math. "
"For date/time questions, use the get_datetime tool. "
"For word definitions, use the word_lookup tool. "
"After receiving tool results, provide a clear, friendly answer to the user."
),
}
]
return client, messages
def execute_tool_call(tool_call):
"""Execute a single tool call and return the result."""
function_name = tool_call.function.name
try:
function_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
return json.dumps({"error": f"Invalid arguments for {function_name}"})
if function_name not in TOOL_REGISTRY:
return json.dumps({"error": f"Unknown tool: {function_name}"})
tool_function = TOOL_REGISTRY[function_name]
try:
result = tool_function(**function_args)
return result
except Exception as e:
return json.dumps({"error": f"Tool execution failed: {str(e)}"})
def chat(client, messages, user_message):
"""Send a message and get a response, handling any tool calls."""
messages.append({"role": "user", "content": user_message})
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
)
response_message = response.choices[0].message
messages.append(response_message)
while response_message.tool_calls:
print(f" [Agent] Calling {len(response_message.tool_calls)} tool(s)...")
for tool_call in response_message.tool_calls:
print(f" -> {tool_call.function.name}({tool_call.function.arguments})")
result = execute_tool_call(tool_call)
print(f" <- {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": result,
})
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
tools=TOOL_DEFINITIONS,
tool_choice="auto",
)
response_message = response.choices[0].message
messages.append(response_message)
return response_message.content
Your agent can now reason with tools autonomously. Next up, you'll add more tools and build an interactive chat so you can have a real conversation with your agent.
Add More Tools and an Interactive Chat
Your agent can solve math problems, but a calculator alone doesn't make for a very useful assistant. Real AI agents have access to multiple tools and pick the right one based on the question.
In this step, you'll expand your agent's toolkit with a datetime tool and a word lookup tool. Then you'll test the full interactive experience by chatting with your agent and watching it choose tools autonomously.
In this step, get ready to:
- Add a datetime tool and a word lookup tool to your agent's toolkit.
- Register the new tools so the AI knows they exist.
- Test your agent interactively with different types of questions.
Add the get_datetime and word_lookup functions
Each tool your agent can use starts as a regular Python function. The get_datetime function returns the current date and time. The word_lookup function returns definitions for technology-related words.
- In tools.py, find the end of the calculate function (after its last return statement).
- Add the get_datetime function below it by copying this code:
def get_datetime(format: str = "%Y-%m-%d %H:%M:%S") -> str:
"""Get the current date and time."""
try:
now = datetime.now()
formatted = now.strftime(format)
return json.dumps({
"datetime": formatted,
"timezone": "local",
"day_of_week": now.strftime("%A"),
})
except Exception as e:
return json.dumps({"error": str(e)})
What does this code do?
- format: str = "%Y-%m-%d %H:%M:%S" is a default parameter. If the AI doesn't specify a format, it uses this standard date/time layout.
- datetime.now() grabs the current date and time from your computer's clock.
- The function returns extra useful info like the day of the week, which makes the AI's responses more natural.
You won't see a change yet. The next function completes the toolkit.
- Below the get_datetime function, add the word_lookup function:
def word_lookup(word: str) -> str:
"""Look up the definition of a word."""
definitions = {
"algorithm": "A step-by-step procedure for solving a problem or accomplishing a task.",
"neural network": "A computing system inspired by biological neural networks in the brain.",
"api": "Application Programming Interface - a set of rules for building software.",
"machine learning": "A subset of AI where systems learn from data without explicit programming.",
"agent": "An autonomous entity that observes and acts upon an environment to achieve goals.",
"inference": "The process of running a trained model to make predictions on new data.",
"token": "A chunk of text (word, subword, or character) that an LLM processes.",
"prompt": "The input text given to a language model to generate a response.",
}
word_lower = word.lower().strip()
if word_lower in definitions:
return json.dumps({"word": word, "definition": definitions[word_lower]})
else:
return json.dumps({"word": word, "definition": f"Definition not found for '{word}'. Available words: {', '.join(definitions.keys())}"})
What does this code do?
- definitions is a dictionary that maps words to their definitions. Think of it as a mini database built right into your code.
- word.lower().strip() normalizes the input so "API", "api", and " api " all find the same entry.
- If the word isn't found, the function returns a helpful message listing all available words instead of crashing.
- Save tools.py.
Seeing a syntax error?
Make sure there's a blank line between the end of calculate and the start of get_datetime. Python needs blank lines between function definitions at the module level.
Also check that the definitions dictionary in word_lookup has a comma after every entry (including the last one).
Help me debug a syntax error in my tools.py file
Register your new tools with the AI
Writing the functions isn't enough. The AI only knows about tools you explicitly describe to it using JSON schema definitions. You need to add definitions for both new tools.
- In tools.py, scroll down to the TOOL_DEFINITIONS list. Find the closing }, of the calculate definition (just before the list's closing ]).
- Add the get_datetime tool definition after it:
{
"type": "function",
"function": {
"name": "get_datetime",
"description": "Get the current date and time. Returns the date, time, timezone, and day of the week.",
"parameters": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "Python strftime format string. Default is '%Y-%m-%d %H:%M:%S'.",
}
},
"required": [],
},
},
},
Why is required an empty list?
The format parameter is optional because get_datetime already has a default value. The AI can call this tool with no arguments at all and still get a useful response.
- Directly below the get_datetime definition, add the word_lookup tool definition:
{
"type": "function",
"function": {
"name": "word_lookup",
"description": "Look up the definition of a technology or AI-related word. Available words include: algorithm, neural network, api, machine learning, agent, inference, token, prompt.",
"parameters": {
"type": "object",
"properties": {
"word": {
"type": "string",
"description": "The word to look up.",
}
},
"required": ["word"],
},
},
},
What does the description field do?
The description is how the AI decides when to use a tool. Notice how the word_lookup description lists the available words. This helps the AI make better decisions about when to call it.
Finally, you need to add both functions to the registry so the agent loop can find them at runtime.
- Scroll down to the TOOL_REGISTRY dictionary at the bottom of tools.py.
- Find this code:
TOOL_REGISTRY = {
"calculate": calculate,
}
- Replace it with this updated registry that includes all three tools:
TOOL_REGISTRY = {
"calculate": calculate,
"get_datetime": get_datetime,
"word_lookup": word_lookup,
}
- Save tools.py.
Not sure your TOOL_DEFINITIONS list is correct?
The TOOL_DEFINITIONS list should now have three items in it (one for calculate, one for get_datetime, one for word_lookup). Make sure each definition ends with a comma before the next one starts.
Help me check my TOOL_DEFINITIONS list structure
✔️ Awesome, I've got everything!
Great. Double check you've saved tools.py with all three functions, all three definitions, and the updated registry.
ⓧ I'd like to double check the full code
Here's the complete tools.py file. Compare yours against it:
import json
import math
from datetime import datetime
# --- Tool Implementations ---
def calculate(expression: str) -> str:
"""Safely evaluate a mathematical expression."""
try:
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "pow": pow, "sqrt": math.sqrt,
"pi": math.pi, "e": math.e, "log": math.log,
"sin": math.sin, "cos": math.cos, "tan": math.tan,
}
result = eval(expression, {"__builtins__": {}}, allowed_names)
return json.dumps({"result": str(result), "expression": expression})
except Exception as e:
return json.dumps({"error": str(e), "expression": expression})
def get_datetime(format: str = "%Y-%m-%d %H:%M:%S") -> str:
"""Get the current date and time."""
try:
now = datetime.now()
formatted = now.strftime(format)
return json.dumps({
"datetime": formatted,
"timezone": "local",
"day_of_week": now.strftime("%A"),
})
except Exception as e:
return json.dumps({"error": str(e)})
def word_lookup(word: str) -> str:
"""Look up the definition of a word."""
definitions = {
"algorithm": "A step-by-step procedure for solving a problem or accomplishing a task.",
"neural network": "A computing system inspired by biological neural networks in the brain.",
"api": "Application Programming Interface - a set of rules for building software.",
"machine learning": "A subset of AI where systems learn from data without explicit programming.",
"agent": "An autonomous entity that observes and acts upon an environment to achieve goals.",
"inference": "The process of running a trained model to make predictions on new data.",
"token": "A chunk of text (word, subword, or character) that an LLM processes.",
"prompt": "The input text given to a language model to generate a response.",
}
word_lower = word.lower().strip()
if word_lower in definitions:
return json.dumps({"word": word, "definition": definitions[word_lower]})
else:
return json.dumps({"word": word, "definition": f"Definition not found for '{word}'. Available words: {', '.join(definitions.keys())}"})
# --- Tool Definitions (JSON Schema for the LLM) ---
TOOL_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports basic arithmetic (+, -, *, /), powers (**), sqrt(), round(), abs(), min(), max(), log(), sin(), cos(), tan(), pi, and e.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The mathematical expression to evaluate, e.g. '15 * 847 / 100 + sqrt(144)'",
}
},
"required": ["expression"],
},
},
},
{
"type": "function",
"function": {
"name": "get_datetime",
"description": "Get the current date and time. Returns the date, time, timezone, and day of the week.",
"parameters": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "Python strftime format string. Default is '%Y-%m-%d %H:%M:%S'.",
}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "word_lookup",
"description": "Look up the definition of a technology or AI-related word. Available words include: algorithm, neural network, api, machine learning, agent, inference, token, prompt.",
"parameters": {
"type": "object",
"properties": {
"word": {
"type": "string",
"description": "The word to look up.",
}
},
"required": ["word"],
},
},
},
]
# --- Tool Registry ---
TOOL_REGISTRY = {
"calculate": calculate,
"get_datetime": get_datetime,
"word_lookup": word_lookup,
}
Test your agent interactively
Your agent now has three tools and a full interactive chat loop in main.py. Time to put it all together and watch the agent choose the right tool for different questions.
- Run your agent by entering this command in your terminal:
python main.py
- You should see the welcome banner with the commands list. At the You: prompt, try a math question:
What is 15% of 847 plus the square root of 144?
You should see the agent call the calculate tool, then provide a friendly answer with the result.
- Now try a date/time question:
What day is it today?
The agent should call get_datetime and respond with today's date and the day of the week.
- Try the word lookup tool:
What does the word 'algorithm' mean?
The agent should call word_lookup and give you the definition.
- Test the error handling by asking about a word that isn't in the dictionary:
Define the word 'blockchain'
The agent handles this gracefully. It receives the "Definition not found" message from your tool and communicates it naturally to you.
- Try the reset command to clear the conversation history, then type quit to exit.
How does the agent pick the right tool?
The AI reads your question and compares it against the description field in each tool definition. A math question matches the calculate description. A time question matches get_datetime. The AI picks the best fit automatically.
If none of the tools fit, the AI just answers directly from its own knowledge without calling any tool.
Agent not calling tools?
If the agent answers questions directly without showing [Agent] Calling 1 tool(s)... in the output, the AI might not be recognizing your questions as tool-worthy. Try more explicit phrasing like "Calculate 2 + 2" or "What is the current date and time?".
If you see an error about GROQ_API_KEY, make sure your .env file is saved in the ai-agent folder and contains your actual API key.
Help me debug why my agent isn't calling tools
Your agent is fully operational. It can do math, tell you the date, and look up word definitions. It handles errors gracefully and lets you chat as long as you want. Next up, you'll push it further with a Secret Mission that chains tools together.
Secret mission
Add a Multi-Step Research Tool
Your agent can use individual tools, but what if a single tool could orchestrate multiple other tools internally? Build a research tool that chains your existing tools together to compile a comprehensive brief from a single query.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with Groq's free API tier, so there are no ongoing costs.
Resources you used:
- ai-agent/ project folder on your Desktop.
- Groq API key (stored in your .env file).
- Python packages (groq and python-dotenv).
Keep everything running
No action needed. Choose this if you're still actively building or want to extend your agent with more tools.
- Your ai-agent/ folder stays on your Desktop, ready to run anytime.
- Your Groq API key remains active for future use.
- The groq and python-dotenv packages stay installed on your system.
Pause - I'll come back to this later
Revoke your API key for security while keeping your project files intact. You can generate a new key when you return.
- Go to console.groq.com and navigate to your API keys.
- Delete your current API key from the console.
- When you come back, create a new key and update your .env file.
Delete - I don't want to use this again
Remove all project files, uninstall packages, and revoke your API key.
- Revoke your API key by going to console.groq.com, navigating to your API keys, and deleting the key you created for this project.
- Uninstall the Python packages by running:
pip uninstall groq python-dotenv -y
- Delete the project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf ai-agent
On Windows, run:
cd ~\Desktop
rmdir /s /q ai-agent
Nice Work!
Nice Work!
You did it. You built an AI agent from scratch that autonomously reasons about which tools to use and executes them.
You've learned how to:
- Build a complete AI agent with a tool-calling loop using Groq's free API.
- Create an extensible tool system using Python functions, dictionaries, and JSON schemas.
- Understand the Reason + Act pattern that powers production AI agents.
- Added multi-step research capabilities with parallel tool calling.
Ready to quiz yourself?