Skip to main content

engram_core/
insights_handler.rs

1use 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}