use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::RwLock;
use skm_core::SkillMetadata;
use skm_embed::{ComponentWeights, EmbeddingIndex, EmbeddingProvider};
use crate::error::SelectError;
use crate::strategy::{
Confidence, LatencyClass, SelectionContext, SelectionResult, SelectionStrategy,
};
#[derive(Debug, Clone)]
pub struct SemanticConfig {
pub top_k: usize,
pub min_score: f32,
pub gap_threshold: f32,
pub component_weights: ComponentWeights,
pub use_adaptive_k: bool,
}
impl Default for SemanticConfig {
fn default() -> Self {
Self {
top_k: 5,
min_score: 0.3,
gap_threshold: 0.15,
component_weights: ComponentWeights::default(),
use_adaptive_k: true,
}
}
}
impl SemanticConfig {
pub fn with_top_k(mut self, top_k: usize) -> Self {
self.top_k = top_k;
self
}
pub fn with_min_score(mut self, min_score: f32) -> Self {
self.min_score = min_score;
self
}
pub fn with_gap_threshold(mut self, gap: f32) -> Self {
self.gap_threshold = gap;
self
}
pub fn without_adaptive_k(mut self) -> Self {
self.use_adaptive_k = false;
self
}
}
pub struct SemanticStrategy {
provider: Arc<dyn EmbeddingProvider>,
index: Arc<RwLock<EmbeddingIndex>>,
config: SemanticConfig,
}
impl SemanticStrategy {
pub fn new(
provider: Arc<dyn EmbeddingProvider>,
index: EmbeddingIndex,
config: SemanticConfig,
) -> Self {
Self {
provider,
index: Arc::new(RwLock::new(index)),
config,
}
}
pub async fn update_index(&self, index: EmbeddingIndex) {
let mut guard = self.index.write().await;
*guard = index;
}
pub async fn index(&self) -> EmbeddingIndex {
self.index.read().await.clone()
}
}
#[async_trait]
impl SelectionStrategy for SemanticStrategy {
async fn select(
&self,
query: &str,
candidates: &[&SkillMetadata],
_ctx: &SelectionContext,
) -> Result<Vec<SelectionResult>, SelectError> {
let query_embedding = self.provider.embed_one(query).await?;
let index = self.index.read().await;
let scored = if self.config.use_adaptive_k {
index.query_adaptive(
&query_embedding,
self.config.min_score,
self.config.top_k,
self.config.gap_threshold,
)
} else {
index.query(&query_embedding, self.config.top_k)
};
let candidate_names: std::collections::HashSet<_> =
candidates.iter().map(|c| &c.name).collect();
let results: Vec<SelectionResult> = scored
.into_iter()
.filter(|s| candidate_names.contains(&s.name))
.filter(|s| s.score >= self.config.min_score)
.map(|s| {
let confidence = Confidence::from_score(s.score);
SelectionResult::new(s.name, s.score, confidence, "semantic").with_reasoning(
format!(
"desc={:.2}, trig={:.2}, tags={:.2}, ex={:.2}",
s.component_scores.description,
s.component_scores.triggers,
s.component_scores.tags,
s.component_scores.examples
),
)
})
.collect();
Ok(results)
}
fn name(&self) -> &str {
"semantic"
}
fn latency_class(&self) -> LatencyClass {
LatencyClass::Milliseconds
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_semantic_config() {
let config = SemanticConfig::default()
.with_top_k(3)
.with_min_score(0.5)
.with_gap_threshold(0.2)
.without_adaptive_k();
assert_eq!(config.top_k, 3);
assert_eq!(config.min_score, 0.5);
assert_eq!(config.gap_threshold, 0.2);
assert!(!config.use_adaptive_k);
}
#[test]
fn test_semantic_config_defaults() {
let config = SemanticConfig::default();
assert_eq!(config.top_k, 5);
assert_eq!(config.min_score, 0.3);
assert!(config.use_adaptive_k);
}
}