Skip to main content

lean_ctx/http_server/
context_views.rs

1use std::collections::{HashMap, HashSet};
2
3use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json};
4use serde::{Deserialize, Serialize};
5
6use crate::core::context_os::{ContextEventKindV1, ContextEventV1};
7
8#[derive(Deserialize)]
9pub struct SummaryQuery {
10    #[serde(rename = "workspaceId")]
11    pub workspace_id: Option<String>,
12    #[serde(rename = "channelId")]
13    pub channel_id: Option<String>,
14    pub limit: Option<usize>,
15}
16
17#[derive(Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ContextSummary {
20    pub workspace_id: String,
21    pub channel_id: String,
22    pub total_events: usize,
23    pub latest_version: i64,
24    pub active_agents: Vec<String>,
25    pub recent_decisions: Vec<DecisionSummary>,
26    pub knowledge_delta: Vec<KnowledgeDelta>,
27    pub conflict_alerts: Vec<ConflictAlert>,
28    pub event_counts_by_kind: HashMap<String, usize>,
29}
30
31#[derive(Clone, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct DecisionSummary {
34    pub agent: String,
35    pub tool: String,
36    pub action: Option<String>,
37    pub reasoning: Option<String>,
38    pub timestamp: String,
39}
40
41#[derive(Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct KnowledgeDelta {
44    pub category: String,
45    pub key: String,
46    pub agent: String,
47    pub timestamp: String,
48}
49
50#[derive(Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct ConflictAlert {
53    pub category: String,
54    pub key: String,
55    pub agents: Vec<String>,
56}
57
58#[derive(Deserialize)]
59pub struct SearchQuery {
60    pub q: String,
61    #[serde(rename = "workspaceId")]
62    pub workspace_id: Option<String>,
63    #[serde(rename = "channelId")]
64    pub channel_id: Option<String>,
65    pub limit: Option<usize>,
66}
67
68pub async fn v1_events_search(Query(q): Query<SearchQuery>) -> impl IntoResponse {
69    let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
70    let limit = q.limit.unwrap_or(20).min(100);
71
72    let rt = crate::core::context_os::runtime();
73    let results = rt.bus.search(&ws, q.channel_id.as_deref(), &q.q, limit);
74
75    (
76        StatusCode::OK,
77        Json(serde_json::json!({
78            "query": q.q,
79            "workspaceId": ws,
80            "channelId": q.channel_id,
81            "results": results,
82            "count": results.len(),
83        })),
84    )
85}
86
87#[derive(Deserialize)]
88pub struct LineageQuery {
89    pub id: i64,
90    pub depth: Option<usize>,
91}
92
93pub async fn v1_event_lineage(Query(q): Query<LineageQuery>) -> impl IntoResponse {
94    let depth = q.depth.unwrap_or(20).min(50);
95
96    let rt = crate::core::context_os::runtime();
97    let chain = rt.bus.lineage(q.id, depth);
98
99    (
100        StatusCode::OK,
101        Json(serde_json::json!({
102            "eventId": q.id,
103            "chain": chain,
104            "depth": chain.len(),
105        })),
106    )
107}
108
109pub async fn v1_context_summary(Query(q): Query<SummaryQuery>) -> impl IntoResponse {
110    let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
111    let ch = q.channel_id.unwrap_or_else(|| "default".to_string());
112    let limit = q.limit.unwrap_or(100).min(500);
113
114    let rt = crate::core::context_os::runtime();
115    let events = rt.bus.read(&ws, &ch, 0, limit);
116
117    let summary = build_summary(&ws, &ch, &events);
118    (
119        StatusCode::OK,
120        Json(serde_json::to_value(summary).unwrap_or_default()),
121    )
122}
123
124fn build_summary(ws: &str, ch: &str, events: &[ContextEventV1]) -> ContextSummary {
125    let mut agents: HashSet<String> = HashSet::new();
126    let mut kind_counts: HashMap<String, usize> = HashMap::new();
127    let mut decisions = Vec::new();
128    let mut knowledge_deltas = Vec::new();
129    let mut latest_version: i64 = 0;
130
131    // Track knowledge writes per category/key for conflict detection.
132    let mut knowledge_writers: HashMap<(String, String), HashSet<String>> = HashMap::new();
133
134    for ev in events {
135        if let Some(ref actor) = ev.actor {
136            agents.insert(actor.clone());
137        }
138        *kind_counts.entry(ev.kind.clone()).or_insert(0) += 1;
139        latest_version = latest_version.max(ev.version);
140
141        let p = &ev.payload;
142        let tool = p
143            .get("tool")
144            .and_then(|v| v.as_str())
145            .unwrap_or("")
146            .to_string();
147        let action = p.get("action").and_then(|v| v.as_str()).map(String::from);
148        let reasoning = p
149            .get("reasoning")
150            .and_then(|v| v.as_str())
151            .map(String::from);
152
153        if ev.kind == ContextEventKindV1::SessionMutated.as_str()
154            || ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str()
155        {
156            decisions.push(DecisionSummary {
157                agent: ev.actor.clone().unwrap_or_default(),
158                tool: tool.clone(),
159                action: action.clone(),
160                reasoning,
161                timestamp: ev.timestamp.to_rfc3339(),
162            });
163        }
164
165        if ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str() {
166            let cat = p
167                .get("category")
168                .and_then(|v| v.as_str())
169                .unwrap_or("")
170                .to_string();
171            let key = p
172                .get("key")
173                .and_then(|v| v.as_str())
174                .unwrap_or("")
175                .to_string();
176            knowledge_deltas.push(KnowledgeDelta {
177                category: cat.clone(),
178                key: key.clone(),
179                agent: ev.actor.clone().unwrap_or_default(),
180                timestamp: ev.timestamp.to_rfc3339(),
181            });
182
183            if let Some(ref actor) = ev.actor {
184                knowledge_writers
185                    .entry((cat, key))
186                    .or_default()
187                    .insert(actor.clone());
188            }
189        }
190    }
191
192    let conflict_alerts: Vec<ConflictAlert> = knowledge_writers
193        .into_iter()
194        .filter(|(_, writers)| writers.len() > 1)
195        .map(|((cat, key), writers)| ConflictAlert {
196            category: cat,
197            key,
198            agents: writers.into_iter().collect(),
199        })
200        .collect();
201
202    let recent_limit = 10;
203    let decisions: Vec<_> = if decisions.len() > recent_limit {
204        decisions[decisions.len() - recent_limit..].to_vec()
205    } else {
206        decisions
207    };
208
209    ContextSummary {
210        workspace_id: ws.to_string(),
211        channel_id: ch.to_string(),
212        total_events: events.len(),
213        latest_version,
214        active_agents: agents.into_iter().collect(),
215        recent_decisions: decisions,
216        knowledge_delta: knowledge_deltas,
217        conflict_alerts,
218        event_counts_by_kind: kind_counts,
219    }
220}