From 1e675947d5c74cd97ab8a36fab304221ddb80484 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 10 Apr 2026 21:24:30 +0800 Subject: [PATCH] feat(butler): upgrade ButlerRouter to semantic skill routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace keyword-only ButlerRouter with SemanticSkillRouter (TF-IDF). 75 skills now participate in intent classification instead of 4 hardcoded domains. - Expose ButlerRouterBackend trait + RoutingHint as pub - Add with_router() constructor for injecting custom backends - Add SemanticRouterAdapter in kernel layer (bridges skills ↔ runtime) - Enhance context injection with skill-level match info --- crates/zclaw-kernel/src/kernel/mod.rs | 35 +++++++++++++++- .../src/middleware/butler_router.rs | 42 +++++++++++++++---- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/crates/zclaw-kernel/src/kernel/mod.rs b/crates/zclaw-kernel/src/kernel/mod.rs index 3305d09..2d76741 100644 --- a/crates/zclaw-kernel/src/kernel/mod.rs +++ b/crates/zclaw-kernel/src/kernel/mod.rs @@ -193,7 +193,40 @@ impl Kernel { // Butler router — semantic skill routing context injection { use std::sync::Arc; - let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::new(); + use zclaw_runtime::middleware::butler_router::{ButlerRouterBackend, RoutingHint}; + use async_trait::async_trait; + use zclaw_skills::semantic_router::SemanticSkillRouter; + + /// Adapter bridging `SemanticSkillRouter` (zclaw-skills) to `ButlerRouterBackend`. + /// Lives here in kernel because kernel depends on both zclaw-runtime and zclaw-skills. + struct SemanticRouterAdapter { + router: Arc, + } + + impl SemanticRouterAdapter { + fn new(router: Arc) -> Self { + Self { router } + } + } + + #[async_trait] + impl ButlerRouterBackend for SemanticRouterAdapter { + async fn classify(&self, query: &str) -> Option { + let result: Option<_> = self.router.route(query).await; + result.map(|r| RoutingHint { + category: "semantic_skill".to_string(), + confidence: r.confidence, + skill_id: Some(r.skill_id), + }) + } + } + + // Build semantic router from the skill registry (75 SKILL.md loaded at boot) + let semantic_router = SemanticSkillRouter::new_tf_idf_only(self.skills.clone()); + let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router)); + let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router( + Box::new(adapter) + ); chain.register(Arc::new(mw)); } diff --git a/crates/zclaw-runtime/src/middleware/butler_router.rs b/crates/zclaw-runtime/src/middleware/butler_router.rs index ec78499..7b15046 100644 --- a/crates/zclaw-runtime/src/middleware/butler_router.rs +++ b/crates/zclaw-runtime/src/middleware/butler_router.rs @@ -24,16 +24,20 @@ pub struct ButlerRouterMiddleware { } /// Backend trait for routing implementations. +/// +/// Implementations can be keyword-based (default), semantic (TF-IDF/embedding), +/// or any custom strategy. The kernel layer provides a `SemanticSkillRouter` +/// adapter that bridges `zclaw_skills::SemanticSkillRouter` to this trait. #[async_trait] -trait ButlerRouterBackend: Send + Sync { +pub trait ButlerRouterBackend: Send + Sync { async fn classify(&self, query: &str) -> Option; } /// A routing hint to inject into the system prompt. -struct RoutingHint { - category: String, - confidence: f32, - skill_id: Option, +pub struct RoutingHint { + pub category: String, + pub confidence: f32, + pub skill_id: Option, } // --------------------------------------------------------------------------- @@ -126,6 +130,14 @@ impl ButlerRouterMiddleware { Self { _router: None } } + /// Create a butler router with a custom semantic routing backend. + /// + /// The kernel layer uses this to inject `SemanticSkillRouter` from `zclaw-skills`, + /// enabling TF-IDF + embedding-based intent classification across all 75 skills. + pub fn with_router(router: Box) -> Self { + Self { _router: Some(router) } + } + /// Domain context to inject into system prompt based on routing hint. fn build_context_injection(hint: &RoutingHint) -> String { let domain_context = match hint.category.as_str() { @@ -133,13 +145,29 @@ impl ButlerRouterMiddleware { "data_report" => "用户可能在请求数据统计或报表相关的工作。请优先提供结构化的数据和建议。", "policy_compliance" => "用户可能在咨询政策法规或合规要求。请引用具体政策文件并给出明确的合规建议。", "meeting_coordination" => "用户可能在处理会议协调或行政事务。请提供简洁的待办清单或行动方案。", + "semantic_skill" => { + // Semantic routing matched a specific skill + if let Some(ref skill_id) = hint.skill_id { + return format!( + "\n\n[语义路由] 匹配技能: {} (置信度: {:.0}%)\n系统检测到用户的意图与已注册技能高度相关,请在回答中充分利用该技能的能力。", + skill_id, + hint.confidence * 100.0 + ); + } + return String::new(); + } _ => return String::new(), }; + let skill_info = hint.skill_id.as_ref().map_or(String::new(), |id| { + format!("\n关联技能: {}", id) + }); + format!( - "\n\n[路由上下文] (置信度: {:.0}%)\n{}", + "\n\n[路由上下文] (置信度: {:.0}%)\n{}{}", hint.confidence * 100.0, - domain_context + domain_context, + skill_info ) } }