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    pub limit: Option<usize>,
64}
65
66pub async fn v1_events_search(Query(q): Query<SearchQuery>) -> impl IntoResponse {
67    let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
68    let limit = q.limit.unwrap_or(20).min(100);
69
70    let rt = crate::core::context_os::runtime();
71    let results = rt.bus.search(&ws, &q.q, limit);
72
73    (
74        StatusCode::OK,
75        Json(serde_json::json!({
76            "query": q.q,
77            "workspaceId": ws,
78            "results": results,
79            "count": results.len(),
80        })),
81    )
82}
83
84#[derive(Deserialize)]
85pub struct LineageQuery {
86    pub id: i64,
87    pub depth: Option<usize>,
88}
89
90pub async fn v1_event_lineage(Query(q): Query<LineageQuery>) -> impl IntoResponse {
91    let depth = q.depth.unwrap_or(20).min(50);
92
93    let rt = crate::core::context_os::runtime();
94    let chain = rt.bus.lineage(q.id, depth);
95
96    (
97        StatusCode::OK,
98        Json(serde_json::json!({
99            "eventId": q.id,
100            "chain": chain,
101            "depth": chain.len(),
102        })),
103    )
104}
105
106pub async fn v1_context_summary(Query(q): Query<SummaryQuery>) -> impl IntoResponse {
107    let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
108    let ch = q.channel_id.unwrap_or_else(|| "default".to_string());
109    let limit = q.limit.unwrap_or(100).min(500);
110
111    let rt = crate::core::context_os::runtime();
112    let events = rt.bus.read(&ws, &ch, 0, limit);
113
114    let summary = build_summary(&ws, &ch, &events);
115    (
116        StatusCode::OK,
117        Json(serde_json::to_value(summary).unwrap_or_default()),
118    )
119}
120
121fn build_summary(ws: &str, ch: &str, events: &[ContextEventV1]) -> ContextSummary {
122    let mut agents: HashSet<String> = HashSet::new();
123    let mut kind_counts: HashMap<String, usize> = HashMap::new();
124    let mut decisions = Vec::new();
125    let mut knowledge_deltas = Vec::new();
126    let mut latest_version: i64 = 0;
127
128    // Track knowledge writes per category/key for conflict detection.
129    let mut knowledge_writers: HashMap<(String, String), HashSet<String>> = HashMap::new();
130
131    for ev in events {
132        if let Some(ref actor) = ev.actor {
133            agents.insert(actor.clone());
134        }
135        *kind_counts.entry(ev.kind.clone()).or_insert(0) += 1;
136        latest_version = latest_version.max(ev.version);
137
138        let p = &ev.payload;
139        let tool = p
140            .get("tool")
141            .and_then(|v| v.as_str())
142            .unwrap_or("")
143            .to_string();
144        let action = p.get("action").and_then(|v| v.as_str()).map(String::from);
145        let reasoning = p
146            .get("reasoning")
147            .and_then(|v| v.as_str())
148            .map(String::from);
149
150        if ev.kind == ContextEventKindV1::SessionMutated.as_str()
151            || ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str()
152        {
153            decisions.push(DecisionSummary {
154                agent: ev.actor.clone().unwrap_or_default(),
155                tool: tool.clone(),
156                action: action.clone(),
157                reasoning,
158                timestamp: ev.timestamp.to_rfc3339(),
159            });
160        }
161
162        if ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str() {
163            let cat = p
164                .get("category")
165                .and_then(|v| v.as_str())
166                .unwrap_or("")
167                .to_string();
168            let key = p
169                .get("key")
170                .and_then(|v| v.as_str())
171                .unwrap_or("")
172                .to_string();
173            knowledge_deltas.push(KnowledgeDelta {
174                category: cat.clone(),
175                key: key.clone(),
176                agent: ev.actor.clone().unwrap_or_default(),
177                timestamp: ev.timestamp.to_rfc3339(),
178            });
179
180            if let Some(ref actor) = ev.actor {
181                knowledge_writers
182                    .entry((cat, key))
183                    .or_default()
184                    .insert(actor.clone());
185            }
186        }
187    }
188
189    let conflict_alerts: Vec<ConflictAlert> = knowledge_writers
190        .into_iter()
191        .filter(|(_, writers)| writers.len() > 1)
192        .map(|((cat, key), writers)| ConflictAlert {
193            category: cat,
194            key,
195            agents: writers.into_iter().collect(),
196        })
197        .collect();
198
199    let recent_limit = 10;
200    let decisions: Vec<_> = if decisions.len() > recent_limit {
201        decisions[decisions.len() - recent_limit..].to_vec()
202    } else {
203        decisions
204    };
205
206    ContextSummary {
207        workspace_id: ws.to_string(),
208        channel_id: ch.to_string(),
209        total_events: events.len(),
210        latest_version,
211        active_agents: agents.into_iter().collect(),
212        recent_decisions: decisions,
213        knowledge_delta: knowledge_deltas,
214        conflict_alerts,
215        event_counts_by_kind: kind_counts,
216    }
217}