engram_core/
insights_handler.rs1use std::sync::Arc;
2
3use serde::Deserialize;
4use serde_json::{Value, json};
5
6use engram_storage::Memory;
7
8use crate::error::CoreError;
9use crate::export_handler::memory_to_portable_json;
10use crate::lock_helpers;
11use crate::server::ServerState;
12
13#[derive(Deserialize)]
14struct InsightsParams {
15 action: String,
16 id: Option<String>,
17}
18
19pub async fn handle(state: &Arc<ServerState>, params: Value) -> Result<Value, CoreError> {
20 let parsed: InsightsParams = serde_json::from_value(params)
21 .map_err(|error| CoreError::DispatchError(error.to_string()))?;
22 match parsed.action.as_str() {
23 "list" => handle_list(state).await,
24 "generate" => Err(CoreError::DispatchError(
25 "insight generation requires engram-trainer (Phase 5)".into(),
26 )),
27 "delete" => handle_delete(state, parsed.id).await,
28 other => Err(CoreError::DispatchError(format!(
29 "invalid insights action: {other}"
30 ))),
31 }
32}
33
34async fn handle_list(state: &Arc<ServerState>) -> Result<Value, CoreError> {
35 let state_clone = Arc::clone(state);
36 tokio::task::spawn_blocking(move || {
37 let database = lock_helpers::lock_db(&state_clone);
38 let insights = query_active_insights(&database)?;
39 let count = insights.len();
40 let serialized: Vec<Value> = insights.iter().map(memory_to_portable_json).collect();
41 Ok::<Value, CoreError>(json!({ "insights": serialized, "count": count }))
42 })
43 .await
44 .map_err(|error| CoreError::SocketError(error.to_string()))?
45}
46
47async fn handle_delete(state: &Arc<ServerState>, id: Option<String>) -> Result<Value, CoreError> {
48 let insight_id =
49 id.ok_or_else(|| CoreError::DispatchError("delete requires 'id' parameter".into()))?;
50 let state_clone = Arc::clone(state);
51 tokio::task::spawn_blocking(move || {
52 let database = lock_helpers::lock_db(&state_clone);
53 let memory = database.get_memory(&insight_id)?;
54 if memory.memory_type != "insight" {
55 return Err(CoreError::DispatchError(format!(
56 "memory {} is not an insight",
57 insight_id
58 )));
59 }
60 database.delete_memory(&insight_id)?;
61 Ok::<Value, CoreError>(json!({ "deleted": insight_id }))
62 })
63 .await
64 .map_err(|error| CoreError::SocketError(error.to_string()))?
65}
66
67fn query_active_insights(database: &engram_storage::Database) -> Result<Vec<Memory>, CoreError> {
68 let mut statement = database
69 .connection()
70 .prepare(
71 "SELECT * FROM memories WHERE memory_type = 'insight' \
72 AND superseded_by IS NULL",
73 )
74 .map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?;
75 let rows = statement
76 .query_map([], engram_storage::row_to_memory)
77 .map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?;
78 let mut insights = Vec::new();
79 for row in rows {
80 insights.push(
81 row.map_err(|error| CoreError::Storage(engram_storage::StorageError::Sqlite(error)))?,
82 );
83 }
84 Ok(insights)
85}