Skip to content

Example 4: Domain & Coordinator Agents

Wire up the full agent hierarchy with LLM-powered orchestration.

examples/bookstore/04_domain_agents.py
#!/usr/bin/env python3
"""Example 4: Domain & Coordinator Agents — LLM-powered orchestration.

Demonstrates:
- Creating DomainAgents that wrap DataAgent sub-agents
- CoordinatorAgent for cross-domain routing
- Delegation: domain agent → data agent tool execution
- The full agent hierarchy: Coordinator → Domain → Data
- How reasoning levels map to models

NOTE: This example works without an API key — it exercises the agent wiring
and delegation layer. Actual LLM calls require GOOGLE_API_KEY.
"""

from ninja_agents.base import CoordinatorAgent, DataAgent, DomainAgent
from ninja_agents.tracing import TraceContext
from ninja_core.schema.agent import ReasoningLevel

from _bookstore_schema import (
    BOOK, CUSTOMER, ORDER, REVIEW,
    CATALOG_DOMAIN, COMMERCE_DOMAIN,
)

# ---------------------------------------------------------------------------
# 1. Build the Agent Hierarchy
# ---------------------------------------------------------------------------

# Data agents (deterministic CRUD)
book_da = DataAgent(entity=BOOK)
review_da = DataAgent(entity=REVIEW)
customer_da = DataAgent(entity=CUSTOMER)
order_da = DataAgent(entity=ORDER)

# Domain agents (LLM-powered, wrapping data agents)
catalog_agent = DomainAgent(
    domain=CATALOG_DOMAIN,
    data_agents=[book_da, review_da],
)
commerce_agent = DomainAgent(
    domain=COMMERCE_DOMAIN,
    data_agents=[customer_da, order_da],
)

# Coordinator (top-level router)
coordinator = CoordinatorAgent(
    domain_agents=[catalog_agent, commerce_agent],
)

print("✅ Agent Hierarchy:")
print(f"   Coordinator: {coordinator.name}")
print(f"     ├── {catalog_agent.name} (model: {catalog_agent.agent.model})")
print(f"     │   ├── {book_da.name}")
print(f"     │   └── {review_da.name}")
print(f"     └── {commerce_agent.name} (model: {commerce_agent.agent.model})")
print(f"         ├── {customer_da.name}")
print(f"         └── {order_da.name}")

# ---------------------------------------------------------------------------
# 2. Domain Agent Delegation
# ---------------------------------------------------------------------------

print("\n--- Domain Agent Delegation ---")

# The domain agent delegates to its data agents
trace = TraceContext()

# Catalog domain: get a book
result = catalog_agent.delegate("Book", "book_get", trace=trace, id="book-001")
print(f"\n📖 catalog.delegate('Book', 'book_get'):")
print(f"   {result}")

# Catalog domain: search reviews semantically
result = catalog_agent.delegate("Review", "review_search_semantic", trace=trace, query="mind-bending sci-fi")
print(f"\n🔍 catalog.delegate('Review', 'review_search_semantic'):")
print(f"   {result}")

# Commerce domain: create an order
result = commerce_agent.delegate("Order", "order_create", trace=trace,
                                  customer_id="cust-001", total=42.50, status="pending")
print(f"\n🛒 commerce.delegate('Order', 'order_create'):")
print(f"   {result}")

# ---------------------------------------------------------------------------
# 3. Cross-Domain Routing via Coordinator
# ---------------------------------------------------------------------------

print("\n--- Coordinator Routing ---")

# Route a request to specific domains
results = coordinator.route(
    request="Show me sci-fi books and the customer's order history",
    target_domains=["Catalog", "Commerce"],
    trace=trace,
)

print(f"\n🎯 coordinator.route(target_domains=['Catalog', 'Commerce']):")
for domain_name, result in results.items():
    print(f"   {domain_name}: {result}")

# ---------------------------------------------------------------------------
# 4. Scope Enforcement
# ---------------------------------------------------------------------------

print("\n--- Scope Enforcement ---")

# Domain agents only know about their own entities
try:
    catalog_agent.delegate("Order", "order_get", id="123")
except KeyError as e:
    print(f"✅ Catalog can't access Order: {e}")

try:
    coordinator.route("test", target_domains=["Shipping"])
except Exception:
    print("✅ Coordinator rejects unknown domains")

# Check the routing result
result = coordinator.route("test", target_domains=["Shipping"])
if "Shipping" in result and "error" in result["Shipping"]:
    print(f"   → {result['Shipping']}")

# ---------------------------------------------------------------------------
# 5. Reasoning Level → Model Mapping
# ---------------------------------------------------------------------------

print("\n--- Reasoning Levels ---")

levels = {
    ReasoningLevel.NONE: "No LLM (deterministic only)",
    ReasoningLevel.LOW: "gemini-2.0-flash",
    ReasoningLevel.MEDIUM: "gemini-2.5-flash",
    ReasoningLevel.HIGH: "gemini-2.5-pro",
}

for level, desc in levels.items():
    print(f"   {level.value:8s}{desc}")

print(f"\n   Catalog agent reasoning:  {catalog_agent.config.reasoning_level.value}{catalog_agent.agent.model}")
print(f"   Commerce agent reasoning: {commerce_agent.config.reasoning_level.value}{commerce_agent.agent.model}")

# ---------------------------------------------------------------------------
# 6. Trace Summary
# ---------------------------------------------------------------------------

print(f"\n--- Trace Summary ({len(trace.spans)} spans) ---")
for span in trace.spans:
    print(f"   [{span.agent_name}] {len(span.tool_calls)} tool calls, {span.duration_ms:.1f}ms")

Run It

PYTHONPATH=examples/bookstore uv run python examples/bookstore/04_domain_agents.py