use std::{collections::BTreeSet, sync::LazyLock};
use rho_sdk::model::ToolCall;
use super::Skill;
pub(crate) const SKILL_TOOL: &str = "skill";
static MENTION: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^\p{Sc}([a-zA-Z0-9][a-zA-Z0-9:_-]*)$").expect("mention pattern is valid")
});
pub(crate) fn skills_to_load<'a>(
leading: &[&'a str],
texts: &[&'a str],
discover: impl FnOnce() -> Vec<Skill>,
) -> Vec<&'a str> {
let mut seen = BTreeSet::new();
leading
.iter()
.copied()
.chain(mentions(texts, discover))
.filter(|name| seen.insert(*name))
.collect()
}
fn mentions<'a>(texts: &[&'a str], discover: impl FnOnce() -> Vec<Skill>) -> Vec<&'a str> {
let names = texts
.iter()
.flat_map(|text| text.split(char::is_whitespace))
.filter_map(|token| MENTION.captures(token)?.get(1))
.map(|name| name.as_str())
.filter(|name| mentionable(name))
.collect::<Vec<_>>();
if names.is_empty() {
return Vec::new();
}
let skills = discover();
names
.into_iter()
.filter(|name| skills.iter().any(|skill| skill.name == *name))
.collect()
}
pub(crate) fn load_call(name: &str) -> ToolCall {
ToolCall {
id: rho_sdk::ToolCallId::new().into_string(),
name: SKILL_TOOL.into(),
arguments: serde_json::json!({ "name": name }),
}
}
pub(crate) fn mentionable(name: &str) -> bool {
name.bytes().any(|byte| byte.is_ascii_alphabetic()) && !reads_as_amount(name)
}
fn reads_as_amount(name: &str) -> bool {
let digits_end = name
.bytes()
.position(|byte| !(byte.is_ascii_digit() || byte == b'_'))
.unwrap_or(name.len());
if digits_end == 0 || !name.as_bytes()[0].is_ascii_digit() {
return false;
}
match name.as_bytes()[digits_end..] {
[] => true,
[unit] => matches!(unit.to_ascii_lowercase(), b'k' | b'm' | b'b' | b't'),
[exponent, ref power @ ..] => {
exponent.eq_ignore_ascii_case(&b'e') && power.iter().all(u8::is_ascii_digit)
}
}
}
#[cfg(test)]
#[path = "mention_tests.rs"]
mod tests;