use std::sync::LazyLock;
use agent_client_protocol::schema::v1::AvailableCommand;
use rho_sdk::{
model::{ContentBlock, ToolCall},
UserInput,
};
use crate::skills::Skill;
pub(super) 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(super) fn available_commands(skills: &[Skill]) -> Vec<AvailableCommand> {
skills
.iter()
.filter(|skill| mentionable(&skill.name))
.map(|skill| AvailableCommand::new(format!("${}", skill.name), skill.description.clone()))
.collect()
}
pub(super) fn first_mention(
input: &UserInput,
discover: impl FnOnce() -> Vec<Skill>,
) -> Option<Skill> {
let names = input
.blocks()
.iter()
.filter_map(|block| match block {
ContentBlock::Text(text) => Some(text.as_str()),
ContentBlock::Image(_) | ContentBlock::ToolCall(_) => None,
})
.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 None;
}
let mut skills = discover();
let index = names
.into_iter()
.find_map(|name| skills.iter().position(|skill| skill.name == name))?;
Some(skills.swap_remove(index))
}
pub(super) fn load_call(name: &str) -> ToolCall {
ToolCall {
id: rho_sdk::ToolCallId::new().into_string(),
name: SKILL_TOOL.into(),
arguments: serde_json::json!({ "name": name }),
}
}
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 = "skills_tests.rs"]
mod tests;