gitdataai/libs/agent/perception/active.rs
ZhenYi 5c1b14c26a refactor(perception): simplify active/auto detection and deduplication
- Remove activation threshold logic from PassiveSkillAwareness
- Add SkillActivation enum with Priority/Keyword/Vector/Auto variants
- Add deduplication via SkillContext.dedupe_key() using rank ordering
- Simplify ActiveSkillAwareness with cleaner regex-based detection
2026-05-17 17:31:50 +08:00

115 lines
4.0 KiB
Rust

//! Active skill awareness: explicit user intent.
//!
//! Active detection has the highest priority. It only fires when the user
//! directly references a skill by slug, name, mention, or clear "use this"
//! wording.
use super::{SkillActivation, SkillContext, SkillEntry, normalize_skill_key};
use once_cell::sync::Lazy;
use regex::Regex;
#[derive(Debug, Clone, Default)]
pub struct ActiveSkillAwareness;
impl ActiveSkillAwareness {
pub fn new() -> Self {
Self
}
pub fn detect(&self, input: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
let input_lower = input.to_lowercase();
self.match_by_prefix_pattern(&input_lower, skills)
.or_else(|| self.match_by_name(&input_lower, skills))
.or_else(|| self.match_by_slug_substring(&input_lower, skills))
}
fn match_by_prefix_pattern(&self, input: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
static USE_PAT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)^\s*(?:use|using|apply|with)\s+([a-z0-9/_-]+)").unwrap());
static SKILL_COLON_PAT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)skill\s*:\s*([a-z0-9/_-]+)").unwrap());
static AT_PAT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"@([a-z0-9][a-z0-9_/-]*[a-z0-9])").unwrap());
static ZH_PAT: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)(?:使用|用|应用|启用|调用|帮我|帮忙|做一次|执行|进行)\s*([a-z0-9][a-z0-9_/\-]{0,60})",
)
.unwrap()
});
for pattern in [&USE_PAT, &SKILL_COLON_PAT, &AT_PAT, &ZH_PAT] {
if let Some(caps) = pattern.captures(input) {
let slug = caps.get(1)?.as_str().trim();
if let Some(skill) = self.find_skill_by_slug(slug, skills) {
return Some(skill);
}
if let Some(skill) = self.find_skill_by_name(slug, skills) {
return Some(skill);
}
}
}
None
}
fn match_by_name(&self, input: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
for skill in skills {
let name_lower = skill.name.to_lowercase();
let normalized_name = name_lower.replace(['-', '_'], " ");
if input.contains(&name_lower) || input.contains(&normalized_name) {
return Some(Self::context_from_skill(skill));
}
}
None
}
fn match_by_slug_substring(&self, input: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
let cleaned = input
.replace("please ", "")
.replace("帮我", "")
.replace("帮忙", "")
.replace("使用", "")
.replace("调用", "")
.replace("启用", "");
for skill in skills {
let slug = normalize_skill_key(&skill.slug);
if cleaned.contains(&slug)
|| slug
.split(['/', '-'])
.any(|seg| seg.len() > 3 && cleaned.contains(seg))
{
return Some(Self::context_from_skill(skill));
}
}
None
}
fn find_skill_by_slug(&self, slug: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
let slug_key = normalize_skill_key(slug);
skills
.iter()
.find(|s| normalize_skill_key(&s.slug) == slug_key)
.map(Self::context_from_skill)
}
fn find_skill_by_name(&self, name: &str, skills: &[SkillEntry]) -> Option<SkillContext> {
let name_lower = name.to_lowercase().replace(['-', '_'], " ");
skills
.iter()
.find(|s| s.name.to_lowercase().replace(['-', '_'], " ") == name_lower)
.map(Self::context_from_skill)
}
fn context_from_skill(skill: &SkillEntry) -> SkillContext {
SkillContext::new(
skill,
SkillActivation::Active,
None,
format!("# {} (actively invoked)\n\n{}", skill.name, skill.content),
None,
)
}
}