## Security Best Practices
### Domain Allowlisting
```bash theme={null}
# Permissive for testing
MCP_ALLOWED_DOMAINS=*
OPENAPI_ALLOWED_DOMAINS=*
```
```bash theme={null}
# Specific domains + localhost
MCP_ALLOWED_DOMAINS=localhost,127.0.0.1,staging-api.example.com
```
```bash theme={null}
# Explicit allowlist only
MCP_ALLOWED_DOMAINS=api.example.com,api.partner.com
OPENAPI_ALLOWED_DOMAINS=api.github.com,api.openweathermap.org
```
### API Key Management
Never hardcode API keys in configuration files!
**❌ Bad:**
```yaml theme={null}
headers:
X-API-Key: "sk-1234567890abcdef"
```
**✅ Good:**
```yaml theme={null}
headers:
X-API-Key: "${WEATHER_API_KEY}"
```
**Store in `.env` (not tracked by git):**
```bash theme={null}
# .env
WEATHER_API_KEY=sk-real-key-here
STOCK_API_KEY=your-stock-key
```
**For production:** Use secrets management
* Docker secrets
* Kubernetes secrets
* HashiCorp Vault
* AWS Secrets Manager
### Dangerous Tools
Mark tools that modify state or access sensitive resources:
```python theme={null}
ToolMetadata(
name="file_write",
dangerous=True, # Triggers OPA policy checks
requires_auth=True, # Requires user authentication
...
)
```
## Vendor Adapter Pattern
**For domain-specific APIs and custom agents**
When integrating proprietary or internal APIs that require domain-specific transformations, use the **vendor adapter pattern** to keep vendor logic separate from Shannon's core infrastructure.
### When to Use
Use vendor adapters when your API integration requires:
* Custom field name aliasing (e.g., `users` → `my:unique_users`)
* Request/response transformations
* Dynamic parameter injection from session context
* Domain-specific validation or normalization
* Specialized agent roles with custom system prompts
### Quick Example
`python/llm-service/llm_service/tools/vendor_adapters/myvendor.py`:
```python theme={null}
class MyVendorAdapter:
def transform_body(self, body, operation_id, prompt_params):
# Transform field names
if isinstance(body.get("metrics"), list):
body["metrics"] = [m.replace("users", "my:users") for m in body["metrics"]]
# Inject session params
if prompt_params and "account_id" in prompt_params:
body["account_id"] = prompt_params["account_id"]
return body
```
`python/llm-service/llm_service/tools/vendor_adapters/__init__.py`:
```python theme={null}
def get_vendor_adapter(name: str):
if name.lower() == "myvendor":
from .myvendor import MyVendorAdapter
return MyVendorAdapter()
return None
```
`config/overlays/shannon.myvendor.yaml`:
```yaml theme={null}
openapi_tools:
myvendor_api:
enabled: true
spec_url: file:///app/config/openapi_specs/myvendor_api.yaml
auth_type: bearer
auth_config:
vendor: myvendor # Triggers adapter loading
token: "${MYVENDOR_API_TOKEN}"
category: custom
```
```bash theme={null}
SHANNON_CONFIG_PATH=config/overlays/shannon.myvendor.yaml
MYVENDOR_API_TOKEN=your_token_here
```
### Benefits
* ✅ **Clean separation:** Vendor code isolated from Shannon core
* ✅ **No core changes:** Shannon infrastructure remains generic
* ✅ **Conditional loading:** Graceful fallback if vendor module unavailable
* ✅ **Easy testing:** Unit test adapters in isolation
* ✅ **Secrets management:** All tokens via environment variables
### Complete Guide
For a comprehensive guide including:
* Custom agent roles for specialized domains
* Session context injection patterns
* Testing strategies
* Best practices and troubleshooting
Learn how to build vendor-specific integrations with the adapter pattern
## Summary
**Three ways to add tools:**
| Method | Command | Config File | Code Changes |
| ------------ | --------------------------------------------------- | ------------------------- | ------------ |
| **MCP** | `docker compose up -d --force-recreate llm-service` | `config/shannon.yaml` | None |
| **OpenAPI** | `docker compose up -d --force-recreate llm-service` | `config/shannon.yaml` | None |
| **Built-in** | `docker compose up -d --force-recreate llm-service` | `api/tools.py` + new file | Python only |
**Key takeaways:**
* ✅ Zero proto/Rust/Go changes (generic `google.protobuf.Struct` containers)
* ✅ Security built-in (domain allowlisting, rate limiting, circuit breakers)
* ✅ Cost tracking automatic (set `cost_per_use` in metadata)
* ✅ Schema-driven (OpenAI-compatible JSON schemas)
## Next Steps
Build vendor-specific integrations
Explore all extension methods
Complete configuration reference
Explore the REST API
# Extending Shannon
Source: https://docs.shannon.run/en/tutorials/extending-shannon
Simple, supported ways to extend Shannon without forking large subsystems
## Overview
This guide outlines extensible patterns for customizing Shannon while maintaining upgrade compatibility and clean separation of concerns.
System 1 - Pre-built workflows with low overhead
Add capabilities via MCP, OpenAPI, or Python
Domain-specific integrations without core changes
Customize research output formatting
## Extension Methods Comparison
| Extension Type | Complexity | Code Changes | Use Case |
| --------------------- | ---------- | --------------- | ---------------------------- |
| **Templates** | Low | YAML only | Repeatable workflows |
| **MCP/OpenAPI Tools** | Low | Config only | External APIs |
| **Built-in Tools** | Medium | Python only | Custom logic |
| **Vendor Adapters** | Medium | Python + Config | Domain-specific integrations |
| **Decomposition** | High | Go + Python | Custom planning logic |
For most use cases, **Templates** and **Vendor Adapters** provide the best balance of power and simplicity.
## Extend Decomposition (System 2)
**For custom planning and reasoning logic**
The orchestrator calls the LLM service endpoint `/agent/decompose` for planning.
### When to Use
* Custom task decomposition strategies
* Domain-specific planning heuristics
* Pre/post-processing of LLM requests
* Integration with external planning systems
### Implementation Options
**Best for:** Pre/post-processing LLM requests
Add heuristics to `go/orchestrator/internal/activities/decompose.go`:
```go theme={null}
// Example: Pre-process query before decomposition
func PreprocessQuery(query string) string {
// Add domain-specific context
// Normalize input format
// Inject additional constraints
return enhancedQuery
}
// Example: Post-process decomposition response
func PostprocessDecomposition(resp *DecompositionResponse) {
// Validate subtasks
// Add fallback steps
// Optimize execution order
}
```
**Best for:** Complete custom planning logic
Add new endpoint in `python/llm-service/llm_service/api/agent.py`:
```python theme={null}
@router.post("/agent/decompose/custom")
async def custom_decompose(request: DecomposeRequest):
"""Custom decomposition logic for domain-specific tasks."""
# Your custom planning logic
subtasks = custom_planning_algorithm(request.query)
# Return compatible response
return DecompositionResponse(
subtasks=subtasks,
pattern="custom",
complexity=calculate_complexity(subtasks)
)
```
Route via feature flag or context key:
```go theme={null}
// In orchestrator
if ctx.CustomDecomposition {
endpoint = "/agent/decompose/custom"
} else {
endpoint = "/agent/decompose"
}
```
Keep response schema compatible with `DecompositionResponse` to avoid breaking orchestrator workflows.
## Add/Customize Templates (System 1)
**For repeatable workflows with low overhead**
### When to Use
* Predefined workflows (data analysis, code review, etc.)
* Quick task execution without AI planning
* Common patterns used frequently
* Performance-critical paths
### Creating Templates
Place templates in your own directory:
```yaml theme={null}
# templates/custom/research-workflow.yaml
name: "research_workflow"
description: "Multi-stage research and analysis workflow"
version: "1.0.0"
extends: [] # Optional: inherit from other templates
defaults:
model_tier: "medium"
budget_agent_max: 5000
require_approval: false
nodes:
- id: "search"
type: "simple"
strategy: "react"
tools_allowlist: ["web_search", "calculator"]
budget_max: 1000
depends_on: []
- id: "analyze"
type: "cognitive"
strategy: "chain_of_thought"
tools_allowlist: ["python_executor", "calculator"]
budget_max: 2000
depends_on: ["search"]
- id: "synthesize"
type: "simple"
strategy: "react"
tools_allowlist: ["web_search"]
budget_max: 1000
depends_on: ["analyze"]
edges:
- from: "search"
to: "analyze"
- from: "analyze"
to: "synthesize"
metadata:
category: "research"
author: "your-team"
```
Initialize registry with your template directory:
```go theme={null}
// In orchestrator initialization
registry := templates.InitTemplateRegistry(
"./templates/builtin",
"./templates/custom", // Your custom templates
)
// Validate all templates
if err := registry.Finalize(); err != nil {
log.Fatal(err)
}
```
Via gRPC API:
```bash theme={null}
grpcurl -plaintext -d '{
"query": "Perform research on AI trends",
"context": {
"template": "research_workflow",
"template_version": "1.0.0",
"disable_ai": true
}
}' localhost:50052 shannon.orchestrator.OrchestratorService/SubmitTask
```
Via HTTP Gateway:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Perform research on AI trends",
"context": {
"template": "research_workflow",
"template_version": "1.0.0"
}
}'
```
```bash theme={null}
# Via gRPC
grpcurl -plaintext -d '{}' localhost:50052 \
shannon.orchestrator.OrchestratorService/ListTemplates
```
Note: HTTP Gateway template listing endpoint may not be implemented yet. Use gRPC for template discovery.
### Template Best Practices
```yaml theme={null}
# Base template (base.yaml)
name: "base_research"
version: "1.0.0"
defaults:
model_tier: "medium"
budget_agent_max: 5000
require_approval: false
# Child template (advanced_research.yaml)
name: "advanced_research"
version: "1.0.0"
extends: ["base_research"] # Inherits all defaults
defaults:
budget_agent_max: 10000 # Override specific values
```
```go theme={null}
registry := templates.InitTemplateRegistry("./templates")
// Validates:
// - YAML syntax
// - Required fields
// - Parameter types
// - Tool references
if err := registry.Finalize(); err != nil {
log.Fatalf("Template validation failed: %v", err)
}
```
```yaml theme={null}
# Explicitly list allowed tools for security
tools_allowlist:
- "web_search"
- "calculator"
- "python_executor"
# Don't use:
# tools_allowlist: ["*"] # Too permissive
```
## Add Tools Safely
**For extending Shannon's capabilities**
Shannon supports three tool integration methods:
External HTTP APIs with zero code changes
Auto-generated from OpenAPI specs
Python tools for complex logic
### Security Considerations
Always use `tools_allowlist` in templates to restrict which tools can be used.
**Good:**
```yaml theme={null}
# Template with restricted tools
tools_allowlist:
- "web_search"
- "calculator"
- "my_custom_tool"
```
**Bad:**
```yaml theme={null}
# Don't allow all tools
tools_allowlist: ["*"]
```
### Keep Experimental Tools Behind Flags
```yaml theme={null}
# config/features.yaml
experimental_tools:
enabled: false # Disabled by default
custom_analytics:
enabled: ${ENABLE_CUSTOM_ANALYTICS} # Env-based toggle
```
```python theme={null}
# In tool registration
if config.get("experimental_tools.enabled"):
registry.register(ExperimentalTool)
```
See the complete guide for adding MCP, OpenAPI, and built-in Python tools
## Vendor Extensions
**For domain-specific agents and API integrations**
The vendor adapter pattern allows you to integrate proprietary APIs and specialized agents without modifying Shannon's core code.
### Architecture
```
Generic Shannon (Open Source)
├── python/llm-service/llm_service/tools/openapi_tool.py # Generic loader
├── python/llm-service/llm_service/roles/presets.py # Generic roles
├── go/orchestrator/internal/activities/agent.go # Generic mirroring
└── config/shannon.yaml # Base config
Vendor Extensions (Private)
├── config/overlays/shannon.vendor.yaml # Vendor configs
├── config/openapi_specs/vendor_api.yaml # API specs
├── python/llm-service/llm_service/tools/vendor_adapters/ # Transformations
│ ├── __init__.py # Registry
│ └── vendor.py # VendorAdapter
└── python/llm-service/llm_service/roles/vendor/ # Agent roles
├── __init__.py
└── custom_agent.py # System prompts
```
### When to Use Vendor Extensions
Use when you need:
* Domain-specific API integrations (analytics, CRM, e-commerce)
* Custom field name transformations
* Specialized agent roles with domain knowledge
* Session context injection (account IDs, tenant IDs)
* Private/proprietary tool configurations
### Quick Start
```python theme={null}
# python/llm-service/llm_service/tools/vendor_adapters/myvendor.py
class MyVendorAdapter:
def transform_body(self, body, operation_id, prompt_params):
# Field aliasing
if "metrics" in body:
body["metrics"] = [m.replace("users", "mv:users") for m in body["metrics"]]
# Inject session context
if prompt_params:
body.update(prompt_params)
return body
```
```python theme={null}
# python/llm-service/llm_service/tools/vendor_adapters/__init__.py
def get_vendor_adapter(name: str):
if name.lower() == "myvendor":
from .myvendor import MyVendorAdapter
return MyVendorAdapter()
return None
```
```yaml theme={null}
# config/overlays/shannon.myvendor.yaml
openapi_tools:
myvendor_api:
enabled: true
spec_url: file:///app/config/openapi_specs/myvendor_api.yaml
auth_type: bearer
auth_config:
vendor: myvendor # Triggers adapter loading
token: "${MYVENDOR_API_TOKEN}"
category: custom
```
```python theme={null}
# python/llm-service/llm_service/roles/myvendor/custom_agent.py
CUSTOM_AGENT_PRESET = {
"name": "myvendor_agent",
"system_prompt": "You are a specialized agent for...",
"allowed_tools": ["myvendor_query", "myvendor_analyze"],
"temperature": 0.7,
}
```
Register with graceful fallback:
```python theme={null}
# roles/presets.py
try:
from .myvendor.custom_agent import CUSTOM_AGENT_PRESET
_PRESETS["myvendor_agent"] = CUSTOM_AGENT_PRESET
except ImportError:
pass # Shannon works without vendor module
```
```bash theme={null}
SHANNON_CONFIG_PATH=config/overlays/shannon.myvendor.yaml
MYVENDOR_API_TOKEN=your_token_here
```
### Benefits
* ✅ **Zero Shannon core changes** - All vendor logic isolated
* ✅ **Clean separation** - Generic infrastructure vs. vendor-specific
* ✅ **Conditional loading** - Graceful fallback if vendor module unavailable
* ✅ **Easy to maintain** - Vendor code in separate directories
* ✅ **Testable in isolation** - Unit test adapters independently
Comprehensive guide with examples, testing strategies, and best practices
## Human Approval
**For gating sensitive operations**
Wire `require_approval` through the SubmitTask request for human-in-the-loop control.
### Configuration
```yaml theme={null}
# config/features.yaml
approvals:
enabled: true
dangerous_tools:
- "file_write"
- "code_execution"
- "database_query"
complexity_threshold: 0.7 # Require approval for complex tasks
timeout_seconds: 1800 # 30 minute approval window
```
### API Usage
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Delete all records from users table",
"require_approval": true
}'
```
### Approval Flow
1. Task submitted with `require_approval: true`
2. Orchestrator pauses before execution
3. Approval request sent via webhook/UI
4. User approves/rejects via API
5. Workflow continues or terminates
**Gateway endpoint (recommended):**
```bash theme={null}
curl -X POST "http://localhost:8080/api/v1/approvals/decision" -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" -d '{
"workflow_id": "",
"approval_id": "",
"approved": true,
"feedback": "Approved for execution"
}'
```
Legacy admin endpoint at `http://localhost:8081/approvals/decision` is deprecated. Use the gateway endpoint instead.
**Approval gates are enforced in the router before execution.**
## Feature Flags & Config
**Runtime configuration without code changes**
Many behaviors are controlled via `config/features.yaml` and environment variables, loaded through `GetWorkflowConfig`.
### Common Feature Flags
```yaml theme={null}
# config/features.yaml
# Template Fallback
template_fallback:
enabled: true # Fallback to AI if template fails
# Tool Selection
tool_selection:
enabled: true # Auto-select tools based on task
max_tools: 5 # Maximum tools per task
# Cognitive Patterns
patterns:
cot_enabled: true # Chain-of-Thought
tot_enabled: true # Tree-of-Thoughts
react_enabled: true # ReAct
# Budget Controls
budgets:
enforce_token_limits: true
enforce_cost_limits: true
default_max_tokens: 10000
default_max_cost_usd: 0.50
# Streaming
streaming:
enabled: true
buffer_size: 1000
chunk_size: 512
```
### Environment Variable Override
```bash theme={null}
# Override via environment
TEMPLATE_FALLBACK_ENABLED=1
TOOL_SELECTION_MAX_TOOLS=10
DEFAULT_MAX_TOKENS=20000
# Start services with overrides
docker compose up -d
```
### Dynamic Config Loading
```go theme={null}
// In orchestrator
config := GetWorkflowConfig()
if config.TemplateFallbackEnabled {
// Use AI fallback
}
if config.BudgetsEnforceTokenLimits {
// Apply token budget
}
```
## Synthesis Templates (Output Customization)
**For customizing how Shannon formats final research answers**
Synthesis templates control how multi-agent research results are formatted and presented. They're particularly useful for Deep Research workflows.
### When to Use
* Customize output format for specific domains (market research, academic, executive summaries)
* Enforce citation styles
* Control answer structure and length
* Inject domain-specific formatting rules
### Template Methods
| Method | Context Parameter | Use Case |
| ----------------- | ------------------------------------ | ------------------------------- |
| Named template | `synthesis_template: "name"` | Reusable `.tmpl` files |
| Verbatim override | `synthesis_template_override: "..."` | One-time custom format |
| Min length | `synthesis_min_length: N` | Enforce minimum character count |
### Using Named Templates
Create templates in `config/templates/synthesis/`:
```go theme={null}
{{/* config/templates/synthesis/market_research.tmpl */}}
{{template "_base.tmpl"}}
## Executive Summary
Provide a 2-3 sentence summary of key findings.
## Market Analysis
- Key trends
- Competitive landscape
- Opportunities and risks
## Data Sources
Use [n] citation format for all claims.
## Recommendations
Actionable insights based on the analysis.
```
Use via API:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze the electric vehicle market in 2025",
"context": {
"force_research": true,
"synthesis_template": "market_research"
}
}'
```
### Verbatim Override
For one-time custom formatting without creating a template file:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare major cloud providers",
"context": {
"force_research": true,
"synthesis_template_override": "You are a cloud infrastructure analyst.\n\n## Analysis Format\n- Start with a comparison table\n- Use [n] citations\n- Include cost analysis section\n- End with recommendations"
}
}'
```
When using `synthesis_template_override`, you bypass the base template's citation contract. You must include citation rules (`[n]` format) in your override text.
### Minimum Length Control
Enforce a minimum output length:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Comprehensive analysis of AI governance",
"context": {
"force_research": true,
"synthesis_min_length": 5000
}
}'
```
### Template Selection Logic
Selection is based on context and workflow signals:
1. If `context.synthesis_template` is set → use that named template.
2. Else if any of:
* `context.workflow_type == "research"`
* `context.force_research == true`
* `context.synthesis_style == "comprehensive"`
* `context.research_areas` is non-empty
→ use `research_comprehensive.tmpl`.
3. Else if `context.synthesis_style == "concise"` → use `research_concise.tmpl`.
4. Otherwise → use `normal_default.tmpl`.
### Available Templates
| Template | Description |
| ----------------------------- | ---------------------------------------------------------- |
| `_base.tmpl` | Protected base with citation rules |
| `normal_default.tmpl` | Default non-research synthesis (minimal structure) |
| `research_comprehensive.tmpl` | Detailed research format with strong coverage requirements |
| `research_concise.tmpl` | Shorter research synthesis with lighter word requirements |
| `research_with_facts.tmpl` | Research format optimized for fact extraction metadata |
| `test_bullet_summary.tmpl` | Example bullet-point summary template |
### Best Practices
1. **Always extend `_base.tmpl`** - Ensures citation contract is maintained
2. **Use named templates** for recurring formats
3. **Use override** for one-off customizations
4. **Test templates** with sample queries before production use
Templates are located in `config/templates/synthesis/`. See `README.md` in that directory for template authoring guidelines.
## Best Practices Summary
* Generic infrastructure: Committed to open source
* Vendor-specific code: Kept private in separate directories
* Configuration overlays: Domain-specific settings isolated
* Conditional imports: Graceful fallback for optional modules
* Use stable interfaces (ToolRegistry, TemplateRegistry, etc.)
* Avoid forking core subsystems
* Keep customizations in separate directories
* Use feature flags for experimental changes
* Allowlist tools in templates
* Enable approvals for dangerous operations
* Use domain allowlisting for external APIs
* Keep secrets in environment variables
* Unit test vendor adapters in isolation
* Integration test with Shannon services
* Use replay testing for workflow determinism
* Validate templates with `registry.Finalize()`
## Extension Decision Tree
## Next Steps
Add MCP, OpenAPI, and built-in tools
Build domain-specific integrations
Complete configuration reference
Understanding Shannon's architecture
# Human-in-the-Loop Review
Source: https://docs.shannon.run/en/tutorials/hitl-review
Use Shannon's HITL review system to review, refine, and approve AI-generated research plans before execution.
# Human-in-the-Loop Review
Shannon's Human-in-the-Loop (HITL) review system lets you **review and refine an AI-generated research plan** before the workflow begins executing. This is particularly valuable for deep research tasks where you want to steer the research direction, add constraints, or ensure the agent focuses on what matters most.
## What You'll Learn
* How the HITL review cycle works (plan generation, feedback, approval)
* Enabling HITL review on task submission
* Interacting with the review API (get state, send feedback, approve)
* Optimistic concurrency control with version tracking
* SSE events emitted during the review cycle
* Python SDK usage for HITL review
* Configuration options and timeouts
## Prerequisites
* Shannon stack running (Docker Compose)
* Gateway reachable at `http://localhost:8080`
* Auth defaults:
* Docker Compose: authentication is disabled by default (`GATEWAY_SKIP_AUTH=1`).
* Local builds: authentication is enabled by default. Set `GATEWAY_SKIP_AUTH=1` to disable auth, or include an API key header `-H "X-API-Key: $API_KEY"`.
## How HITL Review Works
The HITL review cycle inserts a human checkpoint between task submission and research execution:
Submit a research task with `require_review: true` (or `review_plan: "manual"`) in the context.
The LLM service generates a research plan based on the query. The workflow pauses and waits for human input.
You review the proposed plan via the review API. Send feedback to refine the plan iteratively (up to 10 rounds).
Once satisfied, approve the plan. The workflow resumes and executes the confirmed research direction.
The ResearchWorkflow runs with the approved plan injected as context, producing a focused, citation-backed report.
### Architecture
```
User Gateway LLM Service Orchestrator (Temporal)
| | | |
|-- POST /tasks -------->| | |
| (require_review) |-- gRPC SubmitTask ---->| |
| | |-- GenerateResearchPlan |
| | | (Activity) |
| | Redis state init <----| |
| | | |
| SSE: RESEARCH_PLAN_READY | workflow.Select() |
|<-----------------------------------------------| (waiting for signal) |
| | | |
|-- GET /review -------->| (read Redis state) | |
|<-- plan + version -----| | |
| | | |
|-- POST /review ------->| feedback | |
| (action: feedback) |-- call /research-plan->| |
| |<-- updated plan -------| |
|<-- new plan + version -| | |
| | | |
|-- POST /review ------->| | |
| (action: approve) |-- gRPC Signal -------->| (unblock workflow) |
|<-- approved ----------| | |
| | |-- ResearchWorkflow --->|
```
## Enabling HITL Review
Add `require_review: true` to the task context when submitting a research task:
```bash curl theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Research the competitive landscape of AI agent frameworks in 2025",
"context": {
"force_research": true,
"require_review": true
}
}'
```
```bash curl (stream endpoint) theme={null}
curl -s -X POST http://localhost:8080/api/v1/tasks/stream \
-H "Content-Type: application/json" \
-d '{
"query": "Research the competitive landscape of AI agent frameworks in 2025",
"context": {
"force_research": true,
"require_review": true
}
}' | jq
```
```python Python SDK theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
handle = client.submit_task(
"Research the competitive landscape of AI agent frameworks in 2025",
context={
"force_research": True,
"require_review": True,
},
)
```
HITL review requires `force_research: true` because the review cycle is part of the ResearchWorkflow. Without it, the orchestrator may route the task to a different workflow that does not support review.
The alternative context key `review_plan: "manual"` is also accepted and behaves identically. The desktop app uses `require_review: true` when the user disables auto-approve for deep research.
## Retrieving Review State
After submission, poll for the review plan to become ready. The workflow generates an initial plan via the LLM and stores it in Redis.
```bash curl theme={null}
# Get current review state
curl -s http://localhost:8080/api/v1/tasks/{workflow_id}/review \
-H "X-API-Key: $API_KEY" | jq
```
```python Python SDK theme={null}
state = client.get_review_state(handle.workflow_id)
print(f"Status: {state.status}")
print(f"Round: {state.round}, Version: {state.version}")
print(f"Plan: {state.current_plan}")
for r in state.rounds:
print(f" [{r.role}] {r.message[:100]}...")
```
### Response
```json theme={null}
{
"status": "reviewing",
"round": 1,
"version": 1,
"current_plan": "Based on your query, I propose the following research plan...",
"rounds": [
{
"role": "assistant",
"message": "Based on your query, I propose the following research plan...",
"timestamp": "2025-01-15T10:30:00Z"
}
],
"query": "Research the competitive landscape of AI agent frameworks in 2025"
}
```
The response includes an `ETag` header containing the current version number, used for optimistic concurrency control.
| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------------- |
| `status` | string | `"reviewing"` or `"approved"` |
| `round` | int | Current conversation round (starts at 1) |
| `version` | int | Monotonic version for concurrency control |
| `current_plan` | string | The latest actionable plan (set when LLM intent is `"ready"`) |
| `rounds` | array | Full conversation history (role, message, timestamp) |
| `query` | string | Original task query |
## Sending Feedback
Refine the plan by sending feedback. The gateway forwards your message to the LLM, which generates an updated plan incorporating your input.
```bash curl theme={null}
curl -X POST http://localhost:8080/api/v1/tasks/{workflow_id}/review \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "If-Match: 1" \
-d '{
"action": "feedback",
"message": "Focus more on open-source frameworks and include pricing comparisons"
}'
```
```python Python SDK theme={null}
updated = client.submit_review_feedback(
handle.workflow_id,
"Focus more on open-source frameworks and include pricing comparisons",
version=state.version, # optimistic concurrency
)
print(f"New version: {updated.version}")
print(f"Updated plan: {updated.current_plan}")
```
### Feedback Response
```json theme={null}
{
"status": "reviewing",
"plan": {
"message": "I've refined the research plan to focus on open-source frameworks...",
"round": 2,
"version": 2,
"intent": "ready"
}
}
```
The `intent` field indicates the LLM's assessment:
* `"feedback"` -- the LLM is asking clarifying questions (no actionable plan yet)
* `"ready"` -- the LLM has proposed an actionable research direction
The response includes an updated `ETag` header.
### Concurrency Control
The `If-Match` header (curl) or `version` parameter (SDK) enables optimistic concurrency. If another request modified the state since you last read it, the server returns `409 Conflict`:
```json theme={null}
{ "error": "Conflict: state has been modified" }
```
A distributed Redis lock also prevents two feedback requests from racing during the LLM call.
### Round Limits
A maximum of **10 feedback rounds** is enforced. At the final round, the LLM is instructed to produce a definitive plan. Beyond this limit, further feedback is rejected and you must approve:
```json theme={null}
{ "error": "Maximum review rounds reached. Please approve the plan." }
```
## Approving the Plan
Once the plan looks good, approve it to resume the workflow:
```bash curl theme={null}
curl -X POST http://localhost:8080/api/v1/tasks/{workflow_id}/review \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "If-Match: 2" \
-d '{"action": "approve"}'
```
```python Python SDK theme={null}
result = client.approve_review(handle.workflow_id, version=updated.version)
print(result) # {"status": "approved", "message": "Research started"}
```
### Approval Response
```json theme={null}
{
"status": "approved",
"message": "Research started"
}
```
After approval:
1. The gateway sends a Temporal Signal to the waiting workflow
2. The confirmed plan and review conversation are injected into the task context
3. The ResearchWorkflow proceeds with the approved research direction
4. SSE emits `RESEARCH_PLAN_APPROVED`
You cannot approve a plan if `current_plan` is empty. The LLM must have produced at least one plan with intent `"ready"` before approval is possible. If you try to approve without a plan, the server returns:
```json theme={null}
{ "error": "No research plan to approve. Please provide feedback to generate a plan first." }
```
## SSE Events
The HITL review cycle emits dedicated SSE events that you can consume via the streaming endpoint:
```bash theme={null}
curl -N "http://localhost:8080/api/v1/stream/sse?workflow_id={workflow_id}"
```
| Event Type | Description | Payload |
| ------------------------ | ------------------------------------------ | ---------------------------- |
| `RESEARCH_PLAN_READY` | Initial plan generated, waiting for review | `round`, `intent` |
| `REVIEW_USER_FEEDBACK` | User feedback submitted | `round`, `version` |
| `RESEARCH_PLAN_UPDATED` | Plan updated after feedback | `round`, `version`, `intent` |
| `RESEARCH_PLAN_APPROVED` | Plan approved, research starting | -- |
These events are published to the Redis event stream, making the review conversation visible in session history and on page reload.
## Configuration
### Review Timeout
The workflow waits up to **15 minutes** (default) for the review to complete. If no approval is received within this window, the workflow times out:
```json theme={null}
{
"status": "TASK_STATUS_COMPLETED",
"result": "",
"error_message": "research plan review timed out"
}
```
You can customize the timeout via the `review_timeout` context parameter (in seconds):
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "...",
"context": {
"force_research": true,
"require_review": true,
"review_timeout": 1800
}
}'
```
### Approval Workflow (separate feature)
Shannon also has a separate **approval workflow** for non-research tasks. This is configured in `features.yaml` and triggers based on complexity thresholds or dangerous tool usage:
```yaml theme={null}
# config/features.yaml
workflows:
approval:
enabled: false # Enable approval gate
complexity_threshold: 0.5 # Complexity >= this triggers approval
dangerous_tools:
- file_system
- code_execution
- bash
```
The approval workflow uses a different endpoint (`POST /api/v1/approvals/decision`) and is independent of the HITL research review. See the [Approve Task API reference](/api/rest/approve-task) for details.
## Python SDK Reference
The Shannon Python SDK provides dedicated methods for the HITL review cycle:
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
# 1. Submit with review enabled
handle = client.submit_task(
"Analyze the market for AI coding assistants",
context={"force_research": True, "require_review": True},
)
# 2. Wait for plan to be ready (poll)
import time
state = None
for _ in range(30):
try:
state = client.get_review_state(handle.workflow_id)
if state.status == "reviewing":
break
except Exception:
pass
time.sleep(2)
print(f"Initial plan (round {state.round}):")
print(state.current_plan or state.rounds[-1].message)
# 3. Send feedback
updated = client.submit_review_feedback(
handle.workflow_id,
"Include Cursor, GitHub Copilot, and Windsurf. Compare pricing tiers.",
version=state.version,
)
print(f"Refined plan (round {updated.round}):")
print(updated.current_plan)
# 4. Approve
result = client.approve_review(handle.workflow_id, version=updated.version)
print(result)
# 5. Wait for research completion
final = client.wait(handle.task_id, timeout=600)
print(final.result)
client.close()
```
### Async SDK
```python theme={null}
from shannon import AsyncShannonClient
async def review_workflow():
client = AsyncShannonClient(base_url="http://localhost:8080")
handle = await client.submit_task(
"Analyze AI agent frameworks",
context={"force_research": True, "require_review": True},
)
# Poll for plan
state = await client.get_review_state(handle.workflow_id)
# Send feedback
updated = await client.submit_review_feedback(
handle.workflow_id,
"Focus on LangGraph vs CrewAI",
version=state.version,
)
# Approve
await client.approve_review(handle.workflow_id, version=updated.version)
await client.close()
```
## Best Practices
Always pass the `If-Match` header (or `version` parameter in the SDK) to prevent race conditions. This is especially important when multiple users or tabs may interact with the same review.
The default 15-minute timeout works for most interactive use cases. For asynchronous review workflows (e.g., email-based approval), increase `review_timeout` accordingly.
Before approving, ensure the LLM has produced a plan with intent `"ready"`. Feedback rounds with intent `"feedback"` indicate the LLM needs more information.
HITL review works with all research strategy presets (`quick`, `standard`, `deep`, `academic`). The approved plan guides the subsequent research execution.
## Troubleshooting
The review state is stored in Redis with a TTL of 20 minutes (review timeout + 5-minute buffer). If the review session expires, you will receive a 404 error. Re-submit the task to start a new review.
This means another request modified the review state since you last read it, or another feedback request is currently in progress. Re-fetch the current state with `GET /review`, then retry with the latest version.
The LLM has only asked clarifying questions (intent: `"feedback"`) and has not yet produced an actionable plan. Send at least one feedback message so the LLM generates a concrete research direction.
The review took longer than the configured timeout (default: 15 minutes). Increase `review_timeout` in the task context, or approve more quickly.
## Next Steps
Build comprehensive research reports
Approval workflow for non-research tasks
Complete API reference for review endpoints
# Tutorials
Source: https://docs.shannon.run/en/tutorials/index
Learn Shannon through hands-on examples and practical guides
## Welcome to Shannon Tutorials
Learn how to build powerful AI agent applications with Shannon through practical, hands-on tutorials. Each tutorial includes complete working code, step-by-step instructions, and best practices.
This section is actively being developed. New tutorials are added regularly based on community feedback and common use cases.
## Available Tutorials
### Building Agents
Build a multi-source research agent with memory, citations, verification, and adaptive patterns
**Status**: ✅ Available
### Extension & Integration
Complete guide to extending Shannon with MCP, OpenAPI, and built-in Python tools
**Status**: ✅ Available
Build vendor-specific integrations with the adapter pattern for domain-specific APIs
**Status**: ✅ Available
Create deterministic, zero-token workflows using YAML templates with DAG support
**Status**: ✅ Available
Explore all extension methods: templates, tools, vendor adapters, and custom decomposition
**Status**: ✅ Available
## What You'll Learn
Across these tutorials, you'll learn:
* **Agent Design**: How to structure agent tasks and workflows
* **Tool Integration**: Connecting agents to external APIs and services
* **Session Management**: Building stateful, multi-turn conversations
* **Error Handling**: Robust patterns for production agents
* **Cost Optimization**: Budget management and token efficiency
* **Monitoring**: Tracking agent performance and behavior
## Prerequisites
Before starting these tutorials, make sure you have:
1. **Shannon Installed**: Follow the [Installation Guide](/en/quickstart/installation)
2. **Basic Python Knowledge**: Familiarity with Python 3.9+
3. **API Keys**: At least one LLM provider API key (OpenAI, Anthropic, etc.)
4. **Docker**: For running Shannon services locally
## Tutorial Structure
Each tutorial follows a consistent structure:
1. **Overview**: What you'll build and why
2. **Prerequisites**: Required setup and dependencies
3. **Step-by-Step**: Detailed implementation guide
4. **Code Examples**: Complete working code you can run
5. **Testing**: How to verify your implementation
6. **Next Steps**: Ideas for extending the tutorial
## Contributing Tutorials
Have an idea for a tutorial? We welcome community contributions!
1. Check existing [GitHub issues](https://github.com/Kocoro-lab/Shannon/issues?q=label%3Atutorial)
2. Propose new tutorials in [GitHub Discussions](https://github.com/Kocoro-lab/Shannon/discussions)
3. Submit tutorial PRs following our [contribution guidelines](https://github.com/Kocoro-lab/Shannon/blob/main/CONTRIBUTING.md)
## Get Help
If you get stuck on any tutorial:
* Search [GitHub Discussions](https://github.com/Kocoro-lab/Shannon/discussions) for similar questions
* File an issue if you find bugs or unclear instructions
## What's Next?
New to Shannon? Start here
Understand Shannon's fundamentals
Explore the REST API
Use the SDK in your applications
# Model Selection & Routing
Source: https://docs.shannon.run/en/tutorials/model-selection
How Shannon selects models based on tiers, priority, and overrides
## Overview
Shannon automatically selects the optimal LLM model for each task based on:
1. **Task complexity** (analyzed during decomposition)
2. **Explicit tier requests** (`model_tier` parameter)
3. **Model/provider overrides** (`model_override`, `provider_override`)
4. **Priority rankings** (defined in `config/models.yaml`)
5. **Budget constraints** and token limits
This guide explains how model selection works and how to control it.
## Model Tiers
Shannon organizes models into three tiers:
| Tier | Target Usage | Characteristics | Cost Range |
| ---------- | ------------ | ------------------------------------- | ------------------------ |
| **Small** | 50% | Fast, cost-optimized, basic reasoning | \$0.0001-0.0002/1K input |
| **Medium** | 40% | Balanced capability/cost | \$0.002-0.006/1K input |
| **Large** | 10% | Heavy reasoning, complex tasks | \$0.02-0.025/1K input |
**Note**: Percentages are target distributions, not enforced quotas. Actual usage depends on your workload.
## Selection Flow
## Priority Ranking
Within each tier, models are ranked by priority (lower number = higher priority). Shannon attempts models in priority order until one succeeds.
**Example from `config/models.yaml`**:
```yaml theme={null}
model_tiers:
small:
providers:
- provider: openai
model: gpt-5-nano-2025-08-07
priority: 1 # Try first
- provider: anthropic
model: claude-haiku-4-5-20251001
priority: 2 # Fallback if OpenAI fails
- provider: xai
model: grok-3-mini
priority: 3 # Default small tier model for xAI
- provider: xai
model: grok-4-fast-non-reasoning
priority: 4 # Alternative fast model
```
**Fallback Behavior**:
* If priority 1 fails (rate limit, API error), Shannon tries priority 2
* Continues until a model succeeds or all options exhausted
* Failures are logged to orchestrator logs
## Parameter Precedence
When multiple parameters specify model selection, the precedence is:
1. **`model_override`** (highest priority) → Forces specific model
2. **`provider_override`** → Limits to one provider's models
3. **`model_tier`** → Uses requested tier
4. **Auto-detected complexity** (lowest priority) → Default behavior
### Top-Level vs Context Parameters
Top-level parameters **always override** context parameters:
```json theme={null}
{
"query": "Analyze data",
"model_tier": "large", // Top-level (WINS)
"context": {
"model_tier": "small" // Context (IGNORED)
}
}
```
## Usage Examples
### Auto-Selection (Default)
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "X-API-Key: sk_test_123456" \
-d '{"query": "What is 2+2?"}'
```
Shannon analyzes complexity → Selects small tier → Uses `gpt-5-nano-2025-08-07` (priority 1 in small tier)
### Force Specific Tier
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "X-API-Key: sk_test_123456" \
-d '{
"query": "Complex analysis task",
"model_tier": "large"
}'
```
Uses large tier → `gpt-5.1-2025-11-01` (priority 1 in large tier)
### Override to Specific Model
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "X-API-Key: sk_test_123456" \
-d '{
"query": "Analysis",
"model_override": "claude-sonnet-4-5-20250929"
}'
```
Forces Anthropic Claude Sonnet, ignoring tier/priority.
### Force Provider
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "X-API-Key: sk_test_123456" \
-d '{
"query": "Analysis",
"model_tier": "medium",
"provider_override": "anthropic"
}'
```
Uses medium tier **but only Anthropic models** → `claude-sonnet-4-5-20250929`
### Python SDK Examples
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(api_key="sk_test_123456")
# Auto-selection
task = client.tasks.submit(query="Simple task")
# Force tier
task = client.tasks.submit(
query="Complex analysis",
model_tier="large"
)
# Force model
task = client.tasks.submit(
query="Research task",
model_override="gpt-5.1-2025-11-01"
)
# Force provider + tier
task = client.tasks.submit(
query="Analysis",
model_tier="medium",
provider_override="openai"
)
```
## Cost Optimization Strategies
### 1. Start Small, Escalate if Needed
```python theme={null}
# Try small tier first
task = client.tasks.submit(query="Analyze Q4 data", model_tier="small")
status = client.tasks.get(task.task_id)
# If result is insufficient, retry with large
if not_satisfactory(status.result):
task = client.tasks.submit(query="Analyze Q4 data deeply", model_tier="large")
```
### 2. Provider-Specific Optimization
```python theme={null}
# Use cheaper provider for bulk tasks
for item in bulk_data:
client.tasks.submit(
query=f"Summarize {item}",
model_tier="small",
provider_override="deepseek" # Cheaper than OpenAI
)
```
### 3. Session-Based Escalation
```python theme={null}
session_id = "analysis-session-123"
# Start with small model
client.tasks.submit(
query="Initial analysis",
session_id=session_id,
model_tier="small"
)
# Follow-up with larger model (inherits context)
client.tasks.submit(
query="Deeper insights",
session_id=session_id,
model_tier="large"
)
```
## Research Tiered Model Architecture
Added in Shannon v0.3.0. This feature achieves **50-70% cost reduction** compared to uniform large-model usage for research workflows.
Shannon's research workflows automatically assign different model tiers to different stages of execution, using expensive models only where quality matters most.
### How It Works
| Research Stage | Default Tier | Role | Rationale |
| --------------------------------- | ---------------------- | ---------------------- | ----------------------------------------------------------------------------------------- |
| **Exploration / Research agents** | `small` | `research_agent` | Gathering information is breadth-oriented; small models are sufficient and cost-effective |
| **Synthesis / Final output** | `large` (configurable) | `synthesis_agent` | Combining and reasoning over findings requires stronger models |
| **Quick strategy** | `small` | `quick_research_agent` | Forces parallel execution with small models for maximum speed |
### Configuration
The synthesis tier is configurable via `synthesis_model_tier` in your task request or `config/shannon.yaml`:
```yaml theme={null}
# config/shannon.yaml
research:
default_research_tier: small # Tier for exploration agents
synthesis_model_tier: large # Tier for final synthesis
```
You can also override per-request:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "X-API-Key: sk_test_123456" \
-d '{
"query": "Research the impact of LLM scaling laws",
"strategy": "research",
"context": {
"synthesis_model_tier": "medium"
}
}'
```
### Quick Research Strategy
The `quick` strategy forces all research agents to run in parallel using small models, optimizing for speed and cost over depth:
```python theme={null}
task = client.tasks.submit(
query="Quick overview of recent ML papers",
strategy="quick" # Parallel execution, small models, quick_research_agent role
)
```
### Cost Comparison
**Example: 5-agent research workflow**
| Approach | Model Usage | Estimated Cost |
| --------------- | --------------------- | -------------- |
| Uniform `large` | 5 x large | \~\$0.50 |
| Tiered (v0.3.0) | 4 x small + 1 x large | \~\$0.15-0.25 |
| Quick strategy | 5 x small (parallel) | \~\$0.05 |
Result: **50-70% cost reduction** with tiered architecture, maintaining synthesis quality.
## Complexity Analysis
Shannon analyzes task complexity using several factors:
* **Query length** and specificity
* **Number of sub-tasks** identified
* **Tool usage** requirements
* **Context depth** needed
* **Reasoning intensity** (keywords like "analyze", "compare", "synthesize")
**Complexity Thresholds** (configurable):
* `< 0.3` → Small tier (simple Q\&A, basic tasks)
* `0.3 - 0.7` → Medium tier (multi-step, moderate reasoning)
* `> 0.7` → Large tier (complex research, heavy reasoning)
## Monitoring & Debugging
### Check Which Model Was Used
```bash theme={null}
TASK_ID="task-abc123"
curl http://localhost:8080/api/v1/tasks/$TASK_ID \
-H "X-API-Key: sk_test_123456" | jq '{model_used, provider, usage}'
```
Response:
```json theme={null}
{
"model_used": "gpt-5-nano-2025-08-07",
"provider": "openai",
"usage": {
"total_tokens": 245,
"input_tokens": 150,
"output_tokens": 95,
"estimated_cost": 0.000053
}
}
```
### Prometheus Metrics
```bash theme={null}
# Model usage by tier
shannon_llm_requests_total{tier="small"}
shannon_llm_requests_total{tier="medium"}
shannon_llm_requests_total{tier="large"}
# Provider distribution
shannon_llm_requests_total{provider="openai"}
shannon_llm_requests_total{provider="anthropic"}
# Tier drift (when requested tier unavailable)
shannon_tier_drift_total{requested="large", actual="medium"}
```
### Orchestrator Logs
```bash theme={null}
docker compose -f deploy/compose/docker-compose.yml logs orchestrator | grep "Model selected"
```
Look for:
* `"Model selected: gpt-5-nano-2025-08-07 (small tier, priority 1)"`
* `"Falling back to priority 2: claude-haiku-4-5-20251001"`
* `"Falling back to priority 3: grok-3-mini (xAI)"`
* `"Tier override: user requested large → using gpt-5.1-2025-11-01"`
## Configuration
Model tiers and priorities are defined in `config/models.yaml`:
```yaml theme={null}
model_tiers:
small:
providers:
- provider: openai
model: gpt-5-nano-2025-08-07
priority: 1
- provider: anthropic
model: claude-haiku-4-5-20251001
priority: 2
selection_strategy:
mode: priority # priority | round-robin | least-cost
fallback_enabled: true
max_retries: 3
```
**Selection Modes**:
* `priority` (default): Try models in priority order
* `round-robin`: Distribute load evenly across same-priority models
* `least-cost`: Always select cheapest model in tier
## Troubleshooting
### Issue: Wrong tier selected
**Symptoms**: Task uses medium tier when you expected small
**Solutions**:
1. Explicitly set `model_tier: "small"` in request
2. Check complexity score in orchestrator logs
3. Verify query isn't triggering complexity heuristics (avoid words like "analyze deeply")
### Issue: Specific model not used
**Symptoms**: Request `model_override: "gpt-5.1-2025-11-01"` but gets different model
**Solutions**:
1. Verify model is in `config/models.yaml` under `model_catalog`
2. Check API key for provider is set in `.env` (e.g., `OPENAI_API_KEY`, `XAI_API_KEY`)
3. Verify model ID uses canonical name (not alias)
4. Check orchestrator logs for fallback messages
5. Ensure the model is available in your API plan (e.g., GPT-5.1 requires appropriate tier)
### Issue: High costs
**Symptoms**: Costs higher than expected
**Solutions**:
1. Check actual tier distribution via Prometheus
2. Add explicit `model_tier: "small"` to requests
3. Review `shannon_tier_drift_total` for unwanted escalations
4. Set `MAX_COST_PER_REQUEST` in `.env` to enforce budget
### Issue: Rate limiting
**Symptoms**: Frequent 429 errors, slow fallback cascade
**Solutions**:
1. Add more providers to tier priority list
2. Enable `round-robin` mode to distribute load
3. Increase `RATE_LIMIT_WINDOW` for affected providers
4. Consider cheaper providers (DeepSeek, Groq) as fallbacks
## Best Practices
1. **Default to Auto-Selection**: Let Shannon's complexity analysis work
2. **Override Sparingly**: Use `model_override` only when required
3. **Start Small**: Set `model_tier: "small"` for cost-sensitive workloads
4. **Monitor Distribution**: Track tier usage via metrics
5. **Configure Fallbacks**: Ensure each tier has 3+ providers
6. **Test Priority Order**: Verify your preferred models are priority 1
7. **Budget Enforcement**: Set `MAX_COST_PER_REQUEST` for safety
## Related Documentation
List available models and pricing
Task submission with model parameters
Environment variables and YAML config
View model usage and costs
# OpenAPI Tools
Source: https://docs.shannon.run/en/tutorials/openapi-tools
Complete reference for configuring OpenAPI 3.x tools in Shannon
## Overview
Shannon can automatically generate tools from OpenAPI 3.x specifications, allowing you to integrate any REST API without writing code. The OpenAPI loader:
* ✅ Parses OpenAPI 3.0/3.1 specs
* ✅ Generates one tool per operation
* ✅ Handles authentication (Bearer, API Key, Basic)
* ✅ Supports path/query/header parameters
* ✅ Includes circuit breaker and rate limiting
* ✅ Validates requests against schema
* ✅ Resolves `$ref` references locally
**Quick Start**: See [Adding Custom Tools Guide](/en/tutorials/custom-tools#adding-openapi-tools) for step-by-step instructions.
## Configuration Reference
### Basic Configuration
```yaml theme={null}
# config/shannon.yaml or config/overlays/shannon.myvendor.yaml
openapi_tools:
tool_collection_name:
enabled: true | false # Enable/disable this tool collection
spec_path: string # Path to OpenAPI spec file
spec_url: string # OR: URL to fetch spec from
spec_inline: string # OR: Inline YAML/JSON spec
auth_type: none | api_key | bearer | basic
auth_config: object # Auth configuration (see below)
category: string # Tool category (e.g., "analytics", "data")
base_cost_per_use: float # Estimated cost per operation (USD)
rate_limit: integer # Requests per minute (default: 30)
timeout_seconds: float # Request timeout (default: 30)
max_response_bytes: integer # Max response size (default: 10MB)
operations: [string] # Optional: Filter by operationId
tags: [string] # Optional: Filter by tags
base_url: string # Optional: Override spec's base URL
```
### Field Descriptions
| Field | Type | Required | Default | Description |
| -------------------- | -------------- | --------------- | ---------- | ------------------------------------------------------ |
| `enabled` | boolean | Yes | - | Enable this tool collection |
| `spec_path` | string | One of spec\_\* | - | Local file path to OpenAPI spec |
| `spec_url` | string | One of spec\_\* | - | URL to fetch spec from (supports relative server URLs) |
| `spec_inline` | string | One of spec\_\* | - | Inline YAML/JSON spec content |
| `auth_type` | enum | Yes | `none` | Authentication method |
| `auth_config` | object | If auth ≠ none | `{}` | Auth configuration (varies by type) |
| `category` | string | No | `"api"` | Tool category for organization |
| `base_cost_per_use` | float | No | `0.001` | Estimated cost per invocation (USD) |
| `rate_limit` | integer | No | `30` | Max requests per minute per tool |
| `timeout_seconds` | float | No | `30.0` | HTTP request timeout |
| `max_response_bytes` | integer | No | `10485760` | Max response size (10MB) |
| `operations` | array\[string] | No | All | Filter to specific operationIds |
| `tags` | array\[string] | No | All | Filter operations by tags |
| `base_url` | string | No | From spec | Override API base URL |
## Authentication Types
### No Authentication
```yaml theme={null}
openapi_tools:
public_api:
enabled: true
spec_url: https://api.example.com/openapi.json
auth_type: none
```
### Bearer Token
Used by: GitHub, GitLab, most modern APIs
```yaml theme={null}
openapi_tools:
github_api:
enabled: true
spec_url: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json
auth_type: bearer
auth_config:
token: "${GITHUB_TOKEN}" # From environment variable
# OR:
# token: "ghp_xxxxxxxxxxxxx" # Hardcoded (not recommended)
```
**Environment Variable**:
```bash theme={null}
GITHUB_TOKEN=ghp_your_token_here
```
**Headers sent**:
```
Authorization: Bearer ghp_your_token_here
```
### API Key in Header
Used by: OpenAI, Anthropic, many SaaS APIs
```yaml theme={null}
openapi_tools:
openai_api:
enabled: true
spec_url: https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml
auth_type: api_key
auth_config:
api_key_name: Authorization # Header name
api_key_location: header # "header" or "query"
api_key_value: "Bearer ${OPENAI_API_KEY}" # Value with prefix
```
**Environment Variable**:
```bash theme={null}
OPENAI_API_KEY=sk-your-key-here
```
**Headers sent**:
```
Authorization: Bearer sk-your-key-here
```
### API Key in Query Parameter
Used by: OpenWeather, some legacy APIs
```yaml theme={null}
openapi_tools:
weather_api:
enabled: true
spec_url: https://api.openweathermap.org/data/3.0/openapi.json
auth_type: api_key
auth_config:
api_key_name: appid # Query param name
api_key_location: query # "query" not "header"
api_key_value: "${OPENWEATHER_KEY}"
```
**Request URL**:
```
GET https://api.openweathermap.org/data/3.0/weather?appid=your_key_here&q=London
```
### Basic Authentication
Used by: Legacy APIs, internal services
```yaml theme={null}
openapi_tools:
legacy_api:
enabled: true
spec_path: config/openapi_specs/legacy_api.yaml
auth_type: basic
auth_config:
username: "${API_USERNAME}"
password: "${API_PASSWORD}"
```
**Environment Variables**:
```bash theme={null}
API_USERNAME=admin
API_PASSWORD=secret123
```
**Headers sent**:
```
Authorization: Basic YWRtaW46c2VjcmV0MTIz
```
### Custom Headers
For vendor-specific authentication:
```yaml theme={null}
openapi_tools:
custom_api:
enabled: true
spec_url: https://api.custom.com/v1/openapi.json
auth_type: api_key
auth_config:
vendor: myvendor # Triggers vendor adapter
api_key_name: X-API-Key
api_key_location: header
api_key_value: "${CUSTOM_API_KEY}"
extra_headers:
X-Account-ID: "{{body.account_id}}" # Dynamic from request body
X-User-ID: "${CUSTOM_USER_ID}" # Static from environment
X-Timestamp: "{{timestamp}}" # Dynamic template
```
**Dynamic Header Templates**:
* `"${ENV_VAR}"` - Resolved from environment
* `"{{body.field}}"` - Resolved from request body at runtime
* Static strings - Used as-is
## Advanced Features
### Operation Filtering
**By operationId** (recommended):
```yaml theme={null}
openapi_tools:
petstore:
spec_url: https://petstore3.swagger.io/api/v3/openapi.json
operations:
- getPetById # Only generate tool for this operation
- findPetsByStatus
- addPet
```
**By tags**:
```yaml theme={null}
openapi_tools:
petstore:
spec_url: https://petstore3.swagger.io/api/v3/openapi.json
tags:
- pet # Only operations tagged "pet"
- user
```
### Base URL Override
Override the base URL from the spec:
```yaml theme={null}
openapi_tools:
api_staging:
spec_url: https://api.example.com/openapi.json
base_url: https://staging-api.example.com # Use staging instead
```
**Use cases**:
* Testing against staging/dev environments
* Internal proxies or gateways
* Local development
### Rate Limiting
Protect external APIs from overload:
```yaml theme={null}
openapi_tools:
expensive_api:
spec_url: https://expensive.api.com/openapi.json
rate_limit: 10 # Only 10 requests per minute
timeout_seconds: 60 # 60 second timeout for slow operations
```
**Per-tool limits**: Each operation generated from the spec inherits this limit.
**Behavior**:
* Enforced via token bucket algorithm
* Shared across all tool instances (single Shannon instance)
* Returns error if limit exceeded
### Circuit Breaker
Automatic failure protection:
**Configuration** (via environment):
```bash theme={null}
# Circuit breaker opens after 5 failures
# Stays open for 60 seconds before allowing retry
# (These are defaults, not directly configurable per tool yet)
```
**States**:
1. **Closed** (normal): All requests pass through
2. **Open** (failing): All requests immediately rejected
3. **Half-open** (testing): One trial request allowed
**Behavior**:
* After 5 consecutive failures → opens circuit
* Circuit stays open for 60 seconds
* Then allows one trial request (half-open)
* Success → closes circuit
* Failure → reopens for another 60 seconds
### Response Size Limits
Prevent memory exhaustion:
```yaml theme={null}
openapi_tools:
api_with_large_responses:
spec_url: https://api.example.com/openapi.json
max_response_bytes: 52428800 # 50MB limit
```
**Behavior**:
* Responses larger than limit are truncated
* Error returned with truncated marker
## Troubleshooting
### Tool Not Registered
**Symptom**: Tool doesn't appear in `/tools/list`
**Debug**:
```bash theme={null}
# 1. Check if spec is valid
curl -X POST http://localhost:8000/tools/openapi/validate \
-H "Content-Type: application/json" \
-d '{"spec_url": "https://api.example.com/openapi.json"}'
# 2. Check logs
docker logs shannon-llm-service-1 | grep -i "openapi"
# 3. Verify enabled flag
grep -A 5 "my_tool" config/shannon.yaml | grep enabled
```
**Common causes**:
* `enabled: false` in config
* Invalid OpenAPI spec
* Domain not in `OPENAPI_ALLOWED_DOMAINS`
* Spec fetch timeout
* Circular `$ref` references
### Domain Validation Error
**Symptom**: `URL host 'example.com' not in allowed domains`
**Fix**:
```bash theme={null}
# Development: Allow all
OPENAPI_ALLOWED_DOMAINS=*
# Production: Specific domains
OPENAPI_ALLOWED_DOMAINS=api.github.com,api.example.com,api.partner.com
```
**In docker-compose.yml**:
```yaml theme={null}
services:
llm-service:
environment:
- OPENAPI_ALLOWED_DOMAINS=api.example.com
```
### Spec Fetch Timeout
**Symptom**: `Failed to fetch OpenAPI spec: timeout`
**Fix**:
```bash theme={null}
# Increase timeout (default: 30s)
OPENAPI_FETCH_TIMEOUT=60
# Or use local spec file instead
openapi_tools:
my_api:
spec_path: config/openapi_specs/my_api.yaml # Local file
```
### Circuit Breaker Triggered
**Symptom**: `Circuit breaker open for https://api.example.com`
**Debug**:
```bash theme={null}
# Check recent errors
docker logs shannon-llm-service-1 --tail 100 | grep -i "circuit\|failure"
```
**Fix**:
* Wait 60 seconds for automatic recovery
* Fix underlying API issues
* Increase timeout if API is slow:
```yaml theme={null}
timeout_seconds: 60
```
### Rate Limit Exceeded
**Symptom**: `Rate limit exceeded for tool my_tool`
**Fix**:
```yaml theme={null}
# Increase limit
openapi_tools:
my_api:
rate_limit: 120 # 120 requests per minute
```
### Authentication Failures
**Symptom**: `401 Unauthorized` or `403 Forbidden`
**Debug**:
```bash theme={null}
# Check environment variables
env | grep API_KEY
# Test token manually
curl -H "Authorization: Bearer $YOUR_TOKEN" https://api.example.com/endpoint
```
**Common causes**:
* Environment variable not set
* Token expired
* Wrong auth type (should be `bearer` not `api_key`)
* Missing `Bearer` prefix for API key auth
## Examples
### Example 1: GitHub API
```yaml theme={null}
openapi_tools:
github:
enabled: true
spec_url: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json
auth_type: bearer
auth_config:
token: "${GITHUB_TOKEN}"
category: development
rate_limit: 60
operations:
- repos/get
- repos/list-for-user
- issues/list-for-repo
```
**Usage**:
```bash theme={null}
./scripts/submit_task.sh "List all repositories for user octocat"
```
### Example 2: OpenWeather API
```yaml theme={null}
openapi_tools:
openweather:
enabled: true
spec_url: https://api.openweathermap.org/data/3.0/openapi.json
auth_type: api_key
auth_config:
api_key_name: appid
api_key_location: query
api_key_value: "${OPENWEATHER_API_KEY}"
category: data
rate_limit: 60
```
**Usage**:
```bash theme={null}
./scripts/submit_task.sh "What's the weather forecast for Tokyo?"
```
### Example 3: Internal API with Vendor Adapter
```yaml theme={null}
openapi_tools:
internal_analytics:
enabled: true
spec_path: config/openapi_specs/internal_analytics.yaml
auth_type: bearer
auth_config:
vendor: mycompany # Triggers vendor adapter
token: "${ANALYTICS_API_TOKEN}"
extra_headers:
X-Tenant-ID: "{{body.tenant_id}}"
category: analytics
base_cost_per_use: 0.005
```
**Vendor Adapter** (`python/llm-service/llm_service/tools/vendor_adapters/mycompany.py`):
```python theme={null}
class MyCompanyAdapter:
def transform_body(self, body, operation_id, prompt_params):
# Add tenant context
if prompt_params and "tenant_id" in prompt_params:
body["tenant_id"] = prompt_params["tenant_id"]
# Transform metric names
if "metrics" in body:
body["metrics"] = [f"myco:{m}" for m in body["metrics"]]
return body
```
## Security Best Practices
```yaml theme={null}
# ❌ Bad
auth_config:
token: "sk-1234567890"
# ✅ Good
auth_config:
token: "${API_TOKEN}"
```
```bash theme={null}
# Production
OPENAPI_ALLOWED_DOMAINS=api.trusted.com,api.partner.com
# Not: OPENAPI_ALLOWED_DOMAINS=*
```
```yaml theme={null}
rate_limit: 60 # Match your API provider's limits
```
* Shannon automatically upgrades HTTP to HTTPS for external APIs
* localhost/127.0.0.1 allowed on HTTP for development
```yaml theme={null}
max_response_bytes: 10485760 # 10MB default
```
## See Also
Complete tool integration guide
Domain-specific integrations
Other extension methods
Test examples and validation
## Quick Reference
```bash theme={null}
# Validate spec
curl -X POST http://localhost:8000/tools/openapi/validate \
-d '{"spec_url": "https://api.example.com/openapi.json"}' | jq .
# List registered tools
curl http://localhost:8000/tools/list | jq .
# Get tool schema
curl http://localhost:8000/tools/myTool/schema | jq .
# Execute tool
curl -X POST http://localhost:8000/tools/execute \
-d '{"tool_name":"myTool","parameters":{...}}' | jq .
# Test via workflow
./scripts/submit_task.sh "Your query using the tool"
```
**Need Help?**
* Report issues: [GitHub Issues](https://github.com/Kocoro-lab/Shannon/issues)
* Examples: `tests/e2e/06_openapi_petstore_test.sh`
# Build a Deep Research Agent
Source: https://docs.shannon.run/en/tutorials/research-assistant
Use Shannon's ResearchWorkflow to build SOTA multi-agent deep research with memory, entity filtering, gap filling, citations, verification, and adaptive patterns.
# Deep Research Agent
This tutorial shows how to use Shannon's **ResearchWorkflow** for comprehensive, citation-backed research reports.
## What You'll Learn
* **Multi-stage workflow**: Memory retrieval → Refine → Decompose → Execute → Citations → Gap Filling → Synthesis → Verification
* **Adaptive patterns**: React, Parallel, and Hybrid execution based on complexity
* **Advanced features**: Entity filtering, gap-filling, context compression, react-per-task mode
* **Quality controls**: Citation requirements, coverage enforcement, claim verification
* **Language matching**: Automatically responds in the user's query language
## Prerequisites
* Shannon stack running (Docker Compose)
* Gateway reachable at `http://localhost:8080`
* Auth defaults:
* Docker Compose: authentication is disabled by default (`GATEWAY_SKIP_AUTH=1`).
* Local builds: authentication is enabled by default. Set `GATEWAY_SKIP_AUTH=1` to disable auth, or include an API key header `-H "X-API-Key: $API_KEY"`.
## Workflow Architecture
The ResearchWorkflow combines multiple stages to produce high-quality, cited research:
**Key Features:**
* **Memory-aware**: Uses conversation history for contextual research
* **Entity-focused**: Filters results when entity is detected (e.g., specific company/product)
* **Self-correcting**: Gap filling ensures comprehensive coverage
* **Quality-enforced**: Per-area coverage validation (≥600 chars, ≥2 citations per section)
## Quick Start (HTTP)
```bash theme={null}
# Submit a research task (default “standard” strategy)
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Latest breakthroughs in quantum error correction",
"context": { "force_research": true }
}'
# Submit and get a ready-to-use SSE stream URL (recommended)
# Tip: include force_research to route to ResearchWorkflow (otherwise router may select Supervisor)
curl -s -X POST http://localhost:8080/api/v1/tasks/stream \
-H "Content-Type: application/json" \
-d '{
"query": "What are the main transformer architecture trends in 2025?",
"context": { "force_research": true }
}' | jq
```
### Stream Events (SSE)
```bash theme={null}
# After the /tasks/stream call, use "stream_url" from the response:
curl -N "http://localhost:8080/api/v1/stream/sse?workflow_id=task-..."
```
Watch for:
* `LLM_OUTPUT`: final synthesis text
* `DATA_PROCESSING`: progress/usage
* `WORKFLOW_COMPLETED`: completion
## Execution Patterns
Shannon selects the optimal execution pattern based on task complexity and dependencies:
### React Pattern (complexity \< 0.5)
Iterative reason→act→observe loop for simpler research.
```bash theme={null}
# Example: Basic conceptual query triggers React
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "What is quantum computing?",
"context": { "force_research": true }
}'
```
**Behavior**: Single agent performs iterative reasoning, web searches, and observation cycles (default 5 iterations).
***
### Parallel Pattern (no dependencies)
Concurrent subtask execution for multi-faceted research.
```bash theme={null}
# Example: Multi-aspect comparison triggers Parallel
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare LangChain and AutoGen frameworks: architecture, pricing, community, performance",
"context": { "force_research": true }
}'
```
**Behavior**: Decomposition creates independent subtasks (e.g., "Analyze LangChain architecture", "Analyze AutoGen pricing"), executed concurrently with web\_search injection for citations.
***
### Hybrid Pattern (has dependencies)
Topological sort + fan-in/fan-out for sequential research with dependencies.
```bash theme={null}
# Example: Query with logical dependencies triggers Hybrid
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze company X market strategy, then evaluate competitive response, then forecast next moves",
"context": { "force_research": true }
}'
```
**Behavior**: Executes subtasks in dependency order, passing previous results downstream.
***
### React Per Task (deep research mode)
Mini ReAct loops per subtask for high-complexity research.
**Trigger conditions:**
1. Manual: `context.react_per_task = true`
2. Auto-enable: complexity > 0.7 AND strategy in `{deep, academic}`
```bash theme={null}
# Example: Enable react_per_task for iterative per-area research
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Comprehensive analysis of transformer architecture evolution 2017-2025",
"context": {
"force_research": true,
"react_per_task": true,
"react_max_iterations": 6
},
"research_strategy": "deep"
}'
```
**Behavior**: Each parallel subtask runs its own ReAct loop (reason→act→observe), enabling iterative exploration per research area. More thorough but higher token cost.
***
## Strategy Presets
Presets provide opinionated defaults for different research depths. The Gateway validates and maps these to workflow context.
```bash theme={null}
# Quick strategy (minimal iterations)
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "What is quantum error correction?",
"context": { "force_research": true },
"research_strategy": "quick"
}'
# Deep strategy with overrides
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare LangChain and AutoGen frameworks",
"context": {
"force_research": true,
"react_per_task": true,
"react_max_iterations": 6
},
"research_strategy": "deep",
"enable_verification": true
}'
# Academic strategy (auto-enables react_per_task for complexity > 0.7)
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Survey of attention mechanisms in modern NLP architectures",
"context": { "force_research": true },
"research_strategy": "academic"
}'
```
**Quick strategy behavior:** uses the lightweight `quick_research_agent` prompt, always takes the parallel execution path, and skips the iterative coverage loop and gap-filling stages—optimized for fast summaries where depth is not required.
**Available Presets**: `quick` | `standard` | `deep` | `academic`
| Strategy | react\_max\_iterations | max\_concurrent\_agents | verification | gap\_filling |
| -------- | ---------------------- | ----------------------- | ------------ | ------------------------------------ |
| quick | 2 | 3 | ✗ | ✗ |
| standard | 3 | 5 | ✓ | ✓ (max\_gaps: 3, max\_iterations: 2) |
| deep | 4 | 6 | ✓ | ✓ (max\_gaps: 2, max\_iterations: 2) |
| academic | 5 | 8 | ✓ | ✓ (max\_gaps: 3, max\_iterations: 2) |
**Core Parameters**:
* `max_concurrent_agents` (1–20): Controls maximum concurrency for parallel subtasks (only applies when multiple subtasks are executed)
* `react_max_iterations` (2–8, default 5): Controls ReAct loop depth per task (independent parameter, no prerequisites)
**Advanced Flags**:
* `react_per_task` (bool): Enable mini ReAct loops per subtask (auto-enabled for complexity > 0.7 + deep/academic)
* `enable_verification` (bool): Enable claim verification against citations
* `budget_agent_max` (int): Optional per-agent token budget with enforcement
**Gap Filling Configuration**:
* `gap_filling_enabled` (bool): Enable/disable gap filling
* `gap_filling_max_gaps` (1–20): Maximum number of gaps to detect
* `gap_filling_max_iterations` (1–5): Maximum retry iterations per gap
* `gap_filling_check_citations` (bool): Check citation density as gap indicator
**Default preset**: `standard` (if not specified). Presets seed flags only if not already set in context. The Gateway accepts top‑level `research_strategy` on both `/api/v1/tasks` and `/api/v1/tasks/stream` and maps it into context. Preset defaults are loaded from `config/research_strategies.yaml`. Quick strategy skips the iterative coverage loop and gap-filling stages.
## Python SDK
### CLI
```bash theme={null}
python -m shannon.cli --base-url http://localhost:8080 \
submit "Latest quantum computing breakthroughs" \
--force-research \
--research-strategy deep --enable-verification
```
### Programmatic
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
handle = client.submit_task(
"Compare LangChain and AutoGen frameworks",
context={
"force_research": True,
"research_strategy": "deep",
"react_max_iterations": 6,
"enable_verification": True,
},
)
final = client.wait(handle.task_id)
print(final.result)
client.close()
```
## Response Format
Typical status payloads returned by `GET /api/v1/tasks/{id}` include the synthesized result, metadata (citations, verification), model/provider, and timestamps.
```json theme={null}
{
"task_id": "task-00000000-0000-0000-0000-000000000000",
"status": "TASK_STATUS_COMPLETED",
"result": "... final synthesis text ...",
"metadata": {
"citations": [
{
"url": "https://example.com/article",
"title": "Source Title",
"source": "example.com",
"quality_score": 0.92,
"credibility_score": 0.85
}
],
"verification": {
"total_claims": 10,
"overall_confidence": 0.78,
"supported_claims": 7,
"unsupported_claims": [
"Claim text lacking sufficient evidence ..."
],
"claim_details": [
{
"claim": "Concrete claim text ...",
"confidence": 0.81,
"supporting_citations": [1, 3],
"conflicting_citations": []
}
]
}
},
"created_at": "2025-11-07T12:00:00Z",
"updated_at": "2025-11-07T12:02:00Z",
"usage": {
"input_tokens": 1234,
"output_tokens": 5678,
"total_tokens": 6912,
"estimated_cost": 0.0234
},
"model_used": "gpt-5-mini-2025-08-07",
"provider": "openai"
}
```
Notes:
* `result` is the final synthesized markdown/text.
* `metadata.citations` lists collected sources (all are included in the Sources section of the result).
* `metadata.verification` appears when verification is enabled and completed.
* In Gateway status responses, `created_at` / `updated_at` reflect response time; authoritative run timing and totals are persisted in the database (events/timeline).
* `usage` summarizes token usage and may include cost. Gateway status may expose `estimated_cost`; workflow metadata may include `cost_usd`.
* `model_used` and `provider` reflect the model/provider chosen during synthesis.
## Advanced Features
### Memory Retrieval
**Hierarchical Memory** (priority):
* Combines recent (last 5 messages) + semantic (top 5 relevant, similarity ≥ 0.75)
* Enables coherent multi-turn research with conversation context
* Fallback: Session memory (last 20 messages)
**Context Compression**:
* Auto-triggers for sessions >20 messages
* LLM summarizes conversation (target: 37.5% of window)
* Prevents context window overflow in long conversations
***
### Entity Filtering
When a specific entity is detected (e.g., company "Acme Analytics"):
**Query Refinement**:
* Detects canonical name, exact search queries, official domains, disambiguation terms
* Example: `canonical_name: "Acme Analytics"`, `official_domains: ["acme.com"]`
**Citation Filtering**:
* Scoring: domain match +0.6, alias in URL +0.4, text match +0.4
* Threshold: 0.3 (any single match passes)
* Safety floor: minKeep=8 (backfilled by quality × credibility)
* Official domains always preserved (bypass threshold)
**Result Pruning**:
* Filters off-entity tool results (e.g., removes "Mind Inc" when searching for "Acme Mind")
* Keeps reasoning-only outputs; prunes off-entity tool-driven results
***
### Gap Filling
**Auto-detection** (max 2 iterations):
* Missing section headings (`### Area Name`)
* Gap indicator phrases ("limited information", "insufficient data")
* Low citation density (\< 2 inline citations per section)
**Resolution**:
1. Build targeted queries: `"Find detailed information about: "`
2. Execute focused ReAct loops (max 3 iterations per gap)
3. Re-collect citations (global deduplication with original citations)
4. Re-synthesize with combined evidence using large tier
```text theme={null}
Query: "Analyze company X market strategy, competitive landscape, and leadership"
Initial synthesis coverage:
- Market strategy: ✓ 800 chars, 3 citations
- Competitive landscape: ✓ 650 chars, 2 citations
- Leadership: ✗ 200 chars, 0 citations ← Gap detected
Gap filling triggers:
1. Targeted query: "Find detailed information about: company X leadership"
2. ReAct loop executes focused search
3. Re-synthesize with combined evidence
```
### Citations
**Collection**:
* Extracted from `web_search` and `web_fetch` tool outputs
* URL/DOI normalization and deduplication
* Scoring: quality (recency + completeness) × credibility (domain reputation)
* Diversity enforcement (max 3 per domain)
**Enforcement**:
* Minimum 6 inline citations per report (clamped by available, floor 3)
* Per-area requirement: ≥2 inline citations per section
* Sources section lists all collected citations:
* "Used inline" (cited in text)
* "Additional source" (collected but not cited)
***
### Synthesis Continuation
**Trigger**: Model stops early with incomplete output
**Detection** (`looksComplete` validation):
1. Must end with sentence punctuation (`.`, `!`, `?`, `。`)
2. No dangling conjunctions (`and, but, however, therefore...`)
3. Every research area has subsection with ≥600 chars and ≥2 citations
**Continuation**:
* Adaptive margin: min(25% of effective\_max\_completion, 300 tokens)
* Prompt: "Continue from last sentence; maintain headings and citation style"
* Uses large tier for quality (gpt-4.1/opus)
***
### Verification
**Claim Extraction**:
* Identifies factual assertions from synthesis
* Cross-references against collected citations
**Confidence Scoring**:
* Weighted by citation credibility
* Flags conflicts and unsupported claims
* Per-claim details: supporting/conflicting citations
**Example output**:
```json theme={null}
{
"verification": {
"total_claims": 10,
"overall_confidence": 0.78,
"supported_claims": 7,
"unsupported_claims": ["Claim lacking evidence..."],
"claim_details": [
{
"claim": "Transformer architecture introduced in 2017",
"confidence": 0.95,
"supporting_citations": [1, 3],
"conflicting_citations": []
}
]
}
}
```
**Enable**: Set `enable_verification: true` in context.
***
### Language Matching
* Detects user query language (heuristic)
* Synthesis responds in the same language
* Supports English, Chinese, Japanese, Korean, Arabic, Russian, Spanish, French, German (heuristic‑based)
* Generic instructions ensure robustness across languages
## Behavior Guarantees
Hierarchical memory (recent + semantic) injected when available
Automatic for sessions over 20 messages to prevent context overflow
Each research area has dedicated subsection with minimum 600 chars and 2 citations
Auto-detects and re-searches undercovered areas (max 2 iterations)
Response matches user query language
Token usage and cost are aggregated and persisted; per-agent budgets enforced when configured
Triggers only when synthesis is incomplete and capacity nearly exhausted
When entity detected, filters citations and prunes off-entity results
## Tips & Best Practices
* Set `context.force_research=true` to ensure routing to ResearchWorkflow
* Start with `standard` preset, adjust based on results
* Monitor SSE for progress and token usage
* Use `quick` preset for simple queries (saves tokens)
* Use `deep` or `academic` for comprehensive research (auto-enables react\_per\_task at high complexity)
* Set `budget_agent_max` to enforce per-agent token limits
* Enable `react_per_task` manually for iterative per-area exploration
* Use sessions to enable memory retrieval
* Context compression activates automatically for long conversations
* Follow-up queries benefit from hierarchical memory (recent + semantic)
## Troubleshooting
**Common Issues**:
* **Language defaults to English**: Ensure the query clearly uses the target language
* **Missing citations**: Check if the `web_search` tool is available; research workflows auto‑inject `web_search`
* **Incomplete coverage**: Gap filling should auto‑trigger; ensure `gap_filling_v1` is enabled
* **High token usage**: Enable `budget_agent_max` or use `quick` preset; disable `react_per_task`
**Validation**:
* Invalid `web_search` `search_type`: Sanitized (falls back to `auto`)
* Duplicate citations: Handled via URL/DOI normalization
* Entity detection: Performed during refinement when present in the query
**Version Gates** (internal feature flags):
* Memory retrieval: `memory_retrieval_v1`
* Session memory fallback: `session_memory_v1`
* Context compression: `context_compress_v1`
* Gap filling: `gap_filling_v1`
## Next Steps
REST API Documentation
Adding Custom Tools
Vendor-specific Integrations
**Related Resources**:
* GitHub: [https://github.com/Kocoro-lab/Shannon](https://github.com/Kocoro-lab/Shannon)
* Company: [https://kocoro.ai](https://kocoro.ai)
# Swarm Multi-Agent Workflow
Source: https://docs.shannon.run/en/tutorials/swarm-workflow
Step-by-step guide to running persistent multi-agent swarm workflows in Shannon with Lead Agent coordination
# Swarm Multi-Agent Workflow
This tutorial shows how to use Shannon's **SwarmWorkflow** to deploy persistent, collaborating agents coordinated by an LLM-powered Lead Agent. Agents work in parallel with inter-agent messaging, a shared workspace, and dynamic task reassignment.
## What You'll Learn
* How to submit a swarm task via API and Python SDK
* How the Lead Agent coordinates agents through events
* How to monitor agent progress with SSE streaming
* How to configure swarm parameters and budget controls
* Real-world use cases and best practices
## Prerequisites
* Shannon stack running (Docker Compose)
* Gateway reachable at `http://localhost:8080`
* Swarm enabled in `config/features.yaml` (enabled by default)
* Auth defaults:
* Docker Compose: authentication is disabled by default (`GATEWAY_SKIP_AUTH=1`).
* Local builds: authentication is enabled by default. Set `GATEWAY_SKIP_AUTH=1` to disable auth, or include an API key header `-H "X-API-Key: $API_KEY"`.
## Quick Start
### Submit a Swarm Task
Submit a task with `force_swarm: true` in the context to route it to the SwarmWorkflow:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare AI chip markets across US, Japan, and South Korea",
"session_id": "swarm-demo-001",
"context": {
"force_swarm": true
}
}'
```
**Response:**
```json theme={null}
{
"task_id": "task-abc123...",
"status": "STATUS_CODE_OK",
"message": "Task submitted successfully",
"created_at": "2025-11-10T10:00:00Z"
}
```
### Stream Progress Events
Connect to the SSE stream to watch agents work in real-time:
```bash theme={null}
curl -N "http://localhost:8080/api/v1/stream/sse?workflow_id=task-abc123..."
```
You will see events like:
```text theme={null}
data: {"type":"WORKFLOW_STARTED","agent_id":"swarm-supervisor","message":"Lead Agent initializing team"}
data: {"type":"LEAD_DECISION","agent_id":"swarm-lead","message":"Creating initial plan with 3 tasks"}
data: {"type":"TASKLIST_UPDATED","agent_id":"swarm-lead","message":"Task graph updated: 4 tasks (3 research + 1 synthesis)"}
data: {"type":"TEAM_STATUS","agent_id":"swarm-lead","message":"Spawned agent takao"}
data: {"type":"AGENT_STARTED","agent_id":"takao","message":"Agent takao started"}
data: {"type":"TEAM_STATUS","agent_id":"swarm-lead","message":"Spawned agent mitaka"}
data: {"type":"AGENT_STARTED","agent_id":"mitaka","message":"Agent mitaka started"}
data: {"type":"TEAM_STATUS","agent_id":"swarm-lead","message":"Spawned agent kichijoji"}
data: {"type":"AGENT_STARTED","agent_id":"kichijoji","message":"Agent kichijoji started"}
data: {"type":"PROGRESS","agent_id":"takao","message":"Agent takao progress: iteration 1/25, action: tool_call"}
data: {"type":"AGENT_COMPLETED","agent_id":"takao","message":"Agent takao completed"}
data: {"type":"LEAD_DECISION","agent_id":"swarm-lead","message":"Assigning synthesis task to takao"}
data: {"type":"AGENT_COMPLETED","agent_id":"mitaka","message":"Agent mitaka completed"}
data: {"type":"AGENT_COMPLETED","agent_id":"kichijoji","message":"Agent kichijoji completed"}
data: {"type":"LEAD_DECISION","agent_id":"swarm-lead","message":"All tasks complete, finalizing"}
data: {"type":"WORKFLOW_COMPLETED","agent_id":"swarm-supervisor","message":"All done"}
```
### Retrieve the Result
```bash theme={null}
curl "http://localhost:8080/api/v1/tasks/task-abc123..."
```
**Response:**
```json theme={null}
{
"task_id": "task-abc123...",
"status": "TASK_STATUS_COMPLETED",
"result": "## AI Chip Market Comparison\n\n### United States\nThe US market is dominated by NVIDIA...\n\n### Japan\nJapan focuses on edge AI...\n\n### South Korea\nSouth Korea leverages Samsung...",
"metadata": {
"workflow_type": "swarm",
"total_agents": 3,
"total_tokens": 598235,
"model_breakdown": [
{
"model": "claude-haiku-4-5-20251001",
"provider": "anthropic",
"executions": 38,
"tokens": 297524,
"cost_usd": 0.372
},
{
"model": "shannon_web_search",
"provider": "shannon-scraper",
"executions": 12,
"tokens": 90000,
"cost_usd": 0.048
}
]
},
"usage": {
"input_tokens": 289164,
"output_tokens": 309071,
"total_tokens": 598235,
"estimated_cost": 0.780
}
}
```
## Submit + Stream in One Call
For frontend applications, use the combined submit-and-stream endpoint:
```bash theme={null}
curl -s -X POST http://localhost:8080/api/v1/tasks/stream \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze the competitive landscape of cloud AI platforms: AWS, Azure, and GCP",
"context": { "force_swarm": true }
}' | jq
```
**Response:**
```json theme={null}
{
"workflow_id": "task-def456...",
"task_id": "task-def456...",
"stream_url": "/api/v1/stream/sse?workflow_id=task-def456..."
}
```
Then connect to the stream URL for real-time events:
```bash theme={null}
curl -N "http://localhost:8080/api/v1/stream/sse?workflow_id=task-def456..."
```
## Python SDK
### Basic Usage
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
# Submit a swarm task
handle = client.submit_task(
"Compare AI chip markets across US, Japan, and South Korea",
force_swarm=True,
session_id="swarm-demo-001",
)
# Wait for completion and get result
result = client.wait(handle.task_id)
print(result.result)
client.close()
```
### With Streaming
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
# Submit and get stream URL in one call
handle, stream_url = client.submit_and_stream(
"Analyze the competitive landscape of cloud AI platforms",
force_swarm=True,
)
# Stream events in real-time
for event in client.stream(handle.workflow_id):
if event.type == "AGENT_STARTED":
print(f"Agent started: {event.agent_id}")
elif event.type == "LEAD_DECISION":
print(f"Lead decision: {event.message}")
elif event.type == "PROGRESS":
print(f"Progress: {event.message}")
elif event.type == "AGENT_COMPLETED":
print(f"Agent completed: {event.agent_id}")
elif event.type == "WORKFLOW_COMPLETED":
print("Swarm workflow completed")
break
# Get final result
result = client.get_status(handle.task_id)
print(result.result)
client.close()
```
### With Custom Context
```python theme={null}
handle = client.submit_task(
"Research renewable energy policies in the EU, US, and China",
force_swarm=True,
context={
"model_tier": "medium", # Use medium-tier models for agents
},
)
```
## How Agents Collaborate
### Lead Agent Coordination
The Lead Agent acts as an event-driven coordinator. It does not execute tasks itself, but rather plans, assigns, and reassigns work based on events:
* When an agent becomes **idle**, the Lead checks for pending tasks with met dependencies and assigns the next one
* When an agent **completes**, the Lead evaluates whether to reassign it, shut it down, or revise the plan
* On periodic **checkpoints** (every 120s), the Lead reviews overall progress and can adjust the plan
* The Lead skips unnecessary LLM calls when there are no idle agents and no actionable pending tasks
### Team Roster
Each agent receives a team roster showing all agents and their assignments. This enables agents to know who to contact for specific information:
```text theme={null}
## Your Team (shared session workspace)
- **takao (you)**: "Research US AI chip market"
- mitaka: "Research Japan AI chip market"
- kichijoji: "Research South Korea AI chip market"
```
### Publishing Findings
Agents share discoveries via the shared workspace. These appear in every agent's prompt context:
```text theme={null}
## Shared Findings
- takao: NVIDIA dominates US with 80% market share...
- mitaka: Japan focuses on edge AI chips with Preferred Networks leading...
```
### Sending Direct Messages
Agents can send direct messages to specific teammates:
```text theme={null}
## Inbox Messages
- From mitaka (info): {"message": "Check Samsung's foundry plans for AI chips"}
```
### Requesting Help
When an agent needs additional support, it requests help from the Lead Agent:
```json theme={null}
{"action": "request_help", "help_description": "Need help analyzing EU regulatory impact on AI chips", "help_skills": ["web_search"]}
```
The Lead Agent evaluates the request and may spawn a new agent, reassign an existing idle agent, or add the subtask to the pending task queue.
## Configuration
### features.yaml
```yaml theme={null}
workflows:
swarm:
enabled: true
max_agents: 10 # Max total agents (initial + dynamic)
max_iterations_per_agent: 25 # Max reason-act loops per agent
agent_timeout_seconds: 1800 # Per-agent timeout (30 minutes)
max_messages_per_agent: 20 # Max P2P messages per agent
workspace_snippet_chars: 800 # Max chars per workspace entry in prompt
workspace_max_entries: 5 # Max recent entries shown to agents
max_total_llm_calls: 200 # Global LLM call budget
max_total_tokens: 1000000 # Global token budget (1M)
max_wall_clock_minutes: 30 # Max wall-clock time
```
### Configuration Parameters
| Parameter | Default | Range | Description |
| -------------------------- | --------- | -------------- | ------------------------------------------------------ |
| `enabled` | `true` | `true`/`false` | Enable or disable swarm workflows |
| `max_agents` | `10` | 1-50 | Total agent cap including dynamically spawned agents |
| `max_iterations_per_agent` | `25` | 1-100 | Maximum reason-act cycles per agent |
| `agent_timeout_seconds` | `1800` | 60-7200 | Per-agent wall-clock timeout |
| `max_messages_per_agent` | `20` | 1-100 | Cap on P2P messages an agent can send |
| `workspace_snippet_chars` | `800` | 100-4000 | Truncation limit for workspace entries in agent prompt |
| `workspace_max_entries` | `5` | 1-20 | Number of recent workspace entries shown per topic |
| `max_total_llm_calls` | `200` | 10-1000 | Maximum LLM calls across all agents in the swarm |
| `max_total_tokens` | `1000000` | 10000-10000000 | Maximum tokens consumed across all agents |
| `max_wall_clock_minutes` | `30` | 1-120 | Maximum wall-clock time for the entire swarm execution |
## Real-World Use Cases
Agents review, implement, and test code collaboratively with sandboxed execution.
Bull/bear analysts, sentiment agents, and a portfolio manager synthesize investment insights.
Parallel data pipelines with sandboxed Python execution, JSON querying, and statistical analysis.
Monitor competitor websites, pricing, and social media with automatic cross-sharing of discoveries.
### Example: Collaborative Code Review
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Review the Python files in /workspace/src for security vulnerabilities, code quality issues, and missing test coverage. Write fixes and add tests.",
"context": { "force_swarm": true }
}'
```
The Lead Agent creates tasks for each concern (security audit, code quality, test coverage), assigns agents with the `developer` role, and creates a final synthesis task that depends on all reviews completing.
### Example: Multi-Site Price Monitoring
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Compare pricing and features of AWS, Azure, and GCP for a startup needing GPU instances, object storage, and managed Kubernetes",
"context": {
"force_swarm": true,
"model_tier": "medium"
}
}'
```
## Understanding the Response Metadata
The swarm workflow returns metadata with per-model execution breakdown and token usage:
```json theme={null}
{
"metadata": {
"workflow_type": "swarm",
"total_agents": 3,
"total_tokens": 598235,
"model_breakdown": [
{
"model": "claude-haiku-4-5-20251001",
"provider": "anthropic",
"executions": 38,
"tokens": 297524,
"cost_usd": 0.372
},
{
"model": "shannon_web_search",
"provider": "shannon-scraper",
"executions": 12,
"tokens": 90000,
"cost_usd": 0.048
}
]
},
"usage": {
"input_tokens": 289164,
"output_tokens": 309071,
"total_tokens": 598235,
"estimated_cost": 0.780
}
}
```
| Field | Description |
| --------------------------------------- | --------------------------------------------- |
| `metadata.workflow_type` | Always `"swarm"` for swarm workflows |
| `metadata.total_agents` | Number of agents that participated |
| `metadata.total_tokens` | Total tokens consumed across the entire swarm |
| `metadata.model_breakdown[]` | Per-model execution summary |
| `metadata.model_breakdown[].model` | Model identifier |
| `metadata.model_breakdown[].provider` | Provider name |
| `metadata.model_breakdown[].executions` | Number of LLM calls with this model |
| `metadata.model_breakdown[].tokens` | Tokens consumed by this model |
| `metadata.model_breakdown[].cost_usd` | Estimated cost in USD |
| `usage.input_tokens` | Total input tokens across all agents |
| `usage.output_tokens` | Total output tokens across all agents |
| `usage.total_tokens` | Total tokens (input + output) |
| `usage.estimated_cost` | Total estimated cost in USD |
## Tips and Best Practices
* Set `context.force_swarm: true` to route to SwarmWorkflow
* Start with default configuration and adjust based on results
* Monitor SSE events to understand Lead Agent decisions and agent behavior
* Use sessions (`session_id`) for multi-turn swarm conversations
* Watch for `LEAD_DECISION` events to understand coordination logic
* Reduce `max_iterations_per_agent` for faster completion (e.g., 10-15)
* Use `model_tier: "small"` for cost-effective exploration
* Swarm agents use MEDIUM tier by default for balanced quality and speed
* Keep workspace entries concise -- large entries consume prompt tokens
* Knowledge deduplication automatically reduces redundant fetches and searches
* The Lead skips checkpoint LLM calls when no agents are idle and no tasks are actionable
* Set `max_total_llm_calls` to limit total LLM invocations across all agents
* Set `max_total_tokens` to cap token consumption for the entire swarm
* Set `max_wall_clock_minutes` to enforce a hard time limit
* The Lead Agent receives budget info and makes cost-aware decisions automatically
* Simple Q\&A tasks (use standard workflow instead)
* Tasks with a single clear answer (swarm adds unnecessary overhead)
* Cost-sensitive scenarios where token usage matters
* Tasks where agent collaboration does not add value
## Troubleshooting
**Common Issues**:
* **Swarm not triggering**: Ensure `force_swarm: true` is in the `context` object and swarm is enabled in `features.yaml`
* **Agents timing out**: Increase `agent_timeout_seconds` for complex tasks (default is 1800s / 30 minutes)
* **Too many agents**: Reduce the number of subtasks by simplifying your query, or lower `max_agents`
* **High token usage**: Lower `max_iterations_per_agent`, use `model_tier: "small"`, or reduce `max_total_tokens`
* **Agents stuck in loops**: Convergence detection (3 consecutive non-tool iterations) catches this automatically
* **Budget exceeded**: Check `max_total_llm_calls` and `max_total_tokens` settings; the Lead will attempt graceful shutdown when budget is tight
* **Redundant searches**: Knowledge deduplication should catch this; if persisting, check that agents have access to the shared workspace
## Fallback Behavior
If the swarm workflow fails (planning error, all agents fail, etc.), Shannon automatically falls back to standard DAG/Supervisor workflow routing. The `force_swarm` flag is removed from context to prevent recursive failures.
## Next Steps
Understand swarm architecture in depth
Multi-stage research with citations
Full API documentation
# Workflow Templates
Source: https://docs.shannon.run/en/tutorials/templates
Create deterministic, zero-token workflows using Shannon's template system
## Overview
Shannon's template system enables **deterministic, zero-token workflows** for common patterns. Templates define structured workflows in YAML that execute without AI decomposition, providing significant cost savings for repeatable tasks.
Templates bypass LLM calls for workflow routing
Predictable, repeatable workflow behavior
Per-node token limits with automatic degradation
Parallel execution with dependency management
## When to Use Templates
**Use templates when:**
* Workflows are repeatable with known structure
* You want to eliminate decomposition token costs
* Predictable execution order is required
* Budget control per workflow stage is needed
**Use AI decomposition when:**
* Task structure is unknown or variable
* Complex reasoning about workflow design is needed
* One-off or highly dynamic tasks
## Template Structure
Templates are YAML files with this structure:
```yaml theme={null}
name: "workflow_name"
version: "1.0.0"
description: "What this workflow does"
defaults:
model_tier: "medium" # small | medium | large
budget_agent_max: 6000 # Total token budget
require_approval: false # Human approval gate
nodes:
- id: "step_1"
type: "simple" # Node type
strategy: "react" # Execution strategy
tools_allowlist: # Permitted tools
- "web_search"
budget_max: 1500 # Per-node token limit
depends_on: [] # Dependencies
- id: "step_2"
type: "cognitive"
strategy: "chain_of_thought"
budget_max: 2000
depends_on: ["step_1"]
edges:
- from: "step_1"
to: "step_2"
```
### Core Fields
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------- |
| `name` | string | Yes | Unique workflow identifier |
| `version` | string | Yes | Semantic version (e.g., "1.0.0") |
| `description` | string | No | Human-readable description |
| `defaults` | object | No | Global settings for all nodes |
| `nodes` | array | Yes | Workflow execution units |
| `edges` | array | No | Explicit dependency connections |
### Default Settings
```yaml theme={null}
defaults:
model_tier: "medium" # Model size for all nodes
budget_agent_max: 6000 # Total workflow token budget
require_approval: false # Require human approval before execution
```
## Node Types
Shannon supports four node types for different execution patterns:
### Simple Nodes
Single-task execution with direct tool invocation.
```yaml theme={null}
- id: "fetch_data"
type: "simple"
strategy: "react"
tools_allowlist:
- "web_search"
budget_max: 1000
depends_on: []
```
**Use for:** Data fetching, simple transformations, tool-based operations.
### Cognitive Nodes
Complex reasoning with multi-step analysis.
```yaml theme={null}
- id: "analyze"
type: "cognitive"
strategy: "chain_of_thought"
budget_max: 2500
depends_on: ["fetch_data"]
degrade_to: "react" # Fallback if budget exceeded
retries: 1
```
**Use for:** Analysis, reasoning, synthesis tasks.
### DAG Nodes
Parallel execution with internal task dependencies.
```yaml theme={null}
- id: "parallel_analysis"
type: "dag"
budget_max: 5000
depends_on: ["understand"]
metadata:
tasks:
- id: "collect"
tools_allowlist: ["web_search"]
depends_on: []
- id: "quantitative"
tools_allowlist: ["calculator"]
depends_on: ["collect"]
- id: "qualitative"
tools_allowlist: []
depends_on: ["collect"]
- id: "cross_validate"
depends_on: ["quantitative", "qualitative"]
```
**Use for:** Parallel processing, fan-out/fan-in patterns.
### Supervisor Nodes
Hierarchical task decomposition and coordination.
```yaml theme={null}
- id: "orchestrate"
type: "supervisor"
strategy: "reflection"
budget_max: 3000
depends_on: ["parallel_analysis"]
```
**Use for:** Result aggregation, quality control, synthesis.
## Execution Strategies
Strategies define how nodes process tasks:
| Strategy | Token Range | Description |
| ------------------ | ----------- | --------------------------------------- |
| `react` | \~500-1500 | Tool-based reasoning with action cycles |
| `chain_of_thought` | \~1500-3000 | Sequential step-by-step reasoning |
| `tree_of_thoughts` | \~3000-8000 | Explores multiple reasoning branches |
| `reflection` | \~1000-2000 | Self-evaluation and refinement |
| `debate` | \~2000-4000 | Multi-perspective analysis |
### Automatic Degradation
When budget constraints are reached, strategies degrade automatically:
```
tree_of_thoughts → chain_of_thought → react
```
Configure explicit degradation:
```yaml theme={null}
- id: "explore"
type: "cognitive"
strategy: "tree_of_thoughts"
budget_max: 2500
degrade_to: "chain_of_thought" # Fallback strategy
retries: 1 # Retry count on degradation
```
## Creating Templates
Create a YAML file in `config/workflows/examples/` or your custom directory:
```yaml theme={null}
# config/workflows/examples/my_workflow.yaml
name: "my_workflow"
version: "1.0.0"
description: "Custom analysis workflow"
defaults:
model_tier: "medium"
budget_agent_max: 5000
require_approval: false
nodes:
- id: "intake"
type: "simple"
strategy: "react"
tools_allowlist:
- "web_search"
budget_max: 1500
depends_on: []
- id: "analyze"
type: "cognitive"
strategy: "chain_of_thought"
budget_max: 2000
depends_on: ["intake"]
- id: "synthesize"
type: "cognitive"
strategy: "reflection"
budget_max: 1500
depends_on: ["analyze"]
edges:
- from: "intake"
to: "analyze"
- from: "analyze"
to: "synthesize"
```
Templates are loaded at startup via `InitTemplateRegistry`:
```go theme={null}
// In orchestrator initialization
registry := templates.InitTemplateRegistry(
"./config/workflows/builtin",
"./config/workflows/examples", // Your templates
)
// Validate all templates
if err := registry.Finalize(); err != nil {
log.Fatal(err)
}
```
```bash theme={null}
docker compose -f deploy/compose/docker-compose.yml up -d --force-recreate orchestrator
```
Via gRPC:
```bash theme={null}
grpcurl -plaintext -d '{}' localhost:50052 \
shannon.orchestrator.OrchestratorService/ListTemplates
```
## Using Templates
### Via HTTP Gateway
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Analyze the current AI market trends",
"context": {
"template": "my_workflow",
"template_version": "1.0.0"
}
}'
```
### Via gRPC
```bash theme={null}
grpcurl -plaintext -d '{
"query": "Analyze the current AI market trends",
"context": {
"template": "my_workflow",
"template_version": "1.0.0",
"disable_ai": true
}
}' localhost:50052 shannon.orchestrator.OrchestratorService/SubmitTask
```
Set `disable_ai: true` to enforce template-only execution without AI fallback.
### Via Python SDK
```python theme={null}
from shannon import ShannonClient
client = ShannonClient(base_url="http://localhost:8080")
handle = client.submit_task(
"Analyze the current AI market trends",
context={
"template": "my_workflow",
"template_version": "1.0.0",
},
)
result = client.wait(handle.task_id)
print(result.result)
```
## Template Examples
### Simple Analysis Workflow
Two-stage pipeline for quick summarization:
```yaml theme={null}
name: "simple_analysis"
version: "1.0.0"
description: "Quick summarization workflow for short briefs"
defaults:
model_tier: "medium"
budget_agent_max: 3000
require_approval: false
nodes:
- id: "intake"
type: "simple"
strategy: "react"
budget_max: 1000
depends_on: []
- id: "summarize"
type: "cognitive"
strategy: "chain_of_thought"
budget_max: 1200
depends_on: ["intake"]
edges:
- from: "intake"
to: "summarize"
```
### Research Summary Workflow
Four-stage research pipeline with progressive reasoning:
```yaml theme={null}
name: "research_summary"
version: "1.0.0"
description: "Multi-stage research workflow"
defaults:
model_tier: "medium"
budget_agent_max: 6000
require_approval: false
nodes:
- id: "discover"
type: "simple"
strategy: "react"
tools_allowlist:
- "web_search"
budget_max: 1500
depends_on: []
- id: "reason"
type: "cognitive"
strategy: "chain_of_thought"
budget_max: 1500
depends_on: ["discover"]
- id: "explore"
type: "cognitive"
strategy: "tree_of_thoughts"
budget_max: 2500
degrade_to: "chain_of_thought"
retries: 1
depends_on: ["reason"]
- id: "finalize"
type: "cognitive"
strategy: "reflection"
budget_max: 1000
depends_on: ["explore"]
edges:
- from: "discover"
to: "reason"
- from: "reason"
to: "explore"
- from: "explore"
to: "finalize"
```
### Parallel DAG Workflow
Complex workflow with parallel execution branches:
```yaml theme={null}
name: "parallel_analysis"
version: "1.0.0"
description: "Parallel analysis with convergence"
defaults:
model_tier: "medium"
budget_agent_max: 8000
require_approval: false
nodes:
- id: "understand"
type: "simple"
strategy: "react"
budget_max: 1000
depends_on: []
- id: "parallel_analysis"
type: "dag"
budget_max: 5000
depends_on: ["understand"]
metadata:
tasks:
- id: "collect"
tools_allowlist: ["web_search"]
depends_on: []
- id: "quantitative"
tools_allowlist: ["calculator"]
depends_on: ["collect"]
- id: "qualitative"
tools_allowlist: []
depends_on: ["collect"]
- id: "cross_validate"
depends_on: ["quantitative", "qualitative"]
- id: "report"
type: "cognitive"
strategy: "reflection"
budget_max: 2000
degrade_to: "chain_of_thought"
depends_on: ["parallel_analysis"]
edges:
- from: "understand"
to: "parallel_analysis"
- from: "parallel_analysis"
to: "report"
```
## Template Inheritance
Templates can inherit from parent templates using `extends`:
```yaml theme={null}
# base_research.yaml
name: "base_research"
version: "1.0.0"
defaults:
model_tier: "medium"
budget_agent_max: 5000
require_approval: false
```
```yaml theme={null}
# advanced_research.yaml
name: "advanced_research"
version: "1.0.0"
extends: ["base_research"] # Inherits all defaults
defaults:
budget_agent_max: 10000 # Override specific values
nodes:
# ... additional nodes
```
Multiple parents are applied in order, with the derived template's values taking precedence.
## Validation
Templates are validated at load time for:
* YAML syntax correctness
* Required field presence
* DAG acyclicity (no circular dependencies)
* Budget hierarchy (node budgets ≤ agent budget)
* Tool registry existence
* Variable reference resolution
```go theme={null}
registry := templates.InitTemplateRegistry("./templates")
// Validates all templates
if err := registry.Finalize(); err != nil {
log.Fatalf("Template validation failed: %v", err)
}
```
## Best Practices
Each node should do one thing well:
```yaml theme={null}
# Good: Focused nodes
- id: "search"
type: "simple"
strategy: "react"
tools_allowlist: ["web_search"]
- id: "analyze"
type: "cognitive"
strategy: "chain_of_thought"
```
Restrict tools per node for security and predictability:
```yaml theme={null}
# Good: Explicit allowlist
tools_allowlist:
- "web_search"
- "calculator"
# Bad: Allowing all tools
# tools_allowlist: ["*"]
```
Configure fallback strategies for cost control:
```yaml theme={null}
- id: "explore"
strategy: "tree_of_thoughts"
budget_max: 2500
degrade_to: "chain_of_thought"
retries: 1
```
Extract shared defaults into base templates:
```yaml theme={null}
extends: ["base_research"]
defaults:
budget_agent_max: 8000 # Only override what changes
```
Use semantic versioning and specify version in requests:
```yaml theme={null}
name: "my_workflow"
version: "1.0.0" # Increment on breaking changes
```
```json theme={null}
{
"context": {
"template": "my_workflow",
"template_version": "1.0.0"
}
}
```
## Troubleshooting
**Symptom:** `template 'my_workflow' not found`
**Solutions:**
1. Check template file exists in registered directory
2. Verify YAML syntax: `yamllint config/workflows/examples/my_workflow.yaml`
3. Restart orchestrator to reload templates
4. Check orchestrator logs for load errors
**Symptom:** `Template validation failed: circular dependency`
**Solutions:**
1. Review `depends_on` fields for cycles
2. Ensure DAG nodes have acyclic task dependencies
3. Check `edges` don't create loops
**Symptom:** Node execution stops early or degrades unexpectedly
**Solutions:**
1. Increase `budget_max` for affected nodes
2. Configure `degrade_to` for graceful fallback
3. Review total budget vs. sum of node budgets
**Symptom:** `tool 'my_tool' not registered`
**Solutions:**
1. Verify tool is registered in tool registry
2. Check tool name spelling in `tools_allowlist`
3. Ensure LLM service has tool loaded
## Synthesis Templates
Synthesis templates control how Shannon formats final output after all agents complete their work. These Go templates live in `config/templates/synthesis/` and are rendered by the Orchestrator before returning results.
### Available Templates
| Template | Purpose |
| ----------------------------- | ---------------------------------- |
| `normal_default.tmpl` | Default for non-research tasks |
| `research_comprehensive.tmpl` | Detailed research reports |
| `research_concise.tmpl` | Brief research summaries |
| `research_with_facts.tmpl` | Research with fact-based citations |
| `domain_analysis_digest.tmpl` | Domain analysis format |
| `swarm_default.tmpl` | Swarm multi-agent output |
| `test_bullet_summary.tmpl` | Bullet-point test format |
### Base Contract
All synthesis templates inherit behavioral rules from `_base.tmpl`:
* When **CitationAgent is enabled**: no inline `[n]` citations in synthesis — Citation Agent adds them separately
* When **CitationAgent is disabled**: use inline `[n]` matching the `AvailableCitations` list
The `CurrentDate` field is injected into every template, enabling "as of" statements for time-sensitive content.
Templates must preserve structured artifacts (tables, code blocks, JSON) — never flatten them to prose.
Templates should **not** include a "Sources" section. The system appends source citations automatically.
### Template Data
Every synthesis template receives a `SynthesisTemplateData` struct with the following fields:
```go theme={null}
type SynthesisTemplateData struct {
Query string // Original user query
QueryLanguage string // Detected language (e.g., "en", "ja")
ResearchAreas []string // Topics explored by agents
AvailableCitations string // Formatted citation list
CitationCount int // Total citations available
MinCitations int // Minimum citations to include
LanguageInstruction string // Output language directive
AgentResults string // Combined agent output
TargetWords int // Target word count
IsResearch bool // Whether task is research-type
SynthesisStyle string // Selected synthesis style
CitationAgentEnabled bool // Whether Citation Agent is active
CurrentDate string // Current date for temporal context
NewsCount int // Number of news items found
}
```
### Selecting a Synthesis Template
The Orchestrator selects synthesis templates based on the task type and configuration:
```bash theme={null}
curl -X POST http://localhost:8080/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"query": "Research quantum computing trends",
"context": {
"synthesis_style": "research_comprehensive"
}
}'
```
```yaml theme={null}
name: "my_research"
version: "1.0.0"
defaults:
model_tier: "medium"
synthesis_style: "research_with_facts"
```
If no `synthesis_style` is specified, Shannon automatically selects `research_comprehensive` for research tasks and `normal_default` for all others.
## Example Workflows
Shannon ships with 8 example workflow templates in `config/workflows/examples/`. These serve as ready-to-use starting points and reference implementations.
### Workflow Catalog
| Template | Description | Nodes | Pattern |
| ---------------------------------- | ----------------------- | ----- | ------------------------------- |
| `simple_analysis.yaml` | Quick summarization | 2 | Sequential (intake + summarize) |
| `research_summary.yaml` | Multi-stage research | 4 | Sequential with degradation |
| `research_summary_enterprise.yaml` | Enterprise research | 4+ | Sequential with large models |
| `market_analysis.yaml` | Market research | 3+ | Sequential with tools |
| `market_analysis_playbook.yaml` | Market playbook variant | 3+ | Playbook-driven |
| `complex_dag.yaml` | Multi-parallel DAG | 5+ | DAG with fan-out/fan-in |
| `parallel_dag_example.yaml` | Parallel execution demo | 4+ | DAG with parallel branches |
| `parallel_items_example.yaml` | Batch item processing | 3+ | Parallel items |
### YAML Structure Reference
All example workflows follow this structure:
```yaml theme={null}
name: workflow_id
description: "Description"
defaults:
model_tier: small|medium|large # Model size for all nodes
budget_agent_max: 3000 # Total token budget
require_approval: false # Human-in-the-loop gate
nodes:
- id: node_id
type: simple|cognitive|dag # Execution type
strategy: react|chain_of_thought|tree_of_thoughts
depends_on: [other_node_ids] # DAG dependencies
budget_max: 1000 # Per-node token limit
edges:
- from: node1
to: node2
```
### Pattern Degradation
Example workflows demonstrate Shannon's automatic degradation behavior when resources are constrained:
```
DAG → sequential (if insufficient agents available)
tree_of_thoughts → chain_of_thought (if budget constrained)
multi-node → single-node (fallback for minimal environments)
```
Degradation is automatic and logged. Check Orchestrator logs for `strategy_degraded` events to monitor when fallbacks activate.
### Learning Router
When enabled, the **Learning Router** automatically selects workflow templates based on query similarity to past successful executions. This eliminates the need for callers to specify a template explicitly.
```bash theme={null}
# Enable learning router via environment
ORCHESTRATOR_LEARNING_ROUTER_ENABLED=true
# The router will match incoming queries to historical patterns
# and select the best-performing template automatically
```
The Learning Router improves over time. For new deployments, explicitly specify templates until enough execution history accumulates (typically 50+ successful tasks).
## Next Steps
Add tools for your templates
Other extension methods
Environment and YAML config
Understanding Shannon's design
# Vendor Adapters
Source: https://docs.shannon.run/en/tutorials/vendor-adapters
Build vendor-specific integrations with the adapter pattern
## Overview
The **vendor adapter pattern** allows you to integrate domain-specific agents and tools into Shannon without polluting the core codebase. This pattern maintains clean separation between:
* **Generic Shannon infrastructure** (committed to open source)
* **Vendor-specific implementations** (kept private or in separate repositories)
No modifications to Shannon's core codebase required
Generic infrastructure vs. vendor-specific logic
Vendor logic isolated in separate directories
Shannon works even without vendor modules
## When to Use Vendor Adapters
Use vendor adapters when integrating proprietary or internal APIs with domain-specific requirements.
**Use vendor adapters when:**
* Integrating proprietary/internal APIs with domain-specific requirements
* Need custom request/response transformations for OpenAPI tools
* Building specialized agents for specific business domains
* Field naming conventions differ from your internal systems
* Require dynamic parameter injection from session context
* Need custom authentication or header logic
**Example use cases:**
* Analytics platforms (metrics aliasing, time range normalization)
* E-commerce systems (product field mapping, SKU transformations)
* CRM integrations (contact field normalization)
* Internal microservices (custom auth tokens, tenant IDs)
* Domain-specific data validation
## Architecture
### File Structure
```
Shannon/
├── config/
│ ├── shannon.yaml # Base config (generic, committed)
│ └── overlays/
│ └── shannon.myvendor.yaml # Vendor overlay (not committed)
├── config/openapi_specs/
│ └── myvendor_api.yaml # Vendor API spec (not committed)
├── python/llm-service/llm_service/
│ ├── roles/
│ │ ├── presets.py # Generic roles + conditional import
│ │ └── myvendor/ # Vendor role module (not committed)
│ │ ├── __init__.py
│ │ └── custom_agent.py # Specialized agent role
│ └── tools/
│ ├── openapi_tool.py # Generic OpenAPI loader (committed)
│ └── vendor_adapters/ # Vendor adapters (not committed)
│ ├── __init__.py # Adapter registry
│ └── myvendor.py # Vendor-specific transformations
```
### Component Responsibilities
| Component | Responsibility | Committed to OSS |
| -------------------------- | ----------------------------------- | ---------------- |
| **Config Overlay** | Vendor-specific tool configurations | ❌ No |
| **OpenAPI Spec** | API schema definition | ❌ No |
| **Vendor Adapter** | Request/response transformations | ❌ No |
| **Vendor Role** | Specialized agent system prompts | ❌ No |
| **Generic Infrastructure** | Core OpenAPI/role system | ✅ Yes |
## Quick Start Example
Let's create a complete vendor integration for a fictional analytics platform called "DataInsight".
Create `python/llm-service/llm_service/tools/vendor_adapters/datainsight.py`:
```python theme={null}
"""Vendor adapter for DataInsight Analytics API."""
from typing import Any, Dict, List, Optional
class DataInsightAdapter:
"""Transforms requests for DataInsight API conventions."""
def transform_body(
self,
body: Dict[str, Any],
operation_id: str,
prompt_params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Transform request body for DataInsight API.
Args:
body: Original request body from LLM
operation_id: OpenAPI operation ID
prompt_params: Session context parameters (injected by orchestrator)
Returns:
Transformed body matching DataInsight API expectations
"""
if not isinstance(body, dict):
return body
# Inject session context (account_id, user_id from prompt_params)
if prompt_params and isinstance(prompt_params, dict):
if "account_id" in prompt_params and "account_id" not in body:
body["account_id"] = prompt_params["account_id"]
if "user_id" in prompt_params and "user_id" not in body:
body["user_id"] = prompt_params["user_id"]
# Operation-specific transformations
if operation_id == "queryMetrics":
body = self._transform_query_metrics(body)
elif operation_id == "getDimensionValues":
body = self._transform_dimension_values(body)
return body
def _transform_query_metrics(self, body: Dict) -> Dict:
"""Transform metric query requests."""
# Normalize metric names (support shorthand)
metric_aliases = {
"users": "di:unique_users",
"sessions": "di:total_sessions",
"pageviews": "di:page_views",
"bounce_rate": "di:bounce_rate",
}
if isinstance(body.get("metrics"), list):
body["metrics"] = [
metric_aliases.get(m, m) for m in body["metrics"]
]
# Normalize time range format
if "timeRange" in body and isinstance(body["timeRange"], dict):
tr = body["timeRange"]
# Ensure startTime/endTime (not start/end)
if "start" in tr:
tr["startTime"] = tr.pop("start")
if "end" in tr:
tr["endTime"] = tr.pop("end")
# Convert sort to expected format
if isinstance(body.get("sort"), dict):
field = body["sort"].get("field")
order = body["sort"].get("order", "DESC").upper()
body["sort"] = {"field": field, "direction": order}
return body
def _transform_dimension_values(self, body: Dict) -> Dict:
"""Transform dimension value requests."""
dimension_aliases = {
"country": "di:geo_country",
"device": "di:device_type",
"source": "di:traffic_source",
}
if "dimension" in body:
body["dimension"] = dimension_aliases.get(
body["dimension"], body["dimension"]
)
return body
```
Edit `python/llm-service/llm_service/tools/vendor_adapters/__init__.py`:
```python theme={null}
from typing import Optional
def get_vendor_adapter(name: str):
"""Return a vendor adapter instance by name, or None if not available."""
if not name:
return None
try:
if name.lower() == "datainsight":
from .datainsight import DataInsightAdapter
return DataInsightAdapter()
# Add more vendors here
# elif name.lower() == "othervendor":
# from .othervendor import OtherVendorAdapter
# return OtherVendorAdapter()
except Exception:
return None
return None
```
Create `config/overlays/shannon.datainsight.yaml`:
```yaml theme={null}
# DataInsight Analytics Integration
# Usage: SHANNON_CONFIG_PATH=config/overlays/shannon.datainsight.yaml
openapi_tools:
datainsight_analytics:
enabled: true
spec_path: config/openapi_specs/datainsight_api.yaml
auth_type: bearer
auth_config:
vendor: datainsight # This triggers adapter loading
token: "${DATAINSIGHT_API_TOKEN}"
extra_headers:
X-Account-ID: "{{body.account_id}}" # Dynamic from request body
X-User-ID: "${DATAINSIGHT_USER_ID}" # Static from env
category: analytics
base_cost_per_use: 0.002
rate_limit: 60
timeout_seconds: 30
operations:
- queryMetrics
- getDimensionValues
```
Create `python/llm-service/llm_service/roles/datainsight/analytics_agent.py`:
```python theme={null}
"""DataInsight Analytics Agent role preset."""
ANALYTICS_AGENT_PRESET = {
"name": "datainsight_analytics",
"system_prompt": """You are a specialized data analytics agent with access to DataInsight Analytics API.
Your mission: Provide actionable insights from web analytics data.
## Available Tools
- queryMetrics: Retrieve metrics like users, sessions, pageviews, bounce rate
- getDimensionValues: Get dimension values (countries, devices, traffic sources)
## Output Format
Always structure your response as:
1. **dataResult** block (JSON) - for visualization
2. **Summary** section - key findings
3. **Insights** section - actionable recommendations
## Best Practices
- Always include time range in queries
- Use metric aliases: "users", "sessions", "pageviews" (auto-converted)
- Request relevant dimensions for context
- Provide comparative analysis when possible
- Highlight anomalies and trends
Remember: Your goal is to help users understand their data and make better decisions.""",
"allowed_tools": [
"queryMetrics",
"getDimensionValues",
],
"temperature": 0.7,
"response_format": None,
}
```
> Note: `allowed_tools` semantics for `/agent/query`:
>
> * Omit/`null` → role presets may enable tools
> * `[]` → tools disabled
> * `["name", …]` → only these tools are available (names must match registered tools)
Register in `python/llm-service/llm_service/roles/presets.py`:
```python theme={null}
# At the end of _load_presets() function:
try:
from .datainsight.analytics_agent import ANALYTICS_AGENT_PRESET
_PRESETS["datainsight_analytics"] = ANALYTICS_AGENT_PRESET
except ImportError:
pass # Graceful fallback if vendor module not available
```
Add to `.env`:
```bash theme={null}
# DataInsight Configuration
SHANNON_CONFIG_PATH=config/overlays/shannon.datainsight.yaml
DATAINSIGHT_API_TOKEN=your_bearer_token_here
DATAINSIGHT_USER_ID=your_user_id_here
# Domain allowlist (dev: use *, prod: specific domains)
OPENAPI_ALLOWED_DOMAINS=api.datainsight.com
```
Rebuild and test:
```bash theme={null}
# Rebuild services
docker compose -f deploy/compose/docker-compose.yml build --no-cache llm-service orchestrator
docker compose -f deploy/compose/docker-compose.yml up -d
# Wait for health
sleep 10
# Test via gRPC
SESSION_ID="test-$(date +%s)"
grpcurl -plaintext -d '{
"metadata": {
"user_id": "test-user",
"session_id": "'$SESSION_ID'"
},
"query": "Show me user growth trends for the past 30 days",
"context": {
"role": "datainsight_analytics",
"prompt_params": {
"account_id": "acct_12345",
"user_id": "user_67890"
}
}
}' localhost:50052 shannon.orchestrator.OrchestratorService/SubmitTask
```
## Component Guide
### 1. Vendor Adapter Class
**Purpose:** Transform requests/responses for vendor-specific API conventions
**Common transformation patterns:**
* **Field aliasing**: `revenue` → `total_revenue`
* **Metric prefixing**: `users` → `my:users`
* **Time range normalization**: `{start, end}` → `{startTime, endTime}`
* **Sort format conversion**: `{field, order}` → `{column, direction}`
* **Filter structure reshaping**: list → object with logic operators
* **Default injection**: Add missing required fields from session context
### 2. Config Overlay
**Purpose:** Define vendor-specific tool configurations without modifying base config
**Header values:**
* `"${ENV_VAR}"` - Resolved from environment variables
* Static strings - Used as-is
Dynamic header templating from the request body (e.g., `{{body.field}}`) is not supported. If headers must depend on body/session values, either:
* Define those headers as explicit header parameters in the OpenAPI spec and pass them as tool parameters, or
* Use a vendor adapter to shape the request body, while headers remain static/env-driven.
### 3. Vendor Role
**Purpose:** Specialized agent with domain-specific knowledge and tool restrictions
**Template:**
```python theme={null}
"""Vendor-specific agent role."""
MY_AGENT_PRESET = {
"name": "my_agent",
"system_prompt": """You are a specialized agent for [domain].
Your mission: [clear objective]
## Available Tools
- tool1: [description]
- tool2: [description]
## Output Format
[specific format requirements]
## Best Practices
- [guideline 1]
- [guideline 2]
Remember: [key instruction]""",
"allowed_tools": [
"tool1",
"tool2",
],
"temperature": 0.7,
"response_format": None, # or {"type": "json_object"}
}
```
> Note: When you explicitly pass `allowed_tools`, only the listed tools will be available to the LLM. Pass an empty list `[]` to disable tools.
## Best Practices
**✅ Good:** Transform field names, inject defaults
```python theme={null}
def transform_body(self, body, operation_id, prompt_params):
# Generic field normalization
if "start_date" in body and "startTime" not in body:
body["startTime"] = body.pop("start_date")
return body
```
**❌ Bad:** Business logic in adapter
```python theme={null}
def transform_body(self, body, operation_id, prompt_params):
# Don't do complex business logic here
if body["revenue"] > 1000000:
body["alert"] = "high_revenue" # Belongs in application layer
```
```python theme={null}
try:
from .myvendor import MyVendorAdapter
return MyVendorAdapter()
except ImportError:
pass # Shannon works without vendor module
except Exception as e:
logger.warning(f"Failed to load vendor adapter: {e}")
return None
```
```python theme={null}
def transform_body(self, body, operation_id, prompt_params):
"""
Transform body for MyVendor API.
Transformations:
- Metric names: "users" → "mv:unique_users"
- Time range: {start, end} → {startTime, endTime}
- Sort format: {field, order} → {column, direction}
- Inject tenant_id from prompt_params
"""
```
**✅ Good:**
```yaml theme={null}
auth_config:
token: "${MYVENDOR_TOKEN}"
```
**❌ Bad:**
```yaml theme={null}
auth_config:
token: "sk-1234567890abcdef" # Never hardcode!
```
```python theme={null}
# tests/test_myvendor_adapter.py
def test_metric_aliasing():
adapter = MyVendorAdapter()
body = {"metrics": ["users", "sessions"]}
result = adapter.transform_body(body, "queryMetrics", None)
assert result["metrics"] == ["mv:unique_users", "mv:total_sessions"]
```
```python theme={null}
def transform_body(self, body, operation_id, prompt_params):
if not isinstance(body, dict):
return body # Don't transform non-dict
# Validate required fields
if operation_id == "queryData" and "metrics" not in body:
return body # Let API return validation error
```
## Testing & Verification
### Unit Test Adapter
```python theme={null}
# tests/vendor/test_datainsight_adapter.py
import pytest
from llm_service.tools.vendor_adapters.datainsight import DataInsightAdapter
def test_metric_aliasing():
adapter = DataInsightAdapter()
body = {"metrics": ["users", "pageviews"]}
result = adapter.transform_body(body, "queryMetrics", None)
assert result["metrics"] == ["di:unique_users", "di:page_views"]
def test_session_param_injection():
adapter = DataInsightAdapter()
body = {}
prompt_params = {"account_id": "acct_123", "user_id": "user_456"}
result = adapter.transform_body(body, "queryMetrics", prompt_params)
assert result["account_id"] == "acct_123"
assert result["user_id"] == "user_456"
def test_time_range_normalization():
adapter = DataInsightAdapter()
body = {"timeRange": {"start": "2025-01-01", "end": "2025-01-31"}}
result = adapter.transform_body(body, "queryMetrics", None)
assert result["timeRange"]["startTime"] == "2025-01-01"
assert result["timeRange"]["endTime"] == "2025-01-31"
assert "start" not in result["timeRange"]
```
### Integration Test
```bash theme={null}
#!/bin/bash
# tests/e2e/test_datainsight_integration.sh
SESSION_ID="test-datainsight-$(date +%s)"
# Submit test query
grpcurl -plaintext -d '{
"metadata": {"user_id": "test", "session_id": "'$SESSION_ID'"},
"query": "Show user growth for the past week",
"context": {
"role": "datainsight_analytics",
"prompt_params": {
"account_id": "test_account",
"user_id": "test_user"
}
}
}' localhost:50052 shannon.orchestrator.OrchestratorService/SubmitTask
# Check logs for adapter application
docker logs shannon-llm-service-1 --tail 100 | grep "datainsight"
```
## Troubleshooting
Symptom: Logs show "Vendor adapter '' applied" (empty string)
Fix:
```yaml theme={null}
# Ensure vendor name is set in config overlay
auth_config:
vendor: myvendor # Must match adapter name
```
Symptom: `ImportError: No module named 'myvendor'`
Fix:
```python theme={null}
# Use try/except in __init__.py
try:
from .myvendor import MyVendorAdapter
return MyVendorAdapter()
except ImportError as e:
logger.warning(f"Vendor adapter not available: {e}")
return None
```
Symptom: API receives original body, not transformed
Debug:
```python theme={null}
# Add logging in adapter
def transform_body(self, body, operation_id, prompt_params):
logger.info(f"BEFORE transform: {body}")
# ... transformations ...
logger.info(f"AFTER transform: {body}")
return body
```
Check:
1. Adapter registered in `__init__.py`
2. Vendor name matches in config
3. `auth_config.vendor` field present
4. Adapter returns modified dict (not None)
Symptom: `prompt_params` is None in adapter
Cause: Orchestrator not sending session context
Fix: Ensure context sent in gRPC request:
```json theme={null}
{
"context": {
"role": "my_agent",
"prompt_params": {
"account_id": "123",
"user_id": "456"
}
}
}
```
## Summary
* ✅ Clean separation: generic code vs. vendor-specific
* ✅ No Shannon core changes required
* ✅ Conditional loading with graceful fallback
* ✅ Environment-based secrets management
* ✅ Testable in isolation
* ✅ Easy to maintain and extend
**Three components:**
1. **Vendor Adapter** - Request/response transformations
2. **Config Overlay** - Tool configurations
3. **Vendor Role** - Specialized agent (optional)
**Quick reference:**
```bash theme={null}
# Structure
config/overlays/shannon.myvendor.yaml
config/openapi_specs/myvendor_api.yaml
python/llm-service/llm_service/tools/vendor_adapters/myvendor.py
python/llm-service/llm_service/roles/myvendor/my_agent.py
# Environment
SHANNON_CONFIG_PATH=config/overlays/shannon.myvendor.yaml
MYVENDOR_API_TOKEN=your_token
# Test
docker compose build --no-cache llm-service orchestrator
docker compose up -d
./scripts/submit_task.sh "Your query here"
```
## Next Steps
Learn how to add custom tools
Explore other extension methods
Complete configuration reference
Understand Shannon's architecture