1use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::auth::AuthenticatedAgent;
7use axum::Extension;
8use axum::Json;
9use axum::extract::{Path, Query, State};
10use axum::http::{HeaderMap, StatusCode};
11use axum::response::IntoResponse;
12use mentedb_core::edge::EdgeType;
13use mentedb_core::memory::{AttributeValue, MemoryType};
14use mentedb_core::space::Permission;
15use mentedb_core::{MemoryEdge, MemoryNode};
16use mentedb_extraction::{
17 ExtractionConfig, ExtractionPipeline, HttpExtractionProvider,
18 map_extraction_type_to_memory_type,
19};
20use serde_json::{Map, Value, json};
21use tracing::error;
22use uuid::Uuid;
23
24use crate::error::ApiError;
25use crate::state::AppState;
26use mentedb_core::types::{AgentId, MemoryId, SpaceId, UserId};
27
28pub async fn health(State(state): State<Arc<AppState>>) -> impl IntoResponse {
34 let uptime = state.start_time.elapsed().as_secs();
35 Json(json!({
36 "status": "ok",
37 "version": "0.1.0",
38 "uptime_seconds": uptime,
39 }))
40}
41
42pub async fn stats(State(state): State<Arc<AppState>>) -> Result<impl IntoResponse, ApiError> {
48 let uptime = state.start_time.elapsed().as_secs();
49 let db = &*state.db;
50 let memory_count = db.memory_count();
51
52 Ok(Json(json!({
53 "memory_count": memory_count,
54 "uptime_seconds": uptime,
55 })))
56}
57
58pub async fn store_memory(
64 State(state): State<Arc<AppState>>,
65 agent: Option<Extension<AuthenticatedAgent>>,
66 Json(req): Json<Value>,
67) -> Result<impl IntoResponse, ApiError> {
68 let agent_id = AgentId(parse_uuid(&req, "agent_id")?);
69 if let Some(Extension(ref authed)) = agent {
70 let tid: AgentId = authed
71 .agent_id
72 .parse()
73 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
74 if tid != agent_id {
75 return Err(ApiError::Forbidden(
76 "agent_id in request body does not match token".into(),
77 ));
78 }
79 }
80 let memory_type = parse_memory_type(&req)?;
81
82 let content = req
83 .get("content")
84 .and_then(|v| v.as_str())
85 .ok_or_else(|| ApiError::BadRequest("missing or invalid 'content'".into()))?
86 .to_string();
87
88 let embedding = parse_embedding(&req, false)?;
89
90 let tags: Vec<String> = match req.get("tags").and_then(|v| v.as_array()) {
91 Some(arr) => arr
92 .iter()
93 .filter_map(|v| v.as_str().map(String::from))
94 .collect(),
95 None => vec![],
96 };
97
98 let attributes = parse_attributes(req.get("attributes"));
99
100 let space_id = match req.get("space_id") {
101 Some(v) => v
102 .as_str()
103 .and_then(|s| s.parse::<SpaceId>().ok())
104 .ok_or_else(|| ApiError::BadRequest("invalid 'space_id'".into()))?,
105 None => SpaceId::nil(),
106 };
107
108 let now = std::time::SystemTime::now()
109 .duration_since(std::time::UNIX_EPOCH)
110 .unwrap_or_default()
111 .as_micros() as u64;
112
113 let id = MemoryId::new();
114
115 let salience = req.get("salience").and_then(|v| v.as_f64()).unwrap_or(0.5) as f32;
116 let confidence = req
117 .get("confidence")
118 .and_then(|v| v.as_f64())
119 .unwrap_or(1.0) as f32;
120
121 let node = MemoryNode {
122 id,
123 agent_id,
124 user_id: UserId::nil(),
125 memory_type,
126 embedding,
127 content: content.clone(),
128 created_at: now,
129 accessed_at: now,
130 access_count: 0,
131 salience,
132 confidence,
133 space_id,
134 attributes,
135 tags,
136 valid_from: req.get("valid_from").and_then(|v| v.as_u64()),
137 valid_until: req.get("valid_until").and_then(|v| v.as_u64()),
138 context: None,
139 };
140
141 let db = &*state.db;
142 db.store(node).map_err(|e| {
143 error!("store failed: {e}");
144 ApiError::Internal(format!("store failed: {e}"))
145 })?;
146
147 if state.auto_extract
149 && state.extraction_config.is_some()
150 && looks_like_conversation(&content)
151 && let Some(tx) = &state.extraction_tx
152 {
153 let req = crate::extraction_queue::ExtractionRequest {
154 config: state.extraction_config.clone().unwrap(),
155 content: content.clone(),
156 agent_id,
157 space_id,
158 db: state.db.clone(),
159 };
160 if tx.try_send(req).is_err() {
161 tracing::warn!("extraction queue full, skipping auto-extract");
162 }
163 }
164
165 Ok((
166 StatusCode::CREATED,
167 Json(json!({ "id": id.to_string(), "status": "stored" })),
168 ))
169}
170
171pub async fn get_memory(
177 State(state): State<Arc<AppState>>,
178 agent: Option<Extension<AuthenticatedAgent>>,
179 Path(id_str): Path<String>,
180) -> Result<impl IntoResponse, ApiError> {
181 let id: MemoryId = id_str
182 .parse()
183 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
184
185 let db = &*state.db;
186 let node = db
187 .get_memory(id)
188 .map_err(|_| ApiError::NotFound(format!("memory {id} not found")))?;
189
190 if let Some(Extension(ref authed)) = agent {
191 let tid: AgentId = authed
192 .agent_id
193 .parse()
194 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
195 if node.agent_id != tid {
196 return Err(ApiError::Forbidden(
197 "memory belongs to a different agent".into(),
198 ));
199 }
200 }
201
202 Ok(Json(memory_node_to_json(&node)))
203}
204
205pub async fn forget_memory(
211 State(state): State<Arc<AppState>>,
212 agent: Option<Extension<AuthenticatedAgent>>,
213 Path(id_str): Path<String>,
214) -> Result<impl IntoResponse, ApiError> {
215 let id: MemoryId = id_str
216 .parse()
217 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
218
219 let db = &*state.db;
220 if let Some(Extension(ref authed)) = agent {
221 let tid: AgentId = authed
222 .agent_id
223 .parse()
224 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
225 let window = db.recall("RECALL memories LIMIT 1000").map_err(|e| {
226 error!("recall failed: {e}");
227 ApiError::Internal(format!("recall failed: {e}"))
228 })?;
229 let mut found = false;
230 for block in &window.blocks {
231 for scored in &block.memories {
232 if scored.memory.id == id {
233 if scored.memory.agent_id != tid {
234 return Err(ApiError::Forbidden(
235 "memory belongs to a different agent".into(),
236 ));
237 }
238 found = true;
239 break;
240 }
241 }
242 if found {
243 break;
244 }
245 }
246 if !found {
247 return Err(ApiError::NotFound(format!("memory {id} not found")));
248 }
249 }
250 db.forget(id).map_err(|e| {
251 error!("forget failed: {e}");
252 ApiError::Internal(format!("forget failed: {e}"))
253 })?;
254
255 Ok(Json(json!({ "status": "deleted" })))
256}
257
258pub async fn recall_memories(
264 State(state): State<Arc<AppState>>,
265 Json(req): Json<Value>,
266) -> Result<impl IntoResponse, ApiError> {
267 let query = req
268 .get("query")
269 .and_then(|v| v.as_str())
270 .ok_or_else(|| ApiError::BadRequest("missing 'query' field".into()))?;
271
272 let db = &*state.db;
273 let window = db.recall(query).map_err(|e| {
274 error!("recall failed: {e}");
275 ApiError::Internal(format!("recall failed: {e}"))
276 })?;
277
278 let memory_count: usize = window.blocks.iter().map(|b| b.memories.len()).sum();
279 Ok(Json(json!({
280 "context": window.format,
281 "total_tokens": window.total_tokens,
282 "memory_count": memory_count,
283 })))
284}
285
286pub async fn search_similar(
292 State(state): State<Arc<AppState>>,
293 Json(req): Json<Value>,
294) -> Result<impl IntoResponse, ApiError> {
295 let embedding = parse_embedding(&req, true)?;
296 let k = req.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
297
298 let db = &*state.db;
299 let results = db.recall_similar(&embedding, k).map_err(|e| {
300 error!("search failed: {e}");
301 ApiError::Internal(format!("search failed: {e}"))
302 })?;
303
304 let items: Vec<Value> = results
305 .iter()
306 .map(|(id, score)| json!({ "id": id.to_string(), "score": score }))
307 .collect();
308
309 Ok(Json(json!({ "results": items })))
310}
311
312pub async fn create_edge(
318 State(state): State<Arc<AppState>>,
319 Json(req): Json<Value>,
320) -> Result<impl IntoResponse, ApiError> {
321 let source = MemoryId(parse_uuid(&req, "source")?);
322 let target = MemoryId(parse_uuid(&req, "target")?);
323
324 let edge_type_str = req
325 .get("edge_type")
326 .and_then(|v| v.as_str())
327 .ok_or_else(|| ApiError::BadRequest("missing 'edge_type'".into()))?;
328
329 let edge_type = parse_edge_type(edge_type_str)
330 .ok_or_else(|| ApiError::BadRequest(format!("unknown edge_type: {edge_type_str}")))?;
331
332 let weight = req.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
333
334 let now = std::time::SystemTime::now()
335 .duration_since(std::time::UNIX_EPOCH)
336 .unwrap_or_default()
337 .as_micros() as u64;
338
339 let valid_from = req.get("valid_from").and_then(|v| v.as_u64());
340 let valid_until = req.get("valid_until").and_then(|v| v.as_u64());
341
342 let edge = MemoryEdge {
343 source,
344 target,
345 edge_type,
346 weight,
347 created_at: now,
348 valid_from,
349 valid_until,
350 label: req
351 .get("label")
352 .and_then(|v| v.as_str())
353 .map(|s| s.to_string()),
354 };
355
356 let db = &*state.db;
357 db.relate(edge).map_err(|e| {
358 error!("relate failed: {e}");
359 ApiError::Internal(format!("relate failed: {e}"))
360 })?;
361
362 Ok((StatusCode::CREATED, Json(json!({ "status": "created" }))))
363}
364
365pub async fn ingest_conversation(
371 State(state): State<Arc<AppState>>,
372 agent: Option<Extension<AuthenticatedAgent>>,
373 Json(req): Json<Value>,
374) -> Result<impl IntoResponse, ApiError> {
375 let agent_id = AgentId(parse_uuid(&req, "agent_id")?);
376 if let Some(Extension(ref authed)) = agent {
377 let tid: AgentId = authed
378 .agent_id
379 .parse()
380 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
381 if tid != agent_id {
382 return Err(ApiError::Forbidden(
383 "agent_id in request body does not match token".into(),
384 ));
385 }
386 }
387
388 let extraction_config = state.extraction_config.as_ref().ok_or_else(|| {
389 ApiError::ServiceUnavailable(
390 "LLM provider not configured. Set MENTEDB_LLM_PROVIDER env var.".into(),
391 )
392 })?;
393
394 let conversation = req
395 .get("conversation")
396 .and_then(|v| v.as_str())
397 .ok_or_else(|| ApiError::BadRequest("missing or invalid 'conversation'".into()))?;
398
399 let space_id = match req.get("space_id") {
400 Some(v) => v
401 .as_str()
402 .and_then(|s| s.parse::<SpaceId>().ok())
403 .ok_or_else(|| ApiError::BadRequest("invalid 'space_id'".into()))?,
404 None => SpaceId::nil(),
405 };
406
407 let stats = run_extraction(
408 extraction_config,
409 conversation,
410 agent_id,
411 space_id,
412 &state.db,
413 )
414 .await?;
415
416 Ok((StatusCode::OK, Json(stats)))
417}
418
419async fn run_extraction(
421 config: &ExtractionConfig,
422 conversation: &str,
423 agent_id: AgentId,
424 space_id: SpaceId,
425 db: &mentedb::MenteDb,
426) -> Result<Value, ApiError> {
427 let provider = HttpExtractionProvider::new(config.clone()).map_err(|e| {
428 error!("extraction provider init failed: {e}");
429 ApiError::Internal(format!("extraction provider init failed: {e}"))
430 })?;
431
432 let pipeline = ExtractionPipeline::new(provider, config.clone());
433
434 let all_memories = pipeline
435 .extract_from_conversation(conversation)
436 .await
437 .map_err(|e| {
438 error!("extraction failed: {e}");
439 ApiError::Internal(format!("extraction failed: {e}"))
440 })?;
441
442 let total_extracted = all_memories.len();
443 let quality_passed = pipeline.filter_quality(&all_memories);
444 let rejected_low_quality = total_extracted - quality_passed.len();
445
446 let now = std::time::SystemTime::now()
447 .duration_since(std::time::UNIX_EPOCH)
448 .unwrap_or_default()
449 .as_micros() as u64;
450
451 let mut stored_ids = Vec::new();
452 for memory in &quality_passed {
453 let memory_type = map_extraction_type_to_memory_type(&memory.memory_type);
454 let id = MemoryId::new();
455 let node = MemoryNode {
456 id,
457 agent_id,
458 user_id: UserId::nil(),
459 memory_type,
460 embedding: vec![],
461 content: memory.content.clone(),
462 created_at: now,
463 accessed_at: now,
464 access_count: 0,
465 salience: memory.confidence,
466 confidence: memory.confidence,
467 space_id,
468 attributes: std::collections::HashMap::new(),
469 tags: memory.tags.clone(),
470 valid_from: None,
471 valid_until: None,
472 context: None,
473 };
474 match db.store(node) {
475 Ok(()) => stored_ids.push(id.to_string()),
476 Err(e) => {
477 tracing::warn!(error = %e, "failed to store extracted memory, skipping");
478 }
479 }
480 }
481
482 Ok(json!({
483 "memories_stored": stored_ids.len(),
484 "rejected_low_quality": rejected_low_quality,
485 "rejected_duplicate": 0,
486 "contradictions": 0,
487 "stored_ids": stored_ids,
488 }))
489}
490
491fn looks_like_conversation(content: &str) -> bool {
493 let lower = content.to_lowercase();
494 let patterns = ["user:", "assistant:", "human:", "ai:", "\nq:", "\na:"];
495 let matches = patterns.iter().filter(|p| lower.contains(*p)).count();
496 matches >= 2
497}
498
499fn parse_uuid(val: &Value, field: &str) -> Result<Uuid, ApiError> {
504 val.get(field)
505 .and_then(|v| v.as_str())
506 .ok_or_else(|| ApiError::BadRequest(format!("missing '{field}'")))
507 .and_then(|s| {
508 Uuid::parse_str(s)
509 .map_err(|_| ApiError::BadRequest(format!("invalid UUID for '{field}'")))
510 })
511}
512
513fn parse_memory_type(val: &Value) -> Result<MemoryType, ApiError> {
514 let s = val
515 .get("memory_type")
516 .and_then(|v| v.as_str())
517 .ok_or_else(|| ApiError::BadRequest("missing 'memory_type'".into()))?;
518
519 match s.to_lowercase().as_str() {
520 "episodic" => Ok(MemoryType::Episodic),
521 "semantic" => Ok(MemoryType::Semantic),
522 "procedural" => Ok(MemoryType::Procedural),
523 "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
524 "reasoning" => Ok(MemoryType::Reasoning),
525 "correction" => Ok(MemoryType::Correction),
526 _ => Err(ApiError::BadRequest(format!("unknown memory_type: {s}"))),
527 }
528}
529
530fn parse_edge_type(s: &str) -> Option<EdgeType> {
531 match s.to_lowercase().as_str() {
532 "caused" => Some(EdgeType::Caused),
533 "before" => Some(EdgeType::Before),
534 "related" => Some(EdgeType::Related),
535 "contradicts" => Some(EdgeType::Contradicts),
536 "supports" => Some(EdgeType::Supports),
537 "supersedes" => Some(EdgeType::Supersedes),
538 "derived" => Some(EdgeType::Derived),
539 "partof" | "part_of" => Some(EdgeType::PartOf),
540 _ => None,
541 }
542}
543
544fn parse_embedding(val: &Value, required: bool) -> Result<Vec<f32>, ApiError> {
545 match val.get("embedding").and_then(|v| v.as_array()) {
546 Some(arr) => {
547 let mut emb = Vec::with_capacity(arr.len());
548 for v in arr {
549 let f = v.as_f64().ok_or_else(|| {
550 ApiError::BadRequest("embedding values must be numbers".into())
551 })?;
552 emb.push(f as f32);
553 }
554 Ok(emb)
555 }
556 None if required => Err(ApiError::BadRequest("missing 'embedding' array".into())),
557 None => Ok(vec![]),
558 }
559}
560
561fn parse_attributes(val: Option<&Value>) -> HashMap<String, AttributeValue> {
562 let mut map = HashMap::new();
563 if let Some(Value::Object(obj)) = val {
564 for (k, v) in obj {
565 let av = match v {
566 Value::String(s) => AttributeValue::String(s.clone()),
567 Value::Number(n) => {
568 if let Some(i) = n.as_i64() {
569 AttributeValue::Integer(i)
570 } else if let Some(f) = n.as_f64() {
571 AttributeValue::Float(f)
572 } else {
573 continue;
574 }
575 }
576 Value::Bool(b) => AttributeValue::Boolean(*b),
577 _ => continue,
578 };
579 map.insert(k.clone(), av);
580 }
581 }
582 map
583}
584
585fn memory_node_to_json(node: &MemoryNode) -> Value {
586 let mut attrs = Map::new();
587 for (k, v) in &node.attributes {
588 let jv = match v {
589 AttributeValue::String(s) => Value::String(s.clone()),
590 AttributeValue::Integer(i) => json!(i),
591 AttributeValue::Float(f) => json!(f),
592 AttributeValue::Boolean(b) => json!(b),
593 AttributeValue::Bytes(b) => json!(b),
594 };
595 attrs.insert(k.clone(), jv);
596 }
597
598 let memory_type_str = format!("{:?}", node.memory_type).to_lowercase();
599
600 json!({
601 "id": node.id.to_string(),
602 "agent_id": node.agent_id.to_string(),
603 "memory_type": memory_type_str,
604 "embedding": node.embedding,
605 "content": node.content,
606 "created_at": node.created_at,
607 "accessed_at": node.accessed_at,
608 "access_count": node.access_count,
609 "salience": node.salience,
610 "confidence": node.confidence,
611 "space_id": node.space_id.to_string(),
612 "attributes": attrs,
613 "tags": node.tags,
614 })
615}
616
617fn parse_permission(s: &str) -> Result<Permission, ApiError> {
618 match s.to_lowercase().as_str() {
619 "read" => Ok(Permission::Read),
620 "write" => Ok(Permission::Write),
621 "readwrite" | "read_write" => Ok(Permission::ReadWrite),
622 "admin" => Ok(Permission::Admin),
623 _ => Err(ApiError::BadRequest(format!("unknown permission: {s}"))),
624 }
625}
626fn resolve_agent_id(
627 agent: &Option<Extension<AuthenticatedAgent>>,
628 body: Option<&Value>,
629) -> Result<AgentId, ApiError> {
630 if let Some(Extension(a)) = agent {
631 return a
632 .agent_id
633 .parse::<AgentId>()
634 .map_err(|_| ApiError::Unauthorized("invalid agent_id in token".into()));
635 }
636 if let Some(val) = body {
637 return Ok(AgentId(parse_uuid(val, "agent_id")?));
638 }
639 Err(ApiError::Unauthorized("no agent_id available".into()))
640}
641fn is_admin_token(agent: &Option<Extension<AuthenticatedAgent>>) -> bool {
642 matches!(agent, Some(Extension(a)) if a.admin)
643}
644pub async fn create_space(
645 State(state): State<Arc<AppState>>,
646 agent: Option<Extension<AuthenticatedAgent>>,
647 Json(req): Json<Value>,
648) -> Result<impl IntoResponse, ApiError> {
649 let agent_id = resolve_agent_id(&agent, Some(&req))?;
650 let name = req
651 .get("name")
652 .and_then(|v| v.as_str())
653 .ok_or_else(|| ApiError::BadRequest("missing 'name' field".into()))?;
654 let mut spaces = state.spaces.write().await;
655 let space = spaces.create_space(name, agent_id);
656 Ok((
657 StatusCode::CREATED,
658 Json(
659 json!({"id": space.id.to_string(), "name": space.name, "owner": space.owner.to_string()}),
660 ),
661 ))
662}
663pub async fn list_spaces(
664 State(state): State<Arc<AppState>>,
665 agent: Option<Extension<AuthenticatedAgent>>,
666) -> Result<impl IntoResponse, ApiError> {
667 let agent_id = resolve_agent_id(&agent, None)?;
668 let spaces = state.spaces.read().await;
669 let list: Vec<Value> = spaces
670 .list_spaces_for_agent(agent_id)
671 .iter()
672 .map(|s| json!({"id": s.id.to_string(), "name": s.name, "owner": s.owner.to_string()}))
673 .collect();
674 Ok(Json(json!({"spaces": list})))
675}
676pub async fn grant_space_access(
677 State(state): State<Arc<AppState>>,
678 agent: Option<Extension<AuthenticatedAgent>>,
679 Path(space_id_str): Path<String>,
680 Json(req): Json<Value>,
681) -> Result<impl IntoResponse, ApiError> {
682 let space_id: SpaceId = space_id_str
683 .parse()
684 .map_err(|_| ApiError::BadRequest("invalid space ID".into()))?;
685 let caller_id = resolve_agent_id(&agent, None)?;
686 let target_agent = AgentId(parse_uuid(&req, "agent_id")?);
687 let perm_str = req
688 .get("permission")
689 .and_then(|v| v.as_str())
690 .ok_or_else(|| ApiError::BadRequest("missing 'permission' field".into()))?;
691 let perm = parse_permission(perm_str)?;
692 let mut spaces = state.spaces.write().await;
693 if !is_admin_token(&agent) && !spaces.check_access(space_id, caller_id, Permission::Admin) {
694 return Err(ApiError::Forbidden(
695 "only space owner or admin can grant access".into(),
696 ));
697 }
698 spaces.grant_access(space_id, target_agent, perm);
699 Ok(Json(json!({"status": "granted"})))
700}
701
702#[derive(serde::Deserialize)]
708pub struct AdminListParams {
709 pub limit: Option<usize>,
710 pub offset: Option<usize>,
711 pub agent: Option<String>,
713 #[serde(rename = "type")]
715 pub memory_type: Option<String>,
716 pub q: Option<String>,
718}
719
720fn memory_type_from_str(s: &str) -> Result<MemoryType, ApiError> {
721 match s.to_lowercase().as_str() {
722 "episodic" => Ok(MemoryType::Episodic),
723 "semantic" => Ok(MemoryType::Semantic),
724 "procedural" => Ok(MemoryType::Procedural),
725 "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
726 "reasoning" => Ok(MemoryType::Reasoning),
727 "correction" => Ok(MemoryType::Correction),
728 other => Err(ApiError::BadRequest(format!(
729 "unknown memory type: {other}"
730 ))),
731 }
732}
733
734pub async fn admin_list_memories(
736 State(state): State<Arc<AppState>>,
737 headers: HeaderMap,
738 Query(p): Query<AdminListParams>,
739) -> Result<impl IntoResponse, ApiError> {
740 crate::auth::admin_authorized(&headers, &state.admin_key)?;
741 let limit = p.limit.unwrap_or(50).clamp(1, 200);
742 let offset = p.offset.unwrap_or(0);
743 let agent = match p.agent.as_deref().filter(|s| !s.is_empty()) {
744 Some(s) => {
745 Some(AgentId(s.parse().map_err(|_| {
746 ApiError::BadRequest("invalid agent UUID".into())
747 })?))
748 }
749 None => None,
750 };
751 let mtype = match p.memory_type.as_deref().filter(|s| !s.is_empty()) {
752 Some(s) => Some(memory_type_from_str(s)?),
753 None => None,
754 };
755 let q = p.q.as_deref().filter(|s| !s.is_empty());
756
757 let (total, nodes) = state
758 .db
759 .list_memories(limit, offset, agent, mtype, q)
760 .map_err(|e| ApiError::Internal(e.to_string()))?;
761 let memories: Vec<_> = nodes.iter().map(memory_node_to_json).collect();
762 Ok(Json(json!({
763 "total": total,
764 "limit": limit,
765 "offset": offset,
766 "memories": memories,
767 })))
768}
769
770pub async fn admin_delete_memory(
772 State(state): State<Arc<AppState>>,
773 headers: HeaderMap,
774 Path(id_str): Path<String>,
775) -> Result<impl IntoResponse, ApiError> {
776 crate::auth::admin_authorized(&headers, &state.admin_key)?;
777 let id: MemoryId = id_str
778 .parse()
779 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
780 state
781 .db
782 .forget(id)
783 .map_err(|e| ApiError::Internal(e.to_string()))?;
784 Ok(Json(json!({ "status": "deleted", "id": id.to_string() })))
785}
786
787pub async fn admin_run_mql(
790 State(state): State<Arc<AppState>>,
791 headers: HeaderMap,
792 Json(body): Json<Value>,
793) -> Result<impl IntoResponse, ApiError> {
794 crate::auth::admin_authorized(&headers, &state.admin_key)?;
795 let mql = body
796 .get("mql")
797 .and_then(|v| v.as_str())
798 .ok_or_else(|| ApiError::BadRequest("missing 'mql'".into()))?;
799 let scored = state
800 .db
801 .query(mql)
802 .map_err(|e| ApiError::BadRequest(format!("query error: {e}")))?;
803 let memories: Vec<Value> = scored
804 .iter()
805 .map(|s| {
806 let mut j = memory_node_to_json(&s.memory);
807 if let Value::Object(ref mut m) = j {
808 m.insert("score".into(), json!(s.score));
809 }
810 j
811 })
812 .collect();
813 Ok(Json(
814 json!({ "count": memories.len(), "memories": memories }),
815 ))
816}