Skip to main content

rustant_core/channels/
routing.rs

1//! Channel routing — rule-based routing of incoming messages to agents.
2
3use super::{ChannelMessage, ChannelType};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// A routing condition used to match messages.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum RoutingCondition {
10    /// Match by channel type.
11    ChannelType(ChannelType),
12    /// Match by sender user ID.
13    UserId(String),
14    /// Match if the text content contains a substring.
15    MessageContains(String),
16    /// Match by command prefix (e.g., "/agent2").
17    CommandPrefix(String),
18}
19
20/// A routing rule: conditions + target agent.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct RoutingRule {
23    pub priority: u32,
24    pub conditions: Vec<RoutingCondition>,
25    pub target_agent: Uuid,
26}
27
28/// Routes incoming channel messages to the appropriate agent.
29#[derive(Debug, Clone, Default)]
30pub struct ChannelRouter {
31    rules: Vec<RoutingRule>,
32    default_agent: Option<Uuid>,
33}
34
35impl ChannelRouter {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Set the default agent for unmatched messages.
41    pub fn with_default_agent(mut self, agent_id: Uuid) -> Self {
42        self.default_agent = Some(agent_id);
43        self
44    }
45
46    /// Add a routing rule.
47    pub fn add_rule(&mut self, rule: RoutingRule) {
48        self.rules.push(rule);
49        self.rules.sort_by(|a, b| a.priority.cmp(&b.priority));
50    }
51
52    /// Number of rules configured.
53    pub fn rule_count(&self) -> usize {
54        self.rules.len()
55    }
56
57    /// Route a message to the appropriate agent. Returns the target agent ID.
58    pub fn route(&self, msg: &ChannelMessage) -> Option<Uuid> {
59        for rule in &self.rules {
60            if self.matches_rule(rule, msg) {
61                return Some(rule.target_agent);
62            }
63        }
64        self.default_agent
65    }
66
67    fn matches_rule(&self, rule: &RoutingRule, msg: &ChannelMessage) -> bool {
68        rule.conditions
69            .iter()
70            .all(|cond| self.matches_condition(cond, msg))
71    }
72
73    fn matches_condition(&self, cond: &RoutingCondition, msg: &ChannelMessage) -> bool {
74        match cond {
75            RoutingCondition::ChannelType(ct) => msg.channel_type == *ct,
76            RoutingCondition::UserId(id) => msg.sender.id == *id,
77            RoutingCondition::MessageContains(sub) => msg
78                .content
79                .as_text()
80                .map(|t| t.contains(sub.as_str()))
81                .unwrap_or(false),
82            RoutingCondition::CommandPrefix(prefix) => msg
83                .content
84                .as_text()
85                .map(|t| t.starts_with(prefix.as_str()))
86                .unwrap_or(false),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::channels::ChannelUser;
95
96    fn make_msg(channel_type: ChannelType, user_id: &str, text: &str) -> ChannelMessage {
97        let sender = ChannelUser::new(user_id, channel_type);
98        ChannelMessage::text(channel_type, "ch1", sender, text)
99    }
100
101    #[test]
102    fn test_router_no_rules_no_default() {
103        let router = ChannelRouter::new();
104        let msg = make_msg(ChannelType::Telegram, "u1", "hello");
105        assert!(router.route(&msg).is_none());
106    }
107
108    #[test]
109    fn test_router_default_agent() {
110        let default_id = Uuid::new_v4();
111        let router = ChannelRouter::new().with_default_agent(default_id);
112        let msg = make_msg(ChannelType::Slack, "u1", "hello");
113        assert_eq!(router.route(&msg), Some(default_id));
114    }
115
116    #[test]
117    fn test_router_channel_type_rule() {
118        let agent_tg = Uuid::new_v4();
119        let agent_sl = Uuid::new_v4();
120
121        let mut router = ChannelRouter::new();
122        router.add_rule(RoutingRule {
123            priority: 1,
124            conditions: vec![RoutingCondition::ChannelType(ChannelType::Telegram)],
125            target_agent: agent_tg,
126        });
127        router.add_rule(RoutingRule {
128            priority: 2,
129            conditions: vec![RoutingCondition::ChannelType(ChannelType::Slack)],
130            target_agent: agent_sl,
131        });
132
133        let tg_msg = make_msg(ChannelType::Telegram, "u1", "hi");
134        assert_eq!(router.route(&tg_msg), Some(agent_tg));
135
136        let sl_msg = make_msg(ChannelType::Slack, "u1", "hi");
137        assert_eq!(router.route(&sl_msg), Some(agent_sl));
138    }
139
140    #[test]
141    fn test_router_command_prefix_rule() {
142        let special_agent = Uuid::new_v4();
143        let default_agent = Uuid::new_v4();
144
145        let mut router = ChannelRouter::new().with_default_agent(default_agent);
146        router.add_rule(RoutingRule {
147            priority: 1,
148            conditions: vec![RoutingCondition::CommandPrefix("/admin".into())],
149            target_agent: special_agent,
150        });
151
152        let admin_msg = make_msg(ChannelType::Telegram, "u1", "/admin status");
153        assert_eq!(router.route(&admin_msg), Some(special_agent));
154
155        let normal_msg = make_msg(ChannelType::Telegram, "u1", "hello");
156        assert_eq!(router.route(&normal_msg), Some(default_agent));
157    }
158}