Skip to main content

feagi_api/endpoints/
input.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Input API Endpoints - Exact port from Python `/v1/input/*`
5
6// Removed - using crate::common::State instead
7use crate::common::ApiState;
8use crate::common::{ApiError, ApiResult, Json, State};
9use serde::Serialize;
10use std::collections::HashMap;
11
12/// Get vision input configuration and settings.
13#[utoipa::path(
14    get,
15    path = "/v1/input/vision",
16    tag = "input",
17    responses(
18        (status = 200, description = "Vision input configuration", body = HashMap<String, serde_json::Value>),
19        (status = 500, description = "Internal server error")
20    )
21)]
22pub async fn get_vision(
23    State(_state): State<ApiState>,
24) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
25    // TODO: Get vision input configuration
26    Ok(Json(HashMap::new()))
27}
28
29/// Update vision input configuration.
30#[utoipa::path(
31    post,
32    path = "/v1/input/vision",
33    tag = "input",
34    responses(
35        (status = 200, description = "Vision input updated", content_type = "application/json"),
36        (status = 500, description = "Not yet implemented")
37    )
38)]
39pub async fn post_vision(
40    State(_state): State<ApiState>,
41    Json(_req): Json<HashMap<String, serde_json::Value>>,
42) -> ApiResult<Json<HashMap<String, String>>> {
43    Err(ApiError::internal("Not yet implemented"))
44}
45
46/// Get list of available input sources (vision, audio, etc.).
47#[utoipa::path(get, path = "/v1/input/sources", tag = "input")]
48pub async fn get_sources(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
49    Ok(Json(vec!["vision".to_string()]))
50}
51
52/// Configure input sources and their parameters.
53#[utoipa::path(post, path = "/v1/input/configure", tag = "input")]
54pub async fn post_configure(
55    State(_state): State<ApiState>,
56    Json(_req): Json<HashMap<String, serde_json::Value>>,
57) -> ApiResult<Json<HashMap<String, String>>> {
58    Ok(Json(HashMap::from([(
59        "message".to_string(),
60        "Input configured".to_string(),
61    )])))
62}
63
64// ============================================================================
65// SENSORY INPUT SNAPSHOT (runtime tap)
66// ============================================================================
67
68/// Single voxel sample in a sensory area.
69#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
70pub struct SensorTapSample {
71    pub x: u32,
72    pub y: u32,
73    pub z: u32,
74    pub potential: f32,
75}
76
77/// Per-cortical-area sensory activity captured by the sensor tap.
78#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
79pub struct SensorTapArea {
80    pub cortical_id: String,
81    pub cortical_idx: u32,
82    pub neuron_count: usize,
83    pub samples: Vec<SensorTapSample>,
84}
85
86/// Response payload for `GET /v1/input/sensor_snapshot/last`.
87#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
88pub struct SensorSnapshotResponse {
89    /// Burst counter when the snapshot was captured. Zero if no sensory traffic
90    /// has been received since FEAGI started.
91    pub burst_num: u64,
92    /// Wall-clock millisecond timestamp when the snapshot was captured.
93    pub timestamp_ms: i64,
94    /// Convenience flag for clients - true when at least one area was captured.
95    pub has_data: bool,
96    /// Total cortical areas with sensory input this burst.
97    pub total_areas: usize,
98    /// Total samples across all sensory areas this burst.
99    pub total_neurons: usize,
100    /// Per-area sensory samples, ordered as decoded by the burst loop.
101    pub areas: Vec<SensorTapArea>,
102}
103
104/// Get the most recent sensory input decoded by the burst loop.
105///
106/// This taps directly into the sensory ingestion pipeline, surfacing exactly
107/// what the burst loop consumed. Optional `cortical_id` query parameter
108/// filters the `areas` list to a single cortical area.
109#[utoipa::path(
110    get,
111    path = "/v1/input/sensor_snapshot/last",
112    tag = "input",
113    params(
114        ("cortical_id" = Option<String>, Query, description = "Filter areas by base64 cortical id")
115    ),
116    responses(
117        (status = 200, description = "Latest sensory pipeline snapshot", body = SensorSnapshotResponse),
118        (status = 500, description = "Internal server error")
119    )
120)]
121pub async fn get_sensor_snapshot_last(
122    State(_state): State<ApiState>,
123    axum::extract::Query(query): axum::extract::Query<HashMap<String, String>>,
124) -> ApiResult<Json<SensorSnapshotResponse>> {
125    let snap = feagi_npu_burst_engine::BurstTaps::instance().sensor_snapshot();
126    let cortical_filter = query.get("cortical_id").cloned();
127
128    let mut areas: Vec<SensorTapArea> = snap
129        .areas
130        .into_iter()
131        .filter(|a| match &cortical_filter {
132            Some(filter) => filter == &a.cortical_id,
133            None => true,
134        })
135        .map(|a| SensorTapArea {
136            cortical_id: a.cortical_id,
137            cortical_idx: a.cortical_idx,
138            neuron_count: a.neuron_count,
139            samples: a
140                .samples
141                .into_iter()
142                .map(|s| SensorTapSample {
143                    x: s.x,
144                    y: s.y,
145                    z: s.z,
146                    potential: s.potential,
147                })
148                .collect(),
149        })
150        .collect();
151    areas.sort_by(|a, b| a.cortical_id.cmp(&b.cortical_id));
152
153    let total_areas = areas.len();
154    let total_neurons: usize = areas.iter().map(|a| a.neuron_count).sum();
155    let has_data = total_areas > 0 && snap.burst_num > 0;
156
157    Ok(Json(SensorSnapshotResponse {
158        burst_num: snap.burst_num,
159        timestamp_ms: snap.timestamp_ms,
160        has_data,
161        total_areas,
162        total_neurons,
163        areas,
164    }))
165}