1010//! Phase 2+: Actual MLP with learned weights
1111
1212use crate :: types:: { Query , RouterConfig , RoutingDecision } ;
13+ use crate :: mlp:: MLP ;
1314
1415/// Router for determining query routing strategy
1516#[ derive( Debug , Clone ) ]
1617pub struct Router {
1718 config : RouterConfig ,
19+ /// Optional MLP for learned routing (Phase 2+)
20+ mlp : Option < MLP > ,
21+ /// Whether to use MLP or fallback to heuristics
22+ use_mlp : bool ,
1823}
1924
2025impl Router {
2126 /// Create a new router with default configuration
2227 pub fn new ( ) -> Self {
2328 Self {
2429 config : RouterConfig :: default ( ) ,
30+ mlp : None ,
31+ use_mlp : false ,
2532 }
2633 }
2734
2835 /// Create a router with custom configuration
2936 pub fn with_config ( config : RouterConfig ) -> Self {
30- Self { config }
37+ Self {
38+ config,
39+ mlp : None ,
40+ use_mlp : false ,
41+ }
42+ }
43+
44+ /// Create a router with a trained MLP
45+ pub fn with_mlp ( mlp : MLP ) -> Self {
46+ Self {
47+ config : RouterConfig :: default ( ) ,
48+ mlp : Some ( mlp) ,
49+ use_mlp : true ,
50+ }
51+ }
52+
53+ /// Load MLP from persistence
54+ #[ cfg( feature = "persistence" ) ]
55+ pub fn load_mlp ( & mut self , pm : & crate :: persistence:: PersistenceManager , name : & str ) -> Result < ( ) , String > {
56+ match pm. load_mlp ( name) {
57+ Ok ( Some ( mlp) ) => {
58+ self . mlp = Some ( mlp) ;
59+ self . use_mlp = true ;
60+ Ok ( ( ) )
61+ }
62+ Ok ( None ) => Err ( format ! ( "No MLP found with name '{}'" , name) ) ,
63+ Err ( e) => Err ( format ! ( "Failed to load MLP: {}" , e) ) ,
64+ }
65+ }
66+
67+ /// Save MLP to persistence
68+ #[ cfg( feature = "persistence" ) ]
69+ pub fn save_mlp ( & self , pm : & crate :: persistence:: PersistenceManager , name : & str , accuracy : Option < f32 > ) -> Result < ( ) , String > {
70+ if let Some ( ref mlp) = self . mlp {
71+ pm. save_mlp ( name, mlp, accuracy)
72+ . map_err ( |e| format ! ( "Failed to save MLP: {}" , e) )
73+ } else {
74+ Err ( "No MLP to save" . to_string ( ) )
75+ }
76+ }
77+
78+ /// Set the MLP for routing
79+ pub fn set_mlp ( & mut self , mlp : MLP ) {
80+ self . mlp = Some ( mlp) ;
81+ self . use_mlp = true ;
82+ }
83+
84+ /// Enable or disable MLP usage (fallback to heuristics if disabled)
85+ pub fn set_use_mlp ( & mut self , use_mlp : bool ) {
86+ self . use_mlp = use_mlp && self . mlp . is_some ( ) ;
87+ }
88+
89+ /// Extract features from query for MLP input
90+ fn extract_features ( & self , query : & Query ) -> Vec < f32 > {
91+ let mut features = vec ! [ 0.0 ; 384 ] ;
92+
93+ // Feature 0: Normalized query length
94+ features[ 0 ] = ( query. text . len ( ) as f32 / 1000.0 ) . min ( 1.0 ) ;
95+
96+ // Feature 1: Word count normalized
97+ features[ 1 ] = ( query. text . split_whitespace ( ) . count ( ) as f32 / 100.0 ) . min ( 1.0 ) ;
98+
99+ // Feature 2: Has question mark
100+ features[ 2 ] = if query. text . contains ( '?' ) { 1.0 } else { 0.0 } ;
101+
102+ // Feature 3: Priority normalized
103+ features[ 3 ] = query. priority as f32 / 10.0 ;
104+
105+ // Feature 4: Has project context
106+ features[ 4 ] = if query. project_context . is_some ( ) { 1.0 } else { 0.0 } ;
107+
108+ // Feature 5-9: Complex keyword indicators
109+ for ( i, keyword) in self . config . complex_keywords . iter ( ) . enumerate ( ) . take ( 5 ) {
110+ features[ 5 + i] = if query. text . to_lowercase ( ) . contains ( & keyword. to_lowercase ( ) ) {
111+ 1.0
112+ } else {
113+ 0.0
114+ } ;
115+ }
116+
117+ // Feature 10: Uppercase ratio (might indicate emphasis/urgency)
118+ let uppercase_count = query. text . chars ( ) . filter ( |c| c. is_uppercase ( ) ) . count ( ) ;
119+ features[ 10 ] = if !query. text . is_empty ( ) {
120+ uppercase_count as f32 / query. text . len ( ) as f32
121+ } else {
122+ 0.0
123+ } ;
124+
125+ // Feature 11: Punctuation density
126+ let punct_count = query. text . chars ( ) . filter ( |c| c. is_ascii_punctuation ( ) ) . count ( ) ;
127+ features[ 11 ] = if !query. text . is_empty ( ) {
128+ punct_count as f32 / query. text . len ( ) as f32
129+ } else {
130+ 0.0
131+ } ;
132+
133+ // Features 12-19: Query type indicators
134+ features[ 12 ] = if query. text . to_lowercase ( ) . starts_with ( "how" ) { 1.0 } else { 0.0 } ;
135+ features[ 13 ] = if query. text . to_lowercase ( ) . starts_with ( "what" ) { 1.0 } else { 0.0 } ;
136+ features[ 14 ] = if query. text . to_lowercase ( ) . starts_with ( "why" ) { 1.0 } else { 0.0 } ;
137+ features[ 15 ] = if query. text . to_lowercase ( ) . starts_with ( "when" ) { 1.0 } else { 0.0 } ;
138+ features[ 16 ] = if query. text . to_lowercase ( ) . starts_with ( "where" ) { 1.0 } else { 0.0 } ;
139+ features[ 17 ] = if query. text . to_lowercase ( ) . starts_with ( "who" ) { 1.0 } else { 0.0 } ;
140+ features[ 18 ] = if query. text . to_lowercase ( ) . starts_with ( "can" ) { 1.0 } else { 0.0 } ;
141+ features[ 19 ] = if query. text . to_lowercase ( ) . starts_with ( "should" ) { 1.0 } else { 0.0 } ;
142+
143+ // Features 20-379: Simple bag-of-words encoding (placeholder for better embedding)
144+ // In production, replace with sentence-transformers or similar
145+ let words: Vec < & str > = query. text . split_whitespace ( ) . collect ( ) ;
146+ for ( i, word) in words. iter ( ) . enumerate ( ) . take ( 360 ) {
147+ // Simple hash-based encoding
148+ let hash = word. chars ( ) . map ( |c| c as u32 ) . sum :: < u32 > ( ) % 360 ;
149+ features[ 20 + hash as usize ] += 1.0 / words. len ( ) as f32 ;
150+ }
151+
152+ // Features 380-383: Metadata
153+ // Timestamp normalized to [0,1] based on time of day (seconds since midnight / 86400)
154+ features[ 380 ] = ( ( query. timestamp % 86400 ) as f32 / 86400.0 ) . min ( 1.0 ) ;
155+ features[ 381 ] = if query. is_high_priority ( ) { 1.0 } else { 0.0 } ;
156+ features[ 382 ] = if query. text . len ( ) > 200 { 1.0 } else { 0.0 } ; // Long query indicator
157+ features[ 383 ] = if query. text . contains ( "error" ) || query. text . contains ( "bug" ) {
158+ 1.0
159+ } else {
160+ 0.0
161+ } ; // Debugging indicator
162+
163+ features
31164 }
32165
33166 /// Route a query to the appropriate processing path
@@ -39,14 +172,55 @@ impl Router {
39172 /// 3. Check project context - known projects → Local (cached knowledge)
40173 /// 4. Default: Local for simple queries
41174 ///
42- /// # Future ( Phase 2+)
175+ /// # Phase 2+ (MLP-based )
43176 ///
44- /// Replace with actual MLP :
45- /// - Input: query embedding + context features
177+ /// When MLP is available and enabled :
178+ /// - Input: query embedding + context features (384-dim)
46179 /// - Hidden layers: 2-3 layers, ~50-100 neurons each
47180 /// - Output: [local_score, remote_score, hybrid_score]
48181 /// - Decision: argmax(output)
49182 pub fn route ( & self , query : & Query ) -> ( RoutingDecision , f32 ) {
183+ // Phase 2+: Try MLP routing first
184+ if self . use_mlp && self . mlp . is_some ( ) {
185+ return self . route_with_mlp ( query) ;
186+ }
187+
188+ // Phase 1: Fallback to heuristic routing
189+ self . route_heuristic ( query)
190+ }
191+
192+ /// MLP-based routing
193+ fn route_with_mlp ( & self , query : & Query ) -> ( RoutingDecision , f32 ) {
194+ if let Some ( ref mlp) = self . mlp {
195+ // Extract features
196+ let features = self . extract_features ( query) ;
197+
198+ // Forward pass
199+ let logits = mlp. forward ( & features) ;
200+
201+ // Softmax to get probabilities
202+ let probs = MLP :: softmax ( & logits) ;
203+
204+ // Get decision (argmax)
205+ let decision_idx = MLP :: argmax ( & probs) ;
206+ let confidence = probs[ decision_idx] ;
207+
208+ let decision = match decision_idx {
209+ 0 => RoutingDecision :: Local ,
210+ 1 => RoutingDecision :: Remote ,
211+ 2 => RoutingDecision :: Hybrid ,
212+ _ => RoutingDecision :: Local , // Fallback
213+ } ;
214+
215+ ( decision, confidence)
216+ } else {
217+ // Shouldn't reach here, but fallback just in case
218+ self . route_heuristic ( query)
219+ }
220+ }
221+
222+ /// Heuristic-based routing (Phase 1)
223+ fn route_heuristic ( & self , query : & Query ) -> ( RoutingDecision , f32 ) {
50224 // Rule 1: Very long queries are complex
51225 if query. text . len ( ) > self . config . max_local_length {
52226 return ( RoutingDecision :: Remote , 0.9 ) ;
@@ -100,19 +274,10 @@ impl Default for Router {
100274 }
101275}
102276
103- // Future Phase 2: Actual MLP implementation placeholder
104- #[ allow( dead_code) ]
105- struct SimpleMLP {
106- // Placeholder for future MLP implementation
107- // Will use ndarray or similar for matrix operations
108- input_size : usize ,
109- hidden_size : usize ,
110- output_size : usize ,
111- }
112-
113277#[ cfg( test) ]
114278mod tests {
115279 use super :: * ;
280+ use crate :: mlp:: MLP ;
116281
117282 #[ test]
118283 fn test_router_creation ( ) {
@@ -174,4 +339,100 @@ mod tests {
174339 router. update_config ( new_config) ;
175340 assert_eq ! ( router. config( ) . local_threshold, 0.8 ) ;
176341 }
342+
343+ #[ test]
344+ fn test_router_with_mlp ( ) {
345+ // Create a simple MLP for testing
346+ let mlp = MLP :: new ( 384 , vec ! [ 100 , 50 ] , 3 ) ;
347+ let router = Router :: with_mlp ( mlp) ;
348+
349+ let query = Query :: new ( "What is Rust?" ) ;
350+ let ( decision, confidence) = router. route ( & query) ;
351+
352+ // Should use MLP routing (decision can be anything, but confidence should be in [0,1])
353+ assert ! ( confidence >= 0.0 && confidence <= 1.0 ) ;
354+ // Decision should be one of the valid types
355+ assert ! ( matches!(
356+ decision,
357+ RoutingDecision :: Local
358+ | RoutingDecision :: Remote
359+ | RoutingDecision :: Hybrid
360+ | RoutingDecision :: Blocked
361+ ) ) ;
362+ }
363+
364+ #[ test]
365+ fn test_router_mlp_fallback ( ) {
366+ let mut router = Router :: new ( ) ;
367+ // No MLP set, should use heuristics
368+ assert ! ( !router. use_mlp) ;
369+
370+ let query = Query :: new ( "Short query" ) ;
371+ let ( decision, _) = router. route ( & query) ;
372+ assert_eq ! ( decision, RoutingDecision :: Local ) ;
373+
374+ // Set MLP
375+ let mlp = MLP :: new ( 384 , vec ! [ 100 , 50 ] , 3 ) ;
376+ router. set_mlp ( mlp) ;
377+ assert ! ( router. use_mlp) ;
378+
379+ // Disable MLP
380+ router. set_use_mlp ( false ) ;
381+ assert ! ( !router. use_mlp) ;
382+
383+ // Should fallback to heuristics
384+ let ( decision, _) = router. route ( & query) ;
385+ assert_eq ! ( decision, RoutingDecision :: Local ) ;
386+ }
387+
388+ #[ test]
389+ fn test_feature_extraction ( ) {
390+ let router = Router :: new ( ) ;
391+ let query = Query :: new ( "How do I iterate a HashMap in Rust?" ) ;
392+
393+ let features = router. extract_features ( & query) ;
394+
395+ // Check feature vector size
396+ assert_eq ! ( features. len( ) , 384 ) ;
397+
398+ // Check that some basic features are set correctly
399+ assert ! ( features[ 2 ] > 0.0 ) ; // Has question mark
400+ assert ! ( features[ 12 ] > 0.0 ) ; // Starts with "how"
401+
402+ // Check normalization
403+ assert ! ( features. iter( ) . all( |& f| f >= 0.0 && f <= 1.0 ) ) ;
404+ }
405+
406+ #[ test]
407+ #[ cfg( feature = "persistence" ) ]
408+ fn test_mlp_persistence_integration ( ) {
409+ use crate :: persistence:: PersistenceManager ;
410+
411+ let pm = PersistenceManager :: new_in_memory ( ) . unwrap ( ) ;
412+
413+ // Create router with MLP
414+ let mlp = MLP :: new ( 384 , vec ! [ 100 , 50 ] , 3 ) ;
415+ let mut router = Router :: with_mlp ( mlp) ;
416+
417+ // Save MLP
418+ router. save_mlp ( & pm, "test_router" , Some ( 0.85 ) ) . unwrap ( ) ;
419+
420+ // Create new router and load MLP
421+ let mut new_router = Router :: new ( ) ;
422+ new_router. load_mlp ( & pm, "test_router" ) . unwrap ( ) ;
423+
424+ assert ! ( new_router. use_mlp) ;
425+
426+ // Test that it can route
427+ let query = Query :: new ( "Test query" ) ;
428+ let ( decision, confidence) = new_router. route ( & query) ;
429+ assert ! ( confidence >= 0.0 && confidence <= 1.0 ) ;
430+ assert ! ( matches!(
431+ decision,
432+ RoutingDecision :: Local
433+ | RoutingDecision :: Remote
434+ | RoutingDecision :: Hybrid
435+ | RoutingDecision :: Blocked
436+ ) ) ;
437+ }
177438}
0 commit comments