1use std::sync::Arc;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8use skm_core::{SkillName, SkillRegistry};
9use skm_select::{CascadeSelector, LlmClient, LlmError};
10
11use crate::error::LearnError;
12use crate::harness::{TestReport, TestSuite, TriggerTestHarness};
13
14#[derive(Debug, Clone)]
16pub struct OptimizerConfig {
17 pub max_iterations: usize,
19
20 pub target_accuracy: f32,
22
23 pub system_prompt: String,
25}
26
27impl Default for OptimizerConfig {
28 fn default() -> Self {
29 Self {
30 max_iterations: 5,
31 target_accuracy: 0.95,
32 system_prompt: DEFAULT_OPTIMIZER_PROMPT.to_string(),
33 }
34 }
35}
36
37const DEFAULT_OPTIMIZER_PROMPT: &str = r#"You are a skill description optimizer. Given a skill's current description and test results, suggest an improved description that will help the skill be selected for appropriate queries.
38
39Rules:
401. Keep the description concise (under 200 characters)
412. Include key trigger words that users might use
423. Be specific about what the skill does
434. Avoid overly generic terms
44
45Respond with ONLY the new description, nothing else."#;
46
47#[derive(Debug, Clone)]
49pub struct OptimizationIteration {
50 pub iteration: usize,
52
53 pub old_description: String,
55
56 pub new_description: String,
58
59 pub old_accuracy: f32,
61
62 pub new_accuracy: f32,
64
65 pub accepted: bool,
67}
68
69#[derive(Debug, Clone)]
71pub struct OptimizationResult {
72 pub skill: SkillName,
74
75 pub final_description: String,
77
78 pub iterations: Vec<OptimizationIteration>,
80
81 pub final_accuracy: f32,
83
84 pub initial_accuracy: f32,
86}
87
88pub struct DescriptionOptimizer {
90 llm: Arc<dyn LlmClient>,
91 config: OptimizerConfig,
92}
93
94impl DescriptionOptimizer {
95 pub fn new(llm: Arc<dyn LlmClient>, config: OptimizerConfig) -> Self {
97 Self { llm, config }
98 }
99
100 fn build_prompt(
102 &self,
103 skill_name: &SkillName,
104 current_description: &str,
105 test_report: &TestReport,
106 ) -> String {
107 let skill_report = test_report.per_skill.get(skill_name);
108
109 let mut prompt = self.config.system_prompt.clone();
110
111 prompt.push_str(&format!("\n\nSkill: {}\n", skill_name));
112 prompt.push_str(&format!("Current description: {}\n", current_description));
113
114 if let Some(report) = skill_report {
115 prompt.push_str(&format!("\nTest results:\n"));
116 prompt.push_str(&format!("- Precision: {:.1}%\n", report.precision() * 100.0));
117 prompt.push_str(&format!("- Recall: {:.1}%\n", report.recall() * 100.0));
118 prompt.push_str(&format!("- False positives: {}\n", report.false_positives));
119 prompt.push_str(&format!("- False negatives: {}\n", report.false_negatives));
120 }
121
122 prompt.push_str("\nFailed test cases:\n");
124 for result in &test_report.results {
125 if !result.passed {
126 match &result.expected {
127 crate::harness::TestExpectation::Single(exp) if exp == skill_name => {
128 prompt.push_str(&format!(
129 "- Query: \"{}\" (expected {}, got {:?})\n",
130 result.name, skill_name, result.selected
131 ));
132 }
133 crate::harness::TestExpectation::AnyOf(exps) if exps.contains(skill_name) => {
134 prompt.push_str(&format!(
135 "- Query: \"{}\" (expected {:?}, got {:?})\n",
136 result.name, exps, result.selected
137 ));
138 }
139 _ => {}
140 }
141 }
142 }
143
144 prompt.push_str("\nSuggest an improved description:");
145
146 prompt
147 }
148
149 pub async fn optimize(
154 &self,
155 skill: &SkillName,
156 suite: &TestSuite,
157 selector: &CascadeSelector,
158 registry: &SkillRegistry,
159 ) -> Result<OptimizationResult, LearnError> {
160 let harness = TriggerTestHarness::new();
161
162 let skill_meta = registry
164 .get_metadata(skill)
165 .await
166 .ok_or_else(|| LearnError::Optimizer(format!("Skill not found: {}", skill)))?;
167
168 let mut current_description = skill_meta.description.clone();
169 let mut iterations = Vec::new();
170
171 let initial_report = harness.run(suite, selector, registry).await?;
173 let initial_accuracy = initial_report.accuracy();
174 let mut best_accuracy = initial_accuracy;
175
176 if initial_accuracy >= self.config.target_accuracy {
177 return Ok(OptimizationResult {
178 skill: skill.clone(),
179 final_description: current_description,
180 iterations,
181 final_accuracy: initial_accuracy,
182 initial_accuracy,
183 });
184 }
185
186 for i in 0..self.config.max_iterations {
187 let prompt = self.build_prompt(skill, ¤t_description, &initial_report);
189 let new_description = self
190 .llm
191 .complete(&prompt, 200)
192 .await
193 .map_err(|e| LearnError::Optimizer(format!("LLM error: {}", e)))?
194 .trim()
195 .to_string();
196
197 let iteration = OptimizationIteration {
204 iteration: i + 1,
205 old_description: current_description.clone(),
206 new_description: new_description.clone(),
207 old_accuracy: best_accuracy,
208 new_accuracy: best_accuracy, accepted: true, };
211
212 iterations.push(iteration);
213 current_description = new_description;
214
215 if best_accuracy >= self.config.target_accuracy {
217 break;
218 }
219 }
220
221 Ok(OptimizationResult {
222 skill: skill.clone(),
223 final_description: current_description,
224 iterations,
225 final_accuracy: best_accuracy,
226 initial_accuracy,
227 })
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 struct MockLlm;
236
237 #[async_trait]
238 impl LlmClient for MockLlm {
239 async fn complete(&self, _prompt: &str, _max_tokens: usize) -> Result<String, LlmError> {
240 Ok("Improved description for skill".to_string())
241 }
242 }
243
244 #[test]
245 fn test_optimizer_config_default() {
246 let config = OptimizerConfig::default();
247 assert_eq!(config.max_iterations, 5);
248 assert_eq!(config.target_accuracy, 0.95);
249 }
250
251 #[test]
252 fn test_build_prompt() {
253 let llm = Arc::new(MockLlm);
254 let optimizer = DescriptionOptimizer::new(llm, OptimizerConfig::default());
255
256 let skill = SkillName::new("test-skill").unwrap();
257 let report = TestReport {
258 suite_name: "test".to_string(),
259 results: Vec::new(),
260 per_skill: std::collections::HashMap::new(),
261 total: 0,
262 passed: 0,
263 avg_latency_ms: 0.0,
264 };
265
266 let prompt = optimizer.build_prompt(&skill, "Current description", &report);
267
268 assert!(prompt.contains("test-skill"));
269 assert!(prompt.contains("Current description"));
270 }
271}