Skip to main content

skill_tools/
skill.rs

1use crate::{ToolDefinition, ToolError, ToolResult};
2use skill_core::Skill;
3use skill_executor::{ExecutionContext, SkillExecutor};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7use tracing::{debug, error, info, warn};
8
9#[derive(Clone)]
10pub struct SkillTool {
11    skills: HashMap<String, Skill>,
12    executor: Arc<SkillExecutor>,
13}
14
15impl SkillTool {
16    pub fn new(skills: Vec<Skill>, skills_base_dir: PathBuf) -> Self {
17        let executor = SkillExecutor::new(skills_base_dir);
18        let map = skills.into_iter().map(|s| (s.id.clone(), s)).collect();
19        Self {
20            skills: map,
21            executor: Arc::new(executor),
22        }
23    }
24
25    pub fn definition(&self) -> ToolDefinition {
26        ToolDefinition {
27            name: "run_skill".to_string(),
28            description: "Execute a skill by its ID. Use the skill catalog in the system prompt \
29                          to find the right skill_id for the task. Pass the skill_id and the input \
30                          to execute it."
31                .to_string(),
32            parameters: serde_json::json!({
33                "type": "object",
34                "properties": {
35                    "skill_id": {
36                        "type": "string",
37                        "description": "The ID of the skill to execute (from the skill catalog)"
38                    },
39                    "input": {
40                        "type": "string",
41                        "description": "Input to pass to the skill"
42                    }
43                },
44                "required": ["skill_id", "input"]
45            }),
46        }
47    }
48
49    /// Return a lightweight metadata catalog for the system prompt.
50    /// One line per skill: id, name, description, triggers.
51    pub fn skill_catalog(&self) -> String {
52        if self.skills.is_empty() {
53            return String::new();
54        }
55
56        let mut lines: Vec<String> = self
57            .skills
58            .values()
59            .map(|s| {
60                let triggers = if s.triggers.is_empty() {
61                    String::new()
62                } else {
63                    format!(" [triggers: {}]", s.triggers.join(", "))
64                };
65                format!("- {} ({}): {}{}", s.id, s.name, s.description, triggers)
66            })
67            .collect();
68        lines.sort(); // deterministic order
69        lines.join("\n")
70    }
71
72    pub fn skill_count(&self) -> usize {
73        self.skills.len()
74    }
75
76    pub async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
77        let skill_id = params
78            .get("skill_id")
79            .and_then(|v| v.as_str())
80            .ok_or_else(|| ToolError::InvalidParameters("Missing 'skill_id' parameter".into()))?;
81
82        info!("=== SkillTool executing skill '{}' ===", skill_id);
83        debug!("Skill tool params raw: {:?}", params);
84
85        let skill = self.skills.get(skill_id).ok_or_else(|| {
86            let available: Vec<&str> = self.skills.keys().map(|k| k.as_str()).collect();
87            ToolError::NotFound(format!(
88                "Skill '{}' not found. Available skills: {:?}",
89                skill_id, available
90            ))
91        })?;
92
93        let input = Self::extract_input(&params);
94        debug!("Extracted input for skill '{}': {:?}", skill_id, input);
95
96        if input.is_none() {
97            warn!("No input extracted for skill '{}'!", skill_id);
98        }
99
100        let context = ExecutionContext::default();
101
102        info!("Executing skill '{}' with input: {:?}", skill_id, input);
103        let result = self
104            .executor
105            .execute_skill(skill, input.as_deref(), &context)
106            .await
107            .map_err(|e| {
108                error!("Skill '{}' execution error: {}", skill_id, e);
109                ToolError::ExecutionError(e.to_string())
110            })?;
111
112        info!(
113            "Skill '{}' result: success={}, output_len={}, error={:?}",
114            skill_id,
115            result.success,
116            result.output.len(),
117            result.error
118        );
119        debug!(
120            "Skill '{}' output (first 300 chars): {:?}",
121            skill_id,
122            &result.output[..result.output.len().min(300)]
123        );
124
125        Ok(ToolResult {
126            success: result.success,
127            output: result.output,
128            error: result.error,
129        })
130    }
131
132    fn extract_input(params: &serde_json::Value) -> Option<String> {
133        debug!("extract_input called with: {:?}", params);
134
135        if let Some(obj) = params.as_object() {
136            // Try top-level keys first
137            for key in &["input", "url", "query", "value"] {
138                if let Some(v) = obj.get(*key) {
139                    if let Some(s) = v.as_str() {
140                        if !s.is_empty()
141                            && !s.contains("string")
142                            && !s.contains("Input to pass")
143                            && !s.contains("description")
144                        {
145                            debug!("Found input at top-level key '{}': {}", key, s);
146                            return Some(s.to_string());
147                        }
148                    }
149                }
150            }
151
152            // Try nested in "input" object
153            if let Some(input_obj) = obj.get("input").or_else(|| obj.get("query")) {
154                if let Some(s) = input_obj.as_str() {
155                    if !s.is_empty() && !s.contains("string") && !s.contains("Input to pass") {
156                        debug!("Found input in nested 'input': {}", s);
157                        return Some(s.to_string());
158                    }
159                }
160                if let Some(nested) = input_obj.as_object() {
161                    for key in &["value", "url", "query", "description"] {
162                        if let Some(v) = nested.get(*key) {
163                            if let Some(s) = v.as_str() {
164                                if !s.is_empty()
165                                    && !s.contains("string")
166                                    && !s.contains("Input to pass")
167                                {
168                                    debug!("Found input in nested 'input.{}': {}", key, s);
169                                    return Some(s.to_string());
170                                }
171                            }
172                        }
173                    }
174                }
175            }
176        }
177
178        // Direct string
179        if let Some(s) = params.as_str() {
180            if !s.is_empty() {
181                debug!("Found input as direct string: {}", s);
182                return Some(s.to_string());
183            }
184        }
185
186        warn!("No input found in params: {:?}", params);
187        None
188    }
189}