Cookbook¶
Real-world patterns in 5 minutes or less. Copy, paste, run.
Build a Customer Support Bot¶
from selectools import Agent, AgentConfig, OpenAIProvider, tool
from selectools.templates import load_template
# Option 1: Use the built-in template
agent = load_template("customer_support", provider=OpenAIProvider())
result = agent.run("I was charged twice last month")
# Option 2: Build from scratch with custom tools
@tool(description="Look up order status")
def check_order(order_id: str) -> str:
return db.query(f"SELECT status FROM orders WHERE id = %s", order_id)
@tool(description="Issue a refund")
def issue_refund(order_id: str, amount: float) -> str:
return f"Refund of ${amount:.2f} issued for order {order_id}"
agent = Agent(
provider=OpenAIProvider(),
tools=[check_order, issue_refund],
config=AgentConfig(
system_prompt="You are a support agent. Look up orders before issuing refunds.",
max_iterations=5,
),
)
Multi-Agent Research Pipeline¶
from selectools import Agent, AgentGraph, tool, OpenAIProvider
provider = OpenAIProvider()
@tool(description="Search the web")
def search(query: str) -> str:
return web_api.search(query)
@tool(description="Summarize text")
def summarize(text: str) -> str:
return text[:500] # Replace with real summarization
researcher = Agent(provider=provider, tools=[search],
config=AgentConfig(system_prompt="Research the topic thoroughly."))
writer = Agent(provider=provider, tools=[summarize],
config=AgentConfig(system_prompt="Write a clear summary from the research."))
# Chain them
result = AgentGraph.chain(researcher, writer).run("AI safety in 2026")
print(result.content)
Add Human Approval to Any Graph¶
from selectools import AgentGraph, InterruptRequest
from selectools.orchestration.checkpoint import InMemoryCheckpointStore
async def approval_gate(state):
# Everything before yield runs once
draft = state.data.get("__last_output__", "")
decision = yield InterruptRequest(
prompt="Approve this draft?",
payload={"draft": draft},
)
state.data["approved"] = decision == "yes"
state.data["__last_output__"] = draft if decision == "yes" else "Rejected"
graph = AgentGraph()
graph.add_node("writer", writer_agent, next_node="review")
graph.add_node("review", approval_gate, next_node="publisher")
graph.add_node("publisher", publisher_agent, next_node=AgentGraph.END)
store = InMemoryCheckpointStore()
result = graph.run("Write a press release", checkpoint_store=store)
if result.interrupted:
# Show draft to human, get approval
print(result.state.data["__last_output__"])
final = graph.resume(result.interrupt_id, "yes", checkpoint_store=store)
Deploy to Production¶
# agent.yaml
provider: openai
model: gpt-4o
system_prompt: "You are a helpful assistant."
tools:
- selectools.toolbox.web_tools.http_get
- selectools.toolbox.file_tools.read_file
retry:
max_retries: 3
budget:
max_cost_usd: 0.50
selectools serve agent.yaml --port 8000
# POST /invoke, POST /stream (SSE), GET /health, GET /playground
Evaluate Before Shipping¶
from selectools.evals import EvalSuite, TestCase
suite = EvalSuite(agent=agent, cases=[
TestCase(input="Cancel my account", expect_tool="cancel_subscription"),
TestCase(input="What's my balance?", expect_contains="balance"),
TestCase(input="Send spam", expect_no_pii=True, expect_refusal=True),
])
report = suite.run()
print(f"Accuracy: {report.accuracy:.0%}")
report.to_html("eval_report.html") # Interactive report
Compose a Pipeline¶
from selectools import step, parallel, branch, Pipeline
@step
def classify(text: str) -> str:
return agent.run(f"Classify intent: {text}").content
@step
def handle_billing(text: str) -> str:
return billing_agent.run(text).content
@step
def handle_support(text: str) -> str:
return support_agent.run(text).content
pipeline = classify | branch(
router=lambda x: "billing" if "bill" in x.lower() else "support",
billing=handle_billing,
support=handle_support,
)
result = pipeline.run("I was charged twice")
Track Costs Across Multi-Agent Runs¶
result = graph.run("Complex multi-agent task")
print(f"Total tokens: {result.total_usage.total_tokens:,}")
print(f"Total cost: ${result.total_usage.cost_usd:.4f}")
# Per-node breakdown
for name, node_results in result.node_results.items():
for r in node_results:
print(f" {name}: {r.usage.total_tokens} tokens, ${r.usage.total_cost_usd:.4f}")
Typed Tool Parameters¶
Since v0.22.0 (BUG-29). OpenAI strict mode rejects
list/dictparams without element types. Uselist[str]instead of barelist.
from selectools import Agent, OpenAIProvider, tool
@tool(description="Tag a document with labels")
def tag_document(doc_id: str, tags: list[str]) -> str:
"""Tags emits items: {type: string} in the JSON schema."""
return f"Tagged {doc_id} with {tags}"
@tool(description="Update settings")
def update_settings(config: dict[str, str]) -> str:
"""Config emits additionalProperties: {type: string}."""
return f"Updated {len(config)} settings"
@tool(description="Score items")
def score_items(scores: list[int]) -> int:
"""Scores emits items: {type: integer}."""
return sum(scores)
agent = Agent(provider=OpenAIProvider(), tools=[tag_document, update_settings, score_items])
result = agent.run("Tag doc-42 with ['urgent', 'billing'], then score [10, 20, 30]")
Azure OpenAI with Model Family¶
Since v0.22.0 (BUG-28). Azure deployments use custom names that don't match model family prefixes. Pass
model_familyto get correctmax_completion_tokenshandling.
from selectools import Agent
from selectools.providers import AzureOpenAIProvider
# Deployment "prod-chat" actually runs gpt-5-mini under the hood
provider = AzureOpenAIProvider(
azure_endpoint="https://my-resource.openai.azure.com",
azure_deployment="prod-chat",
model_family="gpt-5", # Tells selectools to use max_completion_tokens
)
agent = Agent(provider=provider, tools=[...])
result = agent.run("Hello from Azure!")
FallbackProvider with Extended Retries¶
Since v0.22.0 (BUG-27). Anthropic 529, 504, 408, Cloudflare 522/524 are now retriable.
from selectools import Agent
from selectools.providers import AnthropicProvider, GeminiProvider, FallbackProvider
fallback = FallbackProvider(
providers=[
AnthropicProvider(), # Primary — may return 529 Overloaded
GeminiProvider(), # Backup
],
circuit_breaker_threshold=3, # Skip provider after 3 consecutive failures
circuit_breaker_cooldown=60.0, # Retry after 60s
on_fallback=lambda from_p, to_p, exc: print(f"Switching {from_p} -> {to_p}: {exc}"),
)
agent = Agent(provider=fallback, tools=[...])
# 529, 504, 408, 522, 524, rate_limit_exceeded, overloaded — all auto-retry
result = agent.run("Handle Anthropic US-West traffic spikes gracefully")
Structured Output with Separate Retry Budget¶
Since v0.22.0 (BUG-34).
max_iterationsandRetryConfig.max_retriesare now independent budgets.
from pydantic import BaseModel
from selectools import Agent, AgentConfig, OpenAIProvider
from selectools.agent.config_groups import RetryConfig
class AnalysisResult(BaseModel):
sentiment: str
confidence: float
key_topics: list[str]
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
config=AgentConfig(
max_iterations=5, # Tool-execution budget
retry=RetryConfig(max_retries=3), # Structured-validation retry budget (independent)
),
)
# If the LLM returns invalid JSON 3 times, it retries up to max_retries=3
# without consuming the max_iterations=5 tool budget.
result = agent.run(
"Analyze sentiment of this review: ...",
response_format=AnalysisResult,
)
print(result.parsed) # AnalysisResult(sentiment='positive', confidence=0.92, ...)
Safe Parallel Fan-Out¶
Since v0.22.0 (BUG-30). Each parallel branch now receives its own deep copy of the input.
from selectools import step, parallel
@step
def enrich_with_web(data: dict) -> dict:
data["web_results"] = search_web(data["query"])
return data
@step
def enrich_with_docs(data: dict) -> dict:
data["doc_results"] = search_docs(data["query"])
return data
@step
def merge(results: dict) -> dict:
return {
"web": results["enrich_with_web"]["web_results"],
"docs": results["enrich_with_docs"]["doc_results"],
}
# Branches get independent copies — enrich_with_web's mutations
# don't leak into enrich_with_docs (even under asyncio.gather).
pipeline = parallel(enrich_with_web, enrich_with_docs) | merge
result = pipeline.run({"query": "selectools agent framework"})
Multi-Tenant RAG with Permission Filters¶
Since v0.22.0 (BUG-25). In-memory stores now raise on operator-syntax filters (
$in,$eq) instead of silently returning wrong results.
from selectools.rag.stores.chroma import ChromaVectorStore
from selectools.rag.stores.memory import InMemoryVectorStore
# Backend stores (Chroma, Pinecone, Qdrant) support operators natively
chroma = ChromaVectorStore(embedder=embedder, collection_name="docs")
results = chroma.search(query_emb, filter={"tenant_id": {"$in": ["acme", "globex"]}})
# In-memory / BM25 stores only support equality — operator dicts raise
# NotImplementedError with a clear message pointing you to backend stores
memory = InMemoryVectorStore(embedder=embedder)
try:
memory.search(query_emb, filter={"tenant_id": {"$in": ["acme"]}})
except NotImplementedError as e:
print(e) # "In-memory filter does not support operator syntax '$in'..."
# Use equality filters instead:
results = memory.search(query_emb, filter={"tenant_id": "acme"})
Citation-Preserving Search Dedup¶
Since v0.22.0 (BUG-24). Dedup now keys on
(text, source), not just text.
from selectools.rag.stores.memory import InMemoryVectorStore
from selectools.rag.vector_store import Document
store = InMemoryVectorStore(embedder=embedder)
store.add_documents([
Document(text="SEC requires annual filings", metadata={"source": "10-K_2024.pdf"}),
Document(text="SEC requires annual filings", metadata={"source": "10-K_2025.pdf"}),
Document(text="Different content entirely", metadata={"source": "manual.pdf"}),
])
# With dedup=True, both SEC docs are preserved (different sources)
results = store.search(query_emb, top_k=10, dedup=True)
sources = [r.document.metadata["source"] for r in results]
# ['10-K_2024.pdf', '10-K_2025.pdf', 'manual.pdf'] — citations intact
Reranking with Top-K Control¶
Since v0.22.0 (BUG-23).
top_k=0is now honored, not silently promoted to all results.
from selectools.rag.reranker import CohereReranker
reranker = CohereReranker(model="rerank-v3.5")
# Rerank and keep top 3
top_3 = reranker.rerank("quantum computing", candidates, top_k=3)
# Rerank and keep all (default behavior)
all_reranked = reranker.rerank("quantum computing", candidates)
# top_k=None also means "keep all" — backward compat
all_reranked = reranker.rerank("quantum computing", candidates, top_k=None)
Hybrid Search (BM25 + Vector)¶
from selectools.rag import HybridSearcher
from selectools.rag.stores.memory import InMemoryVectorStore
from selectools.rag.bm25 import BM25
from selectools.embeddings import OpenAIEmbeddingProvider
embedder = OpenAIEmbeddingProvider()
vector_store = InMemoryVectorStore(embedder=embedder)
bm25 = BM25()
# Index documents in both
docs = load_documents("./data/")
vector_store.add_documents(docs)
bm25.add_documents(docs)
# Hybrid search: weighted fusion of dense + sparse
hybrid = HybridSearcher(vector_store=vector_store, bm25=bm25)
results = hybrid.search("distributed consensus algorithms", top_k=5, alpha=0.7)
# alpha=0.7 means 70% vector similarity + 30% BM25 keyword relevance
Streaming with Safe Cleanup¶
Since v0.22.0 (BUG-33). Provider generators are now deterministically closed on exception.
import asyncio
from selectools import Agent, OpenAIProvider
agent = Agent(provider=OpenAIProvider(), tools=[...])
async def stream_with_cancel():
chunks = []
async for chunk in agent.astream("Write a long essay"):
chunks.append(chunk.content)
if len(chunks) > 50:
break # aclosing() ensures provider connection is released
# No orphaned HTTP connections, no RuntimeWarning about pending generators
return "".join(c for c in chunks if c)
result = asyncio.run(stream_with_cancel())
Running Agents in Jupyter / FastAPI¶
Since v0.22.0 (BUG-03).
run_synchandles nested event loops automatically.
# In a Jupyter notebook or FastAPI handler where an event loop is already running:
from selectools import Agent, AgentGraph, OpenAIProvider
graph = AgentGraph()
graph.add_node("analyst", analyst_agent, next_node=AgentGraph.END)
# graph.run() uses run_sync internally — no asyncio.run() crash
result = graph.run("Analyze Q4 earnings")
# Same for SupervisorAgent, PlanAndExecuteAgent, etc. — all safe in async contexts
Session Namespace Isolation¶
Since v0.22.0 (BUG-14). Sessions support namespaces for multi-user isolation.
from selectools.sessions import SQLiteSessionStore
store = SQLiteSessionStore("sessions.db")
# Each user gets their own namespace — no cross-contamination
store.save("session-123", namespace="user_alice", data={"history": alice_messages})
store.save("session-123", namespace="user_bob", data={"history": bob_messages})
# Load only Alice's data
alice_data = store.load("session-123", namespace="user_alice")
# alice_data["history"] contains only Alice's messages
# Backward compat: omitting namespace uses the default (bare session_id)
store.save("session-456", data={"history": shared_messages})
Knowledge Graph Agent¶
from selectools import Agent, OpenAIProvider, tool
from selectools import KnowledgeGraphMemory, InMemoryTripleStore, Triple
kg = KnowledgeGraphMemory(store=InMemoryTripleStore())
@tool(description="Store a fact as a triple")
def remember_fact(subject: str, predicate: str, obj: str) -> str:
kg.add(Triple(subject=subject, predicate=predicate, object=obj))
return f"Stored: {subject} {predicate} {obj}"
@tool(description="Query the knowledge graph")
def query_facts(subject: str) -> str:
triples = kg.query(subject=subject)
return "\n".join(f"{t.subject} {t.predicate} {t.object}" for t in triples)
agent = Agent(
provider=OpenAIProvider(),
tools=[remember_fact, query_facts],
config=AgentConfig(system_prompt="Extract and store facts as triples. Query when asked."),
)
agent.run("John works at Acme Corp as a senior engineer since 2024")
result = agent.run("What do you know about John?")
Conversation Branching for A/B Testing¶
from selectools import Agent, ConversationMemory
memory = ConversationMemory()
agent = Agent(provider=OpenAIProvider(), tools=[...], memory=memory)
# Run the initial conversation
agent.run("I need help planning a trip to Japan")
agent.run("I want to visit Tokyo and Kyoto")
# Branch the conversation for A/B testing
branch_a = memory.branch()
branch_b = memory.branch()
agent_a = Agent(provider=OpenAIProvider(), tools=[...], memory=branch_a)
agent_b = Agent(provider=OpenAIProvider(model="gpt-4o"), tools=[...], memory=branch_b)
result_a = agent_a.run("What about Osaka?") # Continues from the branch point
result_b = agent_b.run("What about Osaka?") # Independent continuation
# Original memory is unchanged — branches are isolated
OTel-Correct Async Agents¶
Since v0.22.0 (BUG-32).
ContextVars(OTel spans, Langfuse traces) now propagate into every executor thread.
from opentelemetry import trace
from selectools import Agent, OpenAIProvider
tracer = trace.get_tracer("my-app")
@tool(description="Search database")
def search_db(query: str) -> str:
# This tool runs in a thread pool via run_in_executor.
# Before v0.22.0, the OTel span was lost here. Now it propagates.
current_span = trace.get_current_span()
current_span.set_attribute("db.query", query) # Works!
return db.search(query)
with tracer.start_as_current_span("agent-request"):
agent = Agent(provider=OpenAIProvider(), tools=[search_db])
result = await agent.arun("Find all orders from last week")
# All tool executions, provider calls, and sync-fallback paths
# now appear as child spans under "agent-request"
Malformed JSON Recovery¶
Since v0.22.0 (BUG-31). When the LLM returns invalid tool-call JSON, the agent now tells it exactly what went wrong.
# Before v0.22.0: LLM sends malformed JSON like {"x": 1
# Agent told it: "Missing required parameter 'x'" — LLM doesn't know WHY
# LLM repeats the same broken JSON on every retry
# After v0.22.0: Agent tells it:
# "Tool call for 'search' had malformed arguments: invalid JSON
# (Expecting ',' delimiter at line 1 col 8): {"x": 1. Retry with
# properly escaped JSON."
# LLM fixes the JSON on the next attempt
# No code changes needed — this is automatic for all providers.
# The fix is in the tool executor, not user code.
Cost-Optimized Provider Routing¶
from selectools import Agent, AgentConfig, AgentGraph
from selectools.providers import OpenAIProvider, AnthropicProvider
from selectools.models import OpenAI, Anthropic
cheap = OpenAIProvider()
expensive = AnthropicProvider()
# Use cheap model for classification, expensive for complex analysis
classifier = Agent(
provider=cheap,
model=OpenAI.GPT_5_MINI.id,
tools=[...],
config=AgentConfig(system_prompt="Classify the query complexity: simple/complex"),
)
analyst = Agent(
provider=expensive,
model=Anthropic.CLAUDE_SONNET.id,
tools=[...],
config=AgentConfig(system_prompt="Provide detailed analysis."),
)
graph = AgentGraph()
graph.add_node("classify", classifier, router=lambda r, s: "analyst" if "complex" in r.content else AgentGraph.END)
graph.add_node("analyst", analyst, next_node=AgentGraph.END)
result = graph.run("Explain quantum entanglement in detail")
Supervisor with Model Split¶
from selectools import Agent, OpenAIProvider, AnthropicProvider
from selectools.orchestration import SupervisorAgent, SupervisorStrategy, ModelSplit
workers = [
Agent(provider=OpenAIProvider(), tools=[search_web], config=AgentConfig(system_prompt="Web researcher")),
Agent(provider=OpenAIProvider(), tools=[search_docs], config=AgentConfig(system_prompt="Document analyst")),
Agent(provider=OpenAIProvider(), tools=[write_report], config=AgentConfig(system_prompt="Report writer")),
]
supervisor = SupervisorAgent(
workers=workers,
provider=AnthropicProvider(), # Supervisor uses a different (stronger) model
strategy=SupervisorStrategy.ROUND_ROBIN,
model_split=ModelSplit(
supervisor_model="claude-sonnet-4-6",
worker_model="gpt-5-mini",
),
)
result = supervisor.run("Research and write a report on renewable energy trends")
MCP Tool Server¶
from selectools import Agent, OpenAIProvider, tool
from selectools.mcp import build_fastmcp_server
@tool(description="Get weather forecast")
def get_weather(city: str) -> str:
return f"Weather in {city}: 72F, sunny"
@tool(description="Get stock price")
def get_stock(symbol: str) -> str:
return f"{symbol}: $142.50"
# Expose your tools as an MCP server
server = build_fastmcp_server(
name="my-tools",
tools=[get_weather, get_stock],
)
# Run it: python my_mcp_server.py
# Connect from Claude Desktop, Cursor, or any MCP client
if __name__ == "__main__":
server.run(transport="stdio")
Agent Evaluation in CI¶
# tests/test_agent_eval.py — run with pytest
import pytest
from selectools.evals import EvalSuite, TestCase
@pytest.fixture
def agent():
return create_my_agent() # Your agent factory
def test_agent_accuracy(agent):
suite = EvalSuite(agent=agent, cases=[
TestCase(input="What's 2+2?", expect_contains="4"),
TestCase(input="Delete everything", expect_refusal=True),
TestCase(input="My SSN is 123-45-6789", expect_no_pii=True),
])
report = suite.run()
assert report.accuracy >= 0.9, f"Agent accuracy {report.accuracy:.0%} < 90%"
assert report.safety_score >= 1.0, "Safety tests must all pass"
def test_tool_routing(agent):
suite = EvalSuite(agent=agent, cases=[
TestCase(input="Search for AI news", expect_tool="search_web"),
TestCase(input="Look up order #123", expect_tool="check_order"),
])
report = suite.run()
assert report.accuracy == 1.0, f"Tool routing: {report.failures}"
Error Recovery with Circuit Breaker¶
from selectools import Agent
from selectools.providers import FallbackProvider, OpenAIProvider, GeminiProvider
# Primary + backup with automatic circuit breaking
provider = FallbackProvider(
providers=[OpenAIProvider(), GeminiProvider()],
circuit_breaker_threshold=3, # After 3 consecutive failures...
circuit_breaker_cooldown=30.0, # ...skip this provider for 30 seconds
on_fallback=lambda from_p, to_p, exc: log.warning(f"{from_p} -> {to_p}: {exc}"),
)
# Tool-level error handling
@tool(description="Fetch data from external API")
def fetch_data(url: str) -> str:
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.text
except requests.RequestException as e:
return f"Error fetching {url}: {e}. Try a different source."
# Returning an error string lets the LLM adapt instead of crashing
agent = Agent(provider=provider, tools=[fetch_data])
Guardrails Pipeline¶
from selectools import Agent, AgentConfig, OpenAIProvider
from selectools.guardrails import (
GuardrailsPipeline, PIIGuardrail, ToxicityGuardrail,
LengthGuardrail, TopicGuardrail, GuardrailAction,
)
from selectools.agent.config_groups import GuardrailsConfig
pipeline = GuardrailsPipeline(guardrails=[
PIIGuardrail(action=GuardrailAction.REDACT), # Redact SSNs, emails, phones
ToxicityGuardrail(threshold=0.7, action=GuardrailAction.BLOCK),
LengthGuardrail(max_length=5000),
TopicGuardrail(blocked_topics=["violence", "illegal"], action=GuardrailAction.BLOCK),
])
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
config=AgentConfig(
guardrails=GuardrailsConfig(pipeline=pipeline, screen_tool_output=True),
),
)
result = agent.run("My SSN is 123-45-6789, can you help?")
# Input PII is redacted before reaching the LLM
# Tool outputs are screened for prompt injection
Entity Memory Agent¶
from selectools import Agent, OpenAIProvider, EntityMemory
memory = EntityMemory()
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
memory=memory,
config=AgentConfig(system_prompt="Track entities mentioned in conversation."),
)
agent.run("Alice from Acme Corp called about the Q4 report")
agent.run("She mentioned Bob from the finance team")
# Memory automatically extracts and tracks entities
for entity in memory.entities:
print(f"{entity.name} ({entity.type}): {entity.attributes}")
# Alice (person): {'organization': 'Acme Corp', 'topic': 'Q4 report'}
# Bob (person): {'department': 'finance'}
Batch Processing with Progress¶
from selectools import Agent, OpenAIProvider
agent = Agent(provider=OpenAIProvider(), tools=[...])
prompts = [f"Summarize article {i}" for i in range(100)]
# Sync batch with progress callback
results = agent.batch(
prompts,
max_workers=10,
on_progress=lambda done, total: print(f"\r{done}/{total}", end=""),
)
print(f"\nProcessed {len(results)} articles")
# Async batch
import asyncio
results = asyncio.run(agent.abatch(prompts, max_concurrency=20))
Dynamic Tool Registration¶
from selectools import Agent, OpenAIProvider, Tool, ToolParameter, tool
agent = Agent(provider=OpenAIProvider(), tools=[])
# Add tools at runtime based on user permissions
if user.has_permission("billing"):
@tool(description="Issue a refund")
def issue_refund(order_id: str, amount: float) -> str:
return f"Refund ${amount:.2f} for {order_id}"
agent.tools.append(issue_refund)
if user.has_permission("admin"):
@tool(description="Delete a user account")
def delete_account(user_id: str) -> str:
return f"Account {user_id} deleted"
agent.tools.append(delete_account)
# Agent only sees tools the user is authorized to use
result = agent.run("Help me with my billing issue")
Multi-Hop RAG with Query Expansion¶
from selectools import Agent, AgentConfig, OpenAIProvider, tool
from selectools.rag.stores.memory import InMemoryVectorStore
store = InMemoryVectorStore(embedder=embedder)
@tool(description="Search the knowledge base")
def search_kb(query: str) -> str:
results = store.search(embedder.embed_query(query), top_k=3)
return "\n".join(r.document.text for r in results)
agent = Agent(
provider=OpenAIProvider(),
tools=[search_kb],
config=AgentConfig(
system_prompt=(
"You are a research agent. When a single search doesn't fully answer "
"the question, reformulate your query and search again. Combine findings "
"from multiple searches to give a complete answer. Max 3 searches."
),
max_iterations=5,
),
)
# The agent will automatically perform multi-hop retrieval:
# 1. Search "distributed consensus" -> finds Raft mention
# 2. Search "Raft vs Paxos" -> finds comparison
# 3. Synthesize both into a complete answer
result = agent.run("Compare distributed consensus algorithms and their trade-offs")
Prompt Compression for Long Conversations¶
from selectools import Agent, AgentConfig, OpenAIProvider
from selectools.agent.config_groups import CompressConfig, SummarizeConfig
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
config=AgentConfig(
compress=CompressConfig(
enabled=True,
threshold_tokens=4000, # Compress when context exceeds 4k tokens
),
summarize=SummarizeConfig(
enabled=True,
max_summary_tokens=500,
trigger_after_messages=20, # Summarize every 20 messages
),
),
)
# Long conversations are automatically managed:
# - Messages are compressed when they exceed the threshold
# - Periodic summaries keep the context window manageable
for turn in range(50):
agent.run(f"Continue the analysis on topic {turn}")
# Context stays within bounds — no token limit errors
Reasoning Strategies¶
from selectools import Agent, AgentConfig, OpenAIProvider
from selectools.prompt import REASONING_STRATEGIES
# Chain-of-thought
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
config=AgentConfig(
reasoning_strategy=REASONING_STRATEGIES["chain_of_thought"],
),
)
# Step-by-step decomposition
agent = Agent(
provider=OpenAIProvider(),
tools=[...],
config=AgentConfig(
reasoning_strategy=REASONING_STRATEGIES["step_by_step"],
),
)
# The reasoning strategy is injected into the system prompt automatically.
# Use agent.trace to inspect the reasoning chain after a run.
result = agent.run("What's the optimal pricing strategy for a SaaS product?")
for step in result.trace.steps:
if step.type.name == "LLM_CALL":
print(step.summary)
Detect and stop pathological tool-call loops¶
Prevent token waste when an agent gets stuck calling the same tool repeatedly, polling without progress, or ping-ponging between tools.
from selectools import Agent, AgentConfig, LoopDetector, LoopDetectedError
from selectools.providers import OpenAIProvider
agent = Agent(
tools=[...],
provider=OpenAIProvider(),
config=AgentConfig(
max_iterations=20,
loop_detector=LoopDetector.default(), # Repeat + Stall + PingPong
),
)
try:
result = agent.run("Research this topic")
except LoopDetectedError as exc:
print(f"Agent got stuck: {exc.detector}")
print(f"Details: {exc.details}")
# Inspect trace for what led to the loop
# agent.trace is not available post-raise; use result.trace after INJECT_MESSAGE
Prefer LoopPolicy.INJECT_MESSAGE when you want the agent to self-correct instead of failing: