subcog/hooks/search_intent/mod.rs
1//! Search intent detection for proactive memory surfacing.
2//!
3//! Detects user intent to search for information and extracts topics for memory injection.
4//!
5//! # Architecture
6//!
7//! This module is organized into focused submodules:
8//!
9//! - [`types`]: Core types (`SearchIntentType`, `DetectionSource`, `SearchIntent`)
10//! - [`keyword`]: Fast pattern-based detection (<10ms)
11//! - [`llm`]: LLM-powered classification for higher accuracy
12//! - [`hybrid`]: Combined detection with timeout support
13//!
14//! # Detection Modes
15//!
16//! | Mode | Latency | Accuracy | Use Case |
17//! |------|---------|----------|----------|
18//! | Keyword | <10ms | Good | Default, always available |
19//! | LLM | ~200ms | Excellent | When LLM provider configured |
20//! | Hybrid | <200ms | Best | Combines both with fallback |
21//!
22//! # Intent Types and Detection
23//!
24//! | Intent | Trigger Patterns | Namespace Weights |
25//! |--------|------------------|-------------------|
26//! | `HowTo` | "how do I", "how to", "implement", "create" | patterns (2.0), learnings (1.5) |
27//! | `Location` | "where is", "find", "locate" | context (1.5), patterns (1.2) |
28//! | `Explanation` | "what is", "explain", "describe" | decisions (1.5), context (1.2) |
29//! | `Comparison` | "difference between", "vs", "compare" | decisions (2.0), patterns (1.5) |
30//! | `Troubleshoot` | "error", "fix", "not working", "debug" | blockers (2.0), learnings (1.5) |
31//! | `General` | "search", "show me" | All namespaces weighted equally |
32//!
33//! # Detection Flow
34//!
35//! ```text
36//! User Prompt
37//! │
38//! ├─► Keyword Detection (<10ms)
39//! │ │
40//! │ └─► Intent + Confidence + Topics
41//! │
42//! └─► LLM Classification (200ms timeout) [optional]
43//! │
44//! └─► Intent + Confidence + Topics
45//! │
46//! └─► Merge Results (hybrid mode)
47//! │
48//! └─► Final Intent + Topics
49//! ```
50//!
51//! # Confidence-Based Memory Injection
52//!
53//! | Confidence | Memory Count | Behavior |
54//! |------------|--------------|----------|
55//! | ≥ 0.8 (high) | 15 memories | Full context injection |
56//! | ≥ 0.5 (medium) | 10 memories | Standard injection |
57//! | < 0.5 (low) | 5 memories | Minimal injection |
58//!
59//! # Configuration
60//!
61//! | Environment Variable | Description | Default |
62//! |---------------------|-------------|---------|
63//! | `SUBCOG_SEARCH_INTENT_ENABLED` | Enable intent detection | `true` |
64//! | `SUBCOG_SEARCH_INTENT_USE_LLM` | Enable LLM classification | `true` |
65//! | `SUBCOG_SEARCH_INTENT_LLM_TIMEOUT_MS` | LLM timeout | `200` |
66//! | `SUBCOG_SEARCH_INTENT_MIN_CONFIDENCE` | Minimum confidence | `0.5` |
67//!
68//! # Examples
69//!
70//! ```rust,ignore
71//! use subcog::hooks::search_intent::{detect_search_intent, SearchIntentType};
72//!
73//! // Simple keyword detection
74//! if let Some(intent) = detect_search_intent("How do I implement OAuth?") {
75//! assert_eq!(intent.intent_type, SearchIntentType::HowTo);
76//! println!("Detected: {} with confidence {}", intent.intent_type, intent.confidence);
77//! }
78//! ```
79
80mod hybrid;
81mod keyword;
82mod llm;
83mod types;
84
85// Re-export public types
86pub use types::{DetectionSource, SearchIntent, SearchIntentType};
87
88// Re-export detection functions
89pub use hybrid::{detect_search_intent_hybrid, detect_search_intent_with_timeout};
90pub use keyword::detect_search_intent;
91pub use llm::classify_intent_with_llm;
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_module_exports() {
99 // Verify all public items are accessible
100 let intent_type = SearchIntentType::HowTo;
101 assert_eq!(intent_type.as_str(), "howto");
102
103 let source = DetectionSource::Keyword;
104 assert_eq!(source.as_str(), "keyword");
105
106 let intent = SearchIntent::default();
107 assert_eq!(intent.intent_type, SearchIntentType::General);
108
109 // Verify functions are accessible
110 let result = detect_search_intent("How do I test?");
111 assert!(result.is_some());
112 }
113
114 #[test]
115 fn test_end_to_end_keyword_detection() {
116 let prompts_and_intents = [
117 ("How do I implement caching?", SearchIntentType::HowTo),
118 ("Where is the config file?", SearchIntentType::Location),
119 (
120 "What is dependency injection?",
121 SearchIntentType::Explanation,
122 ),
123 (
124 "What's the difference between sync and async?",
125 SearchIntentType::Comparison,
126 ),
127 (
128 "Why am I getting this error?",
129 SearchIntentType::Troubleshoot,
130 ),
131 ];
132
133 for (prompt, expected_intent) in prompts_and_intents {
134 let result = detect_search_intent(prompt);
135 assert!(result.is_some(), "Should detect intent for: {prompt}");
136 assert_eq!(
137 result.unwrap().intent_type,
138 expected_intent,
139 "Wrong intent for: {prompt}"
140 );
141 }
142 }
143
144 #[test]
145 fn test_namespace_weights_vary_by_intent() {
146 let howto_weights = SearchIntentType::HowTo.namespace_weights();
147 let troubleshoot_weights = SearchIntentType::Troubleshoot.namespace_weights();
148
149 // HowTo should prioritize patterns
150 assert!(
151 howto_weights
152 .iter()
153 .any(|(ns, w)| *ns == "patterns" && *w >= 2.0)
154 );
155
156 // Troubleshoot should prioritize blockers
157 assert!(
158 troubleshoot_weights
159 .iter()
160 .any(|(ns, w)| *ns == "blockers" && *w >= 2.0)
161 );
162 }
163}