From 883f92409e25fdf034c74f5294a29556a58f4614 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Tue, 24 Feb 2026 13:43:49 -0500 Subject: [PATCH] feat(channels): add query classification routing with logging for channels Add query classification support to channel message processing (Telegram, Discord, Slack, etc.). When query_classification is enabled with model_routes, each incoming message is now classified and routed to the appropriate model with an INFO-level log line. Changes: - Add query_classification and model_routes fields to ChannelRuntimeContext - Add classify_message_route function that logs classification decisions - Update process_channel_message to try classification before default routing - Initialize new fields in channel runtime context - Update all test contexts with new fields The logging matches the existing agent.rs implementation: - target: "query_classification" - fields: hint, model, rule_priority, message_length - level: INFO Closes #1367 Co-Authored-By: Claude Opus 4.6 --- src/channels/mod.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 3f34a01d1..ffd40cd04 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -226,6 +226,8 @@ struct ChannelRuntimeContext { multimodal: crate::config::MultimodalConfig, hooks: Option>, non_cli_excluded_tools: Arc>, + query_classification: crate::config::QueryClassificationConfig, + model_routes: Vec, } #[derive(Clone)] @@ -736,6 +738,32 @@ fn get_route_selection(ctx: &ChannelRuntimeContext, sender_key: &str) -> Channel .unwrap_or_else(|| default_route_selection(ctx)) } +/// Classify a user message and return the appropriate route selection with logging. +/// Returns None if classification is disabled or no rules match. +fn classify_message_route( + ctx: &ChannelRuntimeContext, + message: &str, +) -> Option { + let decision = crate::agent::classifier::classify_with_decision(&ctx.query_classification, message)?; + + // Find the matching model route + let route = ctx.model_routes.iter().find(|r| r.hint == decision.hint)?; + + tracing::info!( + target: "query_classification", + hint = %decision.hint, + model = %route.model, + rule_priority = decision.priority, + message_length = message.len(), + "Classified message route" + ); + + Some(ChannelRouteSelection { + provider: route.provider.clone(), + model: route.model.clone(), + }) +} + fn set_route_selection(ctx: &ChannelRuntimeContext, sender_key: &str, next: ChannelRouteSelection) { let default_route = default_route_selection(ctx); let mut routes = ctx @@ -1567,7 +1595,9 @@ async fn process_channel_message( } let history_key = conversation_history_key(&msg); - let route = get_route_selection(ctx.as_ref(), &history_key); + // Try classification first, fall back to sender/default route + let route = classify_message_route(ctx.as_ref(), &msg.content) + .unwrap_or_else(|| get_route_selection(ctx.as_ref(), &history_key)); let runtime_defaults = runtime_defaults_snapshot(ctx.as_ref()); let active_provider = match get_or_create_provider(ctx.as_ref(), &route.provider).await { Ok(provider) => provider, @@ -3347,6 +3377,8 @@ pub async fn start_channels(config: Config) -> Result<()> { None }, non_cli_excluded_tools: Arc::new(config.autonomy.non_cli_excluded_tools.clone()), + query_classification: config.query_classification.clone(), + model_routes: config.model_routes.clone(), }); run_message_dispatch_loop(rx, runtime_ctx, max_in_flight_messages).await; @@ -3560,6 +3592,8 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }; assert!(compact_sender_history(&ctx, &sender)); @@ -3609,6 +3643,8 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }; append_sender_turn(&ctx, &sender, ChatMessage::user("hello")); @@ -3661,6 +3697,8 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }; assert!(rollback_orphan_user_turn(&ctx, &sender, "pending")); @@ -4136,6 +4174,8 @@ BTC is currently around $65,000 based on latest tool output."# non_cli_excluded_tools: Arc::new(Vec::new()), multimodal: crate::config::MultimodalConfig::default(), hooks: None, + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4195,6 +4235,8 @@ BTC is currently around $65,000 based on latest tool output."# non_cli_excluded_tools: Arc::new(Vec::new()), multimodal: crate::config::MultimodalConfig::default(), hooks: None, + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4268,6 +4310,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4327,6 +4371,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4395,6 +4441,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4484,6 +4532,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4555,6 +4605,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4641,6 +4693,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4712,6 +4766,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4772,6 +4828,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -4943,6 +5001,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); let (tx, rx) = tokio::sync::mpsc::channel::(4); @@ -5023,6 +5083,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); let (tx, rx) = tokio::sync::mpsc::channel::(8); @@ -5115,6 +5177,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); let (tx, rx) = tokio::sync::mpsc::channel::(8); @@ -5189,6 +5253,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -5248,6 +5314,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -5764,6 +5832,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -5849,6 +5919,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -5934,6 +6006,8 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message( @@ -6483,6 +6557,8 @@ This is an example JSON object for profile settings."#; multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); // Simulate a photo attachment message with [IMAGE:] marker. @@ -6549,6 +6625,8 @@ This is an example JSON object for profile settings."#; multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + query_classification: crate::config::QueryClassificationConfig::default(), + model_routes: Vec::new(), }); process_channel_message(