subcog/hooks/search_intent/
types.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum SearchIntentType {
17 HowTo,
19 Location,
21 Explanation,
23 Comparison,
25 Troubleshoot,
27 #[default]
29 General,
30}
31
32impl SearchIntentType {
33 #[must_use]
37 pub fn namespace_weights(&self) -> Vec<(&'static str, f32)> {
38 match self {
39 Self::HowTo => vec![
40 ("patterns", 2.0),
41 ("learnings", 1.5),
42 ("decisions", 1.0),
43 ("context", 1.0),
44 ],
45 Self::Location => vec![("context", 1.5), ("patterns", 1.2), ("decisions", 1.0)],
46 Self::Explanation => vec![("decisions", 1.5), ("context", 1.2), ("learnings", 1.0)],
47 Self::Comparison => vec![("decisions", 2.0), ("patterns", 1.5), ("learnings", 1.0)],
48 Self::Troubleshoot => vec![
49 ("blockers", 2.0),
50 ("learnings", 1.5),
51 ("tech-debt", 1.2),
52 ("patterns", 1.0),
53 ],
54 Self::General => vec![
55 ("decisions", 1.0),
56 ("patterns", 1.0),
57 ("learnings", 1.0),
58 ("context", 1.0),
59 ],
60 }
61 }
62
63 #[must_use]
67 pub fn parse(s: &str) -> Option<Self> {
68 match s.to_lowercase().as_str() {
69 "howto" | "how_to" | "how-to" | "implementation" => Some(Self::HowTo),
70 "location" | "find" | "where" => Some(Self::Location),
71 "explanation" | "explain" | "what" => Some(Self::Explanation),
72 "comparison" | "compare" | "vs" => Some(Self::Comparison),
73 "troubleshoot" | "debug" | "error" | "fix" => Some(Self::Troubleshoot),
74 "general" | "search" | "query" => Some(Self::General),
75 _ => None,
76 }
77 }
78
79 #[must_use]
81 pub const fn as_str(&self) -> &'static str {
82 match self {
83 Self::HowTo => "howto",
84 Self::Location => "location",
85 Self::Explanation => "explanation",
86 Self::Comparison => "comparison",
87 Self::Troubleshoot => "troubleshoot",
88 Self::General => "general",
89 }
90 }
91}
92
93impl std::fmt::Display for SearchIntentType {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(f, "{}", self.as_str())
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
101#[serde(rename_all = "snake_case")]
102pub enum DetectionSource {
103 #[default]
105 Keyword,
106 Llm,
108 Hybrid,
110}
111
112impl DetectionSource {
113 #[must_use]
115 pub const fn as_str(&self) -> &'static str {
116 match self {
117 Self::Keyword => "keyword",
118 Self::Llm => "llm",
119 Self::Hybrid => "hybrid",
120 }
121 }
122}
123
124impl std::fmt::Display for DetectionSource {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.as_str())
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SearchIntent {
136 pub intent_type: SearchIntentType,
138 pub confidence: f32,
140 pub keywords: Vec<String>,
142 pub topics: Vec<String>,
144 pub source: DetectionSource,
146}
147
148impl Default for SearchIntent {
149 fn default() -> Self {
150 Self {
151 intent_type: SearchIntentType::General,
152 confidence: 0.0,
153 keywords: Vec::new(),
154 topics: Vec::new(),
155 source: DetectionSource::Keyword,
156 }
157 }
158}
159
160impl SearchIntent {
161 #[must_use]
163 pub fn new(intent_type: SearchIntentType) -> Self {
164 Self {
165 intent_type,
166 ..Default::default()
167 }
168 }
169
170 #[must_use]
172 pub const fn with_confidence(mut self, confidence: f32) -> Self {
173 self.confidence = confidence;
174 self
175 }
176
177 #[must_use]
179 pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
180 self.keywords = keywords;
181 self
182 }
183
184 #[must_use]
186 pub fn with_topics(mut self, topics: Vec<String>) -> Self {
187 self.topics = topics;
188 self
189 }
190
191 #[must_use]
193 pub const fn with_source(mut self, source: DetectionSource) -> Self {
194 self.source = source;
195 self
196 }
197
198 #[must_use]
200 pub fn is_high_confidence(&self) -> bool {
201 self.confidence >= 0.8
202 }
203
204 #[must_use]
206 pub fn is_medium_confidence(&self) -> bool {
207 self.confidence >= 0.5
208 }
209
210 #[must_use]
216 pub const fn recommended_memory_count(&self) -> usize {
217 if self.confidence >= 0.8 {
218 15
219 } else if self.confidence >= 0.5 {
220 10
221 } else {
222 5
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_intent_type_parse() {
233 assert_eq!(
234 SearchIntentType::parse("howto"),
235 Some(SearchIntentType::HowTo)
236 );
237 assert_eq!(
238 SearchIntentType::parse("HowTo"),
239 Some(SearchIntentType::HowTo)
240 );
241 assert_eq!(
242 SearchIntentType::parse("how_to"),
243 Some(SearchIntentType::HowTo)
244 );
245 assert_eq!(
246 SearchIntentType::parse("troubleshoot"),
247 Some(SearchIntentType::Troubleshoot)
248 );
249 assert_eq!(SearchIntentType::parse("unknown"), None);
250 }
251
252 #[test]
253 fn test_intent_type_as_str() {
254 assert_eq!(SearchIntentType::HowTo.as_str(), "howto");
255 assert_eq!(SearchIntentType::Location.as_str(), "location");
256 assert_eq!(SearchIntentType::General.as_str(), "general");
257 }
258
259 #[test]
260 fn test_detection_source_display() {
261 assert_eq!(DetectionSource::Keyword.to_string(), "keyword");
262 assert_eq!(DetectionSource::Llm.to_string(), "llm");
263 assert_eq!(DetectionSource::Hybrid.to_string(), "hybrid");
264 }
265
266 #[test]
267 fn test_search_intent_default() {
268 let intent = SearchIntent::default();
269 assert_eq!(intent.intent_type, SearchIntentType::General);
270 assert!(intent.confidence.abs() < f32::EPSILON);
271 assert!(intent.keywords.is_empty());
272 assert!(intent.topics.is_empty());
273 assert_eq!(intent.source, DetectionSource::Keyword);
274 }
275
276 #[test]
277 fn test_search_intent_builder() {
278 let intent = SearchIntent::new(SearchIntentType::HowTo)
279 .with_confidence(0.85)
280 .with_keywords(vec!["how".to_string()])
281 .with_topics(vec!["rust".to_string()])
282 .with_source(DetectionSource::Llm);
283
284 assert_eq!(intent.intent_type, SearchIntentType::HowTo);
285 assert!((intent.confidence - 0.85).abs() < f32::EPSILON);
286 assert_eq!(intent.keywords, vec!["how"]);
287 assert_eq!(intent.topics, vec!["rust"]);
288 assert_eq!(intent.source, DetectionSource::Llm);
289 }
290
291 #[test]
292 fn test_confidence_levels() {
293 let high = SearchIntent::new(SearchIntentType::General).with_confidence(0.9);
294 assert!(high.is_high_confidence());
295 assert!(high.is_medium_confidence());
296 assert_eq!(high.recommended_memory_count(), 15);
297
298 let medium = SearchIntent::new(SearchIntentType::General).with_confidence(0.6);
299 assert!(!medium.is_high_confidence());
300 assert!(medium.is_medium_confidence());
301 assert_eq!(medium.recommended_memory_count(), 10);
302
303 let low = SearchIntent::new(SearchIntentType::General).with_confidence(0.3);
304 assert!(!low.is_high_confidence());
305 assert!(!low.is_medium_confidence());
306 assert_eq!(low.recommended_memory_count(), 5);
307 }
308
309 #[test]
310 fn test_namespace_weights() {
311 let weights = SearchIntentType::Troubleshoot.namespace_weights();
312 assert!(
313 weights
314 .iter()
315 .any(|(ns, w)| *ns == "blockers" && (*w - 2.0).abs() < f32::EPSILON)
316 );
317 assert!(
318 weights
319 .iter()
320 .any(|(ns, w)| *ns == "learnings" && (*w - 1.5).abs() < f32::EPSILON)
321 );
322 }
323}