Skip to main content

ipfrs_semantic/
semantic_router_v2.rs

1//! # SemanticRouterV2
2//!
3//! An advanced semantic routing engine that uses embedding similarity to route
4//! queries to specialised handlers, with fallback chains, load balancing, and
5//! routing analytics.
6//!
7//! ## Design Goals
8//!
9//! * **Zero-copy routing** — embeddings stored contiguously; similarity computed in one pass.
10//! * **Pluggable fallback** — four strategies cover the common operational scenarios.
11//! * **Lock-free RNG** — self-contained xorshift64 (no external crate dependency).
12//! * **No `unwrap()`** — every `Option` / `Result` is handled explicitly.
13
14use std::collections::HashMap;
15
16// ────────────────────────────────────────────────────────────────────────────
17// xorshift64 — deterministic PRNG, no external crate needed
18// ────────────────────────────────────────────────────────────────────────────
19
20/// Advance the xorshift64 state by one step and return the new value.
21#[inline]
22fn xorshift64(state: &mut u64) -> u64 {
23    let mut x = *state;
24    x ^= x << 13;
25    x ^= x >> 7;
26    x ^= x << 17;
27    *state = x;
28    x
29}
30
31// ────────────────────────────────────────────────────────────────────────────
32// Public types
33// ────────────────────────────────────────────────────────────────────────────
34
35/// Opaque handle for a registered route handler.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct RouteHandlerId(pub String);
38
39impl RouteHandlerId {
40    /// Create a new handler identifier from any `Into<String>` source.
41    pub fn new(id: impl Into<String>) -> Self {
42        Self(id.into())
43    }
44
45    /// Borrow the inner string slice.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49}
50
51impl std::fmt::Display for RouteHandlerId {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.write_str(&self.0)
54    }
55}
56
57// ─── RouteDefinition ────────────────────────────────────────────────────────
58
59/// A fully-specified route entry registered with the router.
60#[derive(Debug, Clone)]
61pub struct RouteDefinition {
62    /// Unique identifier for this route.
63    pub id: RouteHandlerId,
64    /// Human-readable name.
65    pub name: String,
66    /// Embedding vector that defines the route's semantic region.
67    pub embedding: Vec<f64>,
68    /// Arbitrary key-value metadata (e.g. `"owner"`, `"model_version"`).
69    pub metadata: HashMap<String, String>,
70    /// Soft cap on how many queries per second this handler should receive.
71    pub max_queries_per_second: f64,
72    /// Higher priority (0 = lowest, 255 = highest) routes win tie-breaks.
73    pub priority: u8,
74}
75
76impl RouteDefinition {
77    /// Convenience constructor with defaults for optional fields.
78    pub fn new(id: impl Into<String>, name: impl Into<String>, embedding: Vec<f64>) -> Self {
79        Self {
80            id: RouteHandlerId::new(id),
81            name: name.into(),
82            embedding,
83            metadata: HashMap::new(),
84            max_queries_per_second: f64::MAX,
85            priority: 128,
86        }
87    }
88}
89
90// ─── RoutingDecision (V2) ───────────────────────────────────────────────────
91
92/// The result of routing a single query through `SemanticRouterV2`.
93#[derive(Debug, Clone)]
94pub struct V2RoutingDecision {
95    /// Original query text supplied by the caller.
96    pub query_text: String,
97    /// The embedding vector used to route the query.
98    pub query_embedding: Vec<f64>,
99    /// The handler selected by the router.
100    pub matched_route: RouteHandlerId,
101    /// Cosine similarity between the query embedding and the matched route embedding.
102    /// Set to `0.0` when a fallback strategy is used.
103    pub similarity: f64,
104    /// `true` when the decision was made by the fallback strategy (no route exceeded the threshold).
105    pub fallback_used: bool,
106    /// Advisory latency hint in milliseconds (set to 0 when not measured).
107    pub latency_hint_ms: u32,
108}
109
110// ─── FallbackStrategy ───────────────────────────────────────────────────────
111
112/// Strategy to apply when no route meets the similarity threshold.
113#[derive(Debug, Clone)]
114pub enum FallbackStrategy {
115    /// Always route to the designated default handler.
116    UseDefault { default_id: RouteHandlerId },
117    /// Cycle through all registered routes in registration order.
118    RoundRobin,
119    /// Pick the route with the lowest cumulative query count.
120    LeastLoaded,
121    /// Uniformly random selection using xorshift64 seeded with `seed`.
122    Random { seed: u64 },
123}
124
125// ─── RouterV2Config ─────────────────────────────────────────────────────────
126
127/// Configuration for `SemanticRouterV2`.
128#[derive(Debug, Clone)]
129pub struct RouterV2Config {
130    /// Minimum cosine similarity required for a direct route match.
131    pub similarity_threshold: f64,
132    /// Maximum number of routes that may be registered simultaneously.
133    pub max_routes: usize,
134    /// Strategy to use when no route meets the threshold.
135    pub fallback: FallbackStrategy,
136    /// When `true`, the router collects per-route statistics for load inspection.
137    pub enable_load_balancing: bool,
138}
139
140impl Default for RouterV2Config {
141    fn default() -> Self {
142        Self {
143            similarity_threshold: 0.7,
144            max_routes: 100,
145            fallback: FallbackStrategy::RoundRobin,
146            enable_load_balancing: true,
147        }
148    }
149}
150
151// ─── RouteStats ─────────────────────────────────────────────────────────────
152
153/// Per-route routing statistics.
154#[derive(Debug, Clone)]
155pub struct RouteStats {
156    /// The handler identifier these stats belong to.
157    pub handler_id: String,
158    /// Total number of queries routed to this handler (both direct and fallback).
159    pub total_routed: u64,
160    /// Running average of cosine similarity for *direct* matches (0.0 for fallback-only routes).
161    pub avg_similarity: f64,
162    /// Timestamp of the most recent routing decision (caller-supplied `now` value).
163    pub last_routed_at: u64,
164}
165
166impl RouteStats {
167    fn new(handler_id: impl Into<String>) -> Self {
168        Self {
169            handler_id: handler_id.into(),
170            total_routed: 0,
171            avg_similarity: 0.0,
172            last_routed_at: 0,
173        }
174    }
175
176    /// Update the running average similarity after a direct match with `similarity`.
177    fn record_direct(&mut self, similarity: f64, now: u64) {
178        // Welford online mean update
179        self.total_routed += 1;
180        let n = self.total_routed as f64;
181        self.avg_similarity += (similarity - self.avg_similarity) / n;
182        self.last_routed_at = now;
183    }
184
185    /// Update stats after a fallback routing decision (similarity not available).
186    fn record_fallback(&mut self, now: u64) {
187        self.total_routed += 1;
188        self.last_routed_at = now;
189    }
190}
191
192// ─── RouterV2Stats ──────────────────────────────────────────────────────────
193
194/// Aggregate statistics for the entire `SemanticRouterV2` instance.
195#[derive(Debug, Clone)]
196pub struct RouterV2Stats {
197    /// Number of currently registered routes.
198    pub route_count: usize,
199    /// Total queries processed (since construction or last reset).
200    pub total_queries: u64,
201    /// Queries that fell through to a fallback strategy.
202    pub fallback_count: u64,
203    /// `fallback_count / total_queries`, or `0.0` when no queries have been processed.
204    pub fallback_rate: f64,
205    /// Weighted average similarity across all routes (direct matches only).
206    pub avg_similarity: f64,
207}
208
209// ─── RouterV2Error ──────────────────────────────────────────────────────────
210
211/// Errors that may be returned by `SemanticRouterV2` operations.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum RouterV2Error {
214    /// A route with the same identifier is already registered.
215    RouteAlreadyExists(String),
216    /// No route with the given identifier exists.
217    RouteNotFound(String),
218    /// The number of registered routes would exceed `RouterV2Config::max_routes`.
219    MaxRoutesReached,
220    /// The operation requires at least one registered route, but none are present.
221    EmptyRoutes,
222}
223
224impl std::fmt::Display for RouterV2Error {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        match self {
227            Self::RouteAlreadyExists(id) => write!(f, "route already exists: {id}"),
228            Self::RouteNotFound(id) => write!(f, "route not found: {id}"),
229            Self::MaxRoutesReached => write!(f, "maximum route count reached"),
230            Self::EmptyRoutes => write!(f, "no routes registered"),
231        }
232    }
233}
234
235impl std::error::Error for RouterV2Error {}
236
237// ────────────────────────────────────────────────────────────────────────────
238// SemanticRouterV2
239// ────────────────────────────────────────────────────────────────────────────
240
241/// Advanced semantic routing engine.
242///
243/// Routes natural-language queries (represented as embedding vectors) to the
244/// most semantically appropriate registered handler.  When no handler meets
245/// the configured similarity threshold, a pluggable fallback strategy is
246/// applied.
247///
248/// # Thread Safety
249///
250/// `SemanticRouterV2` is **not** `Send + Sync` by itself — wrap it in an
251/// `Arc<Mutex<_>>` for shared use across tasks.
252pub struct SemanticRouterV2 {
253    /// Router configuration (immutable after construction).
254    pub config: RouterV2Config,
255    /// Ordered list of registered routes.
256    pub routes: Vec<RouteDefinition>,
257    /// Per-handler statistics, keyed by `RouteHandlerId.0`.
258    pub route_stats: HashMap<String, RouteStats>,
259    /// Round-robin cursor (next index to use for `RoundRobin` fallback).
260    pub rr_index: usize,
261    /// Total number of queries processed.
262    pub total_queries: u64,
263    /// Number of queries that required a fallback.
264    pub fallback_count: u64,
265    /// PRNG state (xorshift64).
266    pub rng_state: u64,
267}
268
269impl SemanticRouterV2 {
270    /// Construct a new router with the given configuration.
271    ///
272    /// The PRNG is seeded to `0xCAFEBABE_12345678`.
273    pub fn new(config: RouterV2Config) -> Self {
274        Self {
275            config,
276            routes: Vec::new(),
277            route_stats: HashMap::new(),
278            rr_index: 0,
279            total_queries: 0,
280            fallback_count: 0,
281            rng_state: 0xCAFEBABE_12345678_u64,
282        }
283    }
284
285    /// Register a new route.
286    ///
287    /// # Errors
288    ///
289    /// * [`RouterV2Error::RouteAlreadyExists`] — if a route with the same ID exists.
290    /// * [`RouterV2Error::MaxRoutesReached`] — if `max_routes` would be exceeded.
291    pub fn add_route(&mut self, route: RouteDefinition) -> Result<(), RouterV2Error> {
292        if self.routes.len() >= self.config.max_routes {
293            return Err(RouterV2Error::MaxRoutesReached);
294        }
295        if self.routes.iter().any(|r| r.id == route.id) {
296            return Err(RouterV2Error::RouteAlreadyExists(route.id.0.clone()));
297        }
298        let id_str = route.id.0.clone();
299        self.routes.push(route);
300        self.route_stats
301            .entry(id_str.clone())
302            .or_insert_with(|| RouteStats::new(id_str));
303        Ok(())
304    }
305
306    /// Remove a route by identifier.
307    ///
308    /// Returns `true` if a route was found and removed, `false` otherwise.
309    pub fn remove_route(&mut self, id: &RouteHandlerId) -> bool {
310        let before = self.routes.len();
311        self.routes.retain(|r| r.id != *id);
312        let removed = self.routes.len() < before;
313        if removed {
314            self.route_stats.remove(&id.0);
315            // Reset the round-robin cursor to stay in bounds
316            if !self.routes.is_empty() {
317                self.rr_index %= self.routes.len();
318            } else {
319                self.rr_index = 0;
320            }
321        }
322        removed
323    }
324
325    /// Compute the cosine similarity between two vectors.
326    ///
327    /// Returns `0.0` when either vector has zero magnitude.
328    pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
329        let min_len = a.len().min(b.len());
330        if min_len == 0 {
331            return 0.0;
332        }
333        let mut dot = 0.0_f64;
334        let mut mag_a = 0.0_f64;
335        let mut mag_b = 0.0_f64;
336        for i in 0..min_len {
337            dot += a[i] * b[i];
338            mag_a += a[i] * a[i];
339            mag_b += b[i] * b[i];
340        }
341        let denom = mag_a.sqrt() * mag_b.sqrt();
342        if denom < f64::EPSILON {
343            0.0
344        } else {
345            (dot / denom).clamp(-1.0, 1.0)
346        }
347    }
348
349    /// Find the best matching route for `query_embedding`.
350    ///
351    /// Returns `Some((index, similarity))` for the route with the highest cosine
352    /// similarity that also meets or exceeds `similarity_threshold`.  Returns
353    /// `None` when no route qualifies.
354    pub fn best_match(&self, query_embedding: &[f64]) -> Option<(usize, f64)> {
355        if self.routes.is_empty() {
356            return None;
357        }
358        let mut best_idx = 0usize;
359        let mut best_sim = f64::NEG_INFINITY;
360        for (i, route) in self.routes.iter().enumerate() {
361            let sim = Self::cosine_similarity(query_embedding, &route.embedding);
362            if sim > best_sim {
363                best_sim = sim;
364                best_idx = i;
365            }
366        }
367        if best_sim >= self.config.similarity_threshold {
368            Some((best_idx, best_sim))
369        } else {
370            None
371        }
372    }
373
374    /// Route a single query, returning a routing decision.
375    ///
376    /// * `query_text`      — raw text of the query (stored in the decision for audit).
377    /// * `query_embedding` — embedding of the query.
378    /// * `now`             — caller-supplied monotonic timestamp (e.g. milliseconds since epoch).
379    pub fn route(
380        &mut self,
381        query_text: String,
382        query_embedding: Vec<f64>,
383        now: u64,
384    ) -> V2RoutingDecision {
385        self.total_queries += 1;
386
387        // ── Direct match ──────────────────────────────────────────────────
388        if let Some((idx, similarity)) = self.best_match(&query_embedding) {
389            let matched_id = self.routes[idx].id.clone();
390            self.update_stats_direct(&matched_id, similarity, now);
391            return V2RoutingDecision {
392                query_text,
393                query_embedding,
394                matched_route: matched_id,
395                similarity,
396                fallback_used: false,
397                latency_hint_ms: 0,
398            };
399        }
400
401        // ── Fallback ──────────────────────────────────────────────────────
402        self.fallback_count += 1;
403        let fallback_id = self.apply_fallback(now);
404        V2RoutingDecision {
405            query_text,
406            query_embedding,
407            matched_route: fallback_id,
408            similarity: 0.0,
409            fallback_used: true,
410            latency_hint_ms: 0,
411        }
412    }
413
414    /// Route a batch of queries.
415    ///
416    /// Queries are processed sequentially; order of results mirrors order of input.
417    pub fn route_batch(
418        &mut self,
419        queries: Vec<(String, Vec<f64>)>,
420        now: u64,
421    ) -> Vec<V2RoutingDecision> {
422        queries
423            .into_iter()
424            .map(|(text, emb)| self.route(text, emb, now))
425            .collect()
426    }
427
428    /// Return the top-`k` routes by `total_routed`, descending.
429    pub fn top_routes(&self, k: usize) -> Vec<&RouteStats> {
430        let mut stats: Vec<&RouteStats> = self.route_stats.values().collect();
431        stats.sort_by_key(|s| std::cmp::Reverse(s.total_routed));
432        stats.truncate(k);
433        stats
434    }
435
436    /// Look up a registered route definition by identifier.
437    pub fn route_by_id(&self, id: &RouteHandlerId) -> Option<&RouteDefinition> {
438        self.routes.iter().find(|r| r.id == *id)
439    }
440
441    /// Replace the embedding of an existing route in-place.
442    ///
443    /// Returns `true` if the route was found and updated.
444    pub fn update_route_embedding(&mut self, id: &RouteHandlerId, new_embedding: Vec<f64>) -> bool {
445        if let Some(route) = self.routes.iter_mut().find(|r| r.id == *id) {
446            route.embedding = new_embedding;
447            true
448        } else {
449            false
450        }
451    }
452
453    /// Compute aggregate statistics for the entire router.
454    pub fn router_stats(&self) -> RouterV2Stats {
455        let route_count = self.routes.len();
456        let fallback_rate = if self.total_queries == 0 {
457            0.0
458        } else {
459            self.fallback_count as f64 / self.total_queries as f64
460        };
461
462        // Weighted mean similarity across all routes
463        let (total_sim, total_direct) =
464            self.route_stats
465                .values()
466                .fold((0.0_f64, 0u64), |(acc_sim, acc_n), s| {
467                    // total_routed includes fallback; we re-derive direct count from avg_similarity
468                    // We use avg_similarity * total_routed as sum proxy (fallback routes have avg=0)
469                    (
470                        acc_sim + s.avg_similarity * s.total_routed as f64,
471                        acc_n + s.total_routed,
472                    )
473                });
474        let avg_similarity = if total_direct == 0 {
475            0.0
476        } else {
477            total_sim / total_direct as f64
478        };
479
480        RouterV2Stats {
481            route_count,
482            total_queries: self.total_queries,
483            fallback_count: self.fallback_count,
484            fallback_rate,
485            avg_similarity,
486        }
487    }
488
489    // ── Private helpers ───────────────────────────────────────────────────
490
491    fn update_stats_direct(&mut self, id: &RouteHandlerId, similarity: f64, now: u64) {
492        let stats = self
493            .route_stats
494            .entry(id.0.clone())
495            .or_insert_with(|| RouteStats::new(id.0.clone()));
496        stats.record_direct(similarity, now);
497    }
498
499    fn update_stats_fallback(&mut self, id: &RouteHandlerId, now: u64) {
500        let stats = self
501            .route_stats
502            .entry(id.0.clone())
503            .or_insert_with(|| RouteStats::new(id.0.clone()));
504        stats.record_fallback(now);
505    }
506
507    /// Apply the configured fallback strategy and return the selected handler ID.
508    ///
509    /// Falls back gracefully to index `0` when the route list is empty (the
510    /// caller guarantees that `route` is only called when at least one route
511    /// exists — but we handle the edge case defensively).
512    fn apply_fallback(&mut self, now: u64) -> RouteHandlerId {
513        if self.routes.is_empty() {
514            // Should not happen in normal usage; return a sentinel value.
515            return RouteHandlerId::new("__no_routes__");
516        }
517
518        let selected_id = match self.config.fallback.clone() {
519            FallbackStrategy::UseDefault { default_id } => {
520                // Validate the default_id exists; fall through to round-robin if not.
521                if self.routes.iter().any(|r| r.id == default_id) {
522                    default_id
523                } else {
524                    self.round_robin_id()
525                }
526            }
527            FallbackStrategy::RoundRobin => self.round_robin_id(),
528            FallbackStrategy::LeastLoaded => self.least_loaded_id(),
529            FallbackStrategy::Random { .. } => self.random_id(),
530        };
531
532        self.update_stats_fallback(&selected_id.clone(), now);
533        selected_id
534    }
535
536    fn round_robin_id(&mut self) -> RouteHandlerId {
537        let idx = self.rr_index % self.routes.len();
538        self.rr_index = idx.wrapping_add(1);
539        self.routes[idx].id.clone()
540    }
541
542    fn least_loaded_id(&self) -> RouteHandlerId {
543        // Pick the route whose total_routed count is smallest.
544        self.routes
545            .iter()
546            .min_by_key(|r| self.route_stats.get(&r.id.0).map_or(0, |s| s.total_routed))
547            .map(|r| r.id.clone())
548            .unwrap_or_else(|| self.routes[0].id.clone())
549    }
550
551    fn random_id(&mut self) -> RouteHandlerId {
552        let val = xorshift64(&mut self.rng_state);
553        let idx = (val as usize) % self.routes.len();
554        self.routes[idx].id.clone()
555    }
556}
557
558// ────────────────────────────────────────────────────────────────────────────
559// Tests
560// ────────────────────────────────────────────────────────────────────────────
561
562#[cfg(test)]
563mod tests {
564    use super::{
565        FallbackStrategy, RouteDefinition, RouteHandlerId, RouterV2Config, RouterV2Error,
566        SemanticRouterV2,
567    };
568    use std::collections::HashMap;
569
570    // ── Helpers ──────────────────────────────────────────────────────────
571
572    fn make_router() -> SemanticRouterV2 {
573        SemanticRouterV2::new(RouterV2Config::default())
574    }
575
576    fn unit_route(id: &str, dim: usize, val: f64) -> RouteDefinition {
577        RouteDefinition::new(id, id, vec![val; dim])
578    }
579
580    fn orthogonal_route(id: &str, dim: usize, hot_index: usize) -> RouteDefinition {
581        let mut emb = vec![0.0f64; dim];
582        if hot_index < dim {
583            emb[hot_index] = 1.0;
584        }
585        RouteDefinition::new(id, id, emb)
586    }
587
588    // ── Construction ─────────────────────────────────────────────────────
589
590    #[test]
591    fn test_new_router_has_correct_defaults() {
592        let router = make_router();
593        assert_eq!(router.total_queries, 0);
594        assert_eq!(router.fallback_count, 0);
595        assert!(router.routes.is_empty());
596        assert_eq!(router.rng_state, 0xCAFEBABE_12345678_u64);
597    }
598
599    #[test]
600    fn test_new_router_with_custom_config() {
601        let cfg = RouterV2Config {
602            similarity_threshold: 0.9,
603            max_routes: 5,
604            fallback: FallbackStrategy::RoundRobin,
605            enable_load_balancing: false,
606        };
607        let router = SemanticRouterV2::new(cfg);
608        assert_eq!(router.config.similarity_threshold, 0.9);
609        assert_eq!(router.config.max_routes, 5);
610        assert!(!router.config.enable_load_balancing);
611    }
612
613    // ── add_route ────────────────────────────────────────────────────────
614
615    #[test]
616    fn test_add_route_success() {
617        let mut router = make_router();
618        let route = unit_route("r1", 4, 1.0);
619        assert!(router.add_route(route).is_ok());
620        assert_eq!(router.routes.len(), 1);
621    }
622
623    #[test]
624    fn test_add_duplicate_route_returns_error() {
625        let mut router = make_router();
626        router.add_route(unit_route("r1", 4, 1.0)).ok();
627        let err = router
628            .add_route(unit_route("r1", 4, 0.5))
629            .expect_err("should fail");
630        assert_eq!(err, RouterV2Error::RouteAlreadyExists("r1".into()));
631    }
632
633    #[test]
634    fn test_max_routes_reached() {
635        let cfg = RouterV2Config {
636            max_routes: 2,
637            ..RouterV2Config::default()
638        };
639        let mut router = SemanticRouterV2::new(cfg);
640        router.add_route(unit_route("r1", 4, 1.0)).ok();
641        router.add_route(unit_route("r2", 4, 0.5)).ok();
642        let err = router
643            .add_route(unit_route("r3", 4, 0.25))
644            .expect_err("should fail");
645        assert_eq!(err, RouterV2Error::MaxRoutesReached);
646    }
647
648    // ── remove_route ─────────────────────────────────────────────────────
649
650    #[test]
651    fn test_remove_existing_route() {
652        let mut router = make_router();
653        router.add_route(unit_route("r1", 4, 1.0)).ok();
654        let removed = router.remove_route(&RouteHandlerId::new("r1"));
655        assert!(removed);
656        assert!(router.routes.is_empty());
657    }
658
659    #[test]
660    fn test_remove_nonexistent_route_returns_false() {
661        let mut router = make_router();
662        let removed = router.remove_route(&RouteHandlerId::new("ghost"));
663        assert!(!removed);
664    }
665
666    #[test]
667    fn test_remove_cleans_up_stats() {
668        let mut router = make_router();
669        router.add_route(unit_route("r1", 4, 1.0)).ok();
670        router.remove_route(&RouteHandlerId::new("r1"));
671        assert!(!router.route_stats.contains_key("r1"));
672    }
673
674    // ── cosine_similarity ────────────────────────────────────────────────
675
676    #[test]
677    fn test_cosine_similarity_identical_vectors() {
678        let v = vec![1.0, 2.0, 3.0];
679        let sim = SemanticRouterV2::cosine_similarity(&v, &v);
680        assert!((sim - 1.0).abs() < 1e-9);
681    }
682
683    #[test]
684    fn test_cosine_similarity_orthogonal_vectors() {
685        let a = vec![1.0, 0.0, 0.0];
686        let b = vec![0.0, 1.0, 0.0];
687        let sim = SemanticRouterV2::cosine_similarity(&a, &b);
688        assert!(sim.abs() < 1e-9);
689    }
690
691    #[test]
692    fn test_cosine_similarity_opposite_vectors() {
693        let a = vec![1.0, 0.0];
694        let b = vec![-1.0, 0.0];
695        let sim = SemanticRouterV2::cosine_similarity(&a, &b);
696        assert!((sim - (-1.0)).abs() < 1e-9);
697    }
698
699    #[test]
700    fn test_cosine_similarity_zero_vector() {
701        let a = vec![0.0, 0.0, 0.0];
702        let b = vec![1.0, 2.0, 3.0];
703        let sim = SemanticRouterV2::cosine_similarity(&a, &b);
704        assert_eq!(sim, 0.0);
705    }
706
707    #[test]
708    fn test_cosine_similarity_empty_slices() {
709        let sim = SemanticRouterV2::cosine_similarity(&[], &[]);
710        assert_eq!(sim, 0.0);
711    }
712
713    #[test]
714    fn test_cosine_similarity_mismatched_lengths() {
715        let a = vec![1.0, 0.0, 0.0];
716        let b = vec![1.0, 0.0];
717        // Similarity computed over shorter length
718        let sim = SemanticRouterV2::cosine_similarity(&a, &b);
719        assert!((sim - 1.0).abs() < 1e-9);
720    }
721
722    #[test]
723    fn test_cosine_similarity_known_value() {
724        let a = vec![3.0, 4.0];
725        let b = vec![4.0, 3.0];
726        // dot = 24, |a| = 5, |b| = 5, sim = 24/25 = 0.96
727        let sim = SemanticRouterV2::cosine_similarity(&a, &b);
728        assert!((sim - 0.96).abs() < 1e-9);
729    }
730
731    // ── best_match ───────────────────────────────────────────────────────
732
733    #[test]
734    fn test_best_match_empty_router() {
735        let router = make_router();
736        assert!(router.best_match(&[1.0, 0.0]).is_none());
737    }
738
739    #[test]
740    fn test_best_match_above_threshold() {
741        let mut router = make_router();
742        router.add_route(orthogonal_route("r1", 3, 0)).ok(); // [1,0,0]
743        let result = router.best_match(&[1.0, 0.0, 0.0]);
744        assert!(result.is_some());
745        let (idx, sim) = result.expect("expected match");
746        assert_eq!(idx, 0);
747        assert!((sim - 1.0).abs() < 1e-9);
748    }
749
750    #[test]
751    fn test_best_match_below_threshold() {
752        let mut router = SemanticRouterV2::new(RouterV2Config {
753            similarity_threshold: 0.99,
754            ..RouterV2Config::default()
755        });
756        router.add_route(orthogonal_route("r1", 3, 0)).ok();
757        // Query is [0.5, 0.5, 0] — sim ≈ 0.707 < 0.99
758        let query = vec![0.5_f64, 0.5, 0.0];
759        assert!(router.best_match(&query).is_none());
760    }
761
762    #[test]
763    fn test_best_match_picks_highest_similarity() {
764        let mut router = make_router();
765        router.add_route(orthogonal_route("r1", 3, 0)).ok(); // [1,0,0]
766        router.add_route(orthogonal_route("r2", 3, 1)).ok(); // [0,1,0]
767                                                             // Query mostly aligned with r2
768        let result = router.best_match(&[0.1, 0.99, 0.0]);
769        let (idx, _) = result.expect("expected match");
770        assert_eq!(idx, 1);
771    }
772
773    // ── route / routing decisions ─────────────────────────────────────────
774
775    #[test]
776    fn test_route_direct_match() {
777        let mut router = make_router();
778        router.add_route(orthogonal_route("r1", 3, 0)).ok();
779        let decision = router.route("query".into(), vec![1.0, 0.0, 0.0], 1000);
780        assert!(!decision.fallback_used);
781        assert_eq!(decision.matched_route, RouteHandlerId::new("r1"));
782        assert!((decision.similarity - 1.0).abs() < 1e-9);
783    }
784
785    #[test]
786    fn test_route_fallback_when_no_match() {
787        let mut router = SemanticRouterV2::new(RouterV2Config {
788            similarity_threshold: 0.99,
789            ..RouterV2Config::default()
790        });
791        router.add_route(orthogonal_route("r1", 3, 0)).ok();
792        let decision = router.route("query".into(), vec![0.0, 1.0, 0.0], 1000);
793        assert!(decision.fallback_used);
794        assert_eq!(decision.similarity, 0.0);
795    }
796
797    #[test]
798    fn test_route_increments_total_queries() {
799        let mut router = make_router();
800        router.add_route(unit_route("r1", 3, 1.0)).ok();
801        router.route("q".into(), vec![1.0, 1.0, 1.0], 0);
802        router.route("q".into(), vec![1.0, 1.0, 1.0], 1);
803        assert_eq!(router.total_queries, 2);
804    }
805
806    #[test]
807    fn test_route_increments_fallback_count() {
808        let mut router = SemanticRouterV2::new(RouterV2Config {
809            similarity_threshold: 0.99,
810            ..RouterV2Config::default()
811        });
812        router.add_route(orthogonal_route("r1", 3, 0)).ok();
813        router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
814        assert_eq!(router.fallback_count, 1);
815    }
816
817    #[test]
818    fn test_route_updates_stats() {
819        let mut router = make_router();
820        router.add_route(orthogonal_route("r1", 3, 0)).ok();
821        router.route("q".into(), vec![1.0, 0.0, 0.0], 42);
822        let stats = router.route_stats.get("r1").expect("stats should exist");
823        assert_eq!(stats.total_routed, 1);
824        assert_eq!(stats.last_routed_at, 42);
825    }
826
827    // ── Fallback strategies ───────────────────────────────────────────────
828
829    #[test]
830    fn test_fallback_round_robin_cycles() {
831        let mut router = SemanticRouterV2::new(RouterV2Config {
832            similarity_threshold: 2.0, // impossible — always fallback
833            fallback: FallbackStrategy::RoundRobin,
834            ..RouterV2Config::default()
835        });
836        router.add_route(unit_route("r1", 3, 1.0)).ok();
837        router.add_route(unit_route("r2", 3, 0.0)).ok();
838        let d1 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
839        let d2 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
840        let d3 = router.route("q".into(), vec![0.5, 0.5, 0.5], 0);
841        // r1, r2, r1
842        assert_eq!(d1.matched_route.0, "r1");
843        assert_eq!(d2.matched_route.0, "r2");
844        assert_eq!(d3.matched_route.0, "r1");
845    }
846
847    #[test]
848    fn test_fallback_use_default_valid_id() {
849        let default_id = RouteHandlerId::new("default_handler");
850        let mut router = SemanticRouterV2::new(RouterV2Config {
851            similarity_threshold: 2.0,
852            fallback: FallbackStrategy::UseDefault {
853                default_id: default_id.clone(),
854            },
855            ..RouterV2Config::default()
856        });
857        router.add_route(unit_route("r1", 3, 1.0)).ok();
858        router.add_route(unit_route("default_handler", 3, 0.0)).ok();
859        let decision = router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
860        assert_eq!(decision.matched_route, default_id);
861    }
862
863    #[test]
864    fn test_fallback_use_default_invalid_falls_to_rr() {
865        let mut router = SemanticRouterV2::new(RouterV2Config {
866            similarity_threshold: 2.0,
867            fallback: FallbackStrategy::UseDefault {
868                default_id: RouteHandlerId::new("ghost"),
869            },
870            ..RouterV2Config::default()
871        });
872        router.add_route(unit_route("r1", 3, 1.0)).ok();
873        // Should not panic; falls back to round-robin
874        let decision = router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
875        assert!(decision.fallback_used);
876    }
877
878    #[test]
879    fn test_fallback_least_loaded_picks_min() {
880        let mut router = SemanticRouterV2::new(RouterV2Config {
881            similarity_threshold: 0.9,
882            fallback: FallbackStrategy::LeastLoaded,
883            ..RouterV2Config::default()
884        });
885        router.add_route(orthogonal_route("r1", 3, 0)).ok(); // [1,0,0]
886        router.add_route(orthogonal_route("r2", 3, 1)).ok(); // [0,1,0]
887                                                             // Direct-route r1 multiple times to increase its load
888        router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
889        router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
890        // Now force a fallback with orthogonal query — r2 should win (least loaded)
891        let decision = router.route("q".into(), vec![0.0, 0.0, 1.0], 0);
892        assert!(decision.fallback_used);
893        assert_eq!(decision.matched_route.0, "r2");
894    }
895
896    #[test]
897    fn test_fallback_random_is_deterministic_with_seed() {
898        // Two routers with the same RNG state should make the same random decisions.
899        let cfg = RouterV2Config {
900            similarity_threshold: 2.0,
901            fallback: FallbackStrategy::Random { seed: 0 }, // seed field unused; rng_state drives it
902            ..RouterV2Config::default()
903        };
904        let mut r1 = SemanticRouterV2::new(cfg.clone());
905        r1.rng_state = 42;
906        let mut r2 = SemanticRouterV2::new(cfg);
907        r2.rng_state = 42;
908        r1.add_route(unit_route("a", 3, 1.0)).ok();
909        r1.add_route(unit_route("b", 3, 0.0)).ok();
910        r2.add_route(unit_route("a", 3, 1.0)).ok();
911        r2.add_route(unit_route("b", 3, 0.0)).ok();
912        let d1 = r1.route("q".into(), vec![0.0; 3], 0);
913        let d2 = r2.route("q".into(), vec![0.0; 3], 0);
914        assert_eq!(d1.matched_route, d2.matched_route);
915    }
916
917    // ── route_batch ──────────────────────────────────────────────────────
918
919    #[test]
920    fn test_route_batch_returns_correct_count() {
921        let mut router = make_router();
922        router.add_route(unit_route("r1", 3, 1.0)).ok();
923        let queries = vec![
924            ("q1".to_string(), vec![1.0, 1.0, 1.0]),
925            ("q2".to_string(), vec![1.0, 1.0, 1.0]),
926            ("q3".to_string(), vec![1.0, 1.0, 1.0]),
927        ];
928        let decisions = router.route_batch(queries, 0);
929        assert_eq!(decisions.len(), 3);
930    }
931
932    #[test]
933    fn test_route_batch_updates_total_queries() {
934        let mut router = make_router();
935        router.add_route(unit_route("r1", 3, 1.0)).ok();
936        let queries: Vec<_> = (0..5).map(|i| (format!("q{i}"), vec![1.0; 3])).collect();
937        router.route_batch(queries, 0);
938        assert_eq!(router.total_queries, 5);
939    }
940
941    // ── top_routes ───────────────────────────────────────────────────────
942
943    #[test]
944    fn test_top_routes_ordering() {
945        let mut router = make_router();
946        router.add_route(orthogonal_route("r1", 3, 0)).ok();
947        router.add_route(orthogonal_route("r2", 3, 1)).ok();
948        // Route to r1 three times, r2 once
949        for _ in 0..3 {
950            router.route("q".into(), vec![1.0, 0.0, 0.0], 0);
951        }
952        router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
953        let top = router.top_routes(2);
954        assert_eq!(top[0].handler_id, "r1");
955        assert_eq!(top[1].handler_id, "r2");
956    }
957
958    #[test]
959    fn test_top_routes_k_larger_than_registered() {
960        let mut router = make_router();
961        router.add_route(unit_route("r1", 3, 1.0)).ok();
962        router.route("q".into(), vec![1.0; 3], 0);
963        let top = router.top_routes(100);
964        assert_eq!(top.len(), 1);
965    }
966
967    // ── route_by_id ──────────────────────────────────────────────────────
968
969    #[test]
970    fn test_route_by_id_found() {
971        let mut router = make_router();
972        router.add_route(unit_route("r1", 3, 1.0)).ok();
973        let found = router.route_by_id(&RouteHandlerId::new("r1"));
974        assert!(found.is_some());
975        assert_eq!(found.expect("should find").name, "r1");
976    }
977
978    #[test]
979    fn test_route_by_id_not_found() {
980        let router = make_router();
981        assert!(router.route_by_id(&RouteHandlerId::new("ghost")).is_none());
982    }
983
984    // ── update_route_embedding ───────────────────────────────────────────
985
986    #[test]
987    fn test_update_route_embedding_success() {
988        let mut router = make_router();
989        router.add_route(orthogonal_route("r1", 3, 0)).ok(); // [1,0,0]
990        let updated =
991            router.update_route_embedding(&RouteHandlerId::new("r1"), vec![0.0, 1.0, 0.0]);
992        assert!(updated);
993        let route = router
994            .route_by_id(&RouteHandlerId::new("r1"))
995            .expect("must exist");
996        assert_eq!(route.embedding, vec![0.0, 1.0, 0.0]);
997    }
998
999    #[test]
1000    fn test_update_route_embedding_not_found() {
1001        let mut router = make_router();
1002        let updated = router.update_route_embedding(&RouteHandlerId::new("ghost"), vec![1.0]);
1003        assert!(!updated);
1004    }
1005
1006    // ── router_stats ─────────────────────────────────────────────────────
1007
1008    #[test]
1009    fn test_router_stats_initial() {
1010        let router = make_router();
1011        let stats = router.router_stats();
1012        assert_eq!(stats.route_count, 0);
1013        assert_eq!(stats.total_queries, 0);
1014        assert_eq!(stats.fallback_count, 0);
1015        assert_eq!(stats.fallback_rate, 0.0);
1016    }
1017
1018    #[test]
1019    fn test_router_stats_fallback_rate() {
1020        let mut router = SemanticRouterV2::new(RouterV2Config {
1021            similarity_threshold: 0.99,
1022            ..RouterV2Config::default()
1023        });
1024        router.add_route(orthogonal_route("r1", 3, 0)).ok();
1025        // 2 fallback queries
1026        router.route("q".into(), vec![0.0, 1.0, 0.0], 0);
1027        router.route("q".into(), vec![0.0, 0.0, 1.0], 0);
1028        let stats = router.router_stats();
1029        assert_eq!(stats.total_queries, 2);
1030        assert_eq!(stats.fallback_count, 2);
1031        assert!((stats.fallback_rate - 1.0).abs() < 1e-9);
1032    }
1033
1034    // ── RouteHandlerId ───────────────────────────────────────────────────
1035
1036    #[test]
1037    fn test_route_handler_id_display() {
1038        let id = RouteHandlerId::new("handler_42");
1039        assert_eq!(id.to_string(), "handler_42");
1040    }
1041
1042    #[test]
1043    fn test_route_handler_id_as_str() {
1044        let id = RouteHandlerId::new("x");
1045        assert_eq!(id.as_str(), "x");
1046    }
1047
1048    #[test]
1049    fn test_route_handler_id_equality() {
1050        let a = RouteHandlerId::new("abc");
1051        let b = RouteHandlerId::new("abc");
1052        let c = RouteHandlerId::new("xyz");
1053        assert_eq!(a, b);
1054        assert_ne!(a, c);
1055    }
1056
1057    // ── RouteDefinition ──────────────────────────────────────────────────
1058
1059    #[test]
1060    fn test_route_definition_with_metadata() {
1061        let mut route = RouteDefinition::new("r1", "Route 1", vec![1.0, 0.0]);
1062        route.metadata.insert("owner".into(), "team-a".into());
1063        route.priority = 200;
1064        route.max_queries_per_second = 50.0;
1065        assert_eq!(
1066            route.metadata.get("owner").map(|s| s.as_str()),
1067            Some("team-a")
1068        );
1069        assert_eq!(route.priority, 200);
1070    }
1071
1072    // ── xorshift64 ───────────────────────────────────────────────────────
1073
1074    #[test]
1075    fn test_xorshift64_non_zero() {
1076        use super::xorshift64;
1077        let mut state = 1u64;
1078        let v = xorshift64(&mut state);
1079        assert_ne!(v, 0);
1080        assert_ne!(state, 1); // state changed
1081    }
1082
1083    #[test]
1084    fn test_xorshift64_deterministic() {
1085        use super::xorshift64;
1086        let mut s1 = 12345u64;
1087        let mut s2 = 12345u64;
1088        let v1 = xorshift64(&mut s1);
1089        let v2 = xorshift64(&mut s2);
1090        assert_eq!(v1, v2);
1091    }
1092
1093    // ── RouterV2Error ────────────────────────────────────────────────────
1094
1095    #[test]
1096    fn test_error_display_route_already_exists() {
1097        let e = RouterV2Error::RouteAlreadyExists("foo".into());
1098        assert!(e.to_string().contains("foo"));
1099    }
1100
1101    #[test]
1102    fn test_error_display_route_not_found() {
1103        let e = RouterV2Error::RouteNotFound("bar".into());
1104        assert!(e.to_string().contains("bar"));
1105    }
1106
1107    #[test]
1108    fn test_error_display_max_routes_reached() {
1109        let e = RouterV2Error::MaxRoutesReached;
1110        assert!(!e.to_string().is_empty());
1111    }
1112
1113    #[test]
1114    fn test_error_display_empty_routes() {
1115        let e = RouterV2Error::EmptyRoutes;
1116        assert!(!e.to_string().is_empty());
1117    }
1118
1119    // ── avg_similarity tracking ──────────────────────────────────────────
1120
1121    #[test]
1122    fn test_avg_similarity_welford_convergence() {
1123        let mut router = make_router();
1124        router.add_route(orthogonal_route("r1", 2, 0)).ok(); // [1,0]
1125                                                             // All queries perfectly aligned — avg should stay 1.0
1126        for t in 0..10u64 {
1127            router.route("q".into(), vec![1.0, 0.0], t);
1128        }
1129        let stats = router.route_stats.get("r1").expect("stats");
1130        assert!((stats.avg_similarity - 1.0).abs() < 1e-9);
1131        assert_eq!(stats.total_routed, 10);
1132    }
1133
1134    #[test]
1135    fn test_remove_and_readd_route() {
1136        let mut router = make_router();
1137        router.add_route(unit_route("r1", 3, 1.0)).ok();
1138        router.remove_route(&RouteHandlerId::new("r1"));
1139        // Re-adding after removal should succeed
1140        assert!(router.add_route(unit_route("r1", 3, 1.0)).is_ok());
1141    }
1142
1143    #[test]
1144    fn test_route_decision_preserves_query_text() {
1145        let mut router = make_router();
1146        router.add_route(unit_route("r1", 3, 1.0)).ok();
1147        let decision = router.route("hello world".into(), vec![1.0; 3], 0);
1148        assert_eq!(decision.query_text, "hello world");
1149    }
1150
1151    #[test]
1152    fn test_router_with_many_routes() {
1153        let mut router = make_router();
1154        for i in 0..50usize {
1155            let mut emb = vec![0.0f64; 64];
1156            emb[i % 64] = 1.0;
1157            router
1158                .add_route(RouteDefinition::new(
1159                    format!("r{i}"),
1160                    format!("route {i}"),
1161                    emb,
1162                ))
1163                .ok();
1164        }
1165        assert_eq!(router.routes.len(), 50);
1166        let stats = router.router_stats();
1167        assert_eq!(stats.route_count, 50);
1168    }
1169
1170    #[test]
1171    fn test_batch_and_individual_route_agree() {
1172        let mut router_a = make_router();
1173        let mut router_b = make_router();
1174        router_a.add_route(orthogonal_route("r1", 3, 0)).ok();
1175        router_b.add_route(orthogonal_route("r1", 3, 0)).ok();
1176        let queries = vec![
1177            ("q1".into(), vec![1.0, 0.0, 0.0]),
1178            ("q2".into(), vec![1.0, 0.0, 0.0]),
1179        ];
1180        let batch_results = router_a.route_batch(queries, 0);
1181        let ind1 = router_b.route("q1".into(), vec![1.0, 0.0, 0.0], 0);
1182        let ind2 = router_b.route("q2".into(), vec![1.0, 0.0, 0.0], 0);
1183        assert_eq!(batch_results[0].matched_route, ind1.matched_route);
1184        assert_eq!(batch_results[1].matched_route, ind2.matched_route);
1185    }
1186
1187    #[test]
1188    fn test_route_stats_last_routed_at_updated() {
1189        let mut router = make_router();
1190        router.add_route(orthogonal_route("r1", 2, 0)).ok();
1191        router.route("q".into(), vec![1.0, 0.0], 1000);
1192        let stats = router.route_stats.get("r1").expect("stats");
1193        assert_eq!(stats.last_routed_at, 1000);
1194    }
1195
1196    #[test]
1197    fn test_metadata_stored_in_route_definition() {
1198        let mut route = RouteDefinition::new("r1", "Route 1", vec![1.0, 0.0]);
1199        route.metadata = HashMap::from([
1200            ("region".into(), "us-east".into()),
1201            ("tier".into(), "premium".into()),
1202        ]);
1203        let mut router = make_router();
1204        router.add_route(route).ok();
1205        let found = router
1206            .route_by_id(&RouteHandlerId::new("r1"))
1207            .expect("found");
1208        assert_eq!(
1209            found.metadata.get("region").map(|s| s.as_str()),
1210            Some("us-east")
1211        );
1212    }
1213}