use std::marker::PhantomData;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use skm_core::{SkillMetadata, SkillName};
use crate::error::SelectError;
use crate::strategy::{LatencyClass, SelectionContext, SelectionResult, SelectionStrategy};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FewShotExample {
pub input: String,
pub expected_skill: SkillName,
pub reasoning: Option<String>,
}
impl FewShotExample {
pub fn new(input: impl Into<String>, expected_skill: SkillName) -> Self {
Self {
input: input.into(),
expected_skill,
reasoning: None,
}
}
pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
self.reasoning = Some(reasoning.into());
self
}
}
pub struct FewShotEnhanced<S: SelectionStrategy> {
inner: S,
examples: Vec<FewShotExample>,
top_k_examples: usize,
_marker: PhantomData<S>,
}
impl<S: SelectionStrategy> FewShotEnhanced<S> {
pub fn new(inner: S, examples: Vec<FewShotExample>) -> Self {
Self {
inner,
examples,
top_k_examples: 3,
_marker: PhantomData,
}
}
pub fn with_top_k(mut self, top_k: usize) -> Self {
self.top_k_examples = top_k;
self
}
pub fn add_example(&mut self, example: FewShotExample) {
self.examples.push(example);
}
fn get_relevant_examples(&self, query: &str) -> Vec<&FewShotExample> {
let query_lower = query.to_lowercase();
let mut scored: Vec<_> = self
.examples
.iter()
.map(|ex| {
let ex_lower = ex.input.to_lowercase();
let score = query_lower
.split_whitespace()
.filter(|word| ex_lower.contains(word))
.count();
(score, ex)
})
.filter(|(score, _)| *score > 0)
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0));
scored
.into_iter()
.take(self.top_k_examples)
.map(|(_, ex)| ex)
.collect()
}
}
#[async_trait]
impl<S: SelectionStrategy> SelectionStrategy for FewShotEnhanced<S> {
async fn select(
&self,
query: &str,
candidates: &[&SkillMetadata],
ctx: &SelectionContext,
) -> Result<Vec<SelectionResult>, SelectError> {
let examples = self.get_relevant_examples(query);
let mut augmented_ctx = ctx.clone();
for ex in &examples {
let example_str = format!(
"Example: \"{}\" -> {}{}",
ex.input,
ex.expected_skill,
ex.reasoning
.as_ref()
.map(|r| format!(" ({})", r))
.unwrap_or_default()
);
augmented_ctx.conversation_history.push(example_str);
}
self.inner.select(query, candidates, &augmented_ctx).await
}
fn name(&self) -> &str {
"few-shot"
}
fn latency_class(&self) -> LatencyClass {
self.inner.latency_class()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trigger::TriggerStrategy;
use std::path::PathBuf;
fn make_metadata(name: &str, triggers: Vec<&str>) -> SkillMetadata {
SkillMetadata {
name: SkillName::new(name).unwrap(),
description: "Test skill".to_string(),
tags: Vec::new(),
triggers: triggers.into_iter().map(String::from).collect(),
source_path: PathBuf::new(),
content_hash: 0,
estimated_tokens: 100,
}
}
#[tokio::test]
async fn test_fewshot_enhanced() {
let skills = vec![
make_metadata("pdf-skill", vec!["pdf"]),
make_metadata("weather-skill", vec!["weather"]),
];
let inner = TriggerStrategy::from_metadata(&skills).unwrap();
let examples = vec![
FewShotExample::new("extract text from PDF", SkillName::new("pdf-skill").unwrap())
.with_reasoning("PDF extraction task"),
FewShotExample::new(
"what's the weather",
SkillName::new("weather-skill").unwrap(),
),
];
let strategy = FewShotEnhanced::new(inner, examples);
let refs: Vec<_> = skills.iter().collect();
let ctx = SelectionContext::new();
let results = strategy.select("get pdf text", &refs, &ctx).await.unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_get_relevant_examples() {
let inner = TriggerStrategy::new();
let examples = vec![
FewShotExample::new("extract text from PDF", SkillName::new("pdf-skill").unwrap()),
FewShotExample::new(
"what's the weather today",
SkillName::new("weather-skill").unwrap(),
),
FewShotExample::new("merge two PDFs", SkillName::new("pdf-skill").unwrap()),
];
let strategy = FewShotEnhanced::new(inner, examples);
let relevant = strategy.get_relevant_examples("extract from PDF file");
assert!(!relevant.is_empty());
assert!(relevant
.iter()
.any(|ex| ex.expected_skill.as_str() == "pdf-skill"));
}
}