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
419pub async fn process_turn(
429 State(state): State<Arc<AppState>>,
430 agent: Option<Extension<AuthenticatedAgent>>,
431 Json(req): Json<Value>,
432) -> Result<impl IntoResponse, ApiError> {
433 let user_message = req
434 .get("user_message")
435 .and_then(|v| v.as_str())
436 .ok_or_else(|| ApiError::BadRequest("missing or invalid 'user_message'".into()))?
437 .to_string();
438 let assistant_response = req
439 .get("assistant_response")
440 .and_then(|v| v.as_str())
441 .filter(|s| !s.is_empty())
442 .map(|s| s.to_string());
443 let turn_id = req.get("turn_id").and_then(|v| v.as_u64()).unwrap_or(0);
444 let project_context = req
445 .get("project_context")
446 .and_then(|v| v.as_str())
447 .filter(|s| !s.is_empty())
448 .map(|s| s.to_string());
449 let session_id = req
450 .get("session_id")
451 .and_then(|v| v.as_str())
452 .filter(|s| !s.is_empty())
453 .map(|s| s.to_string());
454 let agent_id = req
455 .get("agent_id")
456 .and_then(|v| v.as_str())
457 .filter(|s| !s.is_empty())
458 .map(|s| {
459 Uuid::parse_str(s).map_err(|_| ApiError::BadRequest("invalid 'agent_id' UUID".into()))
460 })
461 .transpose()?;
462 let user_id = req
463 .get("user_id")
464 .and_then(|v| v.as_str())
465 .filter(|s| !s.is_empty())
466 .map(|s| {
467 Uuid::parse_str(s).map_err(|_| ApiError::BadRequest("invalid 'user_id' UUID".into()))
468 })
469 .transpose()?;
470
471 if let (Some(Extension(authed)), Some(aid)) = (&agent, agent_id) {
473 let tid: AgentId = authed
474 .agent_id
475 .parse()
476 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
477 if tid != AgentId(aid) {
478 return Err(ApiError::Forbidden(
479 "agent_id in request body does not match token".into(),
480 ));
481 }
482 }
483
484 let input = mentedb::process_turn::ProcessTurnInput {
485 user_message,
486 assistant_response,
487 turn_id,
488 project_context,
489 agent_id,
490 user_id,
491 session_id,
492 };
493
494 let tracker_key = agent_id.unwrap_or_else(Uuid::nil).to_string();
499 let mut tracker = {
500 let mut map = state.turn_trackers.lock().await;
501 map.remove(&tracker_key)
502 .unwrap_or_else(mentedb::context::DeltaTracker::new)
503 };
504
505 let db = Arc::clone(&state.db);
506 let joined = tokio::task::spawn_blocking(move || {
507 let result = db.process_turn(&input, &mut tracker);
508 (result, tracker)
509 })
510 .await
511 .map_err(|e| ApiError::Internal(format!("process_turn task failed: {e}")))?;
512 let (result, tracker) = joined;
513 {
514 let mut map = state.turn_trackers.lock().await;
515 map.insert(tracker_key, tracker);
516 }
517 let result = result.map_err(|e| {
518 error!("process_turn failed: {e}");
519 ApiError::Internal(format!("process_turn failed: {e}"))
520 })?;
521
522 let context: Vec<Value> = result
523 .context
524 .iter()
525 .map(|sm| {
526 json!({
527 "id": sm.memory.id.to_string(),
528 "content": sm.memory.content,
529 "memory_type": format!("{:?}", sm.memory.memory_type).to_lowercase(),
530 "relevance_score": sm.score,
531 "created_at": sm.memory.created_at,
532 })
533 })
534 .collect();
535
536 Ok((
537 StatusCode::OK,
538 Json(json!({
539 "ok": true,
540 "context": context,
541 "cache_hit": result.cache_hit,
542 "stored_ids": result.stored_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
543 "episodic_id": result.episodic_id.map(|id| id.to_string()),
544 "contradictions": result.contradiction_count,
545 "facts_extracted": result.facts_extracted,
546 "edges_created": result.edges_created,
547 "inference_actions": result.inference_actions,
548 "pain_warnings": result.pain_warnings.len(),
549 "predicted_topics": result.predicted_topics,
550 "delta_added": result.delta_added.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
551 "delta_removed": result.delta_removed.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
552 })),
553 ))
554}
555
556async fn run_extraction(
558 config: &ExtractionConfig,
559 conversation: &str,
560 agent_id: AgentId,
561 space_id: SpaceId,
562 db: &mentedb::MenteDb,
563) -> Result<Value, ApiError> {
564 let provider = HttpExtractionProvider::new(config.clone()).map_err(|e| {
565 error!("extraction provider init failed: {e}");
566 ApiError::Internal(format!("extraction provider init failed: {e}"))
567 })?;
568
569 let pipeline = ExtractionPipeline::new(provider, config.clone());
570
571 let all_memories = pipeline
572 .extract_from_conversation(conversation)
573 .await
574 .map_err(|e| {
575 error!("extraction failed: {e}");
576 ApiError::Internal(format!("extraction failed: {e}"))
577 })?;
578
579 let total_extracted = all_memories.len();
580 let quality_passed = pipeline.filter_quality(&all_memories);
581 let rejected_low_quality = total_extracted - quality_passed.len();
582
583 let now = std::time::SystemTime::now()
584 .duration_since(std::time::UNIX_EPOCH)
585 .unwrap_or_default()
586 .as_micros() as u64;
587
588 let mut stored_ids = Vec::new();
589 for memory in &quality_passed {
590 let memory_type = map_extraction_type_to_memory_type(&memory.memory_type);
591 let id = MemoryId::new();
592 let node = MemoryNode {
593 id,
594 agent_id,
595 user_id: UserId::nil(),
596 memory_type,
597 embedding: vec![],
598 content: memory.content.clone(),
599 created_at: now,
600 accessed_at: now,
601 access_count: 0,
602 salience: memory.confidence,
603 confidence: memory.confidence,
604 space_id,
605 attributes: std::collections::HashMap::new(),
606 tags: memory.tags.clone(),
607 valid_from: None,
608 valid_until: None,
609 context: None,
610 };
611 match db.store(node) {
612 Ok(()) => stored_ids.push(id.to_string()),
613 Err(e) => {
614 tracing::warn!(error = %e, "failed to store extracted memory, skipping");
615 }
616 }
617 }
618
619 Ok(json!({
620 "memories_stored": stored_ids.len(),
621 "rejected_low_quality": rejected_low_quality,
622 "rejected_duplicate": 0,
623 "contradictions": 0,
624 "stored_ids": stored_ids,
625 }))
626}
627
628fn looks_like_conversation(content: &str) -> bool {
630 let lower = content.to_lowercase();
631 let patterns = ["user:", "assistant:", "human:", "ai:", "\nq:", "\na:"];
632 let matches = patterns.iter().filter(|p| lower.contains(*p)).count();
633 matches >= 2
634}
635
636fn parse_uuid(val: &Value, field: &str) -> Result<Uuid, ApiError> {
641 val.get(field)
642 .and_then(|v| v.as_str())
643 .ok_or_else(|| ApiError::BadRequest(format!("missing '{field}'")))
644 .and_then(|s| {
645 Uuid::parse_str(s)
646 .map_err(|_| ApiError::BadRequest(format!("invalid UUID for '{field}'")))
647 })
648}
649
650fn parse_memory_type(val: &Value) -> Result<MemoryType, ApiError> {
651 let s = val
652 .get("memory_type")
653 .and_then(|v| v.as_str())
654 .ok_or_else(|| ApiError::BadRequest("missing 'memory_type'".into()))?;
655
656 match s.to_lowercase().as_str() {
657 "episodic" => Ok(MemoryType::Episodic),
658 "semantic" => Ok(MemoryType::Semantic),
659 "procedural" => Ok(MemoryType::Procedural),
660 "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
661 "reasoning" => Ok(MemoryType::Reasoning),
662 "correction" => Ok(MemoryType::Correction),
663 _ => Err(ApiError::BadRequest(format!("unknown memory_type: {s}"))),
664 }
665}
666
667fn parse_edge_type(s: &str) -> Option<EdgeType> {
668 match s.to_lowercase().as_str() {
669 "caused" => Some(EdgeType::Caused),
670 "before" => Some(EdgeType::Before),
671 "related" => Some(EdgeType::Related),
672 "contradicts" => Some(EdgeType::Contradicts),
673 "supports" => Some(EdgeType::Supports),
674 "supersedes" => Some(EdgeType::Supersedes),
675 "derived" => Some(EdgeType::Derived),
676 "partof" | "part_of" => Some(EdgeType::PartOf),
677 _ => None,
678 }
679}
680
681fn parse_embedding(val: &Value, required: bool) -> Result<Vec<f32>, ApiError> {
682 match val.get("embedding").and_then(|v| v.as_array()) {
683 Some(arr) => {
684 let mut emb = Vec::with_capacity(arr.len());
685 for v in arr {
686 let f = v.as_f64().ok_or_else(|| {
687 ApiError::BadRequest("embedding values must be numbers".into())
688 })?;
689 emb.push(f as f32);
690 }
691 Ok(emb)
692 }
693 None if required => Err(ApiError::BadRequest("missing 'embedding' array".into())),
694 None => Ok(vec![]),
695 }
696}
697
698fn parse_attributes(val: Option<&Value>) -> HashMap<String, AttributeValue> {
699 let mut map = HashMap::new();
700 if let Some(Value::Object(obj)) = val {
701 for (k, v) in obj {
702 let av = match v {
703 Value::String(s) => AttributeValue::String(s.clone()),
704 Value::Number(n) => {
705 if let Some(i) = n.as_i64() {
706 AttributeValue::Integer(i)
707 } else if let Some(f) = n.as_f64() {
708 AttributeValue::Float(f)
709 } else {
710 continue;
711 }
712 }
713 Value::Bool(b) => AttributeValue::Boolean(*b),
714 _ => continue,
715 };
716 map.insert(k.clone(), av);
717 }
718 }
719 map
720}
721
722fn memory_node_to_json(node: &MemoryNode) -> Value {
723 let mut attrs = Map::new();
724 for (k, v) in &node.attributes {
725 let jv = match v {
726 AttributeValue::String(s) => Value::String(s.clone()),
727 AttributeValue::Integer(i) => json!(i),
728 AttributeValue::Float(f) => json!(f),
729 AttributeValue::Boolean(b) => json!(b),
730 AttributeValue::Bytes(b) => json!(b),
731 };
732 attrs.insert(k.clone(), jv);
733 }
734
735 let memory_type_str = format!("{:?}", node.memory_type).to_lowercase();
736
737 json!({
738 "id": node.id.to_string(),
739 "agent_id": node.agent_id.to_string(),
740 "memory_type": memory_type_str,
741 "embedding": node.embedding,
742 "content": node.content,
743 "created_at": node.created_at,
744 "accessed_at": node.accessed_at,
745 "access_count": node.access_count,
746 "salience": node.salience,
747 "confidence": node.confidence,
748 "space_id": node.space_id.to_string(),
749 "attributes": attrs,
750 "tags": node.tags,
751 })
752}
753
754fn parse_permission(s: &str) -> Result<Permission, ApiError> {
755 match s.to_lowercase().as_str() {
756 "read" => Ok(Permission::Read),
757 "write" => Ok(Permission::Write),
758 "readwrite" | "read_write" => Ok(Permission::ReadWrite),
759 "admin" => Ok(Permission::Admin),
760 _ => Err(ApiError::BadRequest(format!("unknown permission: {s}"))),
761 }
762}
763fn resolve_agent_id(
764 agent: &Option<Extension<AuthenticatedAgent>>,
765 body: Option<&Value>,
766) -> Result<AgentId, ApiError> {
767 if let Some(Extension(a)) = agent {
768 return a
769 .agent_id
770 .parse::<AgentId>()
771 .map_err(|_| ApiError::Unauthorized("invalid agent_id in token".into()));
772 }
773 if let Some(val) = body {
774 return Ok(AgentId(parse_uuid(val, "agent_id")?));
775 }
776 Err(ApiError::Unauthorized("no agent_id available".into()))
777}
778fn is_admin_token(agent: &Option<Extension<AuthenticatedAgent>>) -> bool {
779 matches!(agent, Some(Extension(a)) if a.admin)
780}
781pub async fn create_space(
782 State(state): State<Arc<AppState>>,
783 agent: Option<Extension<AuthenticatedAgent>>,
784 Json(req): Json<Value>,
785) -> Result<impl IntoResponse, ApiError> {
786 let agent_id = resolve_agent_id(&agent, Some(&req))?;
787 let name = req
788 .get("name")
789 .and_then(|v| v.as_str())
790 .ok_or_else(|| ApiError::BadRequest("missing 'name' field".into()))?;
791 let mut spaces = state.spaces.write().await;
792 let space = spaces.create_space(name, agent_id);
793 Ok((
794 StatusCode::CREATED,
795 Json(
796 json!({"id": space.id.to_string(), "name": space.name, "owner": space.owner.to_string()}),
797 ),
798 ))
799}
800pub async fn list_spaces(
801 State(state): State<Arc<AppState>>,
802 agent: Option<Extension<AuthenticatedAgent>>,
803) -> Result<impl IntoResponse, ApiError> {
804 let agent_id = resolve_agent_id(&agent, None)?;
805 let spaces = state.spaces.read().await;
806 let list: Vec<Value> = spaces
807 .list_spaces_for_agent(agent_id)
808 .iter()
809 .map(|s| json!({"id": s.id.to_string(), "name": s.name, "owner": s.owner.to_string()}))
810 .collect();
811 Ok(Json(json!({"spaces": list})))
812}
813pub async fn grant_space_access(
814 State(state): State<Arc<AppState>>,
815 agent: Option<Extension<AuthenticatedAgent>>,
816 Path(space_id_str): Path<String>,
817 Json(req): Json<Value>,
818) -> Result<impl IntoResponse, ApiError> {
819 let space_id: SpaceId = space_id_str
820 .parse()
821 .map_err(|_| ApiError::BadRequest("invalid space ID".into()))?;
822 let caller_id = resolve_agent_id(&agent, None)?;
823 let target_agent = AgentId(parse_uuid(&req, "agent_id")?);
824 let perm_str = req
825 .get("permission")
826 .and_then(|v| v.as_str())
827 .ok_or_else(|| ApiError::BadRequest("missing 'permission' field".into()))?;
828 let perm = parse_permission(perm_str)?;
829 let mut spaces = state.spaces.write().await;
830 if !is_admin_token(&agent) && !spaces.check_access(space_id, caller_id, Permission::Admin) {
831 return Err(ApiError::Forbidden(
832 "only space owner or admin can grant access".into(),
833 ));
834 }
835 spaces.grant_access(space_id, target_agent, perm);
836 Ok(Json(json!({"status": "granted"})))
837}
838
839#[derive(serde::Deserialize)]
845pub struct AdminListParams {
846 pub limit: Option<usize>,
847 pub offset: Option<usize>,
848 pub agent: Option<String>,
850 #[serde(rename = "type")]
852 pub memory_type: Option<String>,
853 pub q: Option<String>,
855}
856
857fn memory_type_from_str(s: &str) -> Result<MemoryType, ApiError> {
858 match s.to_lowercase().as_str() {
859 "episodic" => Ok(MemoryType::Episodic),
860 "semantic" => Ok(MemoryType::Semantic),
861 "procedural" => Ok(MemoryType::Procedural),
862 "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
863 "reasoning" => Ok(MemoryType::Reasoning),
864 "correction" => Ok(MemoryType::Correction),
865 other => Err(ApiError::BadRequest(format!(
866 "unknown memory type: {other}"
867 ))),
868 }
869}
870
871pub async fn admin_list_memories(
873 State(state): State<Arc<AppState>>,
874 headers: HeaderMap,
875 Query(p): Query<AdminListParams>,
876) -> Result<impl IntoResponse, ApiError> {
877 crate::auth::admin_authorized(&headers, &state.admin_key)?;
878 let limit = p.limit.unwrap_or(50).clamp(1, 200);
879 let offset = p.offset.unwrap_or(0);
880 let agent = match p.agent.as_deref().filter(|s| !s.is_empty()) {
881 Some(s) => {
882 Some(AgentId(s.parse().map_err(|_| {
883 ApiError::BadRequest("invalid agent UUID".into())
884 })?))
885 }
886 None => None,
887 };
888 let mtype = match p.memory_type.as_deref().filter(|s| !s.is_empty()) {
889 Some(s) => Some(memory_type_from_str(s)?),
890 None => None,
891 };
892 let q = p.q.as_deref().filter(|s| !s.is_empty());
893
894 let (total, nodes) = state
895 .db
896 .list_memories(limit, offset, agent, mtype, q)
897 .map_err(|e| ApiError::Internal(e.to_string()))?;
898 let memories: Vec<_> = nodes.iter().map(memory_node_to_json).collect();
899 Ok(Json(json!({
900 "total": total,
901 "limit": limit,
902 "offset": offset,
903 "memories": memories,
904 })))
905}
906
907pub async fn admin_delete_memory(
909 State(state): State<Arc<AppState>>,
910 headers: HeaderMap,
911 Path(id_str): Path<String>,
912) -> Result<impl IntoResponse, ApiError> {
913 crate::auth::admin_authorized(&headers, &state.admin_key)?;
914 let id: MemoryId = id_str
915 .parse()
916 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
917 state
918 .db
919 .forget(id)
920 .map_err(|e| ApiError::Internal(e.to_string()))?;
921 Ok(Json(json!({ "status": "deleted", "id": id.to_string() })))
922}
923
924pub async fn admin_run_mql(
927 State(state): State<Arc<AppState>>,
928 headers: HeaderMap,
929 Json(body): Json<Value>,
930) -> Result<impl IntoResponse, ApiError> {
931 crate::auth::admin_authorized(&headers, &state.admin_key)?;
932 let mql = body
933 .get("mql")
934 .and_then(|v| v.as_str())
935 .ok_or_else(|| ApiError::BadRequest("missing 'mql'".into()))?;
936 let scored = state
937 .db
938 .query(mql)
939 .map_err(|e| ApiError::BadRequest(format!("query error: {e}")))?;
940 let memories: Vec<Value> = scored
941 .iter()
942 .map(|s| {
943 let mut j = memory_node_to_json(&s.memory);
944 if let Value::Object(ref mut m) = j {
945 m.insert("score".into(), json!(s.score));
946 }
947 j
948 })
949 .collect();
950 Ok(Json(
951 json!({ "count": memories.len(), "memories": memories }),
952 ))
953}