from aimu.models import OllamaClient
from aimu.agents.prebuilt import ResearchReportAgent
model_client = OllamaClient(OllamaClient.MODELS.QWEN_3_5_9B)
agent = ResearchReportAgent(model_client)
print("Orchestrator model:", model_client.model.value)
print("Worker tools:", [t.__name__ for t in model_client.tools])Prebuilt Agents
Demonstrates the orchestrator + worker tools pattern using three ready-to-use prebuilt agents from aimu.agents.prebuilt.
Each agent class wraps an orchestrator Agent that coordinates specialist worker sub-agents exposed as @aimu.tool-decorated dispatch functions. The LLM decides which workers to call, in what order, and how many times.
| Agent | Workers |
|---|---|
ResearchReportAgent |
research_overview, find_examples, find_counterpoints |
CodeReviewAgent |
review_security, review_performance, review_readability |
ContentCreationAgent |
research_topic, create_outline, write_section |
All three agents are subclasses of OrchestratorAgent (from aimu.agents), which provides the run() and messages interface and handles the tool wiring. The last section shows how to subclass OrchestratorAgent to build your own orchestrator agent.
ResearchReportAgent
An orchestrator Agent autonomously coordinates three worker sub-agents. The LLM decides which workers to call, in what order, and how many times, rather than following a predetermined code path.
research-report-agent (orchestrator)
├─ [tool] research_overview → overview-worker (Agent)
├─ [tool] find_examples → examples-worker (Agent)
└─ [tool] find_counterpoints → counterpoints-worker (Agent)
Setup
Create an OllamaClient and pass it to ResearchReportAgent. The agent internally creates fresh client instances for each worker.
Live Web Search (optional)
ResearchReportAgent accepts an optional worker_tools= list of @aimu.tool-decorated functions. When provided, each worker gets access to those tools (e.g. web search, page fetching) and the orchestrator automatically enables concurrent worker dispatch: all three workers can be called in a single round-trip when the model returns multiple tool calls at once.
# Requires SEARXNG_BASE_URL env var pointing to a SearXNG instance
from aimu.tools import builtin
agent = ResearchReportAgent(model_client, worker_tools=[builtin.web_search, builtin.get_web_content])
# Workers can now call: web_search(query, num_results), get_web_content(url).
# The orchestrator will dispatch all three workers concurrently.Without worker_tools=, workers are pure text-only agents (the default shown below).
Run
Call agent.run() with a research topic. The orchestrator will call each worker tool, then synthesize the results into a final report.
task = "What is retrieval-augmented generation?"
result = agent.run(task)
print(result)Inspect the orchestrator’s message history to see the full tool-calling sequence.
agent.messagesStreaming
run(stream=True) yields StreamChunk objects. TOOL_CALLING chunks show which worker was invoked and the worker’s response; GENERATING chunks are the orchestrator’s synthesis.
from aimu.models import StreamingContentType
task = "What is retrieval-augmented generation?"
for chunk in agent.run(task, stream=True):
if chunk.phase == StreamingContentType.TOOL_CALLING:
worker = chunk.content["name"]
response_preview = chunk.content["response"][:80].replace("\n", " ")
print(f"\n[tool: {worker}]\n {response_preview}...\n")
elif chunk.phase == StreamingContentType.GENERATING:
print(chunk.content, end="", flush=True)del agent, model_clientCodeReviewAgent
An orchestrator Agent autonomously coordinates three specialist reviewer sub-agents. The LLM decides which reviewers to engage based on the submitted code.
code-review-agent (orchestrator)
├─ [tool] review_security → security-reviewer (Agent)
├─ [tool] review_performance → performance-reviewer (Agent)
└─ [tool] review_readability → readability-reviewer (Agent)
Setup
Create an OllamaClient and pass it to CodeReviewAgent.
from aimu.models import OllamaClient
from aimu.agents.prebuilt import CodeReviewAgent
model_client = OllamaClient(OllamaClient.MODELS.QWEN_3_5_9B)
agent = CodeReviewAgent(model_client)
print("Orchestrator model:", model_client.model.value)
print("Reviewer tools:", [t.__name__ for t in model_client.tools])Sample Code
A short snippet with an SQL injection vulnerability and an O(n²) search loop: two issues that span different review dimensions.
code_snippet = """
import sqlite3
def get_user(db_path, username):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Fetch user record
cursor.execute("SELECT * FROM users WHERE name = '" + username + "'")
return cursor.fetchone()
def find_duplicates(items):
dupes = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
if items[i] == items[j] and items[i] not in dupes:
dupes.append(items[i])
return dupes
"""Run
Pass the code to agent.run(). The orchestrator will call each specialist reviewer, then synthesize findings into a prioritized report.
result = agent.run(f"Review this Python code:\n\n{code_snippet}")
print(result)Inspect the orchestrator’s message history to see each reviewer call and its response.
agent.messagesStreaming
TOOL_CALLING chunks reveal which reviewer was called and a preview of their findings; GENERATING chunks are the orchestrator’s synthesis.
from aimu.models import StreamingContentType
for chunk in agent.run(f"Review this Python code:\n\n{code_snippet}", stream=True):
if chunk.phase == StreamingContentType.TOOL_CALLING:
reviewer = chunk.content["name"]
response_preview = chunk.content["response"][:80].replace("\n", " ")
print(f"\n[tool: {reviewer}]\n {response_preview}...\n")
elif chunk.phase == StreamingContentType.GENERATING:
print(chunk.content, end="", flush=True)del agent, model_clientContentCreationAgent
An orchestrator Agent builds content step by step. The LLM determines how many sections to write and how many times to call each worker.
content-creation-agent (orchestrator)
├─ [tool] research_topic → research-worker (Agent)
├─ [tool] create_outline → outline-worker (Agent)
└─ [tool] write_section → section-writer (Agent)
Setup
Create an OllamaClient and pass it to ContentCreationAgent.
from aimu.models import OllamaClient
from aimu.agents.prebuilt import ContentCreationAgent
model_client = OllamaClient(OllamaClient.MODELS.QWEN_3_5_9B)
agent = ContentCreationAgent(model_client)
print("Orchestrator model:", model_client.model.value)
print("Worker tools:", [t.__name__ for t in model_client.tools])Run
Give the agent a content brief. It will research the topic, build an outline, write each section, then assemble the final piece.
task = "Write about the benefits of test-driven development as a blog post for software developers"
result = agent.run(task)
print(result)Inspect the orchestrator’s message history to trace the research → outline → write loop.
agent.messagesStreaming
TOOL_CALLING chunks show which worker was called (and reveal the step-by-step construction); GENERATING chunks are the orchestrator assembling the final output.
from aimu.models import StreamingContentType
task = "Write about the benefits of test-driven development as a blog post for software developers"
for chunk in agent.run(task, stream=True):
if chunk.phase == StreamingContentType.TOOL_CALLING:
worker = chunk.content["name"]
response_preview = chunk.content["response"][:80].replace("\n", " ")
print(f"\n[tool: {worker}]\n {response_preview}...\n")
elif chunk.phase == StreamingContentType.GENERATING:
print(chunk.content, end="", flush=True)Build Your Own Orchestrator Agent
All three example agents follow the same pattern because they inherit from OrchestratorAgent. There are two ways to build your own:
OrchestratorAgent.assemble(client, system_message, workers=[...]): no subclass needed. Each workerAgentis auto-wrapped as a@aimu.toolnamed after the worker, with the description taken from the worker’ssystem_message. Use this when each worker can be dispatched by passing the user’s task verbatim.Subclass
OrchestratorAgentfor full control over each dispatch tool (custom names, descriptions, signatures). The pattern:- Create worker
Agentinstances (one per specialization) - Wrap each worker’s
.run()in an@aimu.tool-decorated dispatch function - Call
self._init_orchestrator(model_client, name=..., system_message=..., tools=[...]), and that’s it
- Create worker
run() and messages are inherited from OrchestratorAgent. The next cell shows the subclass form.
from aimu.agents import Agent, OrchestratorAgent
from aimu.models import OllamaClient
from aimu.models.model_client import ModelClient
import aimu
class TranslationAgent(OrchestratorAgent):
def __init__(self, model_client):
translator = Agent(
ModelClient(model_client.model),
"Translate the given text accurately. Output only the translation.",
name="translator",
)
proofreader = Agent(
ModelClient(model_client.model),
"Review the translation for errors and return the corrected version.",
name="proofreader",
)
@aimu.tool
def translate(text_and_target_language: str) -> str:
"""Translate text to the specified target language."""
return translator.run(text_and_target_language)
@aimu.tool
def proofread(translated_text: str) -> str:
"""Review and correct a translation for accuracy and naturalness."""
return proofreader.run(translated_text)
self._init_orchestrator(
model_client,
name="translation-agent",
system_message=(
"You are a translation coordinator. "
"First use the translate tool to translate the text, "
"then use the proofread tool to check the result. "
"Return the final proofread translation."
),
tools=[translate, proofread],
)
model_client = OllamaClient(OllamaClient.MODELS.QWEN_3_5_9B)
agent = TranslationAgent(model_client)
result = agent.run("Translate to French: 'The quick brown fox jumps over the lazy dog'")
print(result)Clean Up
del agent, model_client