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, auth_token: &str, bridge: MemoryBridge, handle: Handle) {
73 let auth_token = auth_token.to_string();
74 std::thread::spawn(move || {
75 let listener = match TcpListener::bind(("127.0.0.1", port)) {
76 Ok(l) => {
77 eprintln!("HTTP search server listening on 127.0.0.1:{}", port);
78 l
79 }
80 Err(e) => {
81 eprintln!("Failed to bind HTTP port {}: {}", port, e);
82 return;
83 }
84 };
85
86 for stream in listener.incoming() {
87 let stream = match stream {
88 Ok(s) => s,
89 Err(_) => continue,
90 };
91
92 let bridge = bridge.clone();
93 let h = handle.clone();
94 let token = auth_token.clone();
95 std::thread::spawn(move || {
96 handle_connection(stream, &token, bridge, h);
97 });
98 }
99 });
100}
101
102fn handle_connection(
103 mut stream: std::net::TcpStream,
104 auth_token: &str,
105 bridge: MemoryBridge,
106 handle: Handle,
107) {
108 let mut reader = BufReader::new(stream.try_clone().expect("clone"));
109 let mut request_line = String::new();
110 if reader.read_line(&mut request_line).is_err() {
111 return;
112 }
113
114 let parts: Vec<&str> = request_line.split_whitespace().collect();
115 if parts.len() < 2 {
116 return;
117 }
118 let method = parts[0];
119 let path = parts[1];
120
121 let mut content_length = 0;
122 let mut auth_header: Option<String> = None;
123 let mut host_header: Option<String> = None;
124 loop {
125 let mut header = String::new();
126 if reader.read_line(&mut header).is_err() {
127 return;
128 }
129 if header.trim().is_empty() {
130 break;
131 }
132 if let Some(len_str) = header
133 .strip_prefix("Content-Length:")
134 .or_else(|| header.strip_prefix("content-length:"))
135 {
136 content_length = len_str.trim().parse().unwrap_or(0);
137 }
138 if let Some(val) = header
139 .strip_prefix("Authorization:")
140 .or_else(|| header.strip_prefix("authorization:"))
141 {
142 auth_header = Some(val.trim().to_string());
143 }
144 if let Some(val) = header
145 .strip_prefix("Host:")
146 .or_else(|| header.strip_prefix("host:"))
147 {
148 host_header = Some(val.trim().to_string());
149 }
150 }
151
152 let authorized = auth_header
154 .as_deref()
155 .map(|h| h == format!("Bearer {}", auth_token))
156 .unwrap_or(false);
157 if !authorized {
158 let response =
159 "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
160 let _ = stream.write_all(response.as_bytes());
161 return;
162 }
163
164 let host_ok = host_header
166 .as_deref()
167 .map(|h| h.starts_with("127.0.0.1") || h.starts_with("localhost"))
168 .unwrap_or(false);
169 if !host_ok {
170 let response = "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
171 let _ = stream.write_all(response.as_bytes());
172 return;
173 }
174
175 const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
177 if content_length > MAX_BODY_SIZE {
178 let response =
179 "HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
180 let _ = stream.write_all(response.as_bytes());
181 return;
182 }
183
184 let mut body = vec![0u8; content_length];
185 if content_length > 0 && reader.read_exact(&mut body).is_err() {
186 return;
187 }
188 let body_str = String::from_utf8_lossy(&body);
189
190 let (status, response) = match (method, path) {
191 ("GET", "/health") => (
192 "200 OK",
193 serde_json::json!({"ok": true, "service": "semantic-memory-mcp"}),
194 ),
195 ("POST", "/search") => handle_search(&body_str, &bridge, &handle),
196 ("POST", "/search-routed") => handle_search_routed(&body_str, &bridge, &handle),
197 ("POST", "/rerank") => handle_rerank(&body_str),
198 ("POST", "/stats") => handle_stats(&bridge, &handle),
199 ("POST", "/add") => handle_add_fact(&body_str, &bridge, &handle),
200 ("POST", "/record-outcome") => handle_record_outcome(&body_str, &bridge, &handle),
201 ("GET", "/verify-integrity") => handle_verify_integrity(&bridge, &handle),
202 ("POST", "/discord") => handle_discord(&body_str, &bridge, &handle),
203 ("POST", "/maintenance/check") => handle_maintenance_check(&bridge, &handle),
204 ("POST", "/maintenance/vacuum") => handle_maintenance_vacuum(&bridge, &handle),
205 ("POST", "/maintenance/reembed") => handle_maintenance_reembed(&bridge, &handle),
206 ("POST", "/maintenance/reconcile") => {
207 handle_maintenance_reconcile(&body_str, &bridge, &handle)
208 }
209 ("POST", "/maintenance/rebuild-hnsw") => handle_maintenance_rebuild_hnsw(&bridge, &handle),
210 ("POST", "/maintenance/compact-hnsw") => handle_maintenance_compact_hnsw(&bridge, &handle),
211 _ => (
212 "404 Not Found",
213 serde_json::json!({"error": "not found", "path": path}),
214 ),
215 };
216
217 let response_str = serde_json::to_string(&response).unwrap_or_default();
218 let response_bytes = response_str.as_bytes();
219 let http_response = format!(
220 "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
221 status,
222 response_bytes.len()
223 );
224
225 let _ = stream.write_all(http_response.as_bytes());
226 let _ = stream.write_all(response_bytes);
227 let _ = stream.flush();
228}
229
230fn handle_search(
231 body: &str,
232 bridge: &MemoryBridge,
233 handle: &Handle,
234) -> (&'static str, serde_json::Value) {
235 let params: serde_json::Value = match serde_json::from_str(body) {
236 Ok(v) => v,
237 Err(e) => {
238 return (
239 "400 Bad Request",
240 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
241 )
242 }
243 };
244
245 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
246 let top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
247 let namespaces: Option<Vec<String>> = params
248 .get("namespaces")
249 .and_then(|v| serde_json::from_value(v.clone()).ok());
250 let do_rerank = params
251 .get("rerank")
252 .and_then(|v| v.as_bool())
253 .unwrap_or(false);
254
255 if query.is_empty() {
256 return (
257 "400 Bad Request",
258 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
259 );
260 }
261
262 let store = &bridge.store;
263 let ns_slice: Option<Vec<&str>> = namespaces
264 .as_ref()
265 .map(|v| v.iter().map(|s| s.as_str()).collect());
266 let fetch_k = if do_rerank { top_k * 2 } else { top_k };
268 let result = block_in_place(|| {
269 handle.block_on(store.search(query, Some(fetch_k), ns_slice.as_deref(), None))
270 });
271
272 match result {
273 Ok(results) => {
274 let json_results: Vec<serde_json::Value> = results
275 .iter()
276 .map(|r| {
277 let namespace = match &r.source {
278 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
279 semantic_memory::SearchSource::Chunk { document_title, .. } => {
280 document_title.clone()
281 }
282 _ => String::new(),
283 };
284 serde_json::json!({
285 "result_id": r.source.result_id(),
286 "content": r.content,
287 "score": r.score,
288 "cosine_similarity": r.cosine_similarity,
289 "namespace": namespace,
290 })
291 })
292 .collect();
293
294 let final_results: Vec<serde_json::Value> = if do_rerank && !json_results.is_empty() {
295 rerank_results(query, &json_results, "granite4.1:3b")
296 .into_iter()
297 .take(top_k)
298 .collect()
299 } else {
300 json_results
301 };
302
303 let count = final_results.len();
304 let provenance = serde_json::json!({
305 "stages_fired": {
306 "bm25": true,
307 "vector": true,
308 "late_interaction": false,
309 "rerank": do_rerank,
310 },
311 "result_count": count,
312 "view": "semantic",
313 "widening_occurred": null, "widening_reason": null,
315 "verification_status": "verified",
316 });
317 (
318 "200 OK",
319 serde_json::json!({
320 "ok": true,
321 "query": query,
322 "top_k": top_k,
323 "results": final_results,
324 "count": count,
325 "reranked": do_rerank,
326 "provenance": provenance,
327 }),
328 )
329 }
330 Err(e) => (
331 "500 Internal Server Error",
332 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
333 ),
334 }
335}
336
337fn handle_search_routed(
345 body: &str,
346 bridge: &MemoryBridge,
347 handle: &Handle,
348) -> (&'static str, serde_json::Value) {
349 use semantic_memory::integration::plan_execution;
350 use semantic_memory::rl_routing::{is_trained, route_with_policy};
351 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
352
353 let params: serde_json::Value = match serde_json::from_str(body) {
354 Ok(v) => v,
355 Err(e) => {
356 return (
357 "400 Bad Request",
358 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
359 )
360 }
361 };
362
363 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
364 let base_top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(12) as usize;
365 let query_class = params
366 .get("query_class")
367 .and_then(|v| v.as_str())
368 .unwrap_or("A");
369 let namespaces: Option<Vec<String>> = params
370 .get("namespaces")
371 .and_then(|v| serde_json::from_value(v.clone()).ok());
372 let contradictions: Vec<(String, String)> = params
373 .get("contradictions")
374 .and_then(|v| serde_json::from_value(v.clone()).ok())
375 .unwrap_or_default();
376 let group_by_community = params
377 .get("group_by_community")
378 .and_then(|v| v.as_bool())
379 .unwrap_or(false);
380
381 if query.is_empty() {
382 return (
383 "400 Bad Request",
384 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
385 );
386 }
387
388 let router = RetrievalRouter {
390 decoder_enabled: true,
391 discord_enabled: true,
392 corpus_density: 0.5,
393 ..Default::default()
394 };
395 let store = &bridge.store;
396 let policy = match block_in_place(|| handle.block_on(store.load_routing_policy())) {
397 Ok(policy) => policy,
398 Err(e) => {
399 return (
400 "500 Internal Server Error",
401 serde_json::json!({"ok": false, "error": format!("load routing policy error: {e}")}),
402 )
403 }
404 };
405 let profile = QueryProfile::from_query(query);
406 let (decision, routing_source) = match policy.as_ref().filter(|p| is_trained(p)) {
407 Some(policy) => (route_with_policy(policy, &profile), "trained_policy"),
408 None => (router.route(&profile), "heuristic"),
409 };
410 let contras = contradictions.clone();
411 let plan = plan_execution(&decision, contras.clone());
412
413 let top_k = if query_class == "D" {
415 (base_top_k * 2).min(20)
416 } else {
417 base_top_k
418 };
419
420 let ns_slice: Option<Vec<&str>> = namespaces
421 .as_ref()
422 .map(|v| v.iter().map(|s| s.as_str()).collect());
423
424 let result = if query_class == "C" {
426 use semantic_memory::{ExactnessProfile, SearchContext};
427 let mut ctx = SearchContext::default_now();
428 ctx.exactness_profile = ExactnessProfile::PreferExact;
429 block_in_place(|| {
430 handle.block_on(store.search_with_context(
431 query,
432 Some(top_k),
433 ns_slice.as_deref(),
434 None,
435 ctx,
436 ))
437 })
438 .map(|r| r.results)
439 } else {
440 block_in_place(|| {
441 handle.block_on(store.search(query, Some(top_k), ns_slice.as_deref(), None))
442 })
443 };
444
445 match result {
446 Ok(results) => {
447 let json_results: Vec<serde_json::Value> = results
448 .iter()
449 .map(|r| {
450 let namespace = match &r.source {
451 semantic_memory::SearchSource::Fact { namespace, .. } => namespace.clone(),
452 semantic_memory::SearchSource::Chunk { document_title, .. } => {
453 document_title.clone()
454 }
455 _ => String::new(),
456 };
457 serde_json::json!({
458 "result_id": r.source.result_id(),
459 "content": r.content,
460 "score": r.score,
461 "cosine_similarity": r.cosine_similarity,
462 "namespace": namespace,
463 "source_type": match &r.source {
464 semantic_memory::SearchSource::Fact { .. } => "fact",
465 semantic_memory::SearchSource::Chunk { .. } => "chunk",
466 semantic_memory::SearchSource::Message { .. } => "message",
467 _ => "unknown",
468 },
469 })
470 })
471 .collect();
472
473 let mut factor_graph_payload = serde_json::json!({"enabled": false});
474 let mut decoder_executed = false;
475 let mut discord_executed = false;
476 let mut discord_results_payload: Vec<serde_json::Value> = Vec::new();
477
478 if decision.decoder {
480 #[cfg(feature = "full")]
481 {
482 use semantic_memory::factor_graph::{
483 factors_from_edges, FactorGraph, FactorGraphConfig,
484 };
485
486 let graph_edges =
487 block_in_place(|| handle.block_on(store.list_all_graph_edges()));
488
489 if let Ok(edges) = graph_edges {
490 let raw_edges: Vec<(
491 String,
492 String,
493 semantic_memory::GraphEdgeType,
494 f64,
495 Option<String>,
496 )> = edges
497 .iter()
498 .map(|edge| {
499 let parsed_type = edge
500 .edge_type_parsed
501 .clone()
502 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
503 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
504 relation: "unknown".to_string(),
505 });
506 (
507 edge.source.clone(),
508 edge.target.clone(),
509 parsed_type,
510 edge.weight,
511 edge.metadata.clone(),
512 )
513 })
514 .collect();
515
516 let nodes: Vec<(String, f64)> = results
517 .iter()
518 .map(|r| (r.source.result_id(), r.score))
519 .collect();
520 let factors = factors_from_edges(&raw_edges);
521 let graph = FactorGraph::new(&nodes, factors, FactorGraphConfig::default());
522 let propagated = graph.propagate();
523 let top_beliefs = propagated.top_k(top_k);
524
525 factor_graph_payload = serde_json::json!({
526 "enabled": true,
527 "top_k_beliefs": top_beliefs
528 .into_iter()
529 .map(|(item_id, belief)| serde_json::json!({
530 "item_id": item_id,
531 "belief": belief,
532 }))
533 .collect::<Vec<_>>(),
534 "iterations": propagated.iterations,
535 "converged": propagated.converged,
536 "elapsed_ms": propagated.elapsed_ms,
537 "factor_counts": {
538 "semantic": propagated.factor_counts.semantic,
539 "temporal": propagated.factor_counts.temporal,
540 "causal": propagated.factor_counts.causal,
541 "entity": propagated.factor_counts.entity,
542 "total": propagated.factor_counts.total(),
543 },
544 });
545 decoder_executed = true;
546 }
547 }
548
549 if !plan.contradictions.is_empty() {
551 use semantic_memory::decoder::{compute_correction, detect_syndromes};
552 let result_scores: Vec<(String, f64)> = results
553 .iter()
554 .map(|r| (r.source.result_id(), r.score))
555 .collect();
556 let syndromes = detect_syndromes(&result_scores, &plan.contradictions);
557 let _ = compute_correction(&syndromes, 10.0);
558 decoder_executed = true;
559 }
560 }
561
562 if plan.use_discord {
564 use semantic_memory::discord::DiscordScorer;
565 let direct_ids: Vec<String> =
566 results.iter().map(|r| r.source.result_id()).collect();
567 let existing_ids: std::collections::HashSet<String> =
568 direct_ids.iter().cloned().collect();
569 let edges_result = block_in_place(|| {
570 handle.block_on(store.list_graph_edges_for_neighborhood(
571 direct_ids.clone(),
572 2,
573 200,
574 ))
575 });
576 if let Ok(raw_edges) = edges_result {
577 let edge_refs: Vec<semantic_memory::discord::GraphEdgeRef> = raw_edges
578 .iter()
579 .map(|edge| {
580 let parsed_type = edge
581 .edge_type_parsed
582 .clone()
583 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
584 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
585 relation: "unknown".to_string(),
586 });
587 let type_str = match parsed_type {
588 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
589 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
590 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
591 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
592 };
593 semantic_memory::discord::GraphEdgeRef {
594 source: edge.source.clone(),
595 target: edge.target.clone(),
596 edge_type: type_str.to_string(),
597 weight: edge.weight,
598 }
599 })
600 .collect();
601 let scorer = DiscordScorer::with_defaults();
602 let discord_hits = scorer.score(&direct_ids, &edge_refs);
603 for hit in &discord_hits {
604 if !existing_ids.contains(&hit.item_id) {
605 discord_results_payload.push(serde_json::json!({
606 "result_id": hit.item_id,
607 "discord_score": hit.discord_score,
608 "anchor_ids": hit.anchor_ids,
609 "relationship_types": hit.relationship_types,
610 }));
611 }
612 }
613 discord_executed = true;
614 }
615 }
616
617 let grouped_results_payload: serde_json::Value = if group_by_community {
619 let seed_ids: Vec<String> = results.iter().map(|r| r.source.result_id()).collect();
620 let edges_result = block_in_place(|| {
621 handle.block_on(store.list_graph_edges_for_neighborhood(
622 seed_ids.clone(),
623 2,
624 200,
625 ))
626 });
627 let edges: Vec<(String, String)> = match edges_result {
628 Ok(raw_edges) => raw_edges
629 .iter()
630 .map(|edge| (edge.source.clone(), edge.target.clone()))
631 .collect(),
632 Err(_) => Vec::new(),
633 };
634 if !edges.is_empty() {
635 use semantic_memory::community::detect_communities;
636 let communities = detect_communities(&edges, 1.0, 42);
637 let mut member_to_comm: std::collections::HashMap<String, String> =
638 std::collections::HashMap::new();
639 for c in &communities {
640 for m in &c.members {
641 member_to_comm.insert(m.clone(), c.id.clone());
642 }
643 }
644 let mut groups: std::collections::HashMap<String, Vec<serde_json::Value>> =
645 std::collections::HashMap::new();
646 let mut ungrouped: Vec<serde_json::Value> = Vec::new();
647 for r in &json_results {
648 if let Some(rid) = r.get("result_id").and_then(|v| v.as_str()) {
649 match member_to_comm.get(rid).cloned() {
650 Some(cid) => groups.entry(cid).or_default().push(r.clone()),
651 None => ungrouped.push(r.clone()),
652 }
653 }
654 }
655 let mut map = serde_json::Map::new();
656 for (cid, items) in groups {
657 map.insert(format!("community_{cid}"), serde_json::json!(items));
658 }
659 if !ungrouped.is_empty() {
660 map.insert("ungrouped".to_string(), serde_json::json!(ungrouped));
661 }
662 serde_json::Value::Object(map)
663 } else {
664 serde_json::Value::Null
665 }
666 } else {
667 serde_json::Value::Null
668 };
669
670 let provenance = serde_json::json!({
672 "stages_fired": {
673 "bm25": results.iter().any(|r| r.bm25_rank.is_some()),
674 "vector": results.iter().any(|r| r.vector_rank.is_some()),
675 "late_interaction": true,
676 "discord": discord_executed,
677 "decoder": decoder_executed,
678 },
679 "result_count": results.len(),
680 "view": "routed",
681 "query_class": query_class,
682 "widening_occurred": null, "widening_reason": null,
684 "verification_status": "verified",
685 });
686
687 (
688 "200 OK",
689 serde_json::json!({
690 "ok": true,
691 "query": query,
692 "top_k": base_top_k,
693 "results": json_results,
694 "provenance": provenance,
695 "query_class": query_class,
696 "routed": true,
697 "routing_decision": {
698 "source": routing_source,
699 "bm25_coarse": decision.bm25_coarse,
700 "vector_medium": decision.vector_medium,
701 "rerank_fine": decision.rerank_fine,
702 "graph_expansion": decision.graph_expansion,
703 "decoder": decision.decoder,
704 "discord": decision.discord,
705 "no_retrieval": decision.no_retrieval,
706 "reasoning": decision.reasoning,
707 },
708 "decoder_planned": plan.use_decoder,
709 "decoder_executed": decoder_executed,
710 "discord_planned": plan.use_discord,
711 "discord_executed": discord_executed,
712 "discord_results": discord_results_payload,
713 "factor_graph": factor_graph_payload,
714 "grouped_results": grouped_results_payload,
715 }),
716 )
717 }
718 Err(e) => (
719 "500 Internal Server Error",
720 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
721 ),
722 }
723}
724
725fn handle_stats(bridge: &MemoryBridge, handle: &Handle) -> (&'static str, serde_json::Value) {
726 let store = &bridge.store;
727 let core = block_in_place(|| handle.block_on(store.stats()));
728 let graph = block_in_place(|| handle.block_on(store.list_all_graph_edges()));
729 let core_health = match &core {
730 Ok(_) => serde_json::json!({"health":"healthy","error":null}),
731 Err(e) => serde_json::json!({"health":"error","error":e.to_string()}),
732 };
733 let graph_health = match &graph {
734 Ok(_) => serde_json::json!({"health":"healthy","error":null}),
735 Err(e) => serde_json::json!({"health":"error","error":e.to_string()}),
736 };
737 let stats = core.ok();
738 let graph_edges = graph.ok().map(|edges| edges.len());
739 let ok = stats.is_some() && graph_edges.is_some();
740 (
741 if ok {
742 "200 OK"
743 } else {
744 "503 Service Unavailable"
745 },
746 serde_json::json!({
747 "ok": ok,
748 "components": {"core": core_health, "graph": graph_health},
749 "facts": stats.as_ref().map(|s| s.total_facts),
750 "documents": stats.as_ref().map(|s| s.total_documents),
751 "chunks": stats.as_ref().map(|s| s.total_chunks),
752 "graph_edges": graph_edges,
753 "db_size_mb": stats.as_ref().map(|s| (s.database_size_bytes as f64) / (1024.0 * 1024.0)),
754 }),
755 )
756}
757
758fn handle_rerank(body: &str) -> (&'static str, serde_json::Value) {
759 let params: serde_json::Value = match serde_json::from_str(body) {
760 Ok(v) => v,
761 Err(e) => {
762 return (
763 "400 Bad Request",
764 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
765 )
766 }
767 };
768
769 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
770 let model = params
771 .get("model")
772 .and_then(|v| v.as_str())
773 .unwrap_or("granite4.1:3b");
774 let results = match params.get("results").and_then(|v| v.as_array()) {
775 Some(r) => r.clone(),
776 None => {
777 return (
778 "400 Bad Request",
779 serde_json::json!({"ok": false, "error": "missing 'results' array"}),
780 )
781 }
782 };
783
784 if query.is_empty() {
785 return (
786 "400 Bad Request",
787 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
788 );
789 }
790
791 let reranked = rerank_results(query, &results, model);
792 let count = reranked.len();
793 (
794 "200 OK",
795 serde_json::json!({
796 "ok": true,
797 "results": reranked,
798 "count": count,
799 }),
800 )
801}
802
803fn handle_add_fact(
804 body: &str,
805 bridge: &MemoryBridge,
806 handle: &Handle,
807) -> (&'static str, serde_json::Value) {
808 let params: serde_json::Value = match serde_json::from_str(body) {
809 Ok(v) => v,
810 Err(e) => {
811 return (
812 "400 Bad Request",
813 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
814 )
815 }
816 };
817
818 let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
819 let namespace = params
820 .get("namespace")
821 .and_then(|v| v.as_str())
822 .unwrap_or("general");
823 let source = params.get("source").and_then(|v| v.as_str());
824 let idempotency_key = params
825 .get("idempotency_key")
826 .and_then(|v| v.as_str())
827 .filter(|v| !v.trim().is_empty())
828 .map(str::to_owned)
829 .unwrap_or_else(|| format!("http-add:{}", uuid::Uuid::new_v4()));
830
831 if content.is_empty() {
832 return (
833 "400 Bad Request",
834 serde_json::json!({"ok": false, "error": "missing 'content' field"}),
835 );
836 }
837
838 let store = &bridge.store;
839 let source_ref = source.filter(|value| !value.trim().is_empty());
840 let origin = match source_ref {
841 Some(value) => semantic_memory::OriginAuthorityLabelV1::new(
842 semantic_memory::OriginClassV1::ExternalEvidence,
843 "principal:semantic-memory-http",
844 "caller:http-add",
845 format!("blake3:{}", blake3::hash(value.as_bytes()).to_hex()),
846 semantic_memory::OriginRiskV1::Medium,
847 semantic_memory::AuthorityScopesV1 {
848 recall: semantic_memory::AuthorityScopeV1::Universal,
849 assertion: semantic_memory::AuthorityScopeV1::Denied,
850 action: semantic_memory::AuthorityScopeV1::Denied,
851 },
852 semantic_memory::ElevationRequirementV1::ExplicitOperatorApproval,
853 None,
854 semantic_memory::RevocationStatusV1::Active,
855 vec!["principal:semantic-memory-http".into()],
856 )
857 .expect("HTTP external-evidence origin constants are valid"),
858 None => semantic_memory::OriginAuthorityLabelV1::operator_system(
859 "principal:semantic-memory-http",
860 "caller:http-add",
861 ),
862 };
863 let permit = match source_ref {
864 Some(source_ref) => semantic_memory::AuthorityPermit::with_evidence(
865 "principal:semantic-memory-http",
866 "caller:http-add",
867 semantic_memory::AuthorityPermit::APPEND_CAPABILITY,
868 vec![source_ref.to_owned()],
869 ),
870 None => semantic_memory::AuthorityPermit::operator_system(
871 "principal:semantic-memory-http",
872 "caller:http-add",
873 semantic_memory::AuthorityPermit::APPEND_CAPABILITY,
874 ),
875 }
876 .with_origin(origin);
877 let result = block_in_place(|| {
878 handle.block_on(store.authority().append(
879 permit,
880 idempotency_key,
881 namespace.to_owned(),
882 content.to_owned(),
883 source.map(str::to_owned),
884 ))
885 });
886
887 match result {
888 Ok(receipt) => {
889 let fact_id = receipt.affected_ids.first().cloned();
890 (
891 "200 OK",
892 serde_json::json!({"ok": true, "fact_id": fact_id, "authority_receipt": receipt}),
893 )
894 }
895 Err(e) => (
896 "500 Internal Server Error",
897 serde_json::json!({"ok": false, "error": format!("{e}")}),
898 ),
899 }
900}
901
902fn handle_record_outcome(
904 body: &str,
905 bridge: &MemoryBridge,
906 handle: &Handle,
907) -> (&'static str, serde_json::Value) {
908 use semantic_memory::rl_routing::{record_routing_outcome, RoutingOutcome};
909 use semantic_memory::routing::{QueryProfile, RetrievalRouter};
910
911 let params: serde_json::Value = match serde_json::from_str(body) {
912 Ok(v) => v,
913 Err(e) => {
914 return (
915 "400 Bad Request",
916 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
917 )
918 }
919 };
920
921 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
922 let outcome = params
923 .get("outcome")
924 .and_then(|v| v.as_str())
925 .unwrap_or("neutral");
926 let _query_class = params
927 .get("query_class")
928 .and_then(|v| v.as_str())
929 .unwrap_or("A");
930
931 if query.is_empty() {
932 return (
933 "400 Bad Request",
934 serde_json::json!({"ok": false, "error": "missing 'query' field"}),
935 );
936 }
937
938 let outcome_enum = match outcome.to_lowercase().as_str() {
939 "good" => RoutingOutcome::Good,
940 "bad" => RoutingOutcome::Bad,
941 "neutral" => RoutingOutcome::Neutral,
942 _ => {
943 return (
944 "400 Bad Request",
945 serde_json::json!({"ok": false, "error": "outcome must be 'good', 'bad', or 'neutral'"}),
946 )
947 }
948 };
949
950 let profile = QueryProfile::from_query(query);
951 let router = RetrievalRouter::default();
952 let decision = router.route(&profile);
953
954 let store = &bridge.store;
955 let mut policy = match block_in_place(|| handle.block_on(store.load_routing_policy())) {
957 Ok(policy) => policy.unwrap_or_default(),
958 Err(e) => {
959 return (
960 "500 Internal Server Error",
961 serde_json::json!({"ok": false, "error": format!("load routing policy error: {e}")}),
962 )
963 }
964 };
965 record_routing_outcome(&mut policy, &profile, &decision, outcome_enum);
966 if let Err(e) = block_in_place(|| handle.block_on(store.save_routing_policy(&policy))) {
968 return (
969 "500 Internal Server Error",
970 serde_json::json!({"ok": false, "error": format!("persist routing policy error: {e}")}),
971 );
972 }
973
974 (
975 "200 OK",
976 serde_json::json!({
977 "ok": true,
978 "mutating": true,
979 "recorded": true,
980 "feedback": {"kind": "ProxyLabel", "label": outcome},
981 "routing_decision": {
982 "bm25_coarse": decision.bm25_coarse,
983 "vector_medium": decision.vector_medium,
984 "rerank_fine": decision.rerank_fine,
985 "graph_expansion": decision.graph_expansion,
986 "decoder": decision.decoder,
987 "discord": decision.discord,
988 "no_retrieval": decision.no_retrieval,
989 "reasoning": decision.reasoning,
990 },
991 "policy_state": {
992 "trained_examples": policy.trained_examples,
993 "baseline": policy.baseline,
994 },
995 }),
996 )
997}
998
999fn handle_verify_integrity(
1001 bridge: &MemoryBridge,
1002 handle: &Handle,
1003) -> (&'static str, serde_json::Value) {
1004 let store = &bridge.store;
1005 let result = block_in_place(|| {
1006 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
1007 });
1008
1009 match result {
1010 Ok(report) => (
1011 "200 OK",
1012 serde_json::json!({
1013 "ok": report.ok,
1014 "integrity": report.ok,
1015 "schema_version": report.schema_version,
1016 "fact_count": report.fact_count,
1017 "chunk_count": report.chunk_count,
1018 "message_count": report.message_count,
1019 "facts_missing_embeddings": report.facts_missing_embeddings,
1020 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1021 "issues": report.issues,
1022 "issue_count": report.issues.len(),
1023 "message": if report.ok { "All integrity checks passed".to_string() } else { format!("{} integrity issues found", report.issues.len()) },
1024 }),
1025 ),
1026 Err(e) => (
1027 "500 Internal Server Error",
1028 serde_json::json!({"ok": false, "integrity": false, "error": format!("verify_integrity error: {e}")}),
1029 ),
1030 }
1031}
1032
1033fn handle_discord(
1038 body: &str,
1039 bridge: &MemoryBridge,
1040 handle: &Handle,
1041) -> (&'static str, serde_json::Value) {
1042 use semantic_memory::discord::DiscordScorer;
1043
1044 let params: serde_json::Value = match serde_json::from_str(body) {
1045 Ok(v) => v,
1046 Err(e) => {
1047 return (
1048 "400 Bad Request",
1049 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1050 )
1051 }
1052 };
1053
1054 let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
1055 let top_k = params.get("top_k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
1056
1057 let direct_ids: Vec<String> = match params.get("direct_ids").and_then(|v| v.as_array()) {
1059 Some(arr) => arr
1060 .iter()
1061 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1062 .collect(),
1063 None => {
1064 if query.is_empty() {
1066 return (
1067 "400 Bad Request",
1068 serde_json::json!({"ok": false, "error": "either 'direct_ids' or 'query' must be provided"}),
1069 );
1070 }
1071 let store = &bridge.store;
1072 let search_result =
1073 block_in_place(|| handle.block_on(store.search(query, Some(top_k), None, None)));
1074 match search_result {
1075 Ok(results) => results.iter().map(|r| r.source.result_id()).collect(),
1076 Err(e) => {
1077 return (
1078 "500 Internal Server Error",
1079 serde_json::json!({"ok": false, "error": format!("search error: {e}")}),
1080 )
1081 }
1082 }
1083 }
1084 };
1085
1086 if direct_ids.is_empty() {
1087 return (
1088 "200 OK",
1089 serde_json::json!({"ok": true, "discord_results": [], "count": 0, "edges_loaded": 0}),
1090 );
1091 }
1092
1093 let store = &bridge.store;
1094 let edges_result = block_in_place(|| {
1096 handle.block_on(store.list_graph_edges_for_neighborhood(direct_ids.clone(), 2, 200))
1097 });
1098
1099 let edges: Vec<semantic_memory::discord::GraphEdgeRef> = match edges_result {
1100 Ok(raw_edges) => raw_edges
1101 .iter()
1102 .map(|edge| {
1103 let parsed_type = edge
1104 .edge_type_parsed
1105 .clone()
1106 .or_else(|| serde_json::from_str(&edge.edge_type).ok())
1107 .unwrap_or(semantic_memory::GraphEdgeType::Entity {
1108 relation: "unknown".to_string(),
1109 });
1110 let type_str = match parsed_type {
1111 semantic_memory::GraphEdgeType::Semantic { .. } => "semantic",
1112 semantic_memory::GraphEdgeType::Temporal { .. } => "temporal",
1113 semantic_memory::GraphEdgeType::Causal { .. } => "causal",
1114 semantic_memory::GraphEdgeType::Entity { .. } => "entity",
1115 };
1116 semantic_memory::discord::GraphEdgeRef {
1117 source: edge.source.clone(),
1118 target: edge.target.clone(),
1119 edge_type: type_str.to_string(),
1120 weight: edge.weight,
1121 }
1122 })
1123 .collect(),
1124 Err(e) => {
1125 return (
1126 "500 Internal Server Error",
1127 serde_json::json!({"ok": false, "error": format!("failed to load graph edges: {e}")}),
1128 )
1129 }
1130 };
1131
1132 let edges_loaded = edges.len();
1133 let scorer = DiscordScorer::with_defaults();
1134 let discord_hits = scorer.score(&direct_ids, &edges);
1135
1136 let existing: std::collections::HashSet<String> = direct_ids.iter().cloned().collect();
1138 let filtered_hits: Vec<serde_json::Value> = discord_hits
1139 .iter()
1140 .filter(|hit| !existing.contains(&hit.item_id))
1141 .map(|hit| {
1142 serde_json::json!({
1143 "result_id": hit.item_id,
1144 "discord_score": hit.discord_score,
1145 "anchor_ids": hit.anchor_ids,
1146 "relationship_types": hit.relationship_types,
1147 })
1148 })
1149 .collect();
1150
1151 (
1152 "200 OK",
1153 serde_json::json!({
1154 "ok": true,
1155 "discord_results": filtered_hits,
1156 "count": filtered_hits.len(),
1157 "edges_loaded": edges_loaded,
1158 "direct_ids": direct_ids,
1159 }),
1160 )
1161}
1162
1163fn handle_maintenance_check(
1171 bridge: &MemoryBridge,
1172 handle: &Handle,
1173) -> (&'static str, serde_json::Value) {
1174 let store = &bridge.store;
1175 let embeddings_dirty = block_in_place(|| handle.block_on(store.embeddings_are_dirty()));
1176 let embeddings_dirty = match embeddings_dirty {
1177 Ok(v) => v,
1178 Err(e) => {
1179 return (
1180 "500 Internal Server Error",
1181 serde_json::json!({"ok": false, "error": format!("embeddings_are_dirty error: {e}")}),
1182 )
1183 }
1184 };
1185 let integrity_result = block_in_place(|| {
1186 handle.block_on(store.verify_integrity(semantic_memory::VerifyMode::Quick))
1187 });
1188
1189 match integrity_result {
1190 Ok(report) => (
1191 "200 OK",
1192 serde_json::json!({
1193 "ok": report.ok,
1194 "embeddings_are_dirty": embeddings_dirty,
1195 "integrity": {
1196 "ok": report.ok,
1197 "schema_version": report.schema_version,
1198 "fact_count": report.fact_count,
1199 "chunk_count": report.chunk_count,
1200 "message_count": report.message_count,
1201 "facts_missing_embeddings": report.facts_missing_embeddings,
1202 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1203 "issues": report.issues,
1204 "issue_count": report.issues.len(),
1205 },
1206 "message": if report.ok && !embeddings_dirty {
1207 "All checks passed".to_string()
1208 } else if report.ok && embeddings_dirty {
1209 "Integrity OK but embeddings need re-embedding".to_string()
1210 } else {
1211 format!("{} integrity issues found", report.issues.len())
1212 },
1213 }),
1214 ),
1215 Err(e) => (
1216 "500 Internal Server Error",
1217 serde_json::json!({
1218 "ok": false,
1219 "embeddings_are_dirty": embeddings_dirty,
1220 "error": format!("verify_integrity error: {e}"),
1221 }),
1222 ),
1223 }
1224}
1225
1226fn handle_maintenance_vacuum(
1228 bridge: &MemoryBridge,
1229 handle: &Handle,
1230) -> (&'static str, serde_json::Value) {
1231 let store = &bridge.store;
1232 let result = block_in_place(|| handle.block_on(store.vacuum()));
1233
1234 match result {
1235 Ok(()) => (
1236 "200 OK",
1237 serde_json::json!({"ok": true, "action": "vacuum", "message": "Database vacuumed successfully"}),
1238 ),
1239 Err(e) => (
1240 "500 Internal Server Error",
1241 serde_json::json!({"ok": false, "error": format!("vacuum error: {e}")}),
1242 ),
1243 }
1244}
1245
1246fn handle_maintenance_reembed(
1249 bridge: &MemoryBridge,
1250 handle: &Handle,
1251) -> (&'static str, serde_json::Value) {
1252 let store = &bridge.store;
1253 let result = block_in_place(|| handle.block_on(store.reembed_all()));
1254
1255 match result {
1256 Ok(count) => (
1257 "200 OK",
1258 serde_json::json!({"ok": true, "action": "reembed", "reembedded_count": count, "message": format!("Re-embedded {count} items")}),
1259 ),
1260 Err(e) => (
1261 "500 Internal Server Error",
1262 serde_json::json!({"ok": false, "error": format!("reembed_all error: {e}")}),
1263 ),
1264 }
1265}
1266
1267fn handle_maintenance_reconcile(
1270 body: &str,
1271 bridge: &MemoryBridge,
1272 handle: &Handle,
1273) -> (&'static str, serde_json::Value) {
1274 let params: serde_json::Value = match serde_json::from_str(body) {
1275 Ok(v) => v,
1276 Err(e) => {
1277 return (
1278 "400 Bad Request",
1279 serde_json::json!({"ok": false, "error": format!("invalid JSON: {e}")}),
1280 )
1281 }
1282 };
1283
1284 let action_str = params
1285 .get("action")
1286 .and_then(|v| v.as_str())
1287 .unwrap_or("ReportOnly");
1288
1289 let action = match action_str {
1290 "RebuildFts" => semantic_memory::ReconcileAction::RebuildFts,
1291 "ReEmbed" => semantic_memory::ReconcileAction::ReEmbed,
1292 _ => semantic_memory::ReconcileAction::ReportOnly,
1293 };
1294
1295 let store = &bridge.store;
1296 let result = block_in_place(|| handle.block_on(store.reconcile(action)));
1297
1298 match result {
1299 Ok(report) => (
1300 "200 OK",
1301 serde_json::json!({
1302 "ok": report.ok,
1303 "action": "reconcile",
1304 "reconcile_action": action_str,
1305 "integrity": {
1306 "ok": report.ok,
1307 "schema_version": report.schema_version,
1308 "fact_count": report.fact_count,
1309 "chunk_count": report.chunk_count,
1310 "message_count": report.message_count,
1311 "facts_missing_embeddings": report.facts_missing_embeddings,
1312 "chunks_missing_embeddings": report.chunks_missing_embeddings,
1313 "issues": report.issues,
1314 "issue_count": report.issues.len(),
1315 },
1316 "message": if report.ok {
1317 "Reconciliation completed, no issues found".to_string()
1318 } else {
1319 format!("Reconciliation completed with {} issues", report.issues.len())
1320 },
1321 }),
1322 ),
1323 Err(e) => (
1324 "500 Internal Server Error",
1325 serde_json::json!({"ok": false, "error": format!("reconcile error: {e}")}),
1326 ),
1327 }
1328}
1329
1330fn handle_maintenance_rebuild_hnsw(
1336 bridge: &MemoryBridge,
1337 handle: &Handle,
1338) -> (&'static str, serde_json::Value) {
1339 #[cfg(feature = "hnsw")]
1340 {
1341 let store = &bridge.store;
1342 let result = block_in_place(|| handle.block_on(store.rebuild_hnsw_index()));
1343
1344 match result {
1345 Ok(receipt) => (
1346 "200 OK",
1347 serde_json::json!({
1348 "ok": true,
1349 "action": "rebuild-hnsw",
1350 "message": "HNSW index rebuilt successfully",
1351 "generation_id": receipt.generation_id,
1352 "vector_count": receipt.source_row_count,
1353 }),
1354 ),
1355 Err(e) => (
1356 "500 Internal Server Error",
1357 serde_json::json!({"ok": false, "error": format!("rebuild_hnsw error: {e}")}),
1358 ),
1359 }
1360 }
1361
1362 #[cfg(not(feature = "hnsw"))]
1363 {
1364 let _ = bridge;
1365 let _ = handle;
1366 (
1367 "200 OK",
1368 serde_json::json!({
1369 "ok": true,
1370 "action": "rebuild-hnsw",
1371 "message": "HNSW rebuild not applicable — usearch backend does not require rebuild",
1372 "skipped": true,
1373 }),
1374 )
1375 }
1376}
1377
1378fn handle_maintenance_compact_hnsw(
1383 bridge: &MemoryBridge,
1384 handle: &Handle,
1385) -> (&'static str, serde_json::Value) {
1386 #[cfg(feature = "hnsw")]
1387 {
1388 let store = &bridge.store;
1389 let result = block_in_place(|| handle.block_on(store.compact_hnsw()));
1390
1391 match result {
1392 Ok(()) => (
1393 "200 OK",
1394 serde_json::json!({"ok": true, "action": "compact-hnsw", "message": "HNSW index compacted successfully"}),
1395 ),
1396 Err(e) => (
1397 "500 Internal Server Error",
1398 serde_json::json!({"ok": false, "error": format!("compact_hnsw error: {e}")}),
1399 ),
1400 }
1401 }
1402
1403 #[cfg(not(feature = "hnsw"))]
1404 {
1405 let _ = bridge;
1406 let _ = handle;
1407 (
1408 "200 OK",
1409 serde_json::json!({
1410 "ok": true,
1411 "action": "compact-hnsw",
1412 "message": "HNSW compaction not applicable — usearch backend does not require compaction",
1413 "skipped": true,
1414 }),
1415 )
1416 }
1417}