Everyone is talking about AI agents these days, and many explanations make them sound far more complicated than they actually are.
Once you strip away the frameworks, an agent is a relatively small piece of code. In this article, we'll build one from scratch in plain Python in about 150 lines, so you can see exactly what is happening under the hood.
So, What Is an AI Agent?
The simplest way to put it is: a chatbot talks, while an agent does things.
You give an agent a goal, and it works out which tools to use, checks what those tools return, and continues until it has an answer.
Suppose you ask a normal chatbot:
Is it warmer in Mumbai or Delhi right now?Without access to a live weather tool, it cannot reliably answer. It can only make an estimate based on information available to the model.
An agent handles the same question differently:
It calls a weather tool for Mumbai.
It calls the same tool again for Delhi.
It compares the two temperatures.
It tells you which city is warmer.
The interesting part is that nobody explicitly coded those three steps. The model decided which tools to call and in what order.
What We're Going to Build
We'll build a small command-line assistant called Atlas. It will have three tools:
Tool | What It Does | Example Question |
|---|---|---|
get_weather | Fetches live weather for any city using a free API with no API key | "Do I need an umbrella in Pune today?" |
calculate | Performs mathematical calculations safely | "What is 18% GST on 4,750?" |
get_current_time | Returns the date and time for a specified timezone | "What time is it in New York?" |
Atlas itself is a small learning project, but the pattern is widely applicable. Frameworks such as LangChain, CrewAI, Microsoft Agent Framework, and other agent frameworks implement variations of this same basic loop.
Once you understand the loop, these frameworks become much easier to understand.
How the Agent Loop Works
At its core, an agent repeatedly performs four steps:
Think — Send the conversation and the list of available tools to the model.
Decide — The model either answers directly or requests one or more tools.
Act — Python executes the requested tool. The model itself does not execute the code; it only requests the action.
Observe — Send the tool output back to the model and continue the loop.
When the model returns a final answer instead of requesting another tool, the loop ends.
The process looks like this:
User question
|
v
+--------------------+ wants a tool +---------------------+
| Model thinks | -----------------> | Python runs tool |
| (with tool list) | <----------------- | (weather, math...) |
+--------------------+ tool result +---------------------+
|
| final answer
v
Show to userEverything else in this article is essentially these four steps implemented in Python.
What You'll Need
Before starting, make sure you have the following:
Python 3.9 or later. Run
python --versionto check.An Anthropic API key.
A code editor such as VS Code.
This example uses Claude as the model. If you prefer OpenAI, Gemini, Azure OpenAI, or another provider, the overall architecture remains the same. The primary difference is the model client and API call.
Create a Project and Virtual Environment
Create a project folder and virtual environment:
mkdir atlas-agent
cd atlas-agent
python -m venv venv
# Windows
venv\Scripts\activate
# macOS / Linux
source venv/bin/activateInstall the Required Packages
Install the packages used by the project:
pip install anthropic requests python-dotenv tzdataEach package has a specific role:
anthropic — The SDK used to communicate with the model.
requests — Used to call the weather API.
python-dotenv — Loads the API key from the .env file.
tzdata — Provides timezone data, which is especially useful on Windows.
Keep the API Key Out of Your Code
Create a .env file in the project directory:
ANTHROPIC_API_KEY=sk-ant-your-key-hereIf the project uses Git, add .env to `.gitignore before committing the project.
API keys should never be hard-coded into source files or committed to a public repository.
Step 1: Make Sure Python Can Reach the Model
Before building the agent, verify that Python can communicate with the model.
Create a file named hello.py:
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv() # Reads ANTHROPIC_API_KEY from .env
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=500,
messages=[
{
"role": "user",
"content": "Say hello in one short sentence."
}
],
)
print(response.content[0].text)Run the program:
python hello.pyYou should receive a short response from the model.
If you receive an authentication error, check that:
The .env file exists in the project directory.
The API key is correct.
You are running the script from the expected directory.
The environment variable name matches ANTHROPIC_API_KEY.
The messages collection contains the conversation history. Each message has a role and content.
The model does not automatically remember previous API calls. The application needs to send the conversation history with each request. Our agent will continuously add messages to this list as it works through a request.
Step 2: Write the Tools
A tool is simply a Python function that the model can request.
Create agent.py and add the imports and three tools:
import ast
import json
import operator
from datetime import datetime
from zoneinfo import ZoneInfo
import requests
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
client = Anthropic()
MODEL = "claude-sonnet-5-5"
# ---------- Tool 1: Weather ----------
def get_weather(city: str) -> dict:
geo = requests.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1},
timeout=10,
).json()
if not geo.get("results"):
return {"error": f"City '{city}' not found"}
place = geo["results"][0]
weather = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": place["latitude"],
"longitude": place["longitude"],
"current": (
"temperature_2m,"
"relative_humidity_2m,"
"precipitation,"
"wind_speed_10m"
),
},
timeout=10,
).json()
return {
"city": place["name"],
"country": place.get("country"),
**weather["current"],
}
# ---------- Tool 2: Safe Calculator ----------
OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
}
def _evaluate(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPERATORS:
return OPERATORS[type(node.op)](
_evaluate(node.left),
_evaluate(node.right),
)
if isinstance(node, ast.UnaryOp) and type(node.op) in OPERATORS:
return OPERATORS[type(node.op)](
_evaluate(node.operand)
)
raise ValueError("Only numbers and + - * / ** % are allowed")
def calculate(expression: str) -> dict:
try:
result = _evaluate(
ast.parse(expression, mode="eval").body
)
return {
"expression": expression,
"result": result,
}
except Exception as e:
return {"error": str(e)}
# ---------- Tool 3: Current Time ----------
def get_current_time(timezone: str = "UTC") -> dict:
try:
now = datetime.now(ZoneInfo(timezone))
return {
"timezone": timezone,
"datetime": now.strftime(
"%A, %d %B %Y, %I:%M %p"
),
}
except Exception:
return {
"error": (
f"Unknown timezone '{timezone}'. "
"Use a name like 'Asia/Kolkata'."
)
}There are two important practices in this code that are useful for production agent development.
Return Tool Errors Instead of Stopping the Agent
The tools return errors as structured data instead of allowing an exception to stop the entire agent.
For example, if a user enters an invalid city name, the weather tool returns an error that the model can understand. The model can then try another spelling or ask the user for clarification.
Never Use eval() for Model-Generated Expressions
The calculator does not use Python's eval() function.
You should treat anything returned by a model as untrusted input, just as you would treat user input. Passing arbitrary model-generated expressions directly to eval() could allow unintended Python code to execute.
Instead, this example parses the expression using Python's ast module and allows only a specific set of arithmetic operations.
Step 3: Tell the Model Which Tools Are Available
The model cannot directly inspect your Python functions.
Instead, the application sends the model a description of each tool, including:
The tool name.
A description of when it should be used.
A JSON Schema describing its inputs.
Add the following after the tool functions:
TOOLS = [
{
"name": "get_weather",
"description": (
"Get the current weather for a city. Use this for any "
"question about weather, temperature, rain, humidity or wind."
),
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Mumbai'",
}
},
"required": ["city"],
},
},
{
"name": "calculate",
"description": (
"Evaluate a math expression. Use this for any arithmetic "
"instead of calculating in your head."
),
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "e.g. '4750 * 0.18'",
}
},
"required": ["expression"],
},
},
{
"name": "get_current_time",
"description": (
"Get the current date and time in an IANA timezone."
),
"input_schema": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": (
"e.g. 'Asia/Kolkata', 'America/New_York'"
),
}
},
"required": ["timezone"],
},
},
]
TOOL_FUNCTIONS = {
"get_weather": get_weather,
"calculate": calculate,
"get_current_time": get_current_time,
}
Join the conversation! Your thoughts help the community grow.