feat: add Obsidian vault projection with Longhorn storage
- Create PVC (10Gi) for vault on Longhorn persistent storage
- Update deployment to mount vault volume instead of emptyDir
- Add POST /memory/vault endpoint to generate markdown vault
- Generates one .md file per L1 memory (YAML frontmatter + content)
- Index.md for each project from L2 synthesis
- Auto-update on ingest completion via vault endpoint
Vault structure:
/data/vault/
poimen/
index.md (L2 synthesis)
architecture.md (L1 memory for 'architecture-decisions')
infra-root-causes.md (L1 memory for 'infra-root-causes')
Ready for human reading in Obsidian app or programmatic access.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use actix_web::{web, App, HttpServer, HttpResponse, HttpRequest, middleware::Logger};
|
||||
use anyhow::Result;
|
||||
use chrono;
|
||||
use mem_llm::{EmbeddingsClient, RerankClient};
|
||||
use mem_store::{init_schema, VectorStore};
|
||||
use serde_json::json;
|
||||
@@ -74,6 +75,7 @@ pub async fn start_server(port: u16, api_key: String, database_url: &str) -> Res
|
||||
.route("/memory/query", web::get().to(query_handler))
|
||||
.route("/memory/projects", web::get().to(projects_handler))
|
||||
.route("/memory/skills", web::get().to(skills_handler))
|
||||
.route("/memory/vault", web::post().to(vault_handler))
|
||||
})
|
||||
.bind(("0.0.0.0", port))?
|
||||
.run()
|
||||
@@ -298,3 +300,101 @@ pub async fn skills_handler(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /memory/vault — generate Obsidian vault from memories
|
||||
pub async fn vault_handler(
|
||||
req: HttpRequest,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let vault_dir = std::env::var("MEM_HOME").unwrap_or_else(|_| "/data".to_string());
|
||||
|
||||
// Get all projects from database
|
||||
let projects_result = sqlx::query_as::<_, (String,)>(
|
||||
"SELECT DISTINCT project FROM memories_l1 ORDER BY project",
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
match projects_result {
|
||||
Ok(projects) => {
|
||||
let mut generated = 0;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (project,) in projects {
|
||||
let project_vault_dir = format!("{}/vault/{}", vault_dir, project);
|
||||
|
||||
// Create project directory
|
||||
if let Err(e) = std::fs::create_dir_all(&project_vault_dir) {
|
||||
errors.push(format!("Failed to create {}: {}", project_vault_dir, e));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all L1 memories for this project
|
||||
let l1s_result = sqlx::query_as::<_, (String, String, String)>(
|
||||
"SELECT id, query_id, content FROM memories_l1 WHERE project = $1 ORDER BY updated_at DESC",
|
||||
)
|
||||
.bind(&project)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
match l1s_result {
|
||||
Ok(l1s) => {
|
||||
for (id, query_id, content) in l1s {
|
||||
let filename = format!("{}/{}.md", project_vault_dir, query_id);
|
||||
let note = format!(
|
||||
"---\nproject: {}\nlevel: L1\nquery_id: {}\nid: {}\nupdated: {}\n---\n\n{}",
|
||||
project,
|
||||
query_id,
|
||||
id,
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
content
|
||||
);
|
||||
if std::fs::write(&filename, note).is_ok() {
|
||||
generated += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(format!("Failed to fetch L1s for {}: {}", project, e));
|
||||
}
|
||||
}
|
||||
|
||||
// Get L2 synthesis
|
||||
let l2_result = sqlx::query_as::<_, (String,)>(
|
||||
"SELECT content FROM memories_l2 WHERE project = $1",
|
||||
)
|
||||
.bind(&project)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
if let Ok(Some((content,))) = l2_result {
|
||||
let filename = format!("{}/index.md", project_vault_dir);
|
||||
let note = format!(
|
||||
"---\nproject: {}\nlevel: L2\ntitle: {} Synthesis\nupdated: {}\n---\n\n{}",
|
||||
project,
|
||||
project,
|
||||
chrono::Utc::now().to_rfc3339(),
|
||||
content
|
||||
);
|
||||
if std::fs::write(&filename, note).is_ok() {
|
||||
generated += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"status": "generated",
|
||||
"vault_dir": format!("{}/vault", vault_dir),
|
||||
"notes_created": generated,
|
||||
"errors": errors
|
||||
}))
|
||||
}
|
||||
Err(_) => {
|
||||
HttpResponse::InternalServerError().json(json!({"error": "database_error"}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user