backend/src/router/router.py
"""Multi-phase confidence router with structured trace output.""" from __future__ import annotations import logging import time from dataclasses import dataclass from typing import Any from agents.base.agent_context import AgentContext from core.trace_context import PatternMatchDetail, RequestTrace, RoutingTrace from core.tracing import ( log_pattern_match, log_routing_decision, log_routing_guard, log_routing_phase, log_routing_start, ) from models.routing import RoutingDecision from observability import log_event, record_metric from router.config_loader import load_routes, load_thresholds from router.entities import extract_entities from router.pattern_matcher import RouteScore, score_routes logger = logging.getLogger("agentic.request") trace_logger = logging.getLogger("agentic.trace") BLOCKED_PATTERNS = ("ignore previous instructions", "jailbreak") @dataclass class RouteResult: routing: RoutingTrace execute_skill: str = "chat" def _to_pattern_details(scores: list[RouteScore]) -> list[PatternMatchDetail]: return [ PatternMatchDetail( route_id=score.route_id, pattern=score.pattern, confidence=score.confidence, entities=score.entities, matched=score.matched, ) for score in scores ] def _best_match(scores: list[RouteScore]) -> RouteScore | None: for score in scores: if score.matched and score.confidence > 0: return score return None def route_request(trace: RequestTrace, message: str) -> RouteResult: log_routing_start(trace, router_enabled=True) log_event( "begin", component="Router", trace_id=trace.trace_id, fields={"message_preview": message[:80]}, ) trace_logger.debug( "routing_begin_detail trace_id=%s message_len=%s", trace.trace_id, len(message), ) thresholds = load_thresholds() high_confidence = float(thresholds.get("high_confidence", 0.70)) mid_confidence = float(thresholds.get("mid_confidence", 0.40)) phase2_trigger = float(thresholds.get("phase2_trigger", 0.25)) lowered = message.lower() for guard in BLOCKED_PATTERNS: if guard in lowered: log_routing_guard(trace, guard=guard, blocked=True) routing = RoutingTrace( router_enabled=True, phase_reached="phase0", guard_triggered=guard, intent="blocked", destination_type="skill", destination_id="chat", decision_reason=f"Phase 0 guard matched: {guard}", ) trace.routing = routing log_routing_decision(trace, routing) return RouteResult(routing=routing, execute_skill="chat") log_routing_guard(trace, guard="none", blocked=False) log_routing_phase(trace, phase="phase1", reason="pattern and entity matching") scores = score_routes(message, load_routes()) pattern_details = _to_pattern_details(scores) for detail in pattern_details: log_pattern_match(trace, detail) winner = _best_match(scores) if winner is None: confidence = 0.0 else: confidence = winner.confidence if winner and confidence >= high_confidence: routing = RoutingTrace( router_enabled=True, phase_reached="phase1", pattern_matches=pattern_details, winning_route_id=winner.route_id, intent=winner.route_id, confidence=confidence, destination_type=winner.destination_type, destination_id=winner.destination_id, decision_reason=( f"Phase 1 high confidence ({confidence:.2f} >= {high_confidence:.2f})" ), ) trace.routing = routing log_routing_decision(trace, routing) return RouteResult( routing=routing, execute_skill="chat" if winner.destination_type == "skill" else "chat", ) if winner and confidence >= mid_confidence: routing = RoutingTrace( router_enabled=True, phase_reached="phase1", pattern_matches=pattern_details, winning_route_id=winner.route_id, intent=winner.route_id, confidence=confidence, destination_type=winner.destination_type, destination_id=winner.destination_id, decision_reason=( f"Phase 1 mid confidence ({confidence:.2f} >= {mid_confidence:.2f})" ), ) trace.routing = routing log_routing_decision(trace, routing) return RouteResult( routing=routing, execute_skill="chat", ) if confidence < phase2_trigger: log_routing_phase( trace, phase="phase2", reason=( f"confidence {confidence:.2f} below phase2_trigger {phase2_trigger:.2f}" ), ) routing = RoutingTrace( router_enabled=True, phase_reached="phase2", pattern_matches=pattern_details, winning_route_id=winner.route_id if winner else None, intent=winner.route_id if winner else "general_chat", confidence=confidence, destination_type="skill", destination_id="chat", phase2_llm_used=False, decision_reason=( "Phase 2 classifier not implemented — fallback chat skill (no classifier LLM call)" ), ) trace.routing = routing log_routing_decision(trace, routing) return RouteResult(routing=routing, execute_skill="chat") log_routing_phase( trace, phase="fallback", reason=f"confidence {confidence:.2f} in ambiguous band", ) routing = RoutingTrace( router_enabled=True, phase_reached="fallback", pattern_matches=pattern_details, winning_route_id=winner.route_id if winner else None, intent="general_chat", confidence=confidence, destination_type="skill", destination_id="chat", decision_reason="Ambiguous confidence — default chat skill", ) trace.routing = routing log_routing_decision(trace, routing) return RouteResult(routing=routing, execute_skill="chat") def _required_skills_for(route_id: str, routes: list[dict[str, Any]]) -> list[str]: for route in routes: if route.get("id") == route_id: return list(route.get("required_skills", [])) return [] def _emit_route_complete( decision: RoutingDecision, trace_id: str, session_id: str | None, started_at: float, *, guard: str | None = None, ) -> None: latency_ms = (time.perf_counter() - started_at) * 1000 fields: dict[str, Any] = { "session_id": session_id, "route_name": decision.route_name, "skills": decision.skills, "confidence": decision.confidence, "low_confidence": decision.low_confidence, "latency_ms": round(latency_ms, 2), } if guard is not None: fields["guard"] = guard log_event("route_complete", component="Router", trace_id=trace_id, fields=fields) record_metric( "router.latency_ms", latency_ms, tags={"matched": "true" if decision.route_name else "false"}, ) record_metric( "router.matched", 1.0 if decision.route_name else 0.0, ) def route(query: str, agent_context: AgentContext) -> RoutingDecision: """Score the raw query and produce a `RoutingDecision` (`routing-design.md` §2/§6, Revision 6). Takes only `query` + `AgentContext` — never `RequestContext` (see the Router-scores- raw-query invariant). Reuses `score_routes()`/`extract_entities()` unchanged; this function only adds tiering (`low_confidence`), multi-skill resolution, and the concrete no-route-matched shape on top of them. """ trace_id = agent_context.trace_id session_id = agent_context.session_id started_at = time.perf_counter() log_event( "route_start", component="Router", trace_id=trace_id, fields={"session_id": session_id, "message_preview": query[:80]}, ) lowered = query.lower() for guard in BLOCKED_PATTERNS: if guard in lowered: decision = RoutingDecision( route_name=None, skills=[], confidence=0.0, matched_entities={}, low_confidence=False, reasoning=f"Phase 0 guard matched: {guard!r} — routed to fallback chat", ) _emit_route_complete(decision, trace_id, session_id, started_at, guard=guard) return decision thresholds = load_thresholds() high_confidence = float(thresholds.get("high_confidence", 0.70)) fallback_margin = float(thresholds.get("fallback_margin", 0.30)) low_confidence_floor = high_confidence - fallback_margin routes = load_routes() entities = extract_entities(query) scores = score_routes(query, routes) winner = _best_match(scores) best_score_seen = scores[0].confidence if scores else 0.0 if winner and winner.confidence >= high_confidence: decision = RoutingDecision( route_name=winner.route_id, skills=_required_skills_for(winner.route_id, routes), confidence=winner.confidence, matched_entities=entities, low_confidence=False, reasoning=( f"High confidence ({winner.confidence:.2f} >= " f"high_confidence {high_confidence:.2f})" ), ) elif winner and winner.confidence >= low_confidence_floor: decision = RoutingDecision( route_name=winner.route_id, skills=_required_skills_for(winner.route_id, routes), confidence=winner.confidence, matched_entities=entities, low_confidence=True, reasoning=( f"Low confidence, within fallback margin ({winner.confidence:.2f} in " f"[{low_confidence_floor:.2f}, {high_confidence:.2f})) — selected with " f"low_confidence=true" ), ) else: decision = RoutingDecision( route_name=None, skills=[], confidence=best_score_seen, matched_entities=entities, low_confidence=False, reasoning=( f"No route matched (best score seen {best_score_seen:.2f} below " f"fallback floor {low_confidence_floor:.2f})" ), ) _emit_route_complete(decision, trace_id, session_id, started_at) return decision
Follow the work.
Occasional updates on SignalFoundry, MarketCompass, and what we are building at CompassFoundry Labs.