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 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(); 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(¶ms);
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 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 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 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}