engram-core 0.21.1

AI Memory Infrastructure - Persistent memory for AI agents with semantic search
Documentation
//! Agentic evolution tool handlers.
//!
//! Provides MCP tools for memory update detection, utility scoring,
//! sentiment analysis, sentiment timelines, and reflective synthesis.

use serde_json::{json, Value};

use super::HandlerContext;
use crate::storage::enrichment_events::{emit_best_effort, EnrichmentEvent};

// ── memory_detect_updates ─────────────────────────────────────────────────────

/// Detect memories that may need updating given new content.
pub fn memory_detect_updates(ctx: &HandlerContext, params: Value) -> Value {
    use crate::intelligence::memory_update::UpdateDetector;

    let content = match params.get("content").and_then(|v| v.as_str()) {
        Some(c) => c.to_string(),
        None => return json!({"error": "content is required"}),
    };
    let workspace = params
        .get("workspace")
        .and_then(|v| v.as_str())
        .unwrap_or("default");

    let detector = UpdateDetector::new();

    ctx.storage
        .with_connection(|conn| {
            let candidates = detector.detect_updates(conn, &content, workspace)?;
            Ok(json!({
                "workspace": workspace,
                "candidates": candidates,
                "count": candidates.len(),
            }))
        })
        .unwrap_or_else(|e| json!({"error": e.to_string()}))
}

// ── memory_utility_score ──────────────────────────────────────────────────────

/// Compute the Q-value utility score for a memory from its feedback history.
pub fn memory_utility_score(ctx: &HandlerContext, params: Value) -> Value {
    use crate::search::utility::UtilityTracker;

    let id = match params.get("id").and_then(|v| v.as_i64()) {
        Some(id) => id,
        None => return json!({"error": "id is required"}),
    };

    let tracker = UtilityTracker::new();

    ctx.storage
        .with_connection(|conn| {
            let score = tracker.get_utility(conn, id)?;
            Ok(json!({
                "memory_id": id,
                "utility_score": score.score,
                "retrievals": score.retrievals,
                "useful_count": score.useful_count,
                "last_retrieved": score.last_retrieved,
            }))
        })
        .unwrap_or_else(|e| json!({"error": e.to_string()}))
}

// ── memory_sentiment_analyze ──────────────────────────────────────────────────

/// Analyze sentiment of a memory's content.
pub fn memory_sentiment_analyze(ctx: &HandlerContext, params: Value) -> Value {
    use crate::intelligence::emotional::SentimentAnalyzer;
    use crate::storage::queries::get_memory;

    let id = match params.get("id").and_then(|v| v.as_i64()) {
        Some(id) => id,
        None => return json!({"error": "id is required"}),
    };

    ctx.storage
        .with_connection(|conn| {
            let memory = get_memory(conn, id)?;
            let analyzer = SentimentAnalyzer::new();
            let sentiment = analyzer.analyze(&memory.content);
            Ok(json!({
                "memory_id": id,
                "score": sentiment.score,
                "label": sentiment.label.as_str(),
                "confidence": sentiment.confidence,
                "keywords": sentiment.keywords,
            }))
        })
        .unwrap_or_else(|e| json!({"error": e.to_string()}))
}

// ── memory_sentiment_timeline ─────────────────────────────────────────────────

/// Compute a sentiment timeline over memories in a workspace and time range.
pub fn memory_sentiment_timeline(ctx: &HandlerContext, params: Value) -> Value {
    use crate::intelligence::emotional::SentimentAnalyzer;

    let workspace = params
        .get("workspace")
        .and_then(|v| v.as_str())
        .unwrap_or("default");
    let from = params
        .get("from")
        .and_then(|v| v.as_str())
        .unwrap_or("1970-01-01T00:00:00Z");
    let to = params
        .get("to")
        .and_then(|v| v.as_str())
        .unwrap_or("9999-12-31T23:59:59Z");
    let limit: i64 = params.get("limit").and_then(|v| v.as_i64()).unwrap_or(50);

    let analyzer = SentimentAnalyzer::new();

    ctx.storage
        .with_connection(|conn| {
            // Fetch memories in the time range
            let mut stmt = conn.prepare(
                "SELECT id, content, created_at FROM memories
                 WHERE workspace = ?1
                   AND created_at >= ?2
                   AND created_at <= ?3
                 ORDER BY created_at ASC
                 LIMIT ?4",
            )?;

            let rows = stmt
                .query_map(rusqlite::params![workspace, from, to, limit], |row| {
                    Ok((
                        row.get::<_, i64>(0)?,
                        row.get::<_, String>(1)?,
                        row.get::<_, String>(2)?,
                    ))
                })?
                .collect::<Result<Vec<_>, _>>()
                .map_err(crate::error::EngramError::Database)?;

            let timeline: Vec<serde_json::Value> = rows
                .iter()
                .map(|(id, content, ts)| {
                    let s = analyzer.analyze(content);
                    json!({
                        "memory_id": id,
                        "timestamp": ts,
                        "score": s.score,
                        "label": s.label.as_str(),
                    })
                })
                .collect();

            Ok(json!({
                "workspace": workspace,
                "from": from,
                "to": to,
                "entries": timeline,
                "count": timeline.len(),
            }))
        })
        .unwrap_or_else(|e| json!({"error": e.to_string()}))
}

// ── memory_reflect ────────────────────────────────────────────────────────────

/// Generate a reflection over a set of memories.
pub fn memory_reflect(ctx: &HandlerContext, params: Value) -> Value {
    use crate::intelligence::emotional::{ReflectionDepth, ReflectionEngine};
    use crate::storage::queries::get_memory;

    let ids: Vec<i64> = params
        .get("ids")
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_i64()).collect())
        .unwrap_or_default();

    if ids.is_empty() {
        return json!({"error": "ids array is required and must not be empty"});
    }

    let depth_str = params
        .get("depth")
        .and_then(|v| v.as_str())
        .unwrap_or("surface");

    let persist = params
        .get("persist")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);

    let depth = match depth_str {
        "analytical" => ReflectionDepth::Analytical,
        "meta" => ReflectionDepth::Meta,
        _ => ReflectionDepth::Surface,
    };

    ctx.storage
        .with_connection(|conn| {
            let mut pairs: Vec<(i64, String)> = Vec::new();
            for &id in &ids {
                if let Ok(m) = get_memory(conn, id) {
                    pairs.push((id, m.content));
                }
            }

            let memory_refs: Vec<(i64, &str)> =
                pairs.iter().map(|(id, c)| (*id, c.as_str())).collect();

            let engine = ReflectionEngine::new();
            let reflection = engine.create_reflection(conn, &memory_refs, depth)?;

            if persist {
                let op_id = uuid::Uuid::new_v4().to_string();
                emit_best_effort(
                    conn,
                    &EnrichmentEvent {
                        operation_id: &op_id,
                        event_type: "evolution",
                        memory_id: ids.first().copied(),
                        version_id: None,
                        triggered_by: "memory_reflect",
                        agent_id: None,
                        workspace: None,
                        params: json!({"depth": depth_str, "source_ids": &ids}),
                        outcome: json!({"insights": reflection.insights.len()}),
                        status: "completed",
                        dry_run: false,
                    },
                );
            }

            Ok(json!({
                "reflection": reflection.content,
                "source_ids": reflection.source_ids,
                "depth": reflection.depth.as_str(),
                "insights": reflection.insights,
                "created_at": reflection.created_at,
                "persisted": persist,
            }))
        })
        .unwrap_or_else(|e| json!({"error": e.to_string()}))
}