PicoBot/src/tools/get_skill.rs

160 lines
4.7 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use crate::skills::{Skill, SkillsLoader};
use crate::tools::traits::{Tool, ToolResult};
pub struct GetSkillTool {
skills_loader: Arc<SkillsLoader>,
}
impl GetSkillTool {
pub fn new(skills_loader: Arc<SkillsLoader>) -> Self {
Self { skills_loader }
}
fn format_skill(&self, skill: &Skill) -> String {
let mut result = format!("# Skill: {}\n\n{}", skill.name, skill.description);
if let Some(path) = &skill.path {
result.push_str(&format!(
"\n\n**Skill Root Directory:** `{}`\n\nAll files and references in this skill are relative to this directory.",
path.to_string_lossy()
));
}
result.push_str(&format!("\n\n---\n\n{}", skill.content));
result
}
}
#[async_trait]
impl Tool for GetSkillTool {
fn name(&self) -> &str {
"get_skill"
}
fn description(&self) -> &str {
"Get complete content and guidance for a specified skill. Use this when you need detailed instructions for a specific type of task."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": "Name of the skill to retrieve"
}
},
"required": ["skill_name"]
})
}
fn read_only(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
Some(name) => name,
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some("Missing required parameter: skill_name".to_string()),
});
}
};
match self.skills_loader.get_skill(skill_name) {
Some(skill) => {
let formatted = self.format_skill(&skill);
Ok(ToolResult {
success: true,
output: formatted,
error: None,
})
}
None => {
let available = self.skills_loader.list_skills();
let available_str = if available.is_empty() {
"No skills available".to_string()
} else {
available
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(
"Skill '{}' not found. Available skills: {}",
skill_name, available_str
)),
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[tokio::test]
async fn test_get_existing_skill() {
let temp_dir = tempdir().unwrap();
let skill_dir = temp_dir.path().join("test-skill");
std::fs::create_dir(&skill_dir).unwrap();
let mut skill_file = File::create(skill_dir.join("SKILL.md")).unwrap();
writeln!(skill_file, "---").unwrap();
writeln!(skill_file, "name: test-skill").unwrap();
writeln!(skill_file, "description: A test skill").unwrap();
writeln!(skill_file, "---").unwrap();
writeln!(skill_file, "# Test Skill").unwrap();
writeln!(skill_file, "This is the test content.").unwrap();
let mut loader = SkillsLoader::new_for_testing(
temp_dir.path().to_path_buf(),
PathBuf::from("/nonexistent"),
);
loader.load_skills();
let tool = GetSkillTool::new(Arc::new(loader));
let result = tool
.execute(json!({ "skill_name": "test-skill" }))
.await
.unwrap();
assert!(result.success);
assert!(result.output.contains("test-skill"));
assert!(result.output.contains("test content"));
}
#[tokio::test]
async fn test_get_nonexistent_skill() {
let loader = SkillsLoader::new();
let tool = GetSkillTool::new(Arc::new(loader));
let result = tool
.execute(json!({ "skill_name": "nonexistent" }))
.await
.unwrap();
assert!(!result.success);
assert!(result.error.is_some());
}
}