1use std::io::{BufRead, BufReader, Read, Write};
15use std::net::TcpListener;
16use tokio::runtime::Handle;
17use tokio::task::block_in_place;
18
19use crate::bridge::MemoryBridge;
20
21fn rerank_results(
24 query: &str,
25 results: &[serde_json::Value],
26 model: &str,
27) -> Vec<serde_json::Value> {
28 let client = reqwest::blocking::Client::new();
29 let mut scored: Vec<(f64, serde_json::Value)> = results
30 .iter()
31 .map(|r| {
32 let content = r.get("content").and_then(|v| v.as_str()).unwrap_or("");
33 let truncated: String = content.chars().take(500).collect();
34 let prompt = format!(
35 "Rate the relevance of this document to the query on a scale of 1-5. Reply with ONLY the number.\nQuery: {query}\nDocument: {truncated}\nRating:"
36 );
37 let body = serde_json::json!({
38 "model": model,
39 "prompt": prompt,
40 "stream": false,
41 "options": {"temperature": 0, "num_predict": 1}
42 });
43 let rating = client
44 .post("http://127.0.0.1:11434/api/generate")
45 .json(&body)
46 .send()
47 .ok()
48 .and_then(|resp| resp.json::<serde_json::Value>().ok())
49 .and_then(|v| {
50 v.get("response")
51 .and_then(|r| r.as_str())
52 .and_then(|s| s.trim().chars().next())
53 .and_then(|c| c.to_digit(10))
54 .map(|d| d as f64)
55 })
56 .unwrap_or(1.0);
57 (rating, r.clone())
58 })
59 .collect();
60 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
61 scored
62 .into_iter()
63 .map(|(score, mut r)| {
64 if let Some(obj) = r.as_object_mut() {
65 obj.insert("rerank_score".to_string(), serde_json::json!(score));
66 }
67 r
68 })
69 .collect()
70}
71
72pub fn start_http_server(port: u16, bridge: MemoryBridge, handle: Handle) {
73 std::thread::spawn(move || {
74 let listener = match TcpListener::bind(("127.0.0.1", port)) {
75 Ok(l) => {
76 eprintln!("HTTP search server listening on 127.0.0.1:{}", port);
77 l
78 }
79 Err(e) => {
80 eprintln!("Failed to bind HTTP port {}: {}", port, e);
81 return;
82 }
83 };
84
85 for stream in listener.incoming() {
86 let stream = match stream {
87 Ok(s) => s,
88 Err(_) => continue,
89 };
90
91 let bridge = bridge.clone();
92 let h = handle.clone();
93 std::thread::spawn(move || {
94 handle_connection(stream, bridge, h);
95 });
96 }
97 });
98}
99
100fn handle_connection(
101 mut stream: std::net::TcpStream,
102 bridge: MemoryBridge,
103 handle: Handle,
104) {
105 let mut reader = BufReader::new(stream.try_clone().expect("clone"));
106 let mut request_line = String::new();
107 if reader.read_line(&mut request_line).is_err() {
108 return;
109 }
110
111 let parts: Vec<&str> = request_line.split_whitespace().collect();
112 if parts.len() < 2 {
113 return;
114 }
115 let method = parts[0];
116 let path = parts[1];
117
118 let mut content_length = 0;
119 loop {
120 let mut header = String::new();
121 if reader.read_line(&mut header).is_err() {
122 return;
123 }
124 if header.trim().is_empty() {
125 break;
126 }
127 if let Some(len_str) = header
128 .strip_prefix("Content-Length:")
129 .or_else(|| header.strip_prefix("content-length:"))
130 {
131 content_length = len_str.trim().parse().unwrap_or(0);
132 }
133 }
134
135 let mut body = vec![0u8; content_length];
136 if content_length > 0 && reader.read_exact(&mut body).is_err() {
137 return;
138 }
139 let body_str = String::from_utf8_lossy(&body);
140
141 let (status, response) = match (method, path) {
142 ("GET", "/health") => (
143 "200 OK",
144 serde_json::json!({"ok": true, "service": "semantic-memory-mcp"}),
145 ),
146 ("POST", "/search") => handle_search(&body_str, &bridge, &handle),
147 ("POST", "/search-routed") => handle_search_routed(&body_str, &bridge, &handle),
148 ("POST", "/rerank") => handle_rerank(&body_str),
149 ("POST", "/stats") => handle_stats(&bridge, &handle),
150 ("POST", "/add") => handle_add_fact(&body_str, &bridge, &handle),
151 ("POST", "/add-edge") => {
152 handle_add_edge(&body_str, &bridge, &handle)
153 }
154 ("POST", "/delete-fact") => {
155 handle_delete_fact(&body_str, &bridge, &handle)
156 }
157 ("POST", "/record-outcome") => handle_record_outcome(&body_str, &bridge, &handle),
158 ("GET", "/verify-integrity") => handle_verify_integrity(&bridge, &handle),
159 ("POST", "/discord") => handle_discord(&body_str, &bridge, &handle),
160 ("POST", "/maintenance/check") => handle_maintenance_check(&bridge, &handle),
161 ("POST", "/maintenance/vacuum") => handle_maintenance_vacuum(&bridge, &handle),
162 ("POST", "/maintenance/reembed") => handle_maintenance_reembed(&bridge, &handle),
163 ("POST", "/maintenance/reconcile") => handle_maintenance_reconcile(&body_str, &bridge, &handle),
164 ("POST", "/maintenance/compact-hnsw") => handle_maintenance_compact_hnsw(&bridge, &handle),
165 ("POST", "/maintenance/auto-edge") => handle_maintenance_auto_edge(&body_str, &bridge, &handle),
166 _ => (
167 "404 Not Found",
168 serde_json::json!({"error": "not found", "path": path}),
169 ),
170 };
171
172 let response_str = serde_json::to_string(&response).unwrap_or_default();
173 let response_bytes = response_str.as_bytes();
174 let http_response = format!(
175 "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
176 status,
177 response_bytes.len()
178 );
179
180 let _ = stream.write_all(http_response.as_bytes());
181 let _ = stream.write_all(response_bytes);
182 let _ = stream.flush();
183}
184
185fn handle_search(
186 body: &str,
187 bridge: &MemoryBridge,
188 handle: &Handle,
189) -> (&'static str, serde_json::Value) {
190 let params: serde_json::Value = match serde_json::from_str(body) {
191 Ok(v) => v,
192 Err(e) => {
193 return (
194 "400 Bad Request",
195 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
196 )
197 }
198 };
199
200 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
201 let top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
202 let namespaces: Option<Vec<String>> = params
203 .get("namespaces")
204 .and_then(|v| serde_json::from_value(v.clone()).ok());
205 let do_rerank = params.get("rerank").and_then(|v| v.as_bool()).unwrap_or(false);
206
207 if query.is_empty() {
208 return (
209 "400 Bad Request",
210 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
211 );
212 }
213
214 let store = &bridge.store;
215 let ns_slice: Option<Vec<&str>> = namespaces
216 .as_ref()
217 .map(|v| v.iter().map(|s| s.as_str()).collect());
218 let fetch_k = if do_rerank { top_k * 2 } else { top_k };
220 let result = block_in_place(|| {
221 handle.block_on(store.search(query, Some(fetch_k), ns_slice.as_deref(), None))
222 });
223
224 match result {
225 Ok(results) => {
226 let json_results: Vec<serde_json::Value> = results
227 .iter()
228 .map(|r| {
229 let namespace = match &r.source {
230 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
231 semantic_memory::SearchSource::Chunk { document_title, .. } => document_title.clone(),
232 _ => String::new(),
233 };
234 serde_json::json!({
235 "result_id": r.source.result_id(),
236 "content": r.content,
237 "score": r.score,
238 "cosine_similarity": r.cosine_similarity,
239 "namespace": namespace,
240 })
241 })
242 .collect();
243
244 let final_results: Vec<serde_json::Value> = if do_rerank && !json_results.is_empty() {
245 rerank_results(query, &json_results, "granite4.1:3b")
246 .into_iter()
247 .take(top_k)
248 .collect()
249 } else {
250 json_results
251 };
252
253 let count = final_results.len();
254 let provenance = serde_json::json!({
255 "stages_fired": {
256 "bm25": true,
257 "vector": true,
258 "late_interaction": false,
259 "rerank": do_rerank,
260 },
261 "result_count": count,
262 "view": "semantic",
263 "widening_occurred": false,
264 "widening_reason": null,
265 "verification_status": "verified",
266 });
267 (
268 "200 OK",
269 serde_json::json!({
270 "ok": true,
271 "query": query,
272 "top_k": top_k,
273 "results": final_results,
274 "count": count,
275 "reranked": do_rerank,
276 "provenance": provenance,
277 }),
278 )
279 }
280 Err(e) => (
281 "500 Internal Server Error",
282 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
283 ),
284 }
285}
286
287fn handle_search_routed(
295 body: &str,
296 bridge: &MemoryBridge,
297 handle: &Handle,
298) -> (&'static str, serde_json::Value) {
299 use semantic_memory::integration::plan_execution;
300 use semantic_memory::routing::RetrievalRouter;
301
302 let params: serde_json::Value = match serde_json::from_str(body) {
303 Ok(v) => v,
304 Err(e) => {
305 return (
306 "400 Bad Request",
307 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
308 )
309 }
310 };
311
312 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
313 let base_top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(12) as usize;
314 let query_class = params.get("query_class").and_then(|v| v.as_str()).unwrap_or("A");
315 let namespaces: Option<Vec<String>> = params
316 .get("namespaces")
317 .and_then(|v| serde_json::from_value(v.clone()).ok());
318 let contradictions: Vec<(String, String)> = params
319 .get("contradictions")
320 .and_then(|v| serde_json::from_value(v.clone()).ok())
321 .unwrap_or_default();
322 let group_by_community = params.get("group_by_community").and_then(|v| v.as_bool()).unwrap_or(false);
323
324 if query.is_empty() {
325 return (
326 "400 Bad Request",
327 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
328 );
329 }
330
331 let router = RetrievalRouter {
333 decoder_enabled: true,
334 discord_enabled: true,
335 corpus_density: 0.5,
336 ..Default::default()
337 };
338 let decision = router.route_query(query);
339 let contras = contradictions.clone();
340 let plan = plan_execution(&decision, contras.clone());
341
342 let top_k = if query_class == "D" {
344 (base_top_k * 2).min(20)
345 } else {
346 base_top_k
347 };
348
349 let store = &bridge.store;
350 let ns_slice: Option<Vec<&str>> = namespaces
351 .as_ref()
352 .map(|v| v.iter().map(|s| s.as_str()).collect());
353
354 let result = if query_class == "C" {
356 use semantic_memory::{ExactnessProfile, SearchContext};
357 let mut ctx = SearchContext::default_now();
358 ctx.exactness_profile = ExactnessProfile::PreferExact;
359 block_in_place(|| {
360 handle.block_on(store.search_with_context(
361 query,
362 Some(top_k),
363 ns_slice.as_deref(),
364 None,
365 ctx,
366 ))
367 })
368 .map(|r| r.results)
369 } else {
370 block_in_place(|| {
371 handle.block_on(store.search(query, Some(top_k), ns_slice.as_deref(), None))
372 })
373 };
374
375 match result {
376 Ok(results) => {
377 let json_results: Vec<serde_json::Value> = results
378 .iter()
379 .map(|r| {
380 let namespace = match &r.source {
381 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
382 semantic_memory::SearchSource::Chunk { document_title, .. } => {
383 document_title.clone()
384 }
385 _ => String::new(),
386 };
387 serde_json::json!({
388 "result_id": r.source.result_id(),
389 "content": r.content,
390 "score": r.score,
391 "cosine_similarity": r.cosine_similarity,
392 "namespace": namespace,
393 "source_type": match &r.source {
394 semantic_memory::SearchSource::Fact { .. } => "fact",
395 semantic_memory::SearchSource::Chunk { .. } => "chunk",
396 semantic_memory::SearchSource::Message { .. } => "message",
397 _ => "unknown",
398 },
399 })
400 })
401 .collect();
402
403 let mut factor_graph_payload = serde_json::json!({"enabled": false});
404 let mut decoder_executed = false;
405 let mut discord_executed = false;
406 let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
407
408 if decision.decoder {
410 #[cfg(feature = "full")]
411 {
412 use semantic_memory::factor_graph::{factors_from_edges, FactorGraph, FactorGraphConfig};
413
414 let graph_edges = block_in_place(|| {
415 handle.block_on(store.list_all_graph_edges())
416 });
417
418 if let Ok(edges) = graph_edges {
419 let raw_edges: Vec<(
420 String,
421 String,
422 semantic_memory::GraphEdgeType,
423 f64,
424 Option<String>,
425 )> = edges
426 .iter()
427 .map(|edge| {
428 let parsed_type = edge
429 .edge_type_parsed
430 .clone()
431 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
432 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
433 relation: "unknown".to_string(),
434 });
435 (
436 edge.source.clone(),
437 edge.target.clone(),
438 parsed_type,
439 edge.weight,
440 edge.metadata.clone(),
441 )
442 })
443 .collect();
444
445 let nodes: Vec<(String, f64)> = results
446 .iter()
447 .map(|r| (r.source.result_id(), r.score))
448 .collect();
449 let factors = factors_from_edges(&raw_edges);
450 let graph =
451 FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
452 let propagated = graph.propagate();
453 let top_beliefs = propagated.top_k(top_k);
454
455 factor_graph_payload = serde_json::json!({
456 "enabled": true,
457 "top_k_beliefs": top_beliefs
458 .into_iter()
459 .map(|(item_id, belief)| serde_json::json!({
460 "item_id": item_id,
461 "belief": belief,
462 }))
463 .collect::<Vec<_>>(),
464 "iterations": propagated.iterations,
465 "converged": propagated.converged,
466 "elapsed_ms": propagated.elapsed_ms,
467 "factor_counts": {
468 "semantic": propagated.factor_counts.semantic,
469 "temporal": propagated.factor_counts.temporal,
470 "causal": propagated.factor_counts.causal,
471 "entity": propagated.factor_counts.entity,
472 "total": propagated.factor_counts.total(),
473 },
474 });
475 decoder_executed = true;
476 }
477 }
478
479 if !plan.contradictions.is_empty() {
481 use semantic_memory::decoder::{compute_correction, detect_syndromes};
482 let result_scores: Vec<(String, f64)> = results
483 .iter()
484 .map(|r| (r.source.result_id(), r.score))
485 .collect();
486 let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
487 let _ = compute_correction(&syndromes, 10.0);
488 decoder_executed = true;
489 }
490 }
491
492 if plan.use_discord {
494 use semantic_memory::discord::DiscordScorer;
495 let direct_ids: Vec<String> = results.iter().map(|r| r.source.result_id()).collect();
496 let existing_ids: std::collections::HashSet<String> =
497 direct_ids.iter().cloned().collect();
498 let edges_result = block_in_place(|| {
499 handle.block_on(store.list_graph_edges_for_neighborhood(
500 direct_ids.clone(),
501 2,
502 200,
503 ))
504 });
505 if let Ok(raw_edges) = edges_result {
506 let edge_refs: Vec<semantic_memory::discord::GraphEdgeRef> = raw_edges
507 .iter()
508 .map(|edge| {
509 let parsed_type = edge
510 .edge_type_parsed
511 .clone()
512 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
513 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
514 relation: "unknown".to_string(),
515 });
516 let type_str = match parsed_type {
517 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
518 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
519 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
520 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
521 };
522 semantic_memory::discord::GraphEdgeRef {
523 source: edge.source.clone(),
524 target: edge.target.clone(),
525 edge_type: type_str.to_string(),
526 weight: edge.weight,
527 }
528 })
529 .collect();
530 let scorer = DiscordScorer::with_defaults();
531 let discord_hits = scorer.score(&direct_ids, &edge_refs);
532 for hit in &discord_hits {
533 if !existing_ids.contains(&hit.item_id) {
534 let bare_id = hit.item_id.strip_prefix("fact:").unwrap_or(&hit.item_id);
537 let (content, namespace) = {
538 let fact_result = handle.block_on(
539 store.get_fact(bare_id)
540 );
541 match fact_result {
542 Ok(Some(fact)) => (fact.content, fact.namespace),
543 Ok(None) => {
544 eprintln!("[discord] get_fact returned None for id={}", bare_id);
545 (String::new(), String::new())
546 }
547 Err(e) => {
548 eprintln!("[discord] get_fact error for id={}: {}", bare_id, e);
549 (String::new(), String::new())
550 }
551 }
552 };
553 discord_results_payload.push(serde_json::json!({
554 "result_id": hit.item_id,
555 "content": content,
556 "namespace": namespace,
557 "discord_score": hit.discord_score,
558 "anchor_ids": hit.anchor_ids,
559 "relationship_types": hit.relationship_types,
560 }));
561 }
562 }
563 discord_executed = true;
564 }
565 }
566
567 let grouped_results_payload: serde_json::Value = if group_by_community {
569 let seed_ids: Vec<String> = results.iter().map(|r| r.source.result_id()).collect();
570 let edges_result = block_in_place(|| {
571 handle.block_on(store.list_graph_edges_for_neighborhood(
572 seed_ids.clone(),
573 2,
574 200,
575 ))
576 });
577 let edges: Vec<(String, String)> = match edges_result {
578 Ok(raw_edges) => raw_edges
579 .iter()
580 .map(|edge| (edge.source.clone(), edge.target.clone()))
581 .collect(),
582 Err(_) => Vec::new(),
583 };
584 if !edges.is_empty() {
585 use semantic_memory::community::detect_communities;
586 let communities = detect_communities(&edges, 1.0, 42);
587 let mut member_to_comm: std::collections::HashMap<String, String> =
588 std::collections::HashMap::new();
589 for c in &communities {
590 for m in &c.members {
591 member_to_comm.insert(m.clone(), c.id.clone());
592 }
593 }
594 let mut groups: std::collections::HashMap<
595 String,
596 Vec<serde_json::Value>,
597 > = std::collections::HashMap::new();
598 let mut ungrouped: Vec<serde_json::Value> = Vec::new();
599 for r in &json_results {
600 if let Some(rid) = r.get("result_id").and_then(|v| v.as_str()) {
601 match member_to_comm.get(rid).cloned() {
602 Some(cid) => {
603 groups.entry(cid).or_default().push(r.clone())
604 }
605 None => ungrouped.push(r.clone()),
606 }
607 }
608 }
609 let mut map = serde_json::Map::new();
610 for (cid, items) in groups {
611 map.insert(
612 format!("community_{cid}"),
613 serde_json::json!(items),
614 );
615 }
616 if !ungrouped.is_empty() {
617 map.insert(
618 "ungrouped".to_string(),
619 serde_json::json!(ungrouped),
620 );
621 }
622 serde_json::Value::Object(map)
623 } else {
624 serde_json::Value::Null
625 }
626 } else {
627 serde_json::Value::Null
628 };
629
630 let provenance = serde_json::json!({
632 "stages_fired": {
633 "bm25": results.iter().any(|r| r.bm25_rank.is_some()),
634 "vector": results.iter().any(|r| r.vector_rank.is_some()),
635 "late_interaction": true,
636 "discord": discord_executed,
637 "decoder": decoder_executed,
638 },
639 "result_count": results.len(),
640 "view": "routed",
641 "query_class": query_class,
642 "widening_occurred": false,
643 "widening_reason": null,
644 "verification_status": "verified",
645 });
646
647 (
648 "200 OK",
649 serde_json::json!({
650 "ok": true,
651 "query": query,
652 "top_k": base_top_k,
653 "results": json_results,
654 "provenance": provenance,
655 "query_class": query_class,
656 "routed": true,
657 "routing_decision": {
658 "bm25_coarse": decision.bm25_coarse,
659 "vector_medium": decision.vector_medium,
660 "rerank_fine": decision.rerank_fine,
661 "graph_expansion": decision.graph_expansion,
662 "decoder": decision.decoder,
663 "discord": decision.discord,
664 "no_retrieval": decision.no_retrieval,
665 "reasoning": decision.reasoning,
666 },
667 "decoder_planned": plan.use_decoder,
668 "decoder_executed": decoder_executed,
669 "discord_planned": plan.use_discord,
670 "discord_executed": discord_executed,
671 "discord_results": discord_results_payload,
672 "factor_graph": factor_graph_payload,
673 "grouped_results": grouped_results_payload,
674 }),
675 )
676 }
677 Err(e) => (
678 "500 Internal Server Error",
679 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
680 ),
681 }
682}
683
684fn handle_stats(
685 bridge: &MemoryBridge,
686 handle: &Handle,
687) -> (&'static str, serde_json::Value) {
688 let store = &bridge.store;
689 let result = block_in_place(|| handle.block_on(store.stats()));
690 match result {
691 Ok(stats) => (
692 "200 OK",
693 serde_json::json!({
694 "ok": true,
695 "facts": stats.total_facts,
696 "documents": stats.total_documents,
697 "chunks": stats.total_chunks,
698 "db_size_mb": (stats.database_size_bytes as f64) / (1024.0 * 1024.0),
699 }),
700 ),
701 Err(e) => (
702 "500 Internal Server Error",
703 serde_json::json!({"ok": false, "error": format!("{e}")}),
704 ),
705 }
706}
707
708fn handle_rerank(body: &str) -> (&'static str, serde_json::Value) {
709 let params: serde_json::Value = match serde_json::from_str(body) {
710 Ok(v) => v,
711 Err(e) => {
712 return (
713 "400 Bad Request",
714 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
715 )
716 }
717 };
718
719 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
720 let model = params
721 .get("model")
722 .and_then(|v| v.as_str())
723 .unwrap_or("granite4.1:3b");
724 let results = match params.get("results").and_then(|v| v.as_array()) {
725 Some(r) => r.clone(),
726 None => {
727 return (
728 "400 Bad Request",
729 serde_json::json!({"ok": false, "error": "missing 'results' array"}),
730 )
731 }
732 };
733
734 if query.is_empty() {
735 return (
736 "400 Bad Request",
737 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
738 );
739 }
740
741 let reranked = rerank_results(query, &results, model);
742 let count = reranked.len();
743 (
744 "200 OK",
745 serde_json::json!({
746 "ok": true,
747 "results": reranked,
748 "count": count,
749 }),
750 )
751}
752
753fn handle_add_fact(
754 body: &str,
755 bridge: &MemoryBridge,
756 handle: &Handle,
757) -> (&'static str, serde_json::Value) {
758 let params: serde_json::Value = match serde_json::from_str(body) {
759 Ok(v) => v,
760 Err(e) => {
761 return (
762 "400 Bad Request",
763 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
764 )
765 }
766 };
767
768 let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
769 let namespace = params
770 .get("namespace")
771 .and_then(|v| v.as_str())
772 .unwrap_or("general");
773 let source = params.get("source").and_then(|v| v.as_str());
774
775 if content.is_empty() {
776 return (
777 "400 Bad Request",
778 serde_json::json!({"ok": false, "error": "missing 'content' field"}),
779 );
780 }
781
782 let store = &bridge.store;
783 let result =
784 block_in_place(|| handle.block_on(store.add_fact(namespace, content, source, None)));
785
786 match result {
787 Ok(fact_id) => (
788 "200 OK",
789 serde_json::json!({"ok": true, "fact_id": fact_id}),
790 ),
791 Err(e) => (
792 "500 Internal Server Error",
793 serde_json::json!({"ok": false, "error": format!("{e}")}),
794 ),
795 }
796}
797
798fn handle_add_edge(
800 body: &str,
801 bridge: &MemoryBridge,
802 handle: &Handle,
803) -> (&'static str, serde_json::Value) {
804 let params: serde_json::Value = match serde_json::from_str(body) {
805 Ok(v) => v,
806 Err(e) => {
807 return (
808 "400 Bad Request",
809 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
810 )
811 }
812 };
813
814 let source = params.get("source").and_then(|v| v.as_str()).unwrap_or("");
815 let target = params.get("target").and_then(|v| v.as_str()).unwrap_or("");
816 let edge_type_str = params.get("edge_type").and_then(|v| v.as_str()).unwrap_or("semantic");
817 let weight = params.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0);
818 let cosine_similarity = params.get("cosine_similarity").and_then(|v| v.as_f64());
819 let relation = params.get("relation").and_then(|v| v.as_str());
820
821 if source.is_empty() || target.is_empty() {
822 return (
823 "400 Bad Request",
824 serde_json::json!({"ok": false, "error": "missing 'source' or 'target'"}),
825 );
826 }
827
828 let edge_type = match edge_type_str {
829 "semantic" => semantic_memory::GraphEdgeType::Semantic {
830 cosine_similarity: cosine_similarity.unwrap_or(weight) as f32,
831 },
832 "temporal" => semantic_memory::GraphEdgeType::Temporal {
833 delta_secs: params.get("delta_secs").and_then(|v| v.as_u64()).unwrap_or(0),
834 },
835 "causal" => semantic_memory::GraphEdgeType::Causal {
836 confidence: cosine_similarity.unwrap_or(weight) as f32,
837 evidence_ids: Vec::new(),
838 },
839 "entity" => semantic_memory::GraphEdgeType::Entity {
840 relation: relation.unwrap_or("mentions").to_string(),
841 },
842 _ => semantic_memory::GraphEdgeType::Semantic {
843 cosine_similarity: cosine_similarity.unwrap_or(weight) as f32,
844 },
845 };
846
847 let store = &bridge.store;
848 let result = block_in_place(|| {
849 handle.block_on(store.add_graph_edge(source, target, edge_type, weight, None))
850 });
851
852 match result {
853 Ok(edge) => (
854 "200 OK",
855 serde_json::json!({"ok": true, "edge_id": edge.id}),
856 ),
857 Err(e) => (
858 "500 Internal Server Error",
859 serde_json::json!({"ok": false, "error": format!("{e}")}),
860 ),
861 }
862}
863
864fn handle_delete_fact(
866 body: &str,
867 bridge: &MemoryBridge,
868 handle: &Handle,
869) -> (&'static str, serde_json::Value) {
870 let params: serde_json::Value = match serde_json::from_str(body) {
871 Ok(v) => v,
872 Err(e) => {
873 return (
874 "400 Bad Request",
875 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
876 )
877 }
878 };
879
880 let fact_id = params.get("fact_id").and_then(|v| v.as_str()).unwrap_or("");
881 if fact_id.is_empty() {
882 return (
883 "400 Bad Request",
884 serde_json::json!({"ok": false, "error": "missing 'fact_id'"}),
885 );
886 }
887
888 let bare_id = fact_id.strip_prefix("fact:").unwrap_or(fact_id);
889 let store = &bridge.store;
890 let result = block_in_place(|| handle.block_on(store.delete_fact(bare_id)));
891
892 match result {
893 Ok(()) => ("200 OK", serde_json::json!({"ok": true, "deleted": true})),
894 Err(e) => (
895 "500 Internal Server Error",
896 serde_json::json!({"ok": false, "error": format!("{e}")}),
897 ),
898 }
899}
900
901fn handle_record_outcome(
903 body: &str,
904 bridge: &MemoryBridge,
905 handle: &Handle,
906) -> (&'static str, serde_json::Value) {
907 use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome};
908 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
909
910 let params: serde_json::Value = match serde_json::from_str(body) {
911 Ok(v) => v,
912 Err(e) => {
913 return (
914 "400 Bad Request",
915 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
916 )
917 }
918 };
919
920 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
921 let outcome = params.get("outcome").and_then(|v| v.as_str()).unwrap_or("neutral");
922 let _query_class = params.get("query_class").and_then(|v| v.as_str()).unwrap_or("A");
923
924 if query.is_empty() {
925 return (
926 "400 Bad Request",
927 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
928 );
929 }
930
931 let outcome_enum = match outcome.to_lowercase().as_str() {
932 "good" => RoutingOutcome::Good,
933 "bad" => RoutingOutcome::Bad,
934 "neutral" => RoutingOutcome::Neutral,
935 _ => {
936 return (
937 "400 Bad Request",
938 serde_json::json!({"ok": false, "error": "outcome must be 'good', 'bad', or 'neutral'"}),
939 )
940 }
941 };
942
943 let profile = QueryProfile::from_query(query);
944 let router = RetrievalRouter::default();
945 let decision = router.route(&profile);
946
947 let store = &bridge.store;
948 let mut policy = block_in_place(|| handle.block_on(store.load_routing_policy()))
950 .ok()
951 .flatten()
952 .unwrap_or_default();
953 record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
954 let _ = block_in_place(|| handle.block_on(store.save_routing_policy(&policy)));
956
957 (
958 "200 OK",
959 serde_json::json!({
960 "ok": true,
961 "recorded": true,
962 "outcome": outcome,
963 "routing_decision": {
964 "bm25_coarse": decision.bm25_coarse,
965 "vector_medium": decision.vector_medium,
966 "rerank_fine": decision.rerank_fine,
967 "graph_expansion": decision.graph_expansion,
968 "decoder": decision.decoder,
969 "discord": decision.discord,
970 "no_retrieval": decision.no_retrieval,
971 "reasoning": decision.reasoning,
972 },
973 "policy_state": {
974 "trained_examples": policy.trained_examples,
975 "baseline": policy.baseline,
976 },
977 }),
978 )
979}
980
981fn handle_verify_integrity(
983 bridge: &MemoryBridge,
984 handle: &Handle,
985) -> (&'static str, serde_json::Value) {
986 let store = &bridge.store;
987 let result = block_in_place(|| {
988 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
989 });
990
991 match result {
992 Ok(report) => (
993 "200 OK",
994 serde_json::json!({
995 "ok": report.ok,
996 "integrity": report.ok,
997 "schema_version": report.schema_version,
998 "fact_count": report.fact_count,
999 "chunk_count": report.chunk_count,
1000 "message_count": report.message_count,
1001 "facts_missing_embeddings": report.facts_missing_embeddings,
1002 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1003 "issues": report.issues,
1004 "issue_count": report.issues.len(),
1005 "message": if report.ok { "All integrity checks passed".to_string() } else { format!("{} integrity issues found", report.issues.len()) },
1006 }),
1007 ),
1008 Err(e) => (
1009 "500 Internal Server Error",
1010 serde_json::json!({"ok": false, "integrity": false, "error": format!("verify_integrity error: {e}")}),
1011 ),
1012 }
1013}
1014
1015fn handle_discord(
1020 body: &str,
1021 bridge: &MemoryBridge,
1022 handle: &Handle,
1023) -> (&'static str, serde_json::Value) {
1024 use semantic_memory::discord::DiscordScorer;
1025
1026 let params: serde_json::Value = match serde_json::from_str(body) {
1027 Ok(v) => v,
1028 Err(e) => {
1029 return (
1030 "400 Bad Request",
1031 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1032 )
1033 }
1034 };
1035
1036 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
1037 let top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
1038
1039 let direct_ids: Vec<String> = match params.get("direct_ids").and_then(|v| v.as_array()) {
1041 Some(arr) => arr
1042 .iter()
1043 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1044 .collect(),
1045 None => {
1046 if query.is_empty() {
1048 return (
1049 "400 Bad Request",
1050 serde_json::json!({"ok": false, "error": "either 'direct_ids' or 'query' must be provided"}),
1051 );
1052 }
1053 let store = &bridge.store;
1054 let search_result = block_in_place(|| {
1055 handle.block_on(store.search(query, Some(top_k), None, None))
1056 });
1057 match search_result {
1058 Ok(results) => results.iter().map(|r| r.source.result_id()).collect(),
1059 Err(e) => {
1060 return (
1061 "500 Internal Server Error",
1062 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
1063 )
1064 }
1065 }
1066 }
1067 };
1068
1069 if direct_ids.is_empty() {
1070 return (
1071 "200 OK",
1072 serde_json::json!({"ok": true, "discord_results": [], "count": 0, "edges_loaded": 0}),
1073 );
1074 }
1075
1076 let store = &bridge.store;
1077 let edges_result = block_in_place(|| {
1079 handle.block_on(store.list_graph_edges_for_neighborhood(
1080 direct_ids.clone(),
1081 2,
1082 200,
1083 ))
1084 });
1085
1086 let edges: Vec<semantic_memory::discord::GraphEdgeRef> = match edges_result {
1087 Ok(raw_edges) => raw_edges
1088 .iter()
1089 .map(|edge| {
1090 let parsed_type = edge
1091 .edge_type_parsed
1092 .clone()
1093 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1094 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1095 relation: "unknown".to_string(),
1096 });
1097 let type_str = match parsed_type {
1098 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
1099 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
1100 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
1101 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
1102 };
1103 semantic_memory::discord::GraphEdgeRef {
1104 source: edge.source.clone(),
1105 target: edge.target.clone(),
1106 edge_type: type_str.to_string(),
1107 weight: edge.weight,
1108 }
1109 })
1110 .collect(),
1111 Err(e) => {
1112 return (
1113 "500 Internal Server Error",
1114 serde_json::json!({"ok": false, "error": format!("failed to load graph edges: {e}")}),
1115 )
1116 }
1117 };
1118
1119 let edges_loaded = edges.len();
1120 let scorer = DiscordScorer::with_defaults();
1121 let discord_hits = scorer.score(&direct_ids, &edges);
1122
1123 let existing: std::collections::HashSet<String> = direct_ids.iter().cloned().collect();
1125 let filtered_hits: Vec<serde_json::Value> = discord_hits
1126 .iter()
1127 .filter(|hit| !existing.contains(&hit.item_id))
1128 .map(|hit| {
1129 let bare_id = hit.item_id.strip_prefix("fact:").unwrap_or(&hit.item_id);
1131 let (content, namespace) = {
1132 let fact_result = handle.block_on(store.get_fact(bare_id));
1133 match fact_result {
1134 Ok(Some(fact)) => (fact.content, fact.namespace),
1135 _ => (String::new(), String::new()),
1136 }
1137 };
1138 serde_json::json!({
1139 "result_id": hit.item_id,
1140 "content": content,
1141 "namespace": namespace,
1142 "discord_score": hit.discord_score,
1143 "anchor_ids": hit.anchor_ids,
1144 "relationship_types": hit.relationship_types,
1145 })
1146 })
1147 .collect();
1148
1149 (
1150 "200 OK",
1151 serde_json::json!({
1152 "ok": true,
1153 "discord_results": filtered_hits,
1154 "count": filtered_hits.len(),
1155 "edges_loaded": edges_loaded,
1156 "direct_ids": direct_ids,
1157 }),
1158 )
1159}
1160
1161fn handle_maintenance_check(
1169 bridge: &MemoryBridge,
1170 handle: &Handle,
1171) -> (&'static str, serde_json::Value) {
1172 let store = &bridge.store;
1173 let embeddings_dirty = block_in_place(|| handle.block_on(store.embeddings_are_dirty()));
1174 let embeddings_dirty = match embeddings_dirty {
1175 Ok(v) => v,
1176 Err(e) => {
1177 return (
1178 "500 Internal Server Error",
1179 serde_json::json!({"ok": false, "error": format!("embeddings_are_dirty error: {e}")}),
1180 )
1181 }
1182 };
1183 let integrity_result = block_in_place(|| {
1184 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
1185 });
1186
1187 match integrity_result {
1188 Ok(report) => (
1189 "200 OK",
1190 serde_json::json!({
1191 "ok": report.ok,
1192 "embeddings_are_dirty": embeddings_dirty,
1193 "integrity": {
1194 "ok": report.ok,
1195 "schema_version": report.schema_version,
1196 "fact_count": report.fact_count,
1197 "chunk_count": report.chunk_count,
1198 "message_count": report.message_count,
1199 "facts_missing_embeddings": report.facts_missing_embeddings,
1200 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1201 "issues": report.issues,
1202 "issue_count": report.issues.len(),
1203 },
1204 "message": if report.ok && !embeddings_dirty {
1205 "All checks passed".to_string()
1206 } else if report.ok && embeddings_dirty {
1207 "Integrity OK but embeddings need re-embedding".to_string()
1208 } else {
1209 format!("{} integrity issues found", report.issues.len())
1210 },
1211 }),
1212 ),
1213 Err(e) => (
1214 "500 Internal Server Error",
1215 serde_json::json!({
1216 "ok": false,
1217 "embeddings_are_dirty": embeddings_dirty,
1218 "error": format!("verify_integrity error: {e}"),
1219 }),
1220 ),
1221 }
1222}
1223
1224fn handle_maintenance_vacuum(
1226 bridge: &MemoryBridge,
1227 handle: &Handle,
1228) -> (&'static str, serde_json::Value) {
1229 let store = &bridge.store;
1230 let result = block_in_place(|| handle.block_on(store.vacuum()));
1231
1232 match result {
1233 Ok(()) => (
1234 "200 OK",
1235 serde_json::json!({"ok": true, "action": "vacuum", "message": "Database vacuumed successfully"}),
1236 ),
1237 Err(e) => (
1238 "500 Internal Server Error",
1239 serde_json::json!({"ok": false, "error": format!("vacuum error: {e}")}),
1240 ),
1241 }
1242}
1243
1244fn handle_maintenance_reembed(
1247 bridge: &MemoryBridge,
1248 handle: &Handle,
1249) -> (&'static str, serde_json::Value) {
1250 let store = &bridge.store;
1251 let result = block_in_place(|| handle.block_on(store.reembed_all()));
1252
1253 match result {
1254 Ok(count) => (
1255 "200 OK",
1256 serde_json::json!({"ok": true, "action": "reembed", "reembedded_count": count, "message": format!("Re-embedded {count} items")}),
1257 ),
1258 Err(e) => (
1259 "500 Internal Server Error",
1260 serde_json::json!({"ok": false, "error": format!("reembed_all error: {e}")}),
1261 ),
1262 }
1263}
1264
1265fn handle_maintenance_reconcile(
1268 body: &str,
1269 bridge: &MemoryBridge,
1270 handle: &Handle,
1271) -> (&'static str, serde_json::Value) {
1272 let params: serde_json::Value = match serde_json::from_str(body) {
1273 Ok(v) => v,
1274 Err(e) => {
1275 return (
1276 "400 Bad Request",
1277 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1278 )
1279 }
1280 };
1281
1282 let action_str = params
1283 .get("action")
1284 .and_then(|v| v.as_str())
1285 .unwrap_or("ReportOnly");
1286
1287 let action = match action_str {
1288 "RebuildFts" => semantic_memory::ReconcileAction::RebuildFts,
1289 "ReEmbed" => semantic_memory::ReconcileAction::ReEmbed,
1290 _ => semantic_memory::ReconcileAction::ReportOnly,
1291 };
1292
1293 let store = &bridge.store;
1294 let result = block_in_place(|| handle.block_on(store.reconcile(action)));
1295
1296 match result {
1297 Ok(report) => (
1298 "200 OK",
1299 serde_json::json!({
1300 "ok": report.ok,
1301 "action": "reconcile",
1302 "reconcile_action": action_str,
1303 "integrity": {
1304 "ok": report.ok,
1305 "schema_version": report.schema_version,
1306 "fact_count": report.fact_count,
1307 "chunk_count": report.chunk_count,
1308 "message_count": report.message_count,
1309 "facts_missing_embeddings": report.facts_missing_embeddings,
1310 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1311 "issues": report.issues,
1312 "issue_count": report.issues.len(),
1313 },
1314 "message": if report.ok {
1315 "Reconciliation completed, no issues found".to_string()
1316 } else {
1317 format!("Reconciliation completed with {} issues", report.issues.len())
1318 },
1319 }),
1320 ),
1321 Err(e) => (
1322 "500 Internal Server Error",
1323 serde_json::json!({"ok": false, "error": format!("reconcile error: {e}")}),
1324 ),
1325 }
1326}
1327
1328fn handle_maintenance_compact_hnsw(
1333 bridge: &MemoryBridge,
1334 handle: &Handle,
1335) -> (&'static str, serde_json::Value) {
1336 #[cfg(feature = "hnsw")]
1337 {
1338 let store = &bridge.store;
1339 let result = block_in_place(|| handle.block_on(store.compact_hnsw()));
1340
1341 return match result {
1342 Ok(()) => (
1343 "200 OK",
1344 serde_json::json!({"ok": true, "action": "compact-hnsw", "message": "HNSW index compacted successfully"}),
1345 ),
1346 Err(e) => (
1347 "500 Internal Server Error",
1348 serde_json::json!({"ok": false, "error": format!("compact_hnsw error: {e}")}),
1349 ),
1350 };
1351 }
1352
1353 #[cfg(not(feature = "hnsw"))]
1354 {
1355 let _ = bridge;
1356 let _ = handle;
1357 (
1358 "200 OK",
1359 serde_json::json!({
1360 "ok": true,
1361 "action": "compact-hnsw",
1362 "message": "HNSW compaction not applicable — usearch backend does not require compaction",
1363 "skipped": true,
1364 }),
1365 )
1366 }
1367}
1368
1369fn handle_maintenance_auto_edge(
1379 body: &str,
1380 bridge: &MemoryBridge,
1381 handle: &Handle,
1382) -> (&'static str, serde_json::Value) {
1383 let start = std::time::Instant::now();
1384
1385 let params: serde_json::Value = serde_json::from_str(body).unwrap_or(serde_json::json!({}));
1386 let batch_size = params.get("batch_size").and_then(|v| v.as_u64()).unwrap_or(500) as usize;
1387 let max_edges_per_fact =
1388 params.get("max_edges_per_fact").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
1389 let dry_run = params.get("dry_run").and_then(|v| v.as_bool()).unwrap_or(false);
1390 let rebuild = params.get("rebuild").and_then(|v| v.as_bool()).unwrap_or(false);
1391
1392 let store = &bridge.store;
1393
1394 let mut edges_invalidated: u64 = 0;
1396 if rebuild && !dry_run {
1397 let all_edges = block_in_place(|| handle.block_on(store.list_all_graph_edges()));
1398 if let Ok(edges) = all_edges {
1399 for edge in &edges {
1400 let parsed = edge.edge_type_parsed.clone().or_else(|| {
1401 serde_json::from_str::<semantic_memory::GraphEdgeType>(&edge.edge_type).ok()
1402 });
1403 if matches!(parsed, Some(semantic_memory::GraphEdgeType::Entity { .. })) {
1404 let _ = block_in_place(|| {
1405 handle.block_on(store.invalidate_graph_edge(&edge.id, "auto-edge rebuild"))
1406 });
1407 edges_invalidated += 1;
1408 }
1409 }
1410 }
1411 eprintln!("[auto-edge] rebuild: invalidated {edges_invalidated} existing entity edges");
1412 }
1413
1414 const SKIP_NAMESPACES: &[&str] = &[
1416 "mixed", "chatgpt", "twitter", "tool-receipts", "agentguard",
1417 ];
1418 let skip_ns: std::collections::HashSet<&str> = SKIP_NAMESPACES.iter().copied().collect();
1419
1420 const STOPWORDS: &[&str] = &[
1422 "the", "and", "for", "are", "but", "not", "you", "all", "can", "her", "was", "one",
1423 "our", "out", "has", "have", "had", "his", "how", "its", "may", "new", "now", "old",
1424 "see", "him", "way", "who", "did", "yes", "yet", "say", "she", "too", "use", "via",
1425 "any", "few", "get", "got", "let", "put", "run", "set", "try", "two", "bad", "big",
1426 "far", "off", "own", "per", "sub", "top", "end", "add", "also", "been",
1427 "from", "into", "that", "this", "with", "will", "your", "they", "them", "were",
1428 "what", "when", "where", "which", "while", "there", "their", "about", "after",
1429 "before", "between", "because", "being", "would", "could", "should", "than",
1430 "then", "these", "those", "only", "over", "under", "again", "more", "most", "some",
1431 "such", "very", "just", "like", "even", "back", "both", "down", "here", "make",
1432 "made", "each", "want", "need", "know", "same", "other", "many", "much", "last",
1433 "first", "third", "next", "best", "main", "full", "upon", "within",
1434 "without", "through", "during", "above", "below", "against", "among",
1435 "across", "behind", "beside", "beyond",
1436 "project", "work", "thing", "things", "fix", "fixed", "build", "built", "code",
1438 "data", "system", "update", "updated", "check", "checked", "test", "tested",
1439 "error", "issue", "problem", "result", "results", "status", "state", "info",
1440 "note", "notes", "list", "item", "items", "type", "types", "field", "fields",
1441 "name", "names", "value", "values", "line", "lines", "file", "files",
1442 "part", "parts", "section", "sections", "step", "steps", "task", "tasks",
1443 "goal", "goals", "plan", "plans", "done", "open", "close", "closed",
1444 "start", "started", "stop", "stopped", "change", "changed", "changing",
1445 "good", "bad", "right", "wrong", "true", "false", "yes", "no", "ok",
1446 "lots", "lot", "really", "actually", "basically", "probably", "maybe",
1447 "going", "getting", "looking", "trying", "working", "something", "anything",
1448 "everything", "nothing", "someone", "anyone", "everyone",
1449 "still", "always", "never", "sometimes", "usually", "often",
1450 "since", "until", "though", "although", "however", "therefore",
1451 "either", "neither", "both", "all", "any", "some", "none",
1452 "version", "config", "setup", "install", "installed", "running",
1453 "report", "reports", "summary", "detail", "details",
1454 "feature", "features", "function", "functions", "method", "methods",
1455 "class", "classes", "module", "modules", "crate", "crates",
1456 "package", "packages", "library", "libraries", "app", "apps",
1457 "web", "page", "pages", "site", "sites", "link", "links",
1458 "post", "posts", "reply", "replies", "comment", "comments",
1459 "read", "write", "call", "called", "calling", "return", "returns",
1460 "input", "output", "source", "target", "source_id", "target_id",
1461 "create", "created", "creating", "delete", "deleted", "removing",
1462 "find", "found", "finding", "search", "searching", "replace", "replaced",
1463 "show", "shown", "showing", "hide", "hidden", "display", "rendered",
1464 "enable", "enabled", "disable", "disabled", "allow", "allowed",
1465 "require", "required", "requiring", "support", "supported",
1466 "default", "custom", "general", "specific", "standard",
1467 "current", "latest", "previous", "old", "new", "future",
1468 "high", "low", "medium", "critical", "normal", "minor", "major",
1469 "single", "multiple", "total", "count", "number",
1470 "description", "summary", "overview", "introduction", "conclusion",
1471 "todo", "fixme", "wip", "draft", "final", "complete", "completed",
1472 "include", "includes", "included", "exclude", "excludes", "excluded",
1473 "require", "requires", "required", "optional",
1474 "important", "urgent", "priority", "blocking",
1475 "question", "answer", "response", "request",
1476 ];
1477 let stopword_set: std::collections::HashSet<&str> = STOPWORDS.iter().copied().collect();
1478
1479 fn extract_terms(text: &str, stopwords: &std::collections::HashSet<&str>) -> std::collections::HashSet<String> {
1483 let mut terms = std::collections::HashSet::new();
1484 for word in text.split(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') {
1485 let w = word.trim_matches(|c: char| c == '-' || c == '_');
1486 if w.len() < 3 {
1487 continue;
1488 }
1489 let lower = w.to_lowercase();
1490 if stopwords.contains(lower.as_str()) {
1491 continue;
1492 }
1493 if lower.chars().all(|c| c.is_ascii_digit()) {
1495 continue;
1496 }
1497 let has_separator = w.contains('_') || w.contains('-');
1502 let has_upper = w.chars().any(|c| c.is_uppercase());
1503 let starts_upper = w.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
1504 let long_enough = lower.len() >= 5;
1505
1506 if has_separator || has_upper || starts_upper || long_enough {
1507 terms.insert(lower);
1508 }
1509 }
1510 terms
1511 }
1512
1513 let namespaces = match block_in_place(|| handle.block_on(store.list_fact_namespaces())) {
1515 Ok(ns) => ns,
1516 Err(e) => {
1517 return (
1518 "500 Internal Server Error",
1519 serde_json::json!({"ok": false, "error": format!("list_fact_namespaces error: {e}")}),
1520 )
1521 }
1522 };
1523
1524 let namespaces: Vec<String> = namespaces
1526 .into_iter()
1527 .filter(|ns| !skip_ns.contains(ns.as_str()))
1528 .collect();
1529
1530 struct FactInfo {
1532 id: String,
1533 namespace: String,
1534 terms: std::collections::HashSet<String>,
1535 }
1536
1537 let mut all_facts: Vec<FactInfo> = Vec::new();
1538 for ns in &namespaces {
1539 let mut offset = 0usize;
1540 loop {
1541 let batch = match block_in_place(|| {
1542 handle.block_on(store.list_facts(ns, batch_size, offset))
1543 }) {
1544 Ok(facts) => facts,
1545 Err(e) => {
1546 eprintln!("[auto-edge] list_facts error for ns={ns} offset={offset}: {e}");
1547 break;
1548 }
1549 };
1550 if batch.is_empty() {
1551 break;
1552 }
1553 for fact in &batch {
1554 let terms = extract_terms(&fact.content, &stopword_set);
1555 if terms.len() >= 3 {
1557 all_facts.push(FactInfo {
1558 id: fact.id.clone(),
1559 namespace: fact.namespace.clone(),
1560 terms,
1561 });
1562 }
1563 }
1564 offset += batch.len();
1565 if batch.len() < batch_size {
1566 break;
1567 }
1568 }
1569 }
1570
1571 let facts_processed = all_facts.len();
1572
1573 let existing_edges: std::collections::HashSet<(String, String)> =
1575 match block_in_place(|| handle.block_on(store.list_all_graph_edges())) {
1576 Ok(edges) => edges
1577 .iter()
1578 .filter_map(|e| {
1579 let parsed = e.edge_type_parsed.clone().or_else(|| {
1580 serde_json::from_str::<semantic_memory::GraphEdgeType>(&e.edge_type)
1581 .ok()
1582 });
1583 match parsed {
1584 Some(semantic_memory::GraphEdgeType::Entity { .. }) => {
1585 Some((e.source.clone(), e.target.clone()))
1586 }
1587 _ => None,
1588 }
1589 })
1590 .collect(),
1591 Err(_) => std::collections::HashSet::new(),
1592 };
1593
1594 let mut edges_created: u64 = 0;
1597 let mut edges_skipped: u64 = 0;
1598 let mut edge_counts: std::collections::HashMap<String, u64> =
1599 std::collections::HashMap::new();
1600
1601 for i in 0..all_facts.len() {
1602 let fact_i = &all_facts[i];
1603 let src_id = format!("fact:{}", fact_i.id);
1604
1605 if *edge_counts.get(&src_id).unwrap_or(&0) >= max_edges_per_fact as u64 {
1607 continue;
1608 }
1609
1610 for j in (i + 1)..all_facts.len() {
1611 let fact_j = &all_facts[j];
1612
1613 if fact_i.namespace == fact_j.namespace {
1615 continue;
1616 }
1617
1618 let shared: usize = fact_i.terms.intersection(&fact_j.terms).count();
1620 if shared < 3 {
1621 continue;
1622 }
1623
1624 let tgt_id = format!("fact:{}", fact_j.id);
1625
1626 let (edge_source, edge_target) = if src_id <= tgt_id {
1628 (src_id.clone(), tgt_id.clone())
1629 } else {
1630 (tgt_id.clone(), src_id.clone())
1631 };
1632
1633 if existing_edges.contains(&(edge_source.clone(), edge_target.clone())) {
1635 edges_skipped += 1;
1636 continue;
1637 }
1638
1639 let count_src = *edge_counts.get(&src_id).unwrap_or(&0);
1641 let count_tgt = *edge_counts.get(&tgt_id).unwrap_or(&0);
1642 if count_src >= max_edges_per_fact as u64
1643 || count_tgt >= max_edges_per_fact as u64
1644 {
1645 continue;
1646 }
1647
1648 if dry_run {
1649 edges_created += 1;
1650 continue;
1651 }
1652
1653 let relation = format!("shared_terms:{}", shared);
1654 let edge_type = semantic_memory::GraphEdgeType::Entity {
1655 relation: relation.clone(),
1656 };
1657 let weight = (shared.min(10) as f64) / 10.0;
1659
1660 let result = block_in_place(|| {
1661 handle.block_on(store.add_graph_edge(
1662 &edge_source,
1663 &edge_target,
1664 edge_type,
1665 weight,
1666 None,
1667 ))
1668 });
1669
1670 match result {
1671 Ok(_) => {
1672 edges_created += 1;
1673 *edge_counts.entry(src_id.clone()).or_insert(0) += 1;
1674 *edge_counts.entry(tgt_id.clone()).or_insert(0) += 1;
1675 }
1676 Err(e) => {
1677 eprintln!(
1678 "[auto-edge] add_graph_edge error: {e} for {edge_source} -> {edge_target}"
1679 );
1680 }
1681 }
1682
1683 if *edge_counts.get(&src_id).unwrap_or(&0) >= max_edges_per_fact as u64 {
1685 break;
1686 }
1687 }
1688 }
1689
1690 let elapsed = start.elapsed();
1691
1692 (
1693 "200 OK",
1694 serde_json::json!({
1695 "ok": true,
1696 "action": "auto-edge",
1697 "dry_run": dry_run,
1698 "facts_processed": facts_processed,
1699 "namespaces_scanned": namespaces.len(),
1700 "edges_created": edges_created,
1701 "edges_skipped": edges_skipped,
1702 "max_edges_per_fact": max_edges_per_fact,
1703 "time_elapsed_ms": elapsed.as_millis(),
1704 "time_elapsed_secs": (elapsed.as_millis() as f64) / 1000.0,
1705 }),
1706 )
1707}