traitclaw_team/
conditional_router.rs1use regex::Regex;
27
28use crate::router::{AgentId, Router, RoutingDecision, TeamMessage, TeamState};
29
30struct RoutingRule {
32 pattern: Regex,
33 target: AgentId,
34}
35
36pub struct ConditionalRouter {
41 rules: Vec<RoutingRule>,
42 default_target: Option<AgentId>,
43}
44
45impl ConditionalRouter {
46 #[must_use]
48 pub fn new() -> Self {
49 Self {
50 rules: Vec::new(),
51 default_target: None,
52 }
53 }
54
55 #[must_use]
63 pub fn when(mut self, pattern: &str, target: impl Into<AgentId>) -> Self {
64 let re = Regex::new(pattern).unwrap_or_else(|e| panic!("Invalid regex '{pattern}': {e}"));
65 self.rules.push(RoutingRule {
66 pattern: re,
67 target: target.into(),
68 });
69 self
70 }
71
72 #[must_use]
76 pub fn default(mut self, target: impl Into<AgentId>) -> Self {
77 self.default_target = Some(target.into());
78 self
79 }
80}
81
82impl Default for ConditionalRouter {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl Router for ConditionalRouter {
89 fn route(&self, message: &TeamMessage, _state: &TeamState) -> RoutingDecision {
90 let content = &message.content;
91
92 for rule in &self.rules {
94 if rule.pattern.is_match(content) {
95 return RoutingDecision::SendTo(rule.target.clone());
96 }
97 }
98
99 match &self.default_target {
101 Some(target) => RoutingDecision::SendTo(target.clone()),
102 None => RoutingDecision::Complete(String::new()),
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::router::{TeamMessage, TeamState};
111
112 fn state() -> TeamState {
113 TeamState::new(vec!["researcher".into(), "writer".into(), "general".into()])
114 }
115
116 #[test]
117 fn test_conditional_router_matches_first_rule() {
118 let router = ConditionalRouter::new()
120 .when("search|find|look up", "researcher")
121 .when("write|draft", "writer")
122 .default("general");
123
124 let msg = TeamMessage::new("user", "Please search for Rust benchmarks");
125 let decision = router.route(&msg, &state());
126 assert_eq!(decision, RoutingDecision::SendTo("researcher".into()));
127 }
128
129 #[test]
130 fn test_conditional_router_second_rule_matches() {
131 let router = ConditionalRouter::new()
132 .when("search", "researcher")
133 .when("write|draft", "writer")
134 .default("general");
135
136 let msg = TeamMessage::new("user", "Please draft a blog post");
137 let decision = router.route(&msg, &state());
138 assert_eq!(decision, RoutingDecision::SendTo("writer".into()));
139 }
140
141 #[test]
142 fn test_conditional_router_fallback_to_default() {
143 let router = ConditionalRouter::new()
144 .when("search", "researcher")
145 .default("general");
146
147 let msg = TeamMessage::new("user", "Tell me a joke");
148 let decision = router.route(&msg, &state());
149 assert_eq!(decision, RoutingDecision::SendTo("general".into()));
150 }
151
152 #[test]
153 fn test_conditional_router_no_match_no_default() {
154 let router = ConditionalRouter::new().when("search", "researcher");
155
156 let msg = TeamMessage::new("user", "Tell me a joke");
157 let decision = router.route(&msg, &state());
158 assert_eq!(decision, RoutingDecision::Complete(String::new()));
159 }
160
161 #[test]
162 fn test_conditional_router_case_insensitive_via_regex() {
163 let router = ConditionalRouter::new()
165 .when("(?i)search|(?i)find", "researcher")
166 .default("general");
167
168 let msg = TeamMessage::new("user", "SEARCH for information");
169 let decision = router.route(&msg, &state());
170 assert_eq!(decision, RoutingDecision::SendTo("researcher".into()));
171 }
172
173 #[test]
174 fn test_conditional_router_empty_rules_uses_default() {
175 let router = ConditionalRouter::new().default("general");
176
177 let msg = TeamMessage::new("user", "anything at all");
178 let decision = router.route(&msg, &state());
179 assert_eq!(decision, RoutingDecision::SendTo("general".into()));
180 }
181
182 #[test]
183 fn test_conditional_router_is_router_trait_object() {
184 let r: Box<dyn Router> = Box::new(ConditionalRouter::new().default("fallback"));
185 let msg = TeamMessage::new("user", "test");
186 let decision = r.route(&msg, &state());
187 assert_eq!(decision, RoutingDecision::SendTo("fallback".into()));
188 }
189}