use serde::{Deserialize, Serialize};
use skm_core::SkillName;
use crate::embedding::Embedding;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentWeights {
pub description: f32,
pub triggers: f32,
pub tags: f32,
pub examples: f32,
}
impl Default for ComponentWeights {
fn default() -> Self {
Self {
description: 0.45,
triggers: 0.25,
tags: 0.15,
examples: 0.15,
}
}
}
impl ComponentWeights {
pub fn uniform() -> Self {
Self {
description: 0.25,
triggers: 0.25,
tags: 0.25,
examples: 0.25,
}
}
pub fn description_only() -> Self {
Self {
description: 1.0,
triggers: 0.0,
tags: 0.0,
examples: 0.0,
}
}
pub fn normalize(&mut self) {
let sum = self.description + self.triggers + self.tags + self.examples;
if sum > 0.0 {
self.description /= sum;
self.triggers /= sum;
self.tags /= sum;
self.examples /= sum;
}
}
pub fn is_normalized(&self) -> bool {
let sum = self.description + self.triggers + self.tags + self.examples;
(sum - 1.0).abs() < 1e-5
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillEmbeddings {
pub skill_name: SkillName,
pub description: Embedding,
pub triggers: Embedding,
pub tags: Embedding,
pub examples: Embedding,
pub weights: ComponentWeights,
}
impl SkillEmbeddings {
pub fn new(
skill_name: SkillName,
description: Embedding,
triggers: Embedding,
tags: Embedding,
examples: Embedding,
weights: ComponentWeights,
) -> Self {
Self {
skill_name,
description,
triggers,
tags,
examples,
weights,
}
}
pub fn score(&self, query: &Embedding) -> f32 {
self.weights.description * self.description.cosine_similarity(query)
+ self.weights.triggers * self.triggers.cosine_similarity(query)
+ self.weights.tags * self.tags.cosine_similarity(query)
+ self.weights.examples * self.examples.cosine_similarity(query)
}
pub fn component_scores(&self, query: &Embedding) -> ComponentScores {
ComponentScores {
description: self.description.cosine_similarity(query),
triggers: self.triggers.cosine_similarity(query),
tags: self.tags.cosine_similarity(query),
examples: self.examples.cosine_similarity(query),
}
}
pub fn with_weights(mut self, weights: ComponentWeights) -> Self {
self.weights = weights;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentScores {
pub description: f32,
pub triggers: f32,
pub tags: f32,
pub examples: f32,
}
impl ComponentScores {
pub fn weighted_sum(&self, weights: &ComponentWeights) -> f32 {
weights.description * self.description
+ weights.triggers * self.triggers
+ weights.tags * self.tags
+ weights.examples * self.examples
}
pub fn max(&self) -> f32 {
self.description
.max(self.triggers)
.max(self.tags)
.max(self.examples)
}
pub fn best_component(&self) -> &'static str {
let max = self.max();
if (self.description - max).abs() < 1e-6 {
"description"
} else if (self.triggers - max).abs() < 1e-6 {
"triggers"
} else if (self.tags - max).abs() < 1e-6 {
"tags"
} else {
"examples"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_embedding(seed: u64) -> Embedding {
let mut vector = vec![0.0f32; 4];
for (i, v) in vector.iter_mut().enumerate() {
*v = ((seed + i as u64) % 100) as f32 / 100.0;
}
Embedding::new(vector, seed)
}
#[test]
fn test_component_weights_default() {
let weights = ComponentWeights::default();
assert!(weights.is_normalized());
assert!((weights.description - 0.45).abs() < 1e-5);
}
#[test]
fn test_component_weights_uniform() {
let weights = ComponentWeights::uniform();
assert!(weights.is_normalized());
assert!((weights.description - 0.25).abs() < 1e-5);
}
#[test]
fn test_component_weights_normalize() {
let mut weights = ComponentWeights {
description: 2.0,
triggers: 1.0,
tags: 1.0,
examples: 0.0,
};
weights.normalize();
assert!(weights.is_normalized());
assert!((weights.description - 0.5).abs() < 1e-5);
assert!((weights.triggers - 0.25).abs() < 1e-5);
}
#[test]
fn test_skill_embeddings_score() {
let skill_name = SkillName::new("test").unwrap();
let embeddings = SkillEmbeddings::new(
skill_name,
make_embedding(1),
make_embedding(2),
make_embedding(3),
make_embedding(4),
ComponentWeights::uniform(),
);
let query = make_embedding(1);
let score = embeddings.score(&query);
assert!(score >= -1.0 && score <= 1.0);
}
#[test]
fn test_component_scores() {
let skill_name = SkillName::new("test").unwrap();
let embeddings = SkillEmbeddings::new(
skill_name,
make_embedding(1),
make_embedding(2),
make_embedding(3),
make_embedding(4),
ComponentWeights::uniform(),
);
let query = make_embedding(1);
let scores = embeddings.component_scores(&query);
assert!(scores.description > scores.triggers);
}
#[test]
fn test_component_scores_weighted_sum() {
let scores = ComponentScores {
description: 0.8,
triggers: 0.6,
tags: 0.4,
examples: 0.2,
};
let weights = ComponentWeights::uniform();
let sum = scores.weighted_sum(&weights);
assert!((sum - 0.5).abs() < 1e-5);
}
#[test]
fn test_component_scores_best() {
let scores = ComponentScores {
description: 0.3,
triggers: 0.9,
tags: 0.4,
examples: 0.2,
};
assert_eq!(scores.best_component(), "triggers");
assert!((scores.max() - 0.9).abs() < 1e-5);
}
}