Python agent loop
Project description
TinyAgent
A small, modular agent framework for building LLM-powered applications in Python.
Overview
TinyAgent provides a lightweight foundation for creating conversational AI agents with tool use capabilities. It features:
- Streaming-first architecture: All LLM interactions support streaming responses
- Tool execution: Define and execute tools with structured outputs
- Event-driven: Subscribe to agent events for real-time UI updates
- Provider agnostic: Works with OpenRouter, proxy servers, or custom providers
- Type-safe: Full type hints throughout
Quick Start
import asyncio
from tinyagent import Agent, AgentOptions, OpenRouterModel, stream_openrouter
# Create an agent
agent = Agent(
AgentOptions(
stream_fn=stream_openrouter,
session_id="my-session"
)
)
# Configure
agent.set_system_prompt("You are a helpful assistant.")
agent.set_model(OpenRouterModel(id="anthropic/claude-3.5-sonnet"))
# Simple prompt
async def main():
response = await agent.prompt_text("What is the capital of France?")
print(response)
asyncio.run(main())
Installation
pip install tinyagent
Core Concepts
Agent
The Agent class is the main entry point. It manages:
- Conversation state (messages, tools, system prompt)
- Streaming responses
- Tool execution
- Event subscription
Messages
Messages follow a typed dictionary structure:
UserMessage: Input from the userAssistantMessage: Response from the LLMToolResultMessage: Result from tool execution
Tools
Tools are functions the LLM can call:
from tinyagent import AgentTool, AgentToolResult
async def calculate_sum(tool_call_id: str, args: dict, signal, on_update) -> AgentToolResult:
result = args["a"] + args["b"]
return AgentToolResult(
content=[{"type": "text", "text": str(result)}]
)
tool = AgentTool(
name="sum",
description="Add two numbers",
parameters={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
},
execute=calculate_sum
)
agent.set_tools([tool])
Events
The agent emits events during execution:
AgentStartEvent/AgentEndEvent: Agent run lifecycleTurnStartEvent/TurnEndEvent: Single turn lifecycleMessageStartEvent/MessageUpdateEvent/MessageEndEvent: Message streamingToolExecutionStartEvent/ToolExecutionUpdateEvent/ToolExecutionEndEvent: Tool execution
Subscribe to events:
def on_event(event):
print(f"Event: {event.type}")
unsubscribe = agent.subscribe(on_event)
Rust Binding: alchemy_llm_py
TinyAgent ships with an optional Rust-based LLM provider located in
bindings/alchemy_llm_py/. It wraps the alchemy-llm
Rust crate and exposes it to Python via PyO3, giving you native-speed
OpenAI-compatible streaming without leaving the Python process.
Why
The pure-Python providers (openrouter_provider.py, proxy.py) work fine, but the Rust
binding gives you:
- Lower per-token overhead -- SSE parsing, JSON deserialization, and event dispatch all happen in compiled Rust with a multi-threaded Tokio runtime.
- Unified provider abstraction --
alchemy-llmnormalizes differences across providers (OpenRouter, Anthropic, custom endpoints) behind a single streaming interface. - Full event fidelity -- text deltas, thinking deltas, tool call deltas, and terminal events are all surfaced as typed Python dicts.
How it works
Python (async) Rust (Tokio)
───────────────── ─────────────────────────
stream_alchemy_*() ──> alchemy_llm::stream()
│
AlchemyStreamResponse ├─ SSE parse + deserialize
.__anext__() <── ├─ event_to_py_value()
(asyncio.to_thread) └─ mpsc channel -> Python
- Python calls
openai_completions_stream(model, context, options)which is a#[pyfunction]. - The Rust side builds an
alchemy-llmrequest, opens an SSE stream on a shared Tokio runtime, and sends events through anmpscchannel. - Python reads events by calling the blocking
next_event()method viaasyncio.to_thread, making it async-compatible without busy-waiting. - A terminal
doneorerrorevent signals the end of the stream. The finalAssistantMessagedict is available viaresult().
Building
Requires a Rust toolchain (1.70+) and maturin.
pip install maturin
cd bindings/alchemy_llm_py
maturin develop # debug build, installs into current venv
maturin develop --release # optimized build
Python API
Two functions are exposed from the alchemy_llm_py module:
| Function | Description |
|---|---|
collect_openai_completions(model, context, options?) |
Blocking. Consumes the entire stream and returns {"events": [...], "final_message": {...}}. Useful for one-shot calls. |
openai_completions_stream(model, context, options?) |
Returns an OpenAICompletionsStream handle for incremental consumption. |
The OpenAICompletionsStream handle has two methods:
| Method | Description |
|---|---|
next_event() |
Blocking. Returns the next event dict, or None when the stream ends. |
result() |
Blocking. Returns the final assistant message dict. |
All three arguments are plain Python dicts:
model = {
"id": "anthropic/claude-3.5-sonnet",
"base_url": "https://openrouter.ai/api/v1/chat/completions",
"provider": "openrouter", # optional
"headers": {"X-Custom": "val"}, # optional
"reasoning": False, # optional
"context_window": 128000, # optional
"max_tokens": 4096, # optional
}
context = {
"system_prompt": "You are helpful.",
"messages": [
{"role": "user", "content": [{"type": "text", "text": "Hello"}]}
],
"tools": [ # optional
{"name": "sum", "description": "Add numbers", "parameters": {...}}
],
}
options = {
"api_key": "sk-...", # optional
"temperature": 0.7, # optional
"max_tokens": 1024, # optional
}
Using via TinyAgent (high-level)
You don't need to call the Rust binding directly. Use the alchemy_provider module:
from tinyagent import Agent, AgentOptions
from tinyagent.alchemy_provider import OpenAICompatModel, stream_alchemy_openai_completions
agent = Agent(
AgentOptions(
stream_fn=stream_alchemy_openai_completions,
session_id="my-session",
)
)
agent.set_model(
OpenAICompatModel(
id="anthropic/claude-3.5-sonnet",
base_url="https://openrouter.ai/api/v1/chat/completions",
)
)
Limitations
- Only OpenAI-compatible
/chat/completionsstreaming is supported. - Image blocks are not yet supported (text and thinking blocks work).
next_event()is blocking and runs in a thread viaasyncio.to_thread-- this adds slight overhead compared to a native async generator, but keeps the GIL released during the Rust work.
Documentation
- Architecture: System design and component interactions
- API Reference: Detailed module documentation
Project Structure
tinyagent/
├── agent.py # Agent class
├── agent_loop.py # Core agent execution loop
├── agent_tool_execution.py # Tool execution helpers
├── agent_types.py # Type definitions
├── openrouter_provider.py # OpenRouter integration
├── alchemy_provider.py # Rust-based provider
├── proxy.py # Proxy server integration
└── proxy_event_handlers.py # Proxy event parsing
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tiny_agent_os-1.1.1.tar.gz.
File metadata
- Download URL: tiny_agent_os-1.1.1.tar.gz
- Upload date:
- Size: 175.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa036f8943d2cf30af74ab814c84fa137bf961104a6f34faf584a6ee8d38e86f
|
|
| MD5 |
09a660a6b1bc6d5c1a96c29e13ca7d00
|
|
| BLAKE2b-256 |
ed187770e29f2acaf7a52f1abcd65b5002c7c33561ffca24f759b77feaf6b825
|
File details
Details for the file tiny_agent_os-1.1.1-cp310-abi3-manylinux_2_38_x86_64.whl.
File metadata
- Download URL: tiny_agent_os-1.1.1-cp310-abi3-manylinux_2_38_x86_64.whl
- Upload date:
- Size: 4.2 MB
- Tags: CPython 3.10+, manylinux: glibc 2.38+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
475f16d6becc293e1ddedb99e44e460661ee1e9afeeb3073374340da766b62d0
|
|
| MD5 |
c8c427bd9c68dcb5efa9745bfd2d8173
|
|
| BLAKE2b-256 |
cefbf8857c1f473243cfe54248bb94cdcfc30becad418e049c59f83a75fb2989
|