symbiont/symbiont/router.py
Symbiont 49f73e5b46 Fix CLI flags: remove --max-tokens, add --dangerously-skip-permissions
Claude Code CLI uses positional args for prompts, not --prompt flag.
Also enables full access mode so orchestrator runs without permission prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 19:32:00 +00:00

140 lines
4.8 KiB
Python

"""
Router: Classifies tasks and picks the cheapest capable model.
The router itself runs on Haiku (cheapest) to minimize overhead.
It examines the task and assigns a model tier based on complexity.
"""
import json
import logging
from typing import Optional
from .dispatcher import ModelTier, dispatch
logger = logging.getLogger(__name__)
# Classification prompt — this runs on Haiku to keep costs minimal
CLASSIFIER_SYSTEM_PROMPT = """You are a task classifier for an AI orchestration system.
Your job is to assess a task and decide which model tier should handle it.
Tiers:
- HAIKU: Simple tasks. Template filling, reformatting, extraction from structured data,
classification, boilerplate generation, simple Q&A. ~70% of tasks.
- SONNET: Medium tasks. Content writing, code generation, moderate analysis,
customer-facing communication, summarization of complex documents. ~25% of tasks.
- OPUS: Complex tasks. Strategic decisions, novel problem-solving, multi-step reasoning,
quality review of important outputs, creative/nuanced work. ~5% of tasks.
Respond with ONLY a JSON object:
{"tier": "HAIKU"|"SONNET"|"OPUS", "confidence": 0.0-1.0, "reason": "brief explanation"}
"""
def classify_task(task_description: str) -> tuple[ModelTier, float, str]:
"""
Use Haiku to classify which tier should handle this task.
Returns (tier, confidence, reason).
If Haiku is unavailable (rate-limited), falls back to a simple
heuristic classifier.
"""
result = dispatch(
prompt=f"Classify this task:\n\n{task_description}",
tier=ModelTier.HAIKU,
system_prompt=CLASSIFIER_SYSTEM_PROMPT,
)
if result.success:
try:
# Try to parse the JSON response
text = result.output.strip()
# Handle markdown code blocks
if text.startswith("```"):
text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
classification = json.loads(text)
tier_name = classification.get("tier", "SONNET").upper()
confidence = float(classification.get("confidence", 0.5))
reason = classification.get("reason", "no reason given")
tier = {
"HAIKU": ModelTier.HAIKU,
"SONNET": ModelTier.SONNET,
"OPUS": ModelTier.OPUS,
}.get(tier_name, ModelTier.SONNET)
logger.info(f"Classified as {tier.value} (confidence={confidence}): {reason}")
return tier, confidence, reason
except (json.JSONDecodeError, KeyError, ValueError) as e:
logger.warning(f"Failed to parse classification: {e}, falling back to heuristic")
# Fallback: simple heuristic classifier (free, no LLM needed)
return _heuristic_classify(task_description)
def _heuristic_classify(task: str) -> tuple[ModelTier, float, str]:
"""
Dead-simple keyword heuristic. Used when Haiku is unavailable.
Costs zero tokens — pure Python.
"""
task_lower = task.lower()
# OPUS indicators
opus_signals = [
"design", "architect", "strategy", "analyze complex", "review",
"creative", "novel", "brainstorm", "plan", "evaluate",
"multi-step", "reasoning", "trade-off", "nuanced",
]
if any(signal in task_lower for signal in opus_signals):
return ModelTier.OPUS, 0.4, "heuristic: complexity keywords detected"
# HAIKU indicators
haiku_signals = [
"extract", "format", "convert", "classify", "list",
"template", "simple", "boilerplate", "parse", "json",
"reformat", "count", "sort", "filter", "summarize short",
]
if any(signal in task_lower for signal in haiku_signals):
return ModelTier.HAIKU, 0.5, "heuristic: simplicity keywords detected"
# Default to SONNET
return ModelTier.SONNET, 0.3, "heuristic: defaulting to sonnet"
def route_task(
task: str,
system_prompt: Optional[str] = None,
force_tier: Optional[ModelTier] = None,
) -> dict:
"""
Full routing pipeline: classify the task, dispatch to the right model,
return the result with metadata.
"""
if force_tier:
tier = force_tier
confidence = 1.0
reason = "forced by caller"
else:
tier, confidence, reason = classify_task(task)
from .dispatcher import dispatch_with_fallback
result = dispatch_with_fallback(
prompt=task,
preferred_tier=tier,
system_prompt=system_prompt,
)
return {
"output": result.output,
"success": result.success,
"model_used": result.model.value,
"model_requested": tier.value,
"classification_confidence": confidence,
"classification_reason": reason,
"elapsed_seconds": result.elapsed_seconds,
"estimated_cost_usd": result.estimated_cost_usd,
"rate_limited": result.rate_limited,
"error": result.error,
}