//! Active skill awareness — proactive skill retrieval triggered by explicit user intent. //! //! The agent proactively loads a specific skill when the user explicitly references it //! in their message. Patterns include: //! //! - Direct slug mention: "用 code-review", "使用 skill:code-review", "@code-review" //! - Task-based invocation: "帮我 code review", "做一次 security scan" //! - Intent keywords with skill context: "review 我的 PR", "scan for bugs" //! //! This is the highest-priority perception mode — if the user explicitly asks for a //! skill, it always gets injected regardless of auto/passive scores. use super::{SkillContext, SkillEntry}; use once_cell::sync::Lazy; use regex::Regex; /// Active skill awareness that detects explicit skill invocations in user messages. #[derive(Debug, Clone, Default)] pub struct ActiveSkillAwareness; impl ActiveSkillAwareness { pub fn new() -> Self { Self } /// Detect if the user explicitly invoked a skill in their message. /// /// Returns the first matching skill, or `None` if no explicit invocation is found. /// /// Matching patterns: /// - `用 ` / `使用 ` (Chinese: "use / apply ") /// - `skill:` (explicit namespace) /// - `@` (GitHub-style mention) /// - `帮我 ` / ` 帮我` (Chinese: "help me ") /// - `做一次 ` / `进行一次 ` (Chinese: "do a ") pub fn detect(&self, input: &str, skills: &[SkillEntry]) -> Option { let input_lower = input.to_lowercase(); // Try each matching pattern in priority order. if let Some(skill) = self.match_by_prefix_pattern(&input_lower, skills) { return Some(skill); } // Try matching by skill name (for natural language invocations). if let Some(skill) = self.match_by_name(&input_lower, skills) { return Some(skill); } // Try matching by slug substring in the message. self.match_by_slug_substring(&input_lower, skills) } /// Pattern: "用 code-review", "使用 skill:xxx", "@xxx", "skill:xxx" fn match_by_prefix_pattern(&self, input: &str, skills: &[SkillEntry]) -> Option { // Pattern 1: 英文 slug 前缀 "use ", "using ", "apply ", "with " static USE_PAT: Lazy = Lazy::new(|| Regex::new(r"(?i)^\s*(?:use|using|apply|with)\s+([a-z0-9/_-]+)").unwrap()); if let Some(caps) = USE_PAT.captures(input) { let slug = caps.get(1)?.as_str().trim(); return self.find_skill_by_slug(slug, skills); } // Pattern 2: skill:xxx static SKILL_COLON_PAT: Lazy = Lazy::new(|| Regex::new(r"(?i)skill\s*:\s*([a-z0-9/_-]+)").unwrap()); if let Some(caps) = SKILL_COLON_PAT.captures(input) { let slug = caps.get(1)?.as_str().trim(); return self.find_skill_by_slug(slug, skills); } // Pattern 3: @xxx (mention style) static AT_PAT: Lazy = Lazy::new(|| Regex::new(r"@([a-z0-9][a-z0-9_/-]*[a-z0-9])").unwrap()); if let Some(caps) = AT_PAT.captures(input) { let slug = caps.get(1)?.as_str().trim(); return self.find_skill_by_slug(slug, skills); } // Pattern 4: 帮我 xxx, 做一个 xxx, 进行 xxx, 做 xxx static ZH_PAT: Lazy = Lazy::new( || Regex::new(r"(?ix)[\u4e00-\u9fff]+\s+(?:帮我|做一个|进行一次|做|使用|用)\s+([a-z0-9][a-z0-9_/-]{0,30})") .unwrap(), ); if let Some(caps) = ZH_PAT.captures(input) { let slug_or_name = caps.get(1)?.as_str().trim(); return self .find_skill_by_slug(slug_or_name, skills) .or_else(|| self.find_skill_by_name(slug_or_name, skills)); } None } /// Match by skill name in natural language (e.g., "code review" → "code-review") fn match_by_name(&self, input: &str, skills: &[SkillEntry]) -> Option { for skill in skills { // Normalize skill name to a search pattern: "Code Review" -> "code review" let name_lower = skill.name.to_lowercase(); // Direct substring match (the skill name appears in the input). if input.contains(&name_lower) { return Some(SkillContext { label: format!("Active skill: {}", skill.name), content: format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), }); } // Try removing hyphens/underscores: "code-review" contains "code review" let normalized_name = name_lower.replace(['-', '_'], " "); if input.contains(&normalized_name) { return Some(SkillContext { label: format!("Active skill: {}", skill.name), content: format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), }); } } None } /// Match by slug substring anywhere in the message. fn match_by_slug_substring(&self, input: &str, skills: &[SkillEntry]) -> Option { // Remove common command words to isolate the slug. let cleaned = input .replace("please ", "") .replace("帮我", "") .replace("帮我review", "") .replace("帮我 code review", "") .replace("帮我review", ""); for skill in skills { let slug = skill.slug.to_lowercase(); // Check if the slug (or any segment of it) appears as a word. if cleaned.contains(&slug) || slug.split('/').any(|seg| cleaned.contains(seg) && seg.len() > 3) { return Some(SkillContext { label: format!("Active skill: {}", skill.name), content: format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), }); } } None } fn find_skill_by_slug(&self, slug: &str, skills: &[SkillEntry]) -> Option { let slug_lower = slug.to_lowercase(); skills.iter().find(|s| s.slug.to_lowercase() == slug_lower).map(|skill| { SkillContext { label: format!("Active skill: {}", skill.name), content: format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), } }) } fn find_skill_by_name(&self, name: &str, skills: &[SkillEntry]) -> Option { let name_lower = name.to_lowercase(); skills.iter().find(|s| s.name.to_lowercase() == name_lower).map(|skill| { SkillContext { label: format!("Active skill: {}", skill.name), content: format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), } }) } }