Coverage for src / agent_contracts / routing.py: 100%
24 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 00:42 +0900
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-09 00:42 +0900
1"""Routing types for traceable supervisor decisions.
3These types provide structured, explainable routing decisions
4for debugging and observability.
5"""
6from __future__ import annotations
8from pydantic import BaseModel, Field
10from typing import TYPE_CHECKING
12if TYPE_CHECKING:
13 from agent_contracts.supervisor import SupervisorDecision
16# =============================================================================
17# Traceable Routing Types
18# =============================================================================
20class MatchedRule(BaseModel):
21 """A matched trigger condition.
23 Represents a single rule that matched during supervisor evaluation.
25 Attributes:
26 node: Node name that matched
27 condition: Human-readable condition description
28 priority: Trigger priority (higher = evaluated first)
30 Example:
31 rule = MatchedRule(
32 node="search",
33 condition="request.action=search",
34 priority=100,
35 )
36 """
37 node: str = Field(description="Node name")
38 condition: str = Field(description="Human-readable condition description")
39 priority: int = Field(description="Trigger priority")
42class RoutingReason(BaseModel):
43 """Detailed routing decision reason.
45 Provides structured explanation of why a particular node was selected.
47 Attributes:
48 decision_type: Type of decision made
49 matched_rules: List of rules that matched
50 llm_used: Whether LLM was used for the decision
51 llm_reasoning: LLM's reasoning if used
53 Decision Types:
54 - terminal_state: Response type triggered exit
55 - explicit_routing: Answer routed to question owner
56 - rule_match: TriggerCondition matched
57 - llm_decision: LLM made the choice
58 - fallback: No match, using default
59 """
60 decision_type: str = Field(
61 description="Type of decision: terminal_state, explicit_routing, rule_match, llm_decision, fallback"
62 )
63 matched_rules: list[MatchedRule] = Field(
64 default_factory=list,
65 description="List of matched trigger rules"
66 )
67 llm_used: bool = Field(default=False, description="Whether LLM was used for decision")
68 llm_reasoning: str | None = Field(default=None, description="LLM's reasoning if used")
71class RoutingDecision(BaseModel):
72 """Complete routing decision with traceability.
74 The main output of `GenericSupervisor.decide_with_trace()`.
75 Provides full visibility into how a routing decision was made.
77 Attributes:
78 selected_node: The node that was selected
79 reason: Detailed reason for the decision
81 Example:
82 decision = await supervisor.decide_with_trace(state)
84 print(f"Selected: {decision.selected_node}")
85 print(f"Type: {decision.reason.decision_type}")
87 for rule in decision.reason.matched_rules:
88 print(f" P{rule.priority}: {rule.node} - {rule.condition}")
89 """
90 selected_node: str = Field(description="Selected node name")
91 reason: RoutingReason = Field(description="Decision reason details")
93 def to_supervisor_decision(self) -> "SupervisorDecision":
94 """Convert to SupervisorDecision for backward compatibility.
96 Returns:
97 SupervisorDecision with condensed reasoning string
98 """
99 # Import here to avoid circular dependency
100 from agent_contracts.supervisor import SupervisorDecision
102 reasoning_parts = [self.reason.decision_type]
103 if self.reason.matched_rules:
104 rules_str = ", ".join(r.node for r in self.reason.matched_rules)
105 reasoning_parts.append(f"candidates: [{rules_str}]")
106 if self.reason.llm_used:
107 reasoning_parts.append("LLM")
108 return SupervisorDecision(
109 next_node=self.selected_node,
110 reasoning=" | ".join(reasoning_parts)
111 )