gitdataai/libs/agent/perception/active.rs
2026-04-14 19:02:01 +08:00

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),
}
})
}
}