1use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::auth::AuthenticatedAgent;
7use axum::Extension;
8use axum::Json;
9use axum::extract::{Path, State};
10use axum::http::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};
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 memory_type,
125 embedding,
126 content: content.clone(),
127 created_at: now,
128 accessed_at: now,
129 access_count: 0,
130 salience,
131 confidence,
132 space_id,
133 attributes,
134 tags,
135 valid_from: req.get("valid_from").and_then(|v| v.as_u64()),
136 valid_until: req.get("valid_until").and_then(|v| v.as_u64()),
137 };
138
139 let db = &*state.db;
140 db.store(node).map_err(|e| {
141 error!("store failed: {e}");
142 ApiError::Internal(format!("store failed: {e}"))
143 })?;
144
145 if state.auto_extract
147 && state.extraction_config.is_some()
148 && looks_like_conversation(&content)
149 && let Some(tx) = &state.extraction_tx
150 {
151 let req = crate::extraction_queue::ExtractionRequest {
152 config: state.extraction_config.clone().unwrap(),
153 content: content.clone(),
154 agent_id,
155 space_id,
156 db: state.db.clone(),
157 };
158 if tx.try_send(req).is_err() {
159 tracing::warn!("extraction queue full, skipping auto-extract");
160 }
161 }
162
163 Ok((
164 StatusCode::CREATED,
165 Json(json!({ "id": id.to_string(), "status": "stored" })),
166 ))
167}
168
169pub async fn get_memory(
175 State(state): State<Arc<AppState>>,
176 agent: Option<Extension<AuthenticatedAgent>>,
177 Path(id_str): Path<String>,
178) -> Result<impl IntoResponse, ApiError> {
179 let id: MemoryId = id_str
180 .parse()
181 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
182
183 let db = &*state.db;
184 let node = db
185 .get_memory(id)
186 .map_err(|_| ApiError::NotFound(format!("memory {id} not found")))?;
187
188 if let Some(Extension(ref authed)) = agent {
189 let tid: AgentId = authed
190 .agent_id
191 .parse()
192 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
193 if node.agent_id != tid {
194 return Err(ApiError::Forbidden(
195 "memory belongs to a different agent".into(),
196 ));
197 }
198 }
199
200 Ok(Json(memory_node_to_json(&node)))
201}
202
203pub async fn forget_memory(
209 State(state): State<Arc<AppState>>,
210 agent: Option<Extension<AuthenticatedAgent>>,
211 Path(id_str): Path<String>,
212) -> Result<impl IntoResponse, ApiError> {
213 let id: MemoryId = id_str
214 .parse()
215 .map_err(|_| ApiError::BadRequest("invalid memory ID".into()))?;
216
217 let db = &*state.db;
218 if let Some(Extension(ref authed)) = agent {
219 let tid: AgentId = authed
220 .agent_id
221 .parse()
222 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
223 let window = db.recall("RECALL memories LIMIT 1000").map_err(|e| {
224 error!("recall failed: {e}");
225 ApiError::Internal(format!("recall failed: {e}"))
226 })?;
227 let mut found = false;
228 for block in &window.blocks {
229 for scored in &block.memories {
230 if scored.memory.id == id {
231 if scored.memory.agent_id != tid {
232 return Err(ApiError::Forbidden(
233 "memory belongs to a different agent".into(),
234 ));
235 }
236 found = true;
237 break;
238 }
239 }
240 if found {
241 break;
242 }
243 }
244 if !found {
245 return Err(ApiError::NotFound(format!("memory {id} not found")));
246 }
247 }
248 db.forget(id).map_err(|e| {
249 error!("forget failed: {e}");
250 ApiError::Internal(format!("forget failed: {e}"))
251 })?;
252
253 Ok(Json(json!({ "status": "deleted" })))
254}
255
256pub async fn recall_memories(
262 State(state): State<Arc<AppState>>,
263 Json(req): Json<Value>,
264) -> Result<impl IntoResponse, ApiError> {
265 let query = req
266 .get("query")
267 .and_then(|v| v.as_str())
268 .ok_or_else(|| ApiError::BadRequest("missing 'query' field".into()))?;
269
270 let db = &*state.db;
271 let window = db.recall(query).map_err(|e| {
272 error!("recall failed: {e}");
273 ApiError::Internal(format!("recall failed: {e}"))
274 })?;
275
276 let memory_count: usize = window.blocks.iter().map(|b| b.memories.len()).sum();
277 Ok(Json(json!({
278 "context": window.format,
279 "total_tokens": window.total_tokens,
280 "memory_count": memory_count,
281 })))
282}
283
284pub async fn search_similar(
290 State(state): State<Arc<AppState>>,
291 Json(req): Json<Value>,
292) -> Result<impl IntoResponse, ApiError> {
293 let embedding = parse_embedding(&req, true)?;
294 let k = req.get("k").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
295
296 let db = &*state.db;
297 let results = db.recall_similar(&embedding, k).map_err(|e| {
298 error!("search failed: {e}");
299 ApiError::Internal(format!("search failed: {e}"))
300 })?;
301
302 let items: Vec<Value> = results
303 .iter()
304 .map(|(id, score)| json!({ "id": id.to_string(), "score": score }))
305 .collect();
306
307 Ok(Json(json!({ "results": items })))
308}
309
310pub async fn create_edge(
316 State(state): State<Arc<AppState>>,
317 Json(req): Json<Value>,
318) -> Result<impl IntoResponse, ApiError> {
319 let source = MemoryId(parse_uuid(&req, "source")?);
320 let target = MemoryId(parse_uuid(&req, "target")?);
321
322 let edge_type_str = req
323 .get("edge_type")
324 .and_then(|v| v.as_str())
325 .ok_or_else(|| ApiError::BadRequest("missing 'edge_type'".into()))?;
326
327 let edge_type = parse_edge_type(edge_type_str)
328 .ok_or_else(|| ApiError::BadRequest(format!("unknown edge_type: {edge_type_str}")))?;
329
330 let weight = req.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
331
332 let now = std::time::SystemTime::now()
333 .duration_since(std::time::UNIX_EPOCH)
334 .unwrap_or_default()
335 .as_micros() as u64;
336
337 let valid_from = req.get("valid_from").and_then(|v| v.as_u64());
338 let valid_until = req.get("valid_until").and_then(|v| v.as_u64());
339
340 let edge = MemoryEdge {
341 source,
342 target,
343 edge_type,
344 weight,
345 created_at: now,
346 valid_from,
347 valid_until,
348 label: req
349 .get("label")
350 .and_then(|v| v.as_str())
351 .map(|s| s.to_string()),
352 };
353
354 let db = &*state.db;
355 db.relate(edge).map_err(|e| {
356 error!("relate failed: {e}");
357 ApiError::Internal(format!("relate failed: {e}"))
358 })?;
359
360 Ok((StatusCode::CREATED, Json(json!({ "status": "created" }))))
361}
362
363pub async fn ingest_conversation(
369 State(state): State<Arc<AppState>>,
370 agent: Option<Extension<AuthenticatedAgent>>,
371 Json(req): Json<Value>,
372) -> Result<impl IntoResponse, ApiError> {
373 let agent_id = AgentId(parse_uuid(&req, "agent_id")?);
374 if let Some(Extension(ref authed)) = agent {
375 let tid: AgentId = authed
376 .agent_id
377 .parse()
378 .map_err(|_| ApiError::Internal("token contains invalid agent_id UUID".into()))?;
379 if tid != agent_id {
380 return Err(ApiError::Forbidden(
381 "agent_id in request body does not match token".into(),
382 ));
383 }
384 }
385
386 let extraction_config = state.extraction_config.as_ref().ok_or_else(|| {
387 ApiError::ServiceUnavailable(
388 "LLM provider not configured. Set MENTEDB_LLM_PROVIDER env var.".into(),
389 )
390 })?;
391
392 let conversation = req
393 .get("conversation")
394 .and_then(|v| v.as_str())
395 .ok_or_else(|| ApiError::BadRequest("missing or invalid 'conversation'".into()))?;
396
397 let space_id = match req.get("space_id") {
398 Some(v) => v
399 .as_str()
400 .and_then(|s| s.parse::<SpaceId>().ok())
401 .ok_or_else(|| ApiError::BadRequest("invalid 'space_id'".into()))?,
402 None => SpaceId::nil(),
403 };
404
405 let stats = run_extraction(
406 extraction_config,
407 conversation,
408 agent_id,
409 space_id,
410 &state.db,
411 )
412 .await?;
413
414 Ok((StatusCode::OK, Json(stats)))
415}
416
417async fn run_extraction(
419 config: &ExtractionConfig,
420 conversation: &str,
421 agent_id: AgentId,
422 space_id: SpaceId,
423 db: &mentedb::MenteDb,
424) -> Result<Value, ApiError> {
425 let provider = HttpExtractionProvider::new(config.clone()).map_err(|e| {
426 error!("extraction provider init failed: {e}");
427 ApiError::Internal(format!("extraction provider init failed: {e}"))
428 })?;
429
430 let pipeline = ExtractionPipeline::new(provider, config.clone());
431
432 let all_memories = pipeline
433 .extract_from_conversation(conversation)
434 .await
435 .map_err(|e| {
436 error!("extraction failed: {e}");
437 ApiError::Internal(format!("extraction failed: {e}"))
438 })?;
439
440 let total_extracted = all_memories.len();
441 let quality_passed = pipeline.filter_quality(&all_memories);
442 let rejected_low_quality = total_extracted - quality_passed.len();
443
444 let now = std::time::SystemTime::now()
445 .duration_since(std::time::UNIX_EPOCH)
446 .unwrap_or_default()
447 .as_micros() as u64;
448
449 let mut stored_ids = Vec::new();
450 for memory in &quality_passed {
451 let memory_type = map_extraction_type_to_memory_type(&memory.memory_type);
452 let id = MemoryId::new();
453 let node = MemoryNode {
454 id,
455 agent_id,
456 memory_type,
457 embedding: vec![],
458 content: memory.content.clone(),
459 created_at: now,
460 accessed_at: now,
461 access_count: 0,
462 salience: memory.confidence,
463 confidence: memory.confidence,
464 space_id,
465 attributes: std::collections::HashMap::new(),
466 tags: memory.tags.clone(),
467 valid_from: None,
468 valid_until: None,
469 };
470 match db.store(node) {
471 Ok(()) => stored_ids.push(id.to_string()),
472 Err(e) => {
473 tracing::warn!(error = %e, "failed to store extracted memory, skipping");
474 }
475 }
476 }
477
478 Ok(json!({
479 "memories_stored": stored_ids.len(),
480 "rejected_low_quality": rejected_low_quality,
481 "rejected_duplicate": 0,
482 "contradictions": 0,
483 "stored_ids": stored_ids,
484 }))
485}
486
487fn looks_like_conversation(content: &str) -> bool {
489 let lower = content.to_lowercase();
490 let patterns = ["user:", "assistant:", "human:", "ai:", "\nq:", "\na:"];
491 let matches = patterns.iter().filter(|p| lower.contains(*p)).count();
492 matches >= 2
493}
494
495fn parse_uuid(val: &Value, field: &str) -> Result<Uuid, ApiError> {
500 val.get(field)
501 .and_then(|v| v.as_str())
502 .ok_or_else(|| ApiError::BadRequest(format!("missing '{field}'")))
503 .and_then(|s| {
504 Uuid::parse_str(s)
505 .map_err(|_| ApiError::BadRequest(format!("invalid UUID for '{field}'")))
506 })
507}
508
509fn parse_memory_type(val: &Value) -> Result<MemoryType, ApiError> {
510 let s = val
511 .get("memory_type")
512 .and_then(|v| v.as_str())
513 .ok_or_else(|| ApiError::BadRequest("missing 'memory_type'".into()))?;
514
515 match s.to_lowercase().as_str() {
516 "episodic" => Ok(MemoryType::Episodic),
517 "semantic" => Ok(MemoryType::Semantic),
518 "procedural" => Ok(MemoryType::Procedural),
519 "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
520 "reasoning" => Ok(MemoryType::Reasoning),
521 "correction" => Ok(MemoryType::Correction),
522 _ => Err(ApiError::BadRequest(format!("unknown memory_type: {s}"))),
523 }
524}
525
526fn parse_edge_type(s: &str) -> Option<EdgeType> {
527 match s.to_lowercase().as_str() {
528 "caused" => Some(EdgeType::Caused),
529 "before" => Some(EdgeType::Before),
530 "related" => Some(EdgeType::Related),
531 "contradicts" => Some(EdgeType::Contradicts),
532 "supports" => Some(EdgeType::Supports),
533 "supersedes" => Some(EdgeType::Supersedes),
534 "derived" => Some(EdgeType::Derived),
535 "partof" | "part_of" => Some(EdgeType::PartOf),
536 _ => None,
537 }
538}
539
540fn parse_embedding(val: &Value, required: bool) -> Result<Vec<f32>, ApiError> {
541 match val.get("embedding").and_then(|v| v.as_array()) {
542 Some(arr) => {
543 let mut emb = Vec::with_capacity(arr.len());
544 for v in arr {
545 let f = v.as_f64().ok_or_else(|| {
546 ApiError::BadRequest("embedding values must be numbers".into())
547 })?;
548 emb.push(f as f32);
549 }
550 Ok(emb)
551 }
552 None if required => Err(ApiError::BadRequest("missing 'embedding' array".into())),
553 None => Ok(vec![]),
554 }
555}
556
557fn parse_attributes(val: Option<&Value>) -> HashMap<String, AttributeValue> {
558 let mut map = HashMap::new();
559 if let Some(Value::Object(obj)) = val {
560 for (k, v) in obj {
561 let av = match v {
562 Value::String(s) => AttributeValue::String(s.clone()),
563 Value::Number(n) => {
564 if let Some(i) = n.as_i64() {
565 AttributeValue::Integer(i)
566 } else if let Some(f) = n.as_f64() {
567 AttributeValue::Float(f)
568 } else {
569 continue;
570 }
571 }
572 Value::Bool(b) => AttributeValue::Boolean(*b),
573 _ => continue,
574 };
575 map.insert(k.clone(), av);
576 }
577 }
578 map
579}
580
581fn memory_node_to_json(node: &MemoryNode) -> Value {
582 let mut attrs = Map::new();
583 for (k, v) in &node.attributes {
584 let jv = match v {
585 AttributeValue::String(s) => Value::String(s.clone()),
586 AttributeValue::Integer(i) => json!(i),
587 AttributeValue::Float(f) => json!(f),
588 AttributeValue::Boolean(b) => json!(b),
589 AttributeValue::Bytes(b) => json!(b),
590 };
591 attrs.insert(k.clone(), jv);
592 }
593
594 let memory_type_str = format!("{:?}", node.memory_type).to_lowercase();
595
596 json!({
597 "id": node.id.to_string(),
598 "agent_id": node.agent_id.to_string(),
599 "memory_type": memory_type_str,
600 "embedding": node.embedding,
601 "content": node.content,
602 "created_at": node.created_at,
603 "accessed_at": node.accessed_at,
604 "access_count": node.access_count,
605 "salience": node.salience,
606 "confidence": node.confidence,
607 "space_id": node.space_id.to_string(),
608 "attributes": attrs,
609 "tags": node.tags,
610 })
611}
612
613fn parse_permission(s: &str) -> Result<Permission, ApiError> {
614 match s.to_lowercase().as_str() {
615 "read" => Ok(Permission::Read),
616 "write" => Ok(Permission::Write),
617 "readwrite" | "read_write" => Ok(Permission::ReadWrite),
618 "admin" => Ok(Permission::Admin),
619 _ => Err(ApiError::BadRequest(format!("unknown permission: {s}"))),
620 }
621}
622fn resolve_agent_id(
623 agent: &Option<Extension<AuthenticatedAgent>>,
624 body: Option<&Value>,
625) -> Result<AgentId, ApiError> {
626 if let Some(Extension(a)) = agent {
627 return a
628 .agent_id
629 .parse::<AgentId>()
630 .map_err(|_| ApiError::Unauthorized("invalid agent_id in token".into()));
631 }
632 if let Some(val) = body {
633 return Ok(AgentId(parse_uuid(val, "agent_id")?));
634 }
635 Err(ApiError::Unauthorized("no agent_id available".into()))
636}
637fn is_admin_token(agent: &Option<Extension<AuthenticatedAgent>>) -> bool {
638 matches!(agent, Some(Extension(a)) if a.admin)
639}
640pub async fn create_space(
641 State(state): State<Arc<AppState>>,
642 agent: Option<Extension<AuthenticatedAgent>>,
643 Json(req): Json<Value>,
644) -> Result<impl IntoResponse, ApiError> {
645 let agent_id = resolve_agent_id(&agent, Some(&req))?;
646 let name = req
647 .get("name")
648 .and_then(|v| v.as_str())
649 .ok_or_else(|| ApiError::BadRequest("missing 'name' field".into()))?;
650 let mut spaces = state.spaces.write().await;
651 let space = spaces.create_space(name, agent_id);
652 Ok((
653 StatusCode::CREATED,
654 Json(
655 json!({"id": space.id.to_string(), "name": space.name, "owner": space.owner.to_string()}),
656 ),
657 ))
658}
659pub async fn list_spaces(
660 State(state): State<Arc<AppState>>,
661 agent: Option<Extension<AuthenticatedAgent>>,
662) -> Result<impl IntoResponse, ApiError> {
663 let agent_id = resolve_agent_id(&agent, None)?;
664 let spaces = state.spaces.read().await;
665 let list: Vec<Value> = spaces
666 .list_spaces_for_agent(agent_id)
667 .iter()
668 .map(|s| json!({"id": s.id.to_string(), "name": s.name, "owner": s.owner.to_string()}))
669 .collect();
670 Ok(Json(json!({"spaces": list})))
671}
672pub async fn grant_space_access(
673 State(state): State<Arc<AppState>>,
674 agent: Option<Extension<AuthenticatedAgent>>,
675 Path(space_id_str): Path<String>,
676 Json(req): Json<Value>,
677) -> Result<impl IntoResponse, ApiError> {
678 let space_id: SpaceId = space_id_str
679 .parse()
680 .map_err(|_| ApiError::BadRequest("invalid space ID".into()))?;
681 let caller_id = resolve_agent_id(&agent, None)?;
682 let target_agent = AgentId(parse_uuid(&req, "agent_id")?);
683 let perm_str = req
684 .get("permission")
685 .and_then(|v| v.as_str())
686 .ok_or_else(|| ApiError::BadRequest("missing 'permission' field".into()))?;
687 let perm = parse_permission(perm_str)?;
688 let mut spaces = state.spaces.write().await;
689 if !is_admin_token(&agent) && !spaces.check_access(space_id, caller_id, Permission::Admin) {
690 return Err(ApiError::Forbidden(
691 "only space owner or admin can grant access".into(),
692 ));
693 }
694 spaces.grant_access(space_id, target_agent, perm);
695 Ok(Json(json!({"status": "granted"})))
696}