1use std::marker::PhantomData;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8use skm_core::{SkillMetadata, SkillName};
9
10use crate::error::SelectError;
11use crate::strategy::{LatencyClass, SelectionContext, SelectionResult, SelectionStrategy};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct FewShotExample {
16 pub input: String,
18
19 pub expected_skill: SkillName,
21
22 pub reasoning: Option<String>,
24}
25
26impl FewShotExample {
27 pub fn new(input: impl Into<String>, expected_skill: SkillName) -> Self {
29 Self {
30 input: input.into(),
31 expected_skill,
32 reasoning: None,
33 }
34 }
35
36 pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
38 self.reasoning = Some(reasoning.into());
39 self
40 }
41}
42
43pub struct FewShotEnhanced<S: SelectionStrategy> {
48 inner: S,
49 examples: Vec<FewShotExample>,
50 top_k_examples: usize,
51 _marker: PhantomData<S>,
52}
53
54impl<S: SelectionStrategy> FewShotEnhanced<S> {
55 pub fn new(inner: S, examples: Vec<FewShotExample>) -> Self {
57 Self {
58 inner,
59 examples,
60 top_k_examples: 3,
61 _marker: PhantomData,
62 }
63 }
64
65 pub fn with_top_k(mut self, top_k: usize) -> Self {
67 self.top_k_examples = top_k;
68 self
69 }
70
71 pub fn add_example(&mut self, example: FewShotExample) {
73 self.examples.push(example);
74 }
75
76 fn get_relevant_examples(&self, query: &str) -> Vec<&FewShotExample> {
81 let query_lower = query.to_lowercase();
82
83 let mut scored: Vec<_> = self
85 .examples
86 .iter()
87 .map(|ex| {
88 let ex_lower = ex.input.to_lowercase();
89 let score = query_lower
90 .split_whitespace()
91 .filter(|word| ex_lower.contains(word))
92 .count();
93 (score, ex)
94 })
95 .filter(|(score, _)| *score > 0)
96 .collect();
97
98 scored.sort_by(|a, b| b.0.cmp(&a.0));
100
101 scored
103 .into_iter()
104 .take(self.top_k_examples)
105 .map(|(_, ex)| ex)
106 .collect()
107 }
108}
109
110#[async_trait]
111impl<S: SelectionStrategy> SelectionStrategy for FewShotEnhanced<S> {
112 async fn select(
113 &self,
114 query: &str,
115 candidates: &[&SkillMetadata],
116 ctx: &SelectionContext,
117 ) -> Result<Vec<SelectionResult>, SelectError> {
118 let examples = self.get_relevant_examples(query);
120
121 let mut augmented_ctx = ctx.clone();
123 for ex in &examples {
124 let example_str = format!(
125 "Example: \"{}\" -> {}{}",
126 ex.input,
127 ex.expected_skill,
128 ex.reasoning
129 .as_ref()
130 .map(|r| format!(" ({})", r))
131 .unwrap_or_default()
132 );
133 augmented_ctx.conversation_history.push(example_str);
134 }
135
136 self.inner.select(query, candidates, &augmented_ctx).await
138 }
139
140 fn name(&self) -> &str {
141 "few-shot"
142 }
143
144 fn latency_class(&self) -> LatencyClass {
145 self.inner.latency_class()
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::trigger::TriggerStrategy;
153 use std::path::PathBuf;
154
155 fn make_metadata(name: &str, triggers: Vec<&str>) -> SkillMetadata {
156 SkillMetadata {
157 name: SkillName::new(name).unwrap(),
158 description: "Test skill".to_string(),
159 tags: Vec::new(),
160 triggers: triggers.into_iter().map(String::from).collect(),
161 source_path: PathBuf::new(),
162 content_hash: 0,
163 estimated_tokens: 100,
164 }
165 }
166
167 #[tokio::test]
168 async fn test_fewshot_enhanced() {
169 let skills = vec![
170 make_metadata("pdf-skill", vec!["pdf"]),
171 make_metadata("weather-skill", vec!["weather"]),
172 ];
173
174 let inner = TriggerStrategy::from_metadata(&skills).unwrap();
175
176 let examples = vec![
177 FewShotExample::new("extract text from PDF", SkillName::new("pdf-skill").unwrap())
178 .with_reasoning("PDF extraction task"),
179 FewShotExample::new(
180 "what's the weather",
181 SkillName::new("weather-skill").unwrap(),
182 ),
183 ];
184
185 let strategy = FewShotEnhanced::new(inner, examples);
186
187 let refs: Vec<_> = skills.iter().collect();
188 let ctx = SelectionContext::new();
189
190 let results = strategy.select("get pdf text", &refs, &ctx).await.unwrap();
191
192 assert!(!results.is_empty());
193 }
194
195 #[test]
196 fn test_get_relevant_examples() {
197 let inner = TriggerStrategy::new();
198 let examples = vec![
199 FewShotExample::new("extract text from PDF", SkillName::new("pdf-skill").unwrap()),
200 FewShotExample::new(
201 "what's the weather today",
202 SkillName::new("weather-skill").unwrap(),
203 ),
204 FewShotExample::new("merge two PDFs", SkillName::new("pdf-skill").unwrap()),
205 ];
206
207 let strategy = FewShotEnhanced::new(inner, examples);
208
209 let relevant = strategy.get_relevant_examples("extract from PDF file");
210
211 assert!(!relevant.is_empty());
213 assert!(relevant
214 .iter()
215 .any(|ex| ex.expected_skill.as_str() == "pdf-skill"));
216 }
217}