Skip to main content

skm_select/
fewshot.rs

1//! Few-shot enhanced selection strategy.
2
3use 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/// A few-shot example for training.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct FewShotExample {
16    /// Example user query.
17    pub input: String,
18
19    /// Correct skill for this query.
20    pub expected_skill: SkillName,
21
22    /// Why this skill is correct.
23    pub reasoning: Option<String>,
24}
25
26impl FewShotExample {
27    /// Create a new few-shot example.
28    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    /// Add reasoning.
37    pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
38        self.reasoning = Some(reasoning.into());
39        self
40    }
41}
42
43/// Wraps any strategy with dynamic few-shot example injection.
44///
45/// Selects examples semantically similar to the current query
46/// and provides them as context to the inner strategy.
47pub 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    /// Create a new few-shot enhanced strategy.
56    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    /// Set the number of examples to include.
66    pub fn with_top_k(mut self, top_k: usize) -> Self {
67        self.top_k_examples = top_k;
68        self
69    }
70
71    /// Add an example.
72    pub fn add_example(&mut self, example: FewShotExample) {
73        self.examples.push(example);
74    }
75
76    /// Get examples relevant to a query.
77    ///
78    /// Currently uses simple substring matching.
79    /// Could be enhanced with embedding similarity.
80    fn get_relevant_examples(&self, query: &str) -> Vec<&FewShotExample> {
81        let query_lower = query.to_lowercase();
82
83        // Score examples by word overlap
84        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        // Sort by score descending
99        scored.sort_by(|a, b| b.0.cmp(&a.0));
100
101        // Take top k
102        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        // Get relevant examples
119        let examples = self.get_relevant_examples(query);
120
121        // Augment context with examples
122        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        // Call inner strategy with augmented context
137        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        // Should find "extract text from PDF" and "merge two PDFs" (contain "PDF")
212        assert!(!relevant.is_empty());
213        assert!(relevant
214            .iter()
215            .any(|ex| ex.expected_skill.as_str() == "pdf-skill"));
216    }
217}