PicoBot/src/tools/get_skill.rs

203 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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": {
"action": {
"type": "string",
"enum": ["get", "list"],
"description": "操作类型: get 获取指定 skill 完整内容, list 列出所有可用 skill"
},
"skill_name": {
"type": "string",
"description": "Name of the skill to retrieve仅在 action 为 get 时必填"
}
},
"required": []
})
}
fn read_only(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get");
match action {
"list" => self.list_skills_full(),
_ => self.get_skill_by_name(&args),
}
}
}
impl GetSkillTool {
fn get_skill_by_name(&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
)),
})
}
}
}
fn list_skills_full(&self) -> anyhow::Result<ToolResult> {
let skills = self.skills_loader.get_loaded_skills();
if skills.is_empty() {
return Ok(ToolResult {
success: true,
output: "当前没有安装任何 skill".to_string(),
error: None,
});
}
let mut output = format!("可用 skill (共 {} 个):\n", skills.len());
for s in &skills {
let always_mark = if s.always { " [常驻]" } else { "" };
let path_str = s.path.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "".to_string());
output.push_str(&format!(
"- {}{}\n 简介: {}\n 路径: {}\n",
s.name, always_mark, s.description, path_str
));
}
Ok(ToolResult {
success: true,
output,
error: None,
})
}
}
#[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 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());
}
}