use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use skm_core::{SkillName, SkillRegistry};
use skm_select::{CascadeSelector, LlmClient, LlmError};
use crate::error::LearnError;
use crate::harness::{TestReport, TestSuite, TriggerTestHarness};
#[derive(Debug, Clone)]
pub struct OptimizerConfig {
pub max_iterations: usize,
pub target_accuracy: f32,
pub system_prompt: String,
}
impl Default for OptimizerConfig {
fn default() -> Self {
Self {
max_iterations: 5,
target_accuracy: 0.95,
system_prompt: DEFAULT_OPTIMIZER_PROMPT.to_string(),
}
}
}
const 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.
Rules:
1. Keep the description concise (under 200 characters)
2. Include key trigger words that users might use
3. Be specific about what the skill does
4. Avoid overly generic terms
Respond with ONLY the new description, nothing else."#;
#[derive(Debug, Clone)]
pub struct OptimizationIteration {
pub iteration: usize,
pub old_description: String,
pub new_description: String,
pub old_accuracy: f32,
pub new_accuracy: f32,
pub accepted: bool,
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub skill: SkillName,
pub final_description: String,
pub iterations: Vec<OptimizationIteration>,
pub final_accuracy: f32,
pub initial_accuracy: f32,
}
pub struct DescriptionOptimizer {
llm: Arc<dyn LlmClient>,
config: OptimizerConfig,
}
impl DescriptionOptimizer {
pub fn new(llm: Arc<dyn LlmClient>, config: OptimizerConfig) -> Self {
Self { llm, config }
}
fn build_prompt(
&self,
skill_name: &SkillName,
current_description: &str,
test_report: &TestReport,
) -> String {
let skill_report = test_report.per_skill.get(skill_name);
let mut prompt = self.config.system_prompt.clone();
prompt.push_str(&format!("\n\nSkill: {}\n", skill_name));
prompt.push_str(&format!("Current description: {}\n", current_description));
if let Some(report) = skill_report {
prompt.push_str(&format!("\nTest results:\n"));
prompt.push_str(&format!("- Precision: {:.1}%\n", report.precision() * 100.0));
prompt.push_str(&format!("- Recall: {:.1}%\n", report.recall() * 100.0));
prompt.push_str(&format!("- False positives: {}\n", report.false_positives));
prompt.push_str(&format!("- False negatives: {}\n", report.false_negatives));
}
prompt.push_str("\nFailed test cases:\n");
for result in &test_report.results {
if !result.passed {
match &result.expected {
crate::harness::TestExpectation::Single(exp) if exp == skill_name => {
prompt.push_str(&format!(
"- Query: \"{}\" (expected {}, got {:?})\n",
result.name, skill_name, result.selected
));
}
crate::harness::TestExpectation::AnyOf(exps) if exps.contains(skill_name) => {
prompt.push_str(&format!(
"- Query: \"{}\" (expected {:?}, got {:?})\n",
result.name, exps, result.selected
));
}
_ => {}
}
}
}
prompt.push_str("\nSuggest an improved description:");
prompt
}
pub async fn optimize(
&self,
skill: &SkillName,
suite: &TestSuite,
selector: &CascadeSelector,
registry: &SkillRegistry,
) -> Result<OptimizationResult, LearnError> {
let harness = TriggerTestHarness::new();
let skill_meta = registry
.get_metadata(skill)
.await
.ok_or_else(|| LearnError::Optimizer(format!("Skill not found: {}", skill)))?;
let mut current_description = skill_meta.description.clone();
let mut iterations = Vec::new();
let initial_report = harness.run(suite, selector, registry).await?;
let initial_accuracy = initial_report.accuracy();
let mut best_accuracy = initial_accuracy;
if initial_accuracy >= self.config.target_accuracy {
return Ok(OptimizationResult {
skill: skill.clone(),
final_description: current_description,
iterations,
final_accuracy: initial_accuracy,
initial_accuracy,
});
}
for i in 0..self.config.max_iterations {
let prompt = self.build_prompt(skill, ¤t_description, &initial_report);
let new_description = self
.llm
.complete(&prompt, 200)
.await
.map_err(|e| LearnError::Optimizer(format!("LLM error: {}", e)))?
.trim()
.to_string();
let iteration = OptimizationIteration {
iteration: i + 1,
old_description: current_description.clone(),
new_description: new_description.clone(),
old_accuracy: best_accuracy,
new_accuracy: best_accuracy, accepted: true, };
iterations.push(iteration);
current_description = new_description;
if best_accuracy >= self.config.target_accuracy {
break;
}
}
Ok(OptimizationResult {
skill: skill.clone(),
final_description: current_description,
iterations,
final_accuracy: best_accuracy,
initial_accuracy,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockLlm;
#[async_trait]
impl LlmClient for MockLlm {
async fn complete(&self, _prompt: &str, _max_tokens: usize) -> Result<String, LlmError> {
Ok("Improved description for skill".to_string())
}
}
#[test]
fn test_optimizer_config_default() {
let config = OptimizerConfig::default();
assert_eq!(config.max_iterations, 5);
assert_eq!(config.target_accuracy, 0.95);
}
#[test]
fn test_build_prompt() {
let llm = Arc::new(MockLlm);
let optimizer = DescriptionOptimizer::new(llm, OptimizerConfig::default());
let skill = SkillName::new("test-skill").unwrap();
let report = TestReport {
suite_name: "test".to_string(),
results: Vec::new(),
per_skill: std::collections::HashMap::new(),
total: 0,
passed: 0,
avg_latency_ms: 0.0,
};
let prompt = optimizer.build_prompt(&skill, "Current description", &report);
assert!(prompt.contains("test-skill"));
assert!(prompt.contains("Current description"));
}
}