168 lines
6.8 KiB
Rust
168 lines
6.8 KiB
Rust
//! 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:
|
|
/// - `用 <slug>` / `使用 <slug>` (Chinese: "use / apply <slug>")
|
|
/// - `skill:<slug>` (explicit namespace)
|
|
/// - `@<slug>` (GitHub-style mention)
|
|
/// - `帮我 <slug>` / `<name> 帮我` (Chinese: "help me <slug>")
|
|
/// - `做一次 <name>` / `进行一次 <name>` (Chinese: "do a <name>")
|
|
pub fn detect(&self, input: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
|
|
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<SkillContext> {
|
|
// Pattern 1: 英文 slug 前缀 "use ", "using ", "apply ", "with "
|
|
static USE_PAT: Lazy<Regex> =
|
|
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<Regex> =
|
|
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<Regex> =
|
|
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<Regex> = 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<SkillContext> {
|
|
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<SkillContext> {
|
|
// 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<SkillContext> {
|
|
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<SkillContext> {
|
|
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),
|
|
}
|
|
})
|
|
}
|
|
}
|