Introduction
You can build a working AI agent this morning with about twenty lines of Python.
Install and ask something
Modus is a Python framework for building agents, and it installs like any other package. You need Python 3.11 or newer.
pip install modus-ai
Before the first run, give the agent its key. Create a file named .env next to your script:
OPENAI_API_KEY=sk-your-key-here
Every snippet below starts with load_env(), which reads that file so the key never appears in code. Different provider? Swap the variable and the provider argument together.
Now the smallest possible agent: a name, a personality in plain words, and a provider, which is just the model company you want to talk to.
from modus import Agent
from modus.core import load_env
load_env()
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
provider="openai",
)
print(agent.run("What is the capital of France?"))
run takes your message and returns the answer as a string. That is the whole core loop: message in, text out. The key comes from your .env file through load_env(). No key and no network? Point the provider at a local model instead and everything below still works.
Give it a memory
A fresh agent forgets everything between calls. Hand it a memory and it remembers facts across runs.
from modus import Agent
from modus.core import load_env
from modus.memory import Memory
load_env()
memory = Memory("./data")
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant with a long-term memory.",
provider="openai",
memory=memory,
)
print(agent.run("My name is Ada and I prefer dark mode."))
print(agent.run("What do you know about me?"))
Memory("./data") stores facts as plain files in that folder. The second call finds what the first one stored: the agent retrieves relevant facts before answering and saves new ones after. The folder is the database.
Sessions are the other half of continuity. Pass session_path and the conversation history survives restarts:
agent = Agent(..., session_path="./session.json")
| Without | With |
|---|---|
| Each run starts blank | Facts persist in ./data |
| History lost on restart | Conversation resumes from file |
Give it hands with a tool
An agent that can only talk is a chatbot. Tools give it hands: a plain function plus a @tool decorator with its name and arguments. The decorator is required, or register fails with TypeError.
from modus import Agent
from modus.actions import tool
from modus.core import load_env
load_env()
@tool(
name="get_weather",
description="Get a weather forecast for a city",
parameters={
"type": "object",
"properties": {
"city": {"type": "string"},
"days": {"type": "integer", "default": 1},
},
"required": ["city"],
},
)
def get_weather(city: str, days: int = 1) -> str:
return f"{city}: sunny, 24C, next {days} day(s)."
agent = Agent(
name="ToolBot",
provider="openai",
instructions="Use the available tools to answer questions.",
)
agent.actions.register(get_weather)
print(agent.run("What is the weather in Lisbon?"))
The model sees the tool description, proposes a call with arguments, the framework runs your function and feeds the result back, and the model writes the final answer. You never parse anything:
A real get_weather would call a weather API inside the function body. The agent does not care. It only sees the name, the description, and the arguments.
Watch what happened
Every run can be traced for free: model calls, tool runs, token counts, cost estimates, all in a local database.
from modus import Agent
from modus.core import load_env
from modus.observability import configure_tracing
load_env()
configure_tracing("./traces")
agent = Agent(name="ObservedBot", provider="openai")
agent.run("Hello, who are you?")
Then open the dashboard with the modus ui command. Tracing adds about five hundredths of a millisecond per run, and there is a full post on my blog about how that works.
When it breaks
Two errors catch every beginner. Both have one-line fixes:
| Error | Cause | Fix |
|---|---|---|
ModusConfigError: API key not found |
.env missing, wrong variable, or load_env() skipped |
Check the filename, check OPENAI_API_KEY=..., call load_env() first |
TypeError: register() expects a Tool |
Bare function without the decorator | Add @tool(name=..., description=..., parameters=...) above the function |
Confused or empty answers are usually vague instructions. Name the tools and say when to use them.
What to try next
Your second project, in five minutes: add a get_time tool beside get_weather, ask "what is the weather and time in Lisbon?", then open the trace dashboard and watch both tools fire in one run. Two tools, one question, visible proof the loop works.
After that, each direction opens up:
- Memory backends: keyword search works out of the box, vector search plugs in later.
- Structured answers: get a validated object instead of free text.
- Workflows: chain agents into graphs that route to the next step.
- Deploy: serve a workflow over HTTP or on a schedule.
The honest caveat: the model bills per call, and a confused agent can burn its full round budget. Set a spending limit early and keep instructions specific.
Start with one agent, one memory folder, one tool.