1use serde::{Deserialize, Serialize};
7
8use skm_core::SkillName;
9
10use crate::embedding::Embedding;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ComponentWeights {
15 pub description: f32,
17
18 pub triggers: f32,
20
21 pub tags: f32,
23
24 pub examples: f32,
26}
27
28impl Default for ComponentWeights {
29 fn default() -> Self {
30 Self {
31 description: 0.45,
32 triggers: 0.25,
33 tags: 0.15,
34 examples: 0.15,
35 }
36 }
37}
38
39impl ComponentWeights {
40 pub fn uniform() -> Self {
42 Self {
43 description: 0.25,
44 triggers: 0.25,
45 tags: 0.25,
46 examples: 0.25,
47 }
48 }
49
50 pub fn description_only() -> Self {
52 Self {
53 description: 1.0,
54 triggers: 0.0,
55 tags: 0.0,
56 examples: 0.0,
57 }
58 }
59
60 pub fn normalize(&mut self) {
62 let sum = self.description + self.triggers + self.tags + self.examples;
63 if sum > 0.0 {
64 self.description /= sum;
65 self.triggers /= sum;
66 self.tags /= sum;
67 self.examples /= sum;
68 }
69 }
70
71 pub fn is_normalized(&self) -> bool {
73 let sum = self.description + self.triggers + self.tags + self.examples;
74 (sum - 1.0).abs() < 1e-5
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct SkillEmbeddings {
82 pub skill_name: SkillName,
84
85 pub description: Embedding,
87
88 pub triggers: Embedding,
90
91 pub tags: Embedding,
93
94 pub examples: Embedding,
96
97 pub weights: ComponentWeights,
99}
100
101impl SkillEmbeddings {
102 pub fn new(
104 skill_name: SkillName,
105 description: Embedding,
106 triggers: Embedding,
107 tags: Embedding,
108 examples: Embedding,
109 weights: ComponentWeights,
110 ) -> Self {
111 Self {
112 skill_name,
113 description,
114 triggers,
115 tags,
116 examples,
117 weights,
118 }
119 }
120
121 pub fn score(&self, query: &Embedding) -> f32 {
123 self.weights.description * self.description.cosine_similarity(query)
124 + self.weights.triggers * self.triggers.cosine_similarity(query)
125 + self.weights.tags * self.tags.cosine_similarity(query)
126 + self.weights.examples * self.examples.cosine_similarity(query)
127 }
128
129 pub fn component_scores(&self, query: &Embedding) -> ComponentScores {
131 ComponentScores {
132 description: self.description.cosine_similarity(query),
133 triggers: self.triggers.cosine_similarity(query),
134 tags: self.tags.cosine_similarity(query),
135 examples: self.examples.cosine_similarity(query),
136 }
137 }
138
139 pub fn with_weights(mut self, weights: ComponentWeights) -> Self {
141 self.weights = weights;
142 self
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ComponentScores {
149 pub description: f32,
151
152 pub triggers: f32,
154
155 pub tags: f32,
157
158 pub examples: f32,
160}
161
162impl ComponentScores {
163 pub fn weighted_sum(&self, weights: &ComponentWeights) -> f32 {
165 weights.description * self.description
166 + weights.triggers * self.triggers
167 + weights.tags * self.tags
168 + weights.examples * self.examples
169 }
170
171 pub fn max(&self) -> f32 {
173 self.description
174 .max(self.triggers)
175 .max(self.tags)
176 .max(self.examples)
177 }
178
179 pub fn best_component(&self) -> &'static str {
181 let max = self.max();
182 if (self.description - max).abs() < 1e-6 {
183 "description"
184 } else if (self.triggers - max).abs() < 1e-6 {
185 "triggers"
186 } else if (self.tags - max).abs() < 1e-6 {
187 "tags"
188 } else {
189 "examples"
190 }
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn make_embedding(seed: u64) -> Embedding {
199 let mut vector = vec![0.0f32; 4];
201 for (i, v) in vector.iter_mut().enumerate() {
202 *v = ((seed + i as u64) % 100) as f32 / 100.0;
203 }
204 Embedding::new(vector, seed)
205 }
206
207 #[test]
208 fn test_component_weights_default() {
209 let weights = ComponentWeights::default();
210 assert!(weights.is_normalized());
211 assert!((weights.description - 0.45).abs() < 1e-5);
212 }
213
214 #[test]
215 fn test_component_weights_uniform() {
216 let weights = ComponentWeights::uniform();
217 assert!(weights.is_normalized());
218 assert!((weights.description - 0.25).abs() < 1e-5);
219 }
220
221 #[test]
222 fn test_component_weights_normalize() {
223 let mut weights = ComponentWeights {
224 description: 2.0,
225 triggers: 1.0,
226 tags: 1.0,
227 examples: 0.0,
228 };
229 weights.normalize();
230 assert!(weights.is_normalized());
231 assert!((weights.description - 0.5).abs() < 1e-5);
232 assert!((weights.triggers - 0.25).abs() < 1e-5);
233 }
234
235 #[test]
236 fn test_skill_embeddings_score() {
237 let skill_name = SkillName::new("test").unwrap();
238 let embeddings = SkillEmbeddings::new(
239 skill_name,
240 make_embedding(1),
241 make_embedding(2),
242 make_embedding(3),
243 make_embedding(4),
244 ComponentWeights::uniform(),
245 );
246
247 let query = make_embedding(1);
248 let score = embeddings.score(&query);
249
250 assert!(score >= -1.0 && score <= 1.0);
252 }
253
254 #[test]
255 fn test_component_scores() {
256 let skill_name = SkillName::new("test").unwrap();
257 let embeddings = SkillEmbeddings::new(
258 skill_name,
259 make_embedding(1),
260 make_embedding(2),
261 make_embedding(3),
262 make_embedding(4),
263 ComponentWeights::uniform(),
264 );
265
266 let query = make_embedding(1);
267 let scores = embeddings.component_scores(&query);
268
269 assert!(scores.description > scores.triggers);
271 }
272
273 #[test]
274 fn test_component_scores_weighted_sum() {
275 let scores = ComponentScores {
276 description: 0.8,
277 triggers: 0.6,
278 tags: 0.4,
279 examples: 0.2,
280 };
281
282 let weights = ComponentWeights::uniform();
283 let sum = scores.weighted_sum(&weights);
284
285 assert!((sum - 0.5).abs() < 1e-5);
287 }
288
289 #[test]
290 fn test_component_scores_best() {
291 let scores = ComponentScores {
292 description: 0.3,
293 triggers: 0.9,
294 tags: 0.4,
295 examples: 0.2,
296 };
297
298 assert_eq!(scores.best_component(), "triggers");
299 assert!((scores.max() - 0.9).abs() < 1e-5);
300 }
301}