1use std::{collections::HashMap, sync::atomic::Ordering, sync::Arc, time::Instant};
4
5use axum::{
6 body::Bytes,
7 extract::{Path, Query, State},
8 http::{HeaderMap, Response, StatusCode},
9 response::{
10 sse::{Event, KeepAlive, Sse},
11 IntoResponse, Json,
12 },
13};
14use uuid::Uuid;
15
16use crate::auth;
17use crate::state::AppState;
18use crate::types::*;
19
20const UI_HTML: &str = include_str!("../assets/ui.html");
23static OPENAPI_JSON: &str = include_str!("../assets/openapi.json");
24const SWAGGER_UI_HTML: &str = include_str!("../assets/swagger.html");
25
26pub async fn health_handler() -> Json<HealthResponse> {
30 Json(HealthResponse {
31 status: "ok",
32 version: env!("CARGO_PKG_VERSION"),
33 })
34}
35
36pub async fn metrics_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
38 state.metrics.refresh_gauges(&state.processor);
39 let body = state.metrics.render(state.processor.metrics());
40 (
41 [("content-type", "text/plain; version=0.0.4; charset=utf-8")],
42 body,
43 )
44}
45
46pub async fn ui_handler() -> impl IntoResponse {
48 ([("content-type", "text/html; charset=utf-8")], UI_HTML)
49}
50
51pub async fn root_handler() -> axum::response::Redirect {
53 axum::response::Redirect::temporary("/ui")
54}
55
56pub async fn openapi_handler() -> impl IntoResponse {
58 let spec = OPENAPI_JSON.replace("{{VERSION}}", env!("CARGO_PKG_VERSION"));
59 ([("content-type", "application/json")], spec)
60}
61
62pub async fn swagger_ui_handler() -> impl IntoResponse {
64 (
65 [("content-type", "text/html; charset=utf-8")],
66 SWAGGER_UI_HTML,
67 )
68}
69
70pub async fn post_signal_handler(
74 State(state): State<Arc<AppState>>,
75 headers: HeaderMap,
76 Json(body): Json<SignalRequest>,
77) -> Result<Json<signal::SignalResponse>, (StatusCode, String)> {
78 auth::check_auth(&state, &headers, "write")?;
79
80 let t0 = Instant::now();
81 state.metrics.signals_total.fetch_add(1, Ordering::Relaxed);
82
83 let source = signal::SignalSource::parse(body.source.as_deref(), signal::SignalSource::Http);
84 let sig = signal::Signal::from_adapter_request(signal::AdapterRequest {
85 source,
86 content: body.content,
87 channel: body.channel,
88 sender: body.sender,
89 metadata: body.metadata,
90 namespace: body.namespace,
91 agent: body.agent,
92 session_id: body.session_id,
93 default_channel: "http".to_string(),
94 default_sender: "apiclient".to_string(),
95 });
96
97 let signal_id = sig.id;
98 let result = state.processor.process(sig).await;
99
100 let elapsed_ms = t0.elapsed().as_millis() as u64;
101 state
102 .metrics
103 .signals_latency_ms_total
104 .fetch_add(elapsed_ms, Ordering::Relaxed);
105
106 let response = match result {
107 Ok(r) => {
108 state.metrics.signals_ok.fetch_add(1, Ordering::Relaxed);
109 tracing::info!(
110 signal_id = %signal_id,
111 latency_ms = elapsed_ms,
112 "signal processed"
113 );
114 r
115 }
116 Err(e) => {
117 state.metrics.signals_error.fetch_add(1, Ordering::Relaxed);
118 tracing::error!(
119 signal_id = %signal_id,
120 latency_ms = elapsed_ms,
121 error = %e,
122 "signal processing failed"
123 );
124 return Err((
125 StatusCode::INTERNAL_SERVER_ERROR,
126 serde_json::json!({
127 "error": "Signal processing failed",
128 "details": e.to_string()
129 })
130 .to_string(),
131 ));
132 }
133 };
134
135 state.cache.lock().await.put(signal_id, response.clone());
136 Ok(Json(response))
137}
138
139pub async fn get_signal_handler(
141 State(state): State<Arc<AppState>>,
142 headers: HeaderMap,
143 Path(id): Path<String>,
144) -> Result<Json<signal::SignalResponse>, (StatusCode, String)> {
145 auth::check_auth(&state, &headers, "read")?;
146
147 let uuid = Uuid::parse_str(&id)
148 .map_err(|_| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {id}")))?;
149
150 let mut cache = state.cache.lock().await;
151 match cache.get(&uuid) {
152 Some(resp) => Ok(Json(resp.clone())),
153 None => Err((
154 StatusCode::NOT_FOUND,
155 format!("Signal {uuid} not found in cache"),
156 )),
157 }
158}
159
160pub async fn search_memory_handler(
162 State(state): State<Arc<AppState>>,
163 headers: HeaderMap,
164 Json(body): Json<SearchRequest>,
165) -> Result<Json<Vec<FactJson>>, (StatusCode, String)> {
166 auth::check_auth(&state, &headers, "read")?;
167
168 state.metrics.search_total.fetch_add(1, Ordering::Relaxed);
169 let t0 = Instant::now();
170 let top_k = body.top_k.unwrap_or(10);
171 let namespace = body.namespace.as_deref();
172 let results = state
173 .processor
174 .search_facts(&body.query, top_k, namespace)
175 .await;
176 tracing::debug!(latency_ms = t0.elapsed().as_millis() as u64, query = %body.query, "memory search");
177
178 let facts = results
179 .into_iter()
180 .map(|r| FactJson {
181 id: r.fact.id,
182 namespace: r.fact.namespace,
183 category: r.fact.category,
184 subject: r.fact.subject,
185 predicate: r.fact.predicate,
186 object: r.fact.object,
187 confidence: r.fact.confidence,
188 distance: Some(r.distance),
189 })
190 .collect();
191
192 Ok(Json(facts))
193}
194
195pub async fn get_facts_handler(
197 State(state): State<Arc<AppState>>,
198 headers: HeaderMap,
199 Query(params): Query<HashMap<String, String>>,
200) -> Result<Json<Vec<FactJson>>, (StatusCode, String)> {
201 auth::check_auth(&state, &headers, "read")?;
202
203 state.metrics.facts_total.fetch_add(1, Ordering::Relaxed);
204 let namespace = params.get("namespace").map(|s| s.as_str());
205 let facts = state
206 .processor
207 .list_facts(namespace)
208 .into_iter()
209 .map(|f| FactJson {
210 id: f.id,
211 namespace: f.namespace,
212 category: f.category,
213 subject: f.subject,
214 predicate: f.predicate,
215 object: f.object,
216 confidence: f.confidence,
217 distance: None,
218 })
219 .collect();
220
221 Ok(Json(facts))
222}
223
224pub async fn get_namespaces_handler(
226 State(state): State<Arc<AppState>>,
227 headers: HeaderMap,
228) -> Result<Json<Vec<NamespaceJson>>, (StatusCode, String)> {
229 auth::check_auth(&state, &headers, "read")?;
230
231 let namespaces = state
232 .processor
233 .list_namespaces()
234 .into_iter()
235 .map(|n| NamespaceJson {
236 namespace: n.namespace,
237 fact_count: n.fact_count,
238 episode_count: n.episode_count,
239 })
240 .collect();
241
242 Ok(Json(namespaces))
243}
244
245pub async fn export_memory_handler(
247 State(state): State<Arc<AppState>>,
248 headers: HeaderMap,
249) -> Result<Json<ExportJson>, (StatusCode, String)> {
250 auth::check_auth(&state, &headers, "read")?;
251
252 let facts = state.processor.export_facts().map_err(|e| {
253 (
254 StatusCode::INTERNAL_SERVER_ERROR,
255 format!("Export failed: {e}"),
256 )
257 })?;
258 let episodes = state.processor.export_episodes().map_err(|e| {
259 (
260 StatusCode::INTERNAL_SERVER_ERROR,
261 format!("Export failed: {e}"),
262 )
263 })?;
264
265 Ok(Json(ExportJson {
266 version: env!("CARGO_PKG_VERSION").to_string(),
267 exported_at: chrono::Utc::now().to_rfc3339(),
268 facts,
269 episodes,
270 }))
271}
272
273pub async fn import_memory_handler(
275 State(state): State<Arc<AppState>>,
276 headers: HeaderMap,
277 Json(body): Json<ImportRequest>,
278) -> Result<Json<ImportResponse>, (StatusCode, String)> {
279 auth::check_auth(&state, &headers, "write")?;
280
281 if body.dry_run {
282 return Ok(Json(ImportResponse {
283 facts_imported: body.facts.len(),
284 episodes_imported: body.episodes.len(),
285 facts_already_existed: 0,
286 episodes_already_existed: 0,
287 embedded: 0,
288 embed_failed: 0,
289 }));
290 }
291
292 let (facts_imported, new_fact_indices) =
293 state.processor.import_facts(&body.facts).map_err(|e| {
294 (
295 StatusCode::INTERNAL_SERVER_ERROR,
296 format!("Import failed: {e}"),
297 )
298 })?;
299 let episodes_imported = state
300 .processor
301 .import_episodes(&body.episodes)
302 .map_err(|e| {
303 (
304 StatusCode::INTERNAL_SERVER_ERROR,
305 format!("Import failed: {e}"),
306 )
307 })?;
308
309 let mut embedded = 0;
310 let mut embed_failed = 0;
311 if !new_fact_indices.is_empty() {
312 let new_facts: Vec<signal::ExportedFact> = new_fact_indices
313 .iter()
314 .map(|&idx| body.facts[idx].clone())
315 .collect();
316 let (e, f) = state.processor.reembed_facts(&new_facts).await;
317 embedded = e;
318 embed_failed = f;
319 }
320
321 Ok(Json(ImportResponse {
322 facts_imported,
323 episodes_imported,
324 facts_already_existed: body.facts.len() - facts_imported,
325 episodes_already_existed: body.episodes.len() - episodes_imported,
326 embedded,
327 embed_failed,
328 }))
329}
330
331pub async fn list_schedules_handler(
333 State(state): State<Arc<AppState>>,
334 headers: HeaderMap,
335 Query(params): Query<HashMap<String, String>>,
336) -> Result<Json<Vec<serde_json::Value>>, (StatusCode, String)> {
337 auth::check_auth(&state, &headers, "read")?;
338
339 let namespace = params.get("namespace").map(|s| s.as_str());
340 let intents = state
341 .processor
342 .list_scheduled_intents(namespace)
343 .map_err(|e| {
344 (
345 StatusCode::INTERNAL_SERVER_ERROR,
346 format!("Failed to list schedules: {e}"),
347 )
348 })?;
349
350 let json: Vec<serde_json::Value> = intents
351 .iter()
352 .map(|i| {
353 serde_json::json!({
354 "id": i.id,
355 "description": i.description,
356 "cron": i.cron,
357 "namespace": i.namespace,
358 "created_at": i.created_at,
359 "status": i.status,
360 })
361 })
362 .collect();
363
364 Ok(Json(json))
365}
366
367pub async fn cancel_schedule_handler(
369 State(state): State<Arc<AppState>>,
370 headers: HeaderMap,
371 Path(id): Path<String>,
372) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
373 auth::check_auth(&state, &headers, "write")?;
374
375 let cancelled = state.processor.cancel_scheduled_intent(&id).map_err(|e| {
376 (
377 StatusCode::INTERNAL_SERVER_ERROR,
378 format!("Failed to cancel schedule: {e}"),
379 )
380 })?;
381
382 if cancelled {
383 Ok(Json(serde_json::json!({"cancelled": true, "id": id})))
384 } else {
385 Err((
386 StatusCode::NOT_FOUND,
387 format!("No scheduled intent found with ID: {id}"),
388 ))
389 }
390}
391
392pub async fn sse_events_handler(
394 State(state): State<Arc<AppState>>,
395 headers: HeaderMap,
396) -> Result<
397 Sse<impl futures_core::Stream<Item = Result<Event, std::convert::Infallible>>>,
398 (StatusCode, String),
399> {
400 auth::check_auth(&state, &headers, "read")?;
401
402 let mut signal_rx = state.processor.subscribe_events();
403 let mut notif_rx = state.processor.notification_router().map(|r| r.subscribe());
404
405 let stream = async_stream::stream! {
406 loop {
407 tokio::select! {
408 result = signal_rx.recv() => {
409 match result {
410 Ok(event) => {
411 let payload = serde_json::json!({
412 "type": "signal",
413 "signal_id": event.signal_id.to_string(),
414 "source": format!("{:?}", event.source),
415 "status": format!("{:?}", event.status),
416 "response": event.response,
417 "facts_used": event.facts_used,
418 "episodes_used": event.episodes_used,
419 });
420 yield Ok(Event::default()
421 .event("signal")
422 .json_data(payload)
423 .unwrap_or_else(|_| Event::default().data("{}")));
424 }
425 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
426 tracing::warn!(skipped = n, "SSE signal stream lagged");
427 yield Ok(Event::default()
428 .event("error")
429 .data(format!("{{\"lagged\":{n}}}")));
430 }
431 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
432 }
433 }
434 result = async {
435 match notif_rx.as_mut() {
436 Some(rx) => rx.recv().await,
437 None => std::future::pending().await,
438 }
439 } => {
440 match result {
441 Ok(notification) => {
442 let payload = serde_json::json!({
443 "type": "proactive",
444 "content": notification.content,
445 "triggered_by": notification.triggered_by,
446 "priority": notification.priority,
447 "agent": notification.agent,
448 });
449 yield Ok(Event::default()
450 .event("notification")
451 .json_data(payload)
452 .unwrap_or_else(|_| Event::default().data("{}")));
453 }
454 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
455 tracing::warn!(skipped = n, "SSE notification stream lagged");
456 yield Ok(Event::default()
457 .event("error")
458 .data(format!("{{\"lagged\":{n}}}")));
459 }
460 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
461 notif_rx = None;
462 }
463 }
464 }
465 }
466 }
467 };
468
469 Ok(Sse::new(stream).keep_alive(KeepAlive::default()))
470}
471
472pub async fn post_webhook_handler(
479 State(state): State<Arc<AppState>>,
480 Path(id): Path<String>,
481 headers: HeaderMap,
482 body: Bytes,
483) -> impl IntoResponse {
484 let transport = match state.webhook_handlers.get(&id) {
485 Some(t) => t,
486 None => {
487 return Response::builder()
488 .status(StatusCode::NOT_FOUND)
489 .body(format!("Unknown transport ID: {id}"))
490 .unwrap()
491 .into_response();
492 }
493 };
494
495 let resp = transport.handle_request(&headers, &body).await;
496
497 Response::builder()
498 .status(StatusCode::from_u16(resp.status).unwrap_or(StatusCode::OK))
499 .header("content-type", resp.content_type)
500 .body(resp.body)
501 .unwrap()
502 .into_response()
503}