use super::ControlPanelState;
use std::sync::Arc;
pub(crate) async fn memory_entities(
axum::extract::State(state): axum::extract::State<Arc<ControlPanelState>>,
) -> axum::Json<serde_json::Value> {
let kg = match &state.knowledge_graph {
Some(kg) => kg,
None => {
return axum::Json(serde_json::json!({
"ok": true,
"data": { "users": [] }
}));
}
};
let user_ids = kg.user_ids();
let mut users = Vec::new();
for uid in &user_ids {
let (entities, relations) = kg.read_graph(uid);
let entity_data: Vec<serde_json::Value> = entities
.iter()
.map(|e| {
serde_json::json!({
"name": e.name,
"entity_type": e.entity_type,
"observations": e.observations.iter().map(|o| &o.content).collect::<Vec<_>>(),
})
})
.collect();
let relation_data: Vec<serde_json::Value> = relations
.iter()
.map(|r| {
serde_json::json!({
"source": r.source,
"relation_type": r.relation_type,
"target": r.target,
})
})
.collect();
users.push(serde_json::json!({
"user_id": uid,
"entity_count": entities.len(),
"relation_count": relations.len(),
"entities": entity_data,
"relations": relation_data,
}));
}
axum::Json(serde_json::json!({
"ok": true,
"data": { "users": users }
}))
}
#[derive(serde::Deserialize)]
pub(crate) struct MemoryPayload {
content: String,
}
pub(crate) async fn memory_load(
axum::extract::State(state): axum::extract::State<Arc<ControlPanelState>>,
) -> axum::Json<serde_json::Value> {
let config = state.config.load();
let protocol_path = config
.memory
.as_ref()
.map(|m| m.protocol_path.display().to_string())
.unwrap_or_else(|| "context/MEMORY.md".to_string());
let resolved_path = resolve_memory_path(&protocol_path, state.config_path.as_deref());
let content = std::fs::read_to_string(&resolved_path).unwrap_or_default();
axum::Json(serde_json::json!({
"content": content,
"path": resolved_path.display().to_string(),
"exists": resolved_path.exists(),
}))
}
pub(crate) async fn memory_save(
axum::extract::State(state): axum::extract::State<Arc<ControlPanelState>>,
axum::Json(payload): axum::Json<MemoryPayload>,
) -> axum::Json<serde_json::Value> {
let config = state.config.load();
let protocol_path = config
.memory
.as_ref()
.map(|m| m.protocol_path.display().to_string())
.unwrap_or_else(|| "context/MEMORY.md".to_string());
let resolved_path = resolve_memory_path(&protocol_path, state.config_path.as_deref());
if let Some(parent) = resolved_path.parent() {
std::fs::create_dir_all(parent).ok();
}
match std::fs::write(&resolved_path, &payload.content) {
Ok(()) => {
tracing::info!(path = %resolved_path.display(), bytes = payload.content.len(), "memory protocol saved via UI");
axum::Json(serde_json::json!({
"ok": true,
"message": format!("Saved ({} bytes).", payload.content.len()),
}))
}
Err(e) => {
tracing::error!(path = %resolved_path.display(), error = %e, "failed to save memory protocol");
axum::Json(serde_json::json!({
"ok": false,
"message": format!("Failed to save: {e}"),
}))
}
}
}
fn resolve_memory_path(protocol_path: &str, config_path: Option<&std::path::Path>) -> std::path::PathBuf {
let p = std::path::Path::new(protocol_path);
if p.is_absolute() {
return p.to_path_buf();
}
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let cwd_path = cwd.join(p);
if cwd_path.exists() {
return cwd_path;
}
if let Some(cfg) = config_path {
if let Some(parent) = cfg.parent() {
let cfg_path = parent.join(p);
if cfg_path.exists() {
return cfg_path;
}
}
}
cwd_path
}