1use std::collections::HashMap;
4
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8use skm_core::{SkillMetadata, SkillName};
9
10use crate::error::SelectError;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub enum Confidence {
15 None,
17
18 Low,
20
21 Medium,
23
24 High,
26
27 Definite,
29}
30
31impl Confidence {
32 pub fn is_high_enough(&self, threshold: Confidence) -> bool {
34 *self >= threshold
35 }
36
37 pub fn as_score(&self) -> f32 {
39 match self {
40 Self::None => 0.0,
41 Self::Low => 0.25,
42 Self::Medium => 0.5,
43 Self::High => 0.75,
44 Self::Definite => 1.0,
45 }
46 }
47
48 pub fn from_score(score: f32) -> Self {
50 if score >= 0.9 {
51 Self::Definite
52 } else if score >= 0.7 {
53 Self::High
54 } else if score >= 0.5 {
55 Self::Medium
56 } else if score >= 0.25 {
57 Self::Low
58 } else {
59 Self::None
60 }
61 }
62}
63
64impl Default for Confidence {
65 fn default() -> Self {
66 Self::None
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum LatencyClass {
73 Microseconds,
75
76 Milliseconds,
78
79 Seconds,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct SelectionResult {
86 pub skill: SkillName,
88
89 pub score: f32,
91
92 pub confidence: Confidence,
94
95 pub strategy: String,
97
98 pub reasoning: Option<String>,
100}
101
102impl SelectionResult {
103 pub fn new(
105 skill: SkillName,
106 score: f32,
107 confidence: Confidence,
108 strategy: impl Into<String>,
109 ) -> Self {
110 Self {
111 skill,
112 score,
113 confidence,
114 strategy: strategy.into(),
115 reasoning: None,
116 }
117 }
118
119 pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
121 self.reasoning = Some(reasoning.into());
122 self
123 }
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128pub struct SelectionContext {
129 pub conversation_history: Vec<String>,
131
132 pub active_skills: Vec<SkillName>,
134
135 pub user_locale: Option<String>,
137
138 pub custom: HashMap<String, String>,
140}
141
142impl SelectionContext {
143 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn with_history(mut self, history: Vec<String>) -> Self {
150 self.conversation_history = history;
151 self
152 }
153
154 pub fn with_active_skills(mut self, skills: Vec<SkillName>) -> Self {
156 self.active_skills = skills;
157 self
158 }
159
160 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
162 self.user_locale = Some(locale.into());
163 self
164 }
165
166 pub fn with_custom(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
168 self.custom.insert(key.into(), value.into());
169 self
170 }
171}
172
173#[async_trait]
178pub trait SelectionStrategy: Send + Sync {
179 async fn select(
181 &self,
182 query: &str,
183 candidates: &[&SkillMetadata],
184 ctx: &SelectionContext,
185 ) -> Result<Vec<SelectionResult>, SelectError>;
186
187 fn name(&self) -> &str;
189
190 fn latency_class(&self) -> LatencyClass;
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_confidence_ordering() {
200 assert!(Confidence::Definite > Confidence::High);
201 assert!(Confidence::High > Confidence::Medium);
202 assert!(Confidence::Medium > Confidence::Low);
203 assert!(Confidence::Low > Confidence::None);
204 }
205
206 #[test]
207 fn test_confidence_as_score() {
208 assert_eq!(Confidence::None.as_score(), 0.0);
209 assert_eq!(Confidence::Definite.as_score(), 1.0);
210 }
211
212 #[test]
213 fn test_confidence_from_score() {
214 assert_eq!(Confidence::from_score(0.95), Confidence::Definite);
215 assert_eq!(Confidence::from_score(0.8), Confidence::High);
216 assert_eq!(Confidence::from_score(0.6), Confidence::Medium);
217 assert_eq!(Confidence::from_score(0.3), Confidence::Low);
218 assert_eq!(Confidence::from_score(0.1), Confidence::None);
219 }
220
221 #[test]
222 fn test_is_high_enough() {
223 assert!(Confidence::Definite.is_high_enough(Confidence::High));
224 assert!(Confidence::High.is_high_enough(Confidence::High));
225 assert!(!Confidence::Medium.is_high_enough(Confidence::High));
226 }
227
228 #[test]
229 fn test_selection_result() {
230 let skill = SkillName::new("test").unwrap();
231 let result = SelectionResult::new(skill.clone(), 0.9, Confidence::High, "trigger")
232 .with_reasoning("Matched keyword");
233
234 assert_eq!(result.skill, skill);
235 assert_eq!(result.score, 0.9);
236 assert_eq!(result.reasoning, Some("Matched keyword".to_string()));
237 }
238
239 #[test]
240 fn test_selection_context() {
241 let ctx = SelectionContext::new()
242 .with_locale("zh-CN")
243 .with_custom("key", "value");
244
245 assert_eq!(ctx.user_locale, Some("zh-CN".to_string()));
246 assert_eq!(ctx.custom.get("key"), Some(&"value".to_string()));
247 }
248}