1use crate::types::{LearnedPattern, PatternType, QueryTrajectory};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct PatternConfig {
12 pub k_clusters: usize,
14 pub embedding_dim: usize,
16 pub max_iterations: usize,
18 pub convergence_threshold: f32,
20 pub min_cluster_size: usize,
22 pub max_trajectories: usize,
24 pub quality_threshold: f32,
26}
27
28impl Default for PatternConfig {
29 fn default() -> Self {
30 Self {
34 k_clusters: 100, embedding_dim: 256,
36 max_iterations: 100,
37 convergence_threshold: 0.001,
38 min_cluster_size: 5,
39 max_trajectories: 10000,
40 quality_threshold: 0.3, }
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct ReasoningBank {
48 config: PatternConfig,
50 trajectories: Vec<TrajectoryEntry>,
52 patterns: HashMap<u64, LearnedPattern>,
54 next_pattern_id: u64,
56 pattern_index: Vec<(Vec<f32>, u64)>,
58}
59
60#[derive(Clone, Debug)]
62struct TrajectoryEntry {
63 embedding: Vec<f32>,
65 quality: f32,
67 cluster: Option<usize>,
69 trajectory_id: u64,
71}
72
73impl ReasoningBank {
74 pub fn new(config: PatternConfig) -> Self {
76 Self {
77 config,
78 trajectories: Vec::new(),
79 patterns: HashMap::new(),
80 next_pattern_id: 0,
81 pattern_index: Vec::new(),
82 }
83 }
84
85 pub fn add_trajectory(&mut self, trajectory: &QueryTrajectory) {
87 let embedding = self.compute_embedding(trajectory);
89
90 let entry = TrajectoryEntry {
91 embedding,
92 quality: trajectory.final_quality,
93 cluster: None,
94 trajectory_id: trajectory.id,
95 };
96
97 if self.trajectories.len() >= self.config.max_trajectories {
99 let to_remove = self.trajectories.len() - self.config.max_trajectories + 1;
101 self.trajectories.drain(0..to_remove);
102 }
103
104 self.trajectories.push(entry);
105 }
106
107 fn compute_embedding(&self, trajectory: &QueryTrajectory) -> Vec<f32> {
109 let dim = self.config.embedding_dim;
110 let mut embedding = vec![0.0f32; dim];
111
112 let query_len = trajectory.query_embedding.len().min(dim);
114 embedding[..query_len].copy_from_slice(&trajectory.query_embedding[..query_len]);
115
116 if !trajectory.steps.is_empty() {
118 let mut total_reward = 0.0f32;
119
120 for step in &trajectory.steps {
121 let weight = step.reward.max(0.0);
122 total_reward += weight;
123
124 for (i, &act) in step.activations.iter().enumerate() {
125 if i < dim {
126 embedding[i] += act * weight;
127 }
128 }
129 }
130
131 if total_reward > 0.0 {
132 for e in &mut embedding {
133 *e /= total_reward + 1.0; }
135 }
136 }
137
138 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
140 if norm > 1e-8 {
141 for e in &mut embedding {
142 *e /= norm;
143 }
144 }
145
146 embedding
147 }
148
149 pub fn extract_patterns(&mut self) -> Vec<LearnedPattern> {
151 if self.trajectories.is_empty() {
152 return Vec::new();
153 }
154
155 let k = self.config.k_clusters.min(self.trajectories.len());
156 if k == 0 {
157 return Vec::new();
158 }
159
160 let centroids = self.kmeans_plus_plus_init(k);
162
163 let (final_centroids, assignments) = self.run_kmeans(centroids);
165
166 let mut patterns = Vec::new();
168
169 for (cluster_idx, centroid) in final_centroids.into_iter().enumerate() {
170 let members: Vec<_> = self.trajectories.iter()
172 .enumerate()
173 .filter(|(i, _)| assignments.get(*i) == Some(&cluster_idx))
174 .map(|(_, t)| t)
175 .collect();
176
177 if members.len() < self.config.min_cluster_size {
178 continue;
179 }
180
181 let cluster_size = members.len();
183 let total_weight: f32 = members.iter().map(|t| t.quality).sum();
184 let avg_quality = total_weight / cluster_size as f32;
185
186 if avg_quality < self.config.quality_threshold {
187 continue;
188 }
189
190 let pattern_id = self.next_pattern_id;
191 self.next_pattern_id += 1;
192
193 let pattern = LearnedPattern {
194 id: pattern_id,
195 centroid,
196 cluster_size,
197 total_weight,
198 avg_quality,
199 created_at: std::time::SystemTime::now()
200 .duration_since(std::time::UNIX_EPOCH)
201 .unwrap_or_default()
202 .as_secs(),
203 last_accessed: std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .unwrap_or_default()
206 .as_secs(),
207 access_count: 0,
208 pattern_type: PatternType::General,
209 };
210
211 self.patterns.insert(pattern_id, pattern.clone());
212 self.pattern_index.push((pattern.centroid.clone(), pattern_id));
213 patterns.push(pattern);
214 }
215
216 for (i, cluster) in assignments.into_iter().enumerate() {
218 if i < self.trajectories.len() {
219 self.trajectories[i].cluster = Some(cluster);
220 }
221 }
222
223 patterns
224 }
225
226 fn kmeans_plus_plus_init(&self, k: usize) -> Vec<Vec<f32>> {
228 let mut centroids = Vec::with_capacity(k);
229 let n = self.trajectories.len();
230
231 if n == 0 || k == 0 {
232 return centroids;
233 }
234
235 let first_idx = 0;
237 centroids.push(self.trajectories[first_idx].embedding.clone());
238
239 for _ in 1..k {
241 let mut distances: Vec<f32> = self.trajectories.iter()
243 .map(|t| {
244 centroids.iter()
245 .map(|c| self.squared_distance(&t.embedding, c))
246 .fold(f32::MAX, f32::min)
247 })
248 .collect();
249
250 let total: f32 = distances.iter().sum();
252 if total > 0.0 {
253 for d in &mut distances {
254 *d /= total;
255 }
256 }
257
258 let (next_idx, _) = distances.iter()
260 .enumerate()
261 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
262 .unwrap_or((0, &0.0));
263
264 centroids.push(self.trajectories[next_idx].embedding.clone());
265 }
266
267 centroids
268 }
269
270 fn run_kmeans(&self, mut centroids: Vec<Vec<f32>>) -> (Vec<Vec<f32>>, Vec<usize>) {
272 let n = self.trajectories.len();
273 let k = centroids.len();
274 let dim = self.config.embedding_dim;
275
276 let mut assignments = vec![0usize; n];
277
278 for _iter in 0..self.config.max_iterations {
279 let mut changed = false;
281 for (i, t) in self.trajectories.iter().enumerate() {
282 let (nearest, _) = centroids.iter()
283 .enumerate()
284 .map(|(j, c)| (j, self.squared_distance(&t.embedding, c)))
285 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
286 .unwrap_or((0, 0.0));
287
288 if assignments[i] != nearest {
289 assignments[i] = nearest;
290 changed = true;
291 }
292 }
293
294 if !changed {
295 break;
296 }
297
298 let mut new_centroids = vec![vec![0.0f32; dim]; k];
300 let mut counts = vec![0usize; k];
301
302 for (i, t) in self.trajectories.iter().enumerate() {
303 let cluster = assignments[i];
304 counts[cluster] += 1;
305 for (j, &e) in t.embedding.iter().enumerate() {
306 new_centroids[cluster][j] += e;
307 }
308 }
309
310 let mut max_shift = 0.0f32;
312 for (i, new_c) in new_centroids.iter_mut().enumerate() {
313 if counts[i] > 0 {
314 for e in new_c.iter_mut() {
315 *e /= counts[i] as f32;
316 }
317 let shift = self.squared_distance(new_c, ¢roids[i]).sqrt();
318 max_shift = max_shift.max(shift);
319 }
320 }
321
322 centroids = new_centroids;
323
324 if max_shift < self.config.convergence_threshold {
325 break;
326 }
327 }
328
329 (centroids, assignments)
330 }
331
332 fn squared_distance(&self, a: &[f32], b: &[f32]) -> f32 {
334 a.iter()
335 .zip(b.iter())
336 .map(|(&x, &y)| (x - y) * (x - y))
337 .sum()
338 }
339
340 pub fn find_similar(&self, query: &[f32], k: usize) -> Vec<&LearnedPattern> {
342 let mut scored: Vec<_> = self.patterns.values()
343 .map(|p| (p, p.similarity(query)))
344 .collect();
345
346 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
347
348 scored.into_iter()
349 .take(k)
350 .map(|(p, _)| p)
351 .collect()
352 }
353
354 pub fn get_pattern(&self, id: u64) -> Option<&LearnedPattern> {
356 self.patterns.get(&id)
357 }
358
359 pub fn get_pattern_mut(&mut self, id: u64) -> Option<&mut LearnedPattern> {
361 self.patterns.get_mut(&id)
362 }
363
364 pub fn trajectory_count(&self) -> usize {
366 self.trajectories.len()
367 }
368
369 pub fn pattern_count(&self) -> usize {
371 self.patterns.len()
372 }
373
374 pub fn clear_trajectories(&mut self) {
376 self.trajectories.clear();
377 }
378
379 pub fn prune_patterns(&mut self, min_quality: f32, min_accesses: u32, max_age_secs: u64) {
381 let to_remove: Vec<u64> = self.patterns.iter()
382 .filter(|(_, p)| p.should_prune(min_quality, min_accesses, max_age_secs))
383 .map(|(id, _)| *id)
384 .collect();
385
386 for id in to_remove {
387 self.patterns.remove(&id);
388 }
389
390 self.pattern_index.retain(|(_, id)| self.patterns.contains_key(id));
392 }
393
394 pub fn get_all_patterns(&self) -> Vec<LearnedPattern> {
396 self.patterns.values().cloned().collect()
397 }
398
399 pub fn consolidate(&mut self, similarity_threshold: f32) {
401 let pattern_ids: Vec<u64> = self.patterns.keys().copied().collect();
402 let mut merged = Vec::new();
403
404 for i in 0..pattern_ids.len() {
405 for j in i+1..pattern_ids.len() {
406 let id1 = pattern_ids[i];
407 let id2 = pattern_ids[j];
408
409 if merged.contains(&id1) || merged.contains(&id2) {
410 continue;
411 }
412
413 if let (Some(p1), Some(p2)) = (self.patterns.get(&id1), self.patterns.get(&id2)) {
414 let sim = p1.similarity(&p2.centroid);
415 if sim > similarity_threshold {
416 let merged_pattern = p1.merge(p2);
418 self.patterns.insert(id1, merged_pattern);
419 merged.push(id2);
420 }
421 }
422 }
423 }
424
425 for id in merged {
427 self.patterns.remove(&id);
428 }
429
430 self.pattern_index.retain(|(_, id)| self.patterns.contains_key(id));
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 fn make_trajectory(id: u64, embedding: Vec<f32>, quality: f32) -> QueryTrajectory {
440 let mut t = QueryTrajectory::new(id, embedding);
441 t.finalize(quality, 1000);
442 t
443 }
444
445 #[test]
446 fn test_bank_creation() {
447 let bank = ReasoningBank::new(PatternConfig::default());
448 assert_eq!(bank.trajectory_count(), 0);
449 assert_eq!(bank.pattern_count(), 0);
450 }
451
452 #[test]
453 fn test_add_trajectory() {
454 let config = PatternConfig {
455 embedding_dim: 4,
456 ..Default::default()
457 };
458 let mut bank = ReasoningBank::new(config);
459
460 let t = make_trajectory(1, vec![0.1, 0.2, 0.3, 0.4], 0.8);
461 bank.add_trajectory(&t);
462
463 assert_eq!(bank.trajectory_count(), 1);
464 }
465
466 #[test]
467 fn test_extract_patterns() {
468 let config = PatternConfig {
469 embedding_dim: 4,
470 k_clusters: 2,
471 min_cluster_size: 2,
472 quality_threshold: 0.0,
473 ..Default::default()
474 };
475 let mut bank = ReasoningBank::new(config);
476
477 for i in 0..5 {
479 let t = make_trajectory(i, vec![1.0, 0.0, 0.0, 0.0], 0.8);
480 bank.add_trajectory(&t);
481 }
482 for i in 5..10 {
483 let t = make_trajectory(i, vec![0.0, 1.0, 0.0, 0.0], 0.7);
484 bank.add_trajectory(&t);
485 }
486
487 let patterns = bank.extract_patterns();
488 assert!(!patterns.is_empty());
489 }
490
491 #[test]
492 fn test_find_similar() {
493 let config = PatternConfig {
494 embedding_dim: 4,
495 k_clusters: 2,
496 min_cluster_size: 2,
497 quality_threshold: 0.0,
498 ..Default::default()
499 };
500 let mut bank = ReasoningBank::new(config);
501
502 for i in 0..10 {
503 let emb = if i < 5 {
504 vec![1.0, 0.0, 0.0, 0.0]
505 } else {
506 vec![0.0, 1.0, 0.0, 0.0]
507 };
508 bank.add_trajectory(&make_trajectory(i, emb, 0.8));
509 }
510
511 bank.extract_patterns();
512
513 let query = vec![0.9, 0.1, 0.0, 0.0];
514 let similar = bank.find_similar(&query, 1);
515 assert!(!similar.is_empty());
516 }
517
518 #[test]
519 fn test_consolidate() {
520 let config = PatternConfig {
521 embedding_dim: 4,
522 k_clusters: 3,
523 min_cluster_size: 1,
524 quality_threshold: 0.0,
525 ..Default::default()
526 };
527 let mut bank = ReasoningBank::new(config);
528
529 for i in 0..9 {
531 let emb = vec![1.0 + (i as f32 * 0.001), 0.0, 0.0, 0.0];
532 bank.add_trajectory(&make_trajectory(i, emb, 0.8));
533 }
534
535 bank.extract_patterns();
536 let before = bank.pattern_count();
537
538 bank.consolidate(0.99);
539 let after = bank.pattern_count();
540
541 assert!(after <= before);
542 }
543}