start
Quickstart
A working agent with tool use, on your own machine, for free. Two minutes.
1. Install
pip install mantis-agent-sdk2. Get a model
Free and local, no account:
ollama pull qwen2.5-coder:7bAlready have a hosted key? Skip this and see step 5.
3. Your first agent
quickstart.py:
import asyncio
from mantis_agent import MantisAgentOptions, query, tool
@tool
async def get_weather(city: str) -> str:
"""Get the current weather for a city. Returns a one-line summary."""
return f"{city}: 67°F, partly cloudy, wind 8 mph NW"
async def main() -> None:
async for msg in query(
prompt="What's the weather in Lagos?",
options=MantisAgentOptions(
model="qwen2.5-coder:7b",
tools=[get_weather],
max_turns=4,
),
):
if msg.type == "assistant":
for block in msg.content:
if getattr(block, "text", None):
print(block.text)
elif msg.type == "result":
print(f"[{msg.subtype}] {msg.num_turns} turns, ${msg.total_cost_usd:.4f}")
asyncio.run(main())python quickstart.pyThe current weather in Lagos is 67°F with partly cloudy skies.
[success] 2 turns, $0.0000Two turns: the model called get_weather, then answered from the result. No
backend argument — qwen2.5-coder:7b is Ollama tag form, so it routed to
http://localhost:11434 on its own.
4. What each piece does
@toolturns an async function into something the model can call. The signature becomes the JSON schema; the docstring is what the model reads to decide when to call it, so write it for that audience.query()runs the whole loop — model call, tool dispatch, feeding the result back — and yields messages as they happen.MantisAgentOptionsis the typed form. It infers a backend from the model name, and yields flat messages (msg.content).max_turns=4is a hard ceiling on model calls. Pair withmax_budget_usd=once you're on a paid provider.
!!! warning "If you get silence, check msg.subtype"
`query()` does not raise on a provider failure — it reports it on the final
message. A loop that only prints assistant text will print *nothing* and
look like a hang. Always read the result:
```python
async def report(messages):
async for msg in messages:
if msg.type == "result" and msg.is_error:
print("failed:", msg.errors) # e.g. ['ProviderError: Not Found']
```
`ProviderError: Not Found` almost always means the model isn't pulled, or the
backend URL is wrong. `ollama list` tells you the first.5. Point it somewhere else
One line changes, nothing else does:
import os
from mantis_agent import MantisAgentOptions
local = MantisAgentOptions(model="qwen2.5-coder:7b")
hosted = MantisAgentOptions(
model="accounts/fireworks/models/deepseek-v3",
base_url="https://api.fireworks.ai/inference/v1",
api_key=os.environ["FIREWORKS_API_KEY"],
)
claude = MantisAgentOptions(
model="claude-opus-5",
backend="anthropic",
api_key=os.environ["ANTHROPIC_API_KEY"],
)See Models and backends for the full routing and auth rules.
6. Next
- Recipes — a coding agent, JSON extraction, guardrails, sub-agents, each a complete script.
- How configuration works — the option shapes, and why an unknown key never errors.
- Tools — schemas, errors, built-ins.
The other option shape
A plain dict also works, and it's what the wire-shape examples use. Two
differences that bite: it does not infer a backend, and its messages nest
under .message.
import asyncio
from mantis_agent import query
async def main() -> None:
async for msg in query(
prompt="What's the weather in Lagos?",
options={
"model": "qwen2.5-coder:7b",
"backend": "http://localhost:11434", # required here
"max_turns": 4,
},
):
if msg.type == "assistant":
for block in msg.message.content: # note: .message.content
if getattr(block, "text", None):
print(block.text)
asyncio.run(main())