Cookbook¶
Practical recipes for common Axio patterns.
Agent with memory persistence¶
Save and restore conversation history:
import asyncio
from axio import Agent, MemoryContextStore
from axio.testing import StubTransport, make_text_response
transport = StubTransport([make_text_response("Hello!")])
async def main() -> None:
context = MemoryContextStore()
agent = Agent(
system="You are a helpful assistant.",
tools=[],
transport=transport,
)
reply = await agent.run("Hi", context)
print(reply)
asyncio.run(main())
Streaming in FastAPI¶
Build a web API with streaming events:
from axio import Agent, MemoryContextStore, TextDelta
from axio.testing import StubTransport, make_text_response
transport = StubTransport([make_text_response("Hello!")])
async def stream_events(message: str, agent: Agent, context: MemoryContextStore):
"""Pattern for streaming - yields events."""
async for event in agent.run_stream(message, context):
yield event
RAG with custom tools¶
Combine retrieval and generation:
from axio import Tool
async def retrieve_context(query: str) -> str:
"""Retrieve relevant context from a knowledge base."""
return f"Results for: {query}"
async def generate_response(context: str, question: str) -> str:
"""Generate a response using retrieved context."""
return f"Generated: {context[:50]}"
# Create tools
retrieve_tool = Tool(name="retrieve", handler=retrieve_context)
generate_tool = Tool(name="generate", handler=generate_response)
Multi-agent workflow¶
Coordinate multiple agents:
from axio import Agent, MemoryContextStore
from axio.testing import StubTransport, make_text_response
async def main():
shared_context = MemoryContextStore()
transport = StubTransport(
[
make_text_response("Research result"),
make_text_response("Final summary"),
]
)
research_agent = Agent(
system="Research the topic.",
tools=[],
transport=transport,
context=shared_context,
)
writer_agent = Agent(
system="Write a summary.",
tools=[],
transport=transport,
context=shared_context,
)
research_result = await research_agent.run("What is async?")
final_result = await writer_agent.run(f"Summary: {research_result}")
print(final_result)
Retrying another transport¶
A transport that wraps another one and retries it. Retrying is only safe before the first event reaches the caller. Once a delta has been yielded the turn is half-delivered, and starting over repeats it.
import asyncio
from collections.abc import AsyncIterator
from typing import Any
from axio import (
Agent,
MemoryContextStore,
StopReason,
StreamEvent,
TextDelta,
Tool,
Usage,
)
from axio.events import IterationEnd
from axio.exceptions import StreamError
from axio.messages import Message
class RetryTransport:
"""Retries the wrapped transport on StreamError, with exponential backoff."""
def __init__(
self, inner: Any, max_retries: int = 3, base_delay: float = 0.01
) -> None:
self.inner = inner
self.max_retries = max_retries
self.base_delay = base_delay
async def stream(
self,
messages: list[Message],
tools: list[Tool[Any]],
system: str,
) -> AsyncIterator[StreamEvent]:
for attempt in range(1, self.max_retries + 1):
delivered = False
try:
async for event in self.inner.stream(messages, tools, system):
delivered = True
yield event
return
except StreamError:
# Nothing to retry once the caller has seen part of the turn.
if delivered or attempt == self.max_retries:
raise
await asyncio.sleep(self.base_delay * 2 ** (attempt - 1))
class Flaky:
"""Fails twice, then answers."""
def __init__(self) -> None:
self.calls = 0
def stream(
self,
messages: list[Message],
tools: list[Tool[Any]],
system: str,
) -> AsyncIterator[StreamEvent]:
self.calls += 1
return self._answer()
async def _answer(self) -> AsyncIterator[StreamEvent]:
if self.calls < 3:
raise StreamError("503 Service Unavailable")
yield TextDelta(index=0, delta="ok")
yield IterationEnd(
iteration=0, stop_reason=StopReason.end_turn, usage=Usage(1, 1)
)
async def main() -> None:
flaky = Flaky()
agent = Agent(system="", transport=RetryTransport(flaky))
assert await agent.run("hi", MemoryContextStore()) == "ok"
assert flaky.calls == 3
asyncio.run(main())
The shipped transports retry inside themselves rather than through a wrapper. They prefer the
Retry-After header over their own backoff when the response carries one. See
Writing Transports for what a transport is expected to do with errors.
Rate limiting tool¶
import asyncio
from axio import Tool, CONTEXT
RATE_LIMIT = 10
TIME_WINDOW = 60
async def rate_limited_action(data: str) -> str:
"""Tool with rate limiting."""
calls: list[float] = CONTEXT.get()
now = asyncio.get_event_loop().time()
# Prune old calls outside the window
calls[:] = [t for t in calls if now - t < TIME_WINDOW]
if len(calls) >= RATE_LIMIT:
raise RuntimeError(f"Rate limit: {RATE_LIMIT}/{TIME_WINDOW}s")
calls.append(now)
return "done"
call_log: list[float] = []
tool = Tool(name="rate_limited_action", handler=rate_limited_action, context=call_log)
API key guard¶
Check for required environment variables:
import os
from typing import Any
from axio import PermissionGuard, GuardError
class ApiKeyGuard(PermissionGuard):
"""Ensure required environment variables are set."""
required_keys = ("OPENAI_API_KEY",)
async def check(self, handler: Any) -> Any:
missing = [k for k in self.required_keys if not os.environ.get(k)]
if missing:
raise GuardError(f"Missing: {', '.join(missing)}")
return handler
Tool with guards¶
Apply guards to specific tools:
from typing import Any
from axio import Tool, PermissionGuard
async def sensitive_operation(data: str) -> str:
"""Process sensitive data."""
return f"Processed: {data}"
class AllowGuard(PermissionGuard):
async def check(self, tool: Any, **kwargs: Any) -> dict[str, Any]:
return kwargs
sensitive_tool = Tool(
name="sensitive_operation",
handler=sensitive_operation,
guards=(AllowGuard(),),
)
assert len(sensitive_tool.guards) == 1