use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use super::{ContextDecision, ContextRequest, ContextRouter, clamp_complexity};
pub const FEATURE_DIM: usize = 64;
pub const DEFAULT_RANK: usize = 4;
#[derive(Debug, Clone)]
pub struct MicroLoraPattern {
pub id: String,
pub features: Vec<f32>,
pub skills: Vec<String>,
pub archetype: Option<String>,
pub complexity_bias: f32,
}
#[derive(Debug)]
struct MicroLoraState {
patterns: Vec<MicroLoraPattern>,
w_down: Vec<Vec<f32>>,
w_up: Vec<f32>,
rank: usize,
shadow: bool,
last_shadow: Option<ContextDecision>,
}
pub struct MicroLoraRouter {
state: Mutex<MicroLoraState>,
}
impl MicroLoraRouter {
pub fn new() -> Self {
Self::with_rank(DEFAULT_RANK)
}
pub fn with_rank(rank: usize) -> Self {
let rank = rank.clamp(1, 16);
let w_down = (0..rank)
.map(|i| {
(0..FEATURE_DIM)
.map(|j| (((i * 17 + j * 3) % 100) as f32 - 50.0) * 0.001)
.collect()
})
.collect();
let w_up = vec![0.01; rank];
Self {
state: Mutex::new(MicroLoraState {
patterns: Vec::new(),
w_down,
w_up,
rank,
shadow: false,
last_shadow: None,
}),
}
}
pub fn with_seed_skills(skills: impl IntoIterator<Item = impl Into<String>>) -> Self {
let r = Self::new();
for s in skills {
let name = s.into();
r.upsert_pattern(MicroLoraPattern {
id: name.clone(),
features: feature_bag(&name),
skills: vec![name],
archetype: None,
complexity_bias: 0.0,
});
}
r
}
pub fn set_shadow(&self, shadow: bool) {
if let Ok(mut g) = self.state.lock() {
g.shadow = shadow;
}
}
pub fn last_shadow_decision(&self) -> Option<ContextDecision> {
self.state.lock().ok().and_then(|g| g.last_shadow.clone())
}
pub fn upsert_pattern(&self, pattern: MicroLoraPattern) {
let Ok(mut g) = self.state.lock() else {
return;
};
if let Some(i) = g.patterns.iter().position(|p| p.id == pattern.id) {
g.patterns[i] = pattern;
} else {
g.patterns.push(pattern);
}
}
pub fn pattern_count(&self) -> usize {
self.state.lock().map(|g| g.patterns.len()).unwrap_or(0)
}
pub fn adapt(&self, content: &str, quality: f32, learning_rate: f32) {
let q = feature_bag(content);
let quality = quality.clamp(0.0, 1.0);
let lr = learning_rate.clamp(1e-5, 0.5);
let Ok(mut g) = self.state.lock() else {
return;
};
let target = (quality - 0.5) * 0.2;
let current = lora_residual(&q, &g.w_down, &g.w_up);
let err = target - current;
for i in 0..g.rank {
let mut act = 0.0f32;
for (w, x) in g.w_down[i].iter().zip(q.iter()).take(FEATURE_DIM) {
act += w * x;
}
g.w_up[i] += lr * err * act;
let w_up_i = g.w_up[i];
for (w, x) in g.w_down[i].iter_mut().zip(q.iter()).take(FEATURE_DIM) {
*w += lr * err * w_up_i * x * 0.1;
}
}
}
fn route_inner(&self, request: &ContextRequest) -> ContextDecision {
let bag = feature_bag(&request.content);
let Ok(g) = self.state.lock() else {
return ContextDecision::default();
};
if g.patterns.is_empty() {
return ContextDecision::default();
}
let mut scored: Vec<(f32, &MicroLoraPattern)> = g
.patterns
.iter()
.map(|p| (cosine(&bag, &p.features), p))
.collect();
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let top_k = scored.into_iter().take(5).collect::<Vec<_>>();
if top_k.is_empty() || top_k[0].0 < 0.05 {
return ContextDecision::default();
}
let mut skills = Vec::new();
let mut seen = HashMap::new();
for (score, p) in &top_k {
if *score < 0.05 {
continue;
}
for s in &p.skills {
if seen.insert(s.clone(), ()).is_none() {
skills.push(s.clone());
}
}
}
skills.truncate(8);
let residual = lora_residual(&bag, &g.w_down, &g.w_up);
let complexity_hint = clamp_complexity(top_k[0].1.complexity_bias + residual);
let archetype = top_k[0].1.archetype.clone().or_else(|| {
Some(archetype_from_text(&request.content))
});
ContextDecision {
skills,
tool_subset: None,
complexity_hint,
archetype,
fallback_used: false,
}
}
}
impl Default for MicroLoraRouter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ContextRouter for MicroLoraRouter {
async fn route(&self, request: &ContextRequest) -> ContextDecision {
let decision = self.route_inner(request);
if let Ok(mut g) = self.state.lock()
&& g.shadow {
g.last_shadow = Some(decision.clone());
}
decision
}
}
pub fn feature_bag(text: &str) -> Vec<f32> {
let mut v = vec![0.0f32; FEATURE_DIM];
for tok in text.split(|c: char| !c.is_alphanumeric()) {
if tok.is_empty() {
continue;
}
let h = fnv1a64(tok.to_ascii_lowercase().as_bytes());
let idx = (h as usize) % FEATURE_DIM;
let sign = if (h >> 63) & 1 == 1 { -1.0 } else { 1.0 };
v[idx] += sign;
}
let n = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 1e-8 {
for x in &mut v {
*x /= n;
}
}
v
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x100000001b3);
}
h
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let n = a.len().min(b.len());
let mut dot = 0.0f32;
for i in 0..n {
dot += a[i] * b[i];
}
dot
}
fn lora_residual(q: &[f32], w_down: &[Vec<f32>], w_up: &[f32]) -> f32 {
let mut s = 0.0f32;
for (i, row) in w_down.iter().enumerate() {
let mut act = 0.0f32;
for j in 0..FEATURE_DIM.min(q.len()).min(row.len()) {
act += row[j] * q[j];
}
s += w_up.get(i).copied().unwrap_or(0.0) * act;
}
clamp_complexity(s)
}
fn archetype_from_text(text: &str) -> String {
let t = text.to_ascii_lowercase();
if t.contains("code") || t.contains("compile") || t.contains("rust") || t.contains("fn ") {
"CodeGen".into()
} else if t.contains("why") || t.contains("explain") || t.contains("reason") {
"Reasoning".into()
} else if t.contains("analy") || t.contains("metric") || t.contains("data") {
"Analysis".into()
} else if t.contains("story") || t.contains("poem") || t.contains("creative") {
"Creative".into()
} else {
"Conversational".into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn unlimited_patterns_beyond_eleven() {
let r = MicroLoraRouter::new();
for i in 0..40 {
r.upsert_pattern(MicroLoraPattern {
id: format!("skill-{i}"),
features: feature_bag(&format!("skill domain {i} keywords unique {i}")),
skills: vec![format!("skill-{i}")],
archetype: Some("Analysis".into()),
complexity_bias: 0.0,
});
}
assert_eq!(r.pattern_count(), 40); let req = ContextRequest {
content: "skill domain 7 keywords unique 7 please".into(),
channel: "cli".into(),
chat_id: "c1".into(),
metadata: HashMap::new(),
};
let d = r.route(&req).await;
assert!(
d.skills.iter().any(|s| s.contains('7')),
"expected skill-7 near top, got {:?}",
d.skills
);
}
#[tokio::test]
async fn adapt_moves_complexity() {
let r = MicroLoraRouter::with_seed_skills(["code-review", "chat"]);
let req = ContextRequest {
content: "please review this rust code compile error".into(),
channel: "cli".into(),
chat_id: "c1".into(),
metadata: HashMap::new(),
};
let before = r.route(&req).await.complexity_hint;
for _ in 0..20 {
r.adapt(&req.content, 0.95, 0.05);
}
let after = r.route(&req).await.complexity_hint;
assert!(after.abs() <= 0.3);
assert!(before.abs() <= 0.3);
let _ = (before, after);
}
#[tokio::test]
async fn shadow_records_decision() {
let r = MicroLoraRouter::with_seed_skills(["alpha"]);
r.set_shadow(true);
let req = ContextRequest {
content: "alpha topic".into(),
channel: "cli".into(),
chat_id: "c".into(),
metadata: HashMap::new(),
};
let d = r.route(&req).await;
let shadow = r.last_shadow_decision().expect("shadow");
assert_eq!(d.skills, shadow.skills);
}
#[test]
fn feature_bag_normalized() {
let v = feature_bag("hello world hello");
let n: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((n - 1.0).abs() < 1e-4 || n < 1e-6);
}
}