use mentedb_core::MemoryNode;
use mentedb_core::error::{MenteError, MenteResult};
pub trait ExportModel: Send + Sync {
fn complete(&self, system: &str, user: &str) -> MenteResult<String>;
}
const EXPORT_SYSTEM: &str = "You are a data extraction engine. Read the user's memories and output a single JSON value that conforms exactly to the provided JSON schema. Use only information supported by the memories; for any field with no support, use null. Do not invent values. Output only the JSON, with no prose or code fences.";
pub fn export_structured(
memories: &[MemoryNode],
schema: &str,
instructions: Option<&str>,
model: &dyn ExportModel,
) -> MenteResult<serde_json::Value> {
let mut user = String::with_capacity(256 + memories.len() * 64);
user.push_str("JSON schema to fill:\n");
user.push_str(schema.trim());
if let Some(i) = instructions {
user.push_str("\n\nAdditional instructions:\n");
user.push_str(i.trim());
}
user.push_str("\n\nMemories:\n");
for m in memories {
user.push_str("- ");
user.push_str(m.content.trim());
user.push('\n');
}
let raw = model.complete(EXPORT_SYSTEM, &user)?;
let cleaned = strip_code_fences(&raw);
serde_json::from_str(cleaned).map_err(|e| {
MenteError::Serialization(format!(
"structured export: model did not return valid JSON: {e}"
))
})
}
fn strip_code_fences(s: &str) -> &str {
let t = s.trim();
let t = t
.strip_prefix("```json")
.or_else(|| t.strip_prefix("```"))
.unwrap_or(t);
let t = t.strip_suffix("```").unwrap_or(t);
t.trim()
}
#[cfg(test)]
mod tests {
use super::*;
use mentedb_core::memory::MemoryType;
struct MockModel {
reply: String,
seen_user: std::sync::Mutex<String>,
}
impl ExportModel for MockModel {
fn complete(&self, _system: &str, user: &str) -> MenteResult<String> {
*self.seen_user.lock().unwrap() = user.to_string();
Ok(self.reply.clone())
}
}
fn mem(content: &str) -> MemoryNode {
MemoryNode::new(
mentedb_core::types::AgentId::nil(),
MemoryType::Semantic,
content.to_string(),
vec![],
)
}
#[test]
fn fills_schema_and_includes_memories_in_prompt() {
let model = MockModel {
reply: r#"{"name":"Nam","city":"NYC"}"#.to_string(),
seen_user: std::sync::Mutex::new(String::new()),
};
let memories = [mem("User's name is Nam"), mem("User lives in NYC")];
let out = export_structured(
&memories,
r#"{"name": "string", "city": "string"}"#,
None,
&model,
)
.unwrap();
assert_eq!(out["name"], "Nam");
assert_eq!(out["city"], "NYC");
let prompt = model.seen_user.lock().unwrap().clone();
assert!(prompt.contains("User lives in NYC"));
assert!(prompt.contains("JSON schema to fill"));
}
#[test]
fn tolerates_code_fenced_json() {
let model = MockModel {
reply: "```json\n{\"ok\": true}\n```".to_string(),
seen_user: std::sync::Mutex::new(String::new()),
};
let out = export_structured(&[mem("x")], "{}", None, &model).unwrap();
assert_eq!(out["ok"], true);
}
#[test]
fn invalid_json_is_an_error() {
let model = MockModel {
reply: "not json at all".to_string(),
seen_user: std::sync::Mutex::new(String::new()),
};
assert!(export_structured(&[mem("x")], "{}", None, &model).is_err());
}
}