atheneum 0.12.0

Agent coordination graph database - episodic and semantic memory for multi-agent workflows
Documentation
#![cfg(feature = "web-ui")]

use crate::{AtheneumGraph, LintConfig};
use askama::Template;
use axum::{
    extract::State,
    response::{Html, IntoResponse},
    routing::{get, post},
    Json, Router,
};
use serde_json::json;
use std::sync::Arc;

#[derive(Template)]
#[template(
    source = r##"<!DOCTYPE html>
<html>
<head>
    <title>Atheneum Force-Directed Graph</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        body { margin: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #121212; color: #e0e0e0; overflow: hidden; }
        #canvas-container { width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; }
        .node { stroke: #121212; stroke-width: 2px; cursor: pointer; }
        .link { stroke: #555; stroke-opacity: 0.4; stroke-width: 1.5px; }
        #control-panel { position: absolute; top: 15px; left: 15px; z-index: 100; background: rgba(30, 30, 30, 0.9); padding: 15px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); width: 280px; }
        button { background: #3f51b5; border: none; color: white; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-top: 5px; width: 100%; font-weight: bold; }
        button:hover { background: #5c6bc0; }
        #tooltip { position: absolute; display: none; background: rgba(0,0,0,0.9); color: #fff; padding: 10px; border-radius: 4px; font-size: 12px; max-width: 350px; z-index: 200; pointer-events: none; border: 1px solid #333; }
        h3 { margin-top: 0; color: #3f51b5; }
    </style>
</head>
<body>
    <div id="control-panel">
        <h3>Atheneum Graph Panel</h3>
        <p>Hover over nodes to inspect properties.</p>
        <button onclick="triggerDream()">Reflective Dream</button>
        <button onclick="triggerLint()">Run Lint Graph</button>
        <div id="status" style="margin-top: 10px; font-size: 12px; color: #aaa;"></div>
    </div>
    <div id="tooltip"></div>
    <div id="canvas-container"><svg id="graph-svg" style="width:100%; height:100%;"></svg></div>
    
    <script>
        const svg = d3.select("#graph-svg"),
              width = window.innerWidth,
              height = window.innerHeight;
              
        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(d => d.id).distance(120))
            .force("charge", d3.forceManyBody().strength(-300))
            .force("center", d3.forceCenter(width / 2, height / 2));
            
        d3.json("/api/graph").then(data => {
            const link = svg.append("g").selectAll(".link")
                .data(data.links)
                .enter().append("line")
                .attr("class", "link");
                
            const node = svg.append("g").selectAll(".node")
                .data(data.nodes)
                .enter().append("circle")
                .attr("class", "node")
                .attr("r", d => d.kind === "Concept" ? 10 : 7)
                .attr("fill", d => {
                    if (d.kind === "Concept") return "#2196f3";
                    if (d.kind === "Memory") return "#ffeb3b";
                    if (d.kind === "WikiPage") return "#4caf50";
                    return "#9c27b0";
                })
                .call(d3.drag()
                    .on("start", dragstarted)
                    .on("drag", dragged)
                    .on("end", dragended))
                .on("mouseover", showTooltip)
                .on("mouseout", hideTooltip);
                
            simulation.nodes(data.nodes).on("tick", ticked);
            simulation.force("link").links(data.links);
            
            function ticked() {
                link.attr("x1", d => d.source.x)
                    .attr("y1", d => d.source.y)
                    .attr("x2", d => d.target.x)
                    .attr("y2", d => d.target.y);
                node.attr("cx", d => d.x).attr("cy", d => d.y);
            }
        });

        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x; d.fy = d.y;
        }
        function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null; d.fy = null;
        }

        const tooltip = d3.select("#tooltip");
        function showTooltip(event, d) {
            tooltip.style("display", "block")
                   .html(`<strong>${d.name}</strong><br/>Kind: ${d.kind}<br/><pre>${JSON.stringify(d.data, null, 2)}</pre>`)
                   .style("left", (event.pageX + 10) + "px")
                   .style("top", (event.pageY + 10) + "px");
        }
        function hideTooltip() { tooltip.style("display", "none"); }

        function triggerDream() {
            document.getElementById("status").innerText = "Dreaming...";
            fetch("/api/dream", { method: "POST" })
                .then(r => r.json())
                .then(data => {
                    document.getElementById("status").innerText = `Dream run complete! Scanned ${data.pages_scanned} pages.`;
                });
        }
        function triggerLint() {
            document.getElementById("status").innerText = "Linting...";
            fetch("/api/lint")
                .then(r => r.json())
                .then(data => {
                    document.getElementById("status").innerText = `Lint done! Found ${data.orphans.length} orphans, ${data.expired.length} expired.`;
                });
        }
    </script>
</body>
</html>"##,
    ext = "html"
)]
struct DashboardTemplate;

async fn handle_dashboard() -> impl IntoResponse {
    Html(DashboardTemplate.render().unwrap())
}

async fn handle_get_graph(
    State(graph): State<Arc<tokio::sync::Mutex<AtheneumGraph>>>,
) -> impl IntoResponse {
    let graph = graph.lock().await;
    let entities = graph
        .with_raw_connection(|conn| {
            let mut stmt = conn.prepare("SELECT id, kind, name, data FROM graph_entities")?;
            let rows = stmt.query_map([], |row| {
                let data_str: String = row.get(3)?;
                let data: serde_json::Value = serde_json::from_str(&data_str).unwrap_or(json!({}));
                Ok(json!({
                    "id": row.get::<_, i64>(0)?,
                    "kind": row.get::<_, String>(1)?,
                    "name": row.get::<_, String>(2)?,
                    "data": data,
                }))
            })?;
            let mut out = Vec::new();
            for r in rows {
                out.push(r?);
            }
            Ok::<_, anyhow::Error>(out)
        })
        .unwrap_or_default();

    let edges = graph
        .with_raw_connection(|conn| {
            let mut stmt = conn.prepare("SELECT from_id, to_id, edge_type FROM graph_edges")?;
            let rows = stmt.query_map([], |row| {
                Ok(json!({
                    "source": row.get::<_, i64>(0)?,
                    "target": row.get::<_, i64>(1)?,
                    "type": row.get::<_, String>(2)?,
                }))
            })?;
            let mut out = Vec::new();
            for r in rows {
                out.push(r?);
            }
            Ok::<_, anyhow::Error>(out)
        })
        .unwrap_or_default();

    Json(json!({ "nodes": entities, "links": edges }))
}

async fn handle_get_lint(
    State(graph): State<Arc<tokio::sync::Mutex<AtheneumGraph>>>,
) -> impl IntoResponse {
    let graph = graph.lock().await;
    let config = LintConfig::default();
    let report = graph.lint_graph(&config).unwrap();
    Json(report)
}

async fn handle_post_dream(
    State(graph): State<Arc<tokio::sync::Mutex<AtheneumGraph>>>,
) -> impl IntoResponse {
    let graph = graph.lock().await;
    let report = graph
        .dream_pass(
            crate::DreamMode::AutoMerge,
            None,
            None,
            &crate::DreamConfig::default(),
        )
        .unwrap();
    Json(report)
}

pub async fn start_web_server(graph: AtheneumGraph, port: u16) -> anyhow::Result<()> {
    let shared_graph = Arc::new(tokio::sync::Mutex::new(graph));
    let app = Router::new()
        .route("/", get(handle_dashboard))
        .route("/api/graph", get(handle_get_graph))
        .route("/api/lint", get(handle_get_lint))
        .route("/api/dream", post(handle_post_dream))
        .with_state(shared_graph);

    let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await?;
    println!("Atheneum Dashboard running at http://127.0.0.1:{}", port);
    axum::serve(listener, app).await?;
    Ok(())
}