1use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, State};
13use serde::Serialize;
15use serde_json::{json, Value};
16use std::collections::HashMap;
17
18#[utoipa::path(
24 get,
25 path = "/v1/output/targets",
26 tag = "outputs",
27 responses(
28 (status = 200, description = "Output targets", body = HashMap<String, serde_json::Value>),
29 (status = 500, description = "Internal server error")
30 )
31)]
32pub async fn get_targets(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
33 let agent_service = state
35 .agent_service
36 .as_ref()
37 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
38
39 let agent_ids = agent_service
40 .list_agents()
41 .await
42 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
43
44 let mut motor_agents = Vec::new();
46 for agent_id in agent_ids {
47 if let Ok(props) = agent_service.get_agent_properties(&agent_id).await {
49 if props.capabilities.contains_key("motor")
51 || props.capabilities.contains_key("output")
52 || props.agent_type.to_lowercase().contains("motor")
53 {
54 motor_agents.push(agent_id);
55 }
56 }
57 }
58
59 let mut response = HashMap::new();
60 response.insert("targets".to_string(), json!(motor_agents));
61
62 Ok(Json(response))
63}
64
65#[utoipa::path(
67 post,
68 path = "/v1/output/configure",
69 tag = "outputs",
70 responses(
71 (status = 200, description = "Outputs configured", body = HashMap<String, String>),
72 (status = 500, description = "Internal server error")
73 )
74)]
75pub async fn post_configure(
76 State(_state): State<ApiState>,
77 Json(request): Json<HashMap<String, Value>>,
78) -> ApiResult<Json<HashMap<String, String>>> {
79 let config = request
81 .get("config")
82 .ok_or_else(|| ApiError::invalid_input("Missing 'config' field"))?;
83
84 if !config.is_object() {
87 return Err(ApiError::invalid_input("'config' must be an object"));
88 }
89
90 tracing::info!(target: "feagi-api", "Output configuration updated: {} targets",
91 config.as_object().map(|o| o.len()).unwrap_or(0));
92
93 Ok(Json(HashMap::from([(
94 "message".to_string(),
95 "Outputs configured successfully".to_string(),
96 )])))
97}
98
99#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
105pub struct MotorTapSample {
106 pub x: u32,
107 pub y: u32,
108 pub z: u32,
109 pub potential: f32,
110}
111
112#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
114pub struct MotorTapArea {
115 pub cortical_id: String,
116 pub cortical_idx: u32,
117 pub neuron_count: usize,
118 pub samples: Vec<MotorTapSample>,
119}
120
121#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
123pub struct MotorTapAgent {
124 pub agent_id: String,
125 pub burst_num: u64,
126 pub timestamp_ms: i64,
127 pub byte_count: usize,
128 pub published: bool,
129 pub last_error: String,
130 pub subscribed_cortical_ids: Vec<String>,
131}
132
133#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
135pub struct MotorSnapshotResponse {
136 pub burst_num: u64,
139 pub timestamp_ms: i64,
141 pub has_data: bool,
143 pub total_areas: usize,
145 pub total_neurons: usize,
147 pub areas: Vec<MotorTapArea>,
149 pub agents: Vec<MotorTapAgent>,
151}
152
153#[utoipa::path(
163 get,
164 path = "/v1/output/motor_snapshot/last",
165 tag = "outputs",
166 params(
167 ("agent_id" = Option<String>, Query, description = "Filter agents by id"),
168 ("cortical_id" = Option<String>, Query, description = "Filter motor areas to one cortical id (base64)")
169 ),
170 responses(
171 (status = 200, description = "Latest motor pipeline snapshot", body = MotorSnapshotResponse),
172 (status = 500, description = "Internal server error")
173 )
174)]
175pub async fn get_motor_snapshot_last(
176 State(_state): State<ApiState>,
177 axum::extract::Query(query): axum::extract::Query<HashMap<String, String>>,
178) -> ApiResult<Json<MotorSnapshotResponse>> {
179 let snap = feagi_npu_burst_engine::BurstTaps::instance().motor_snapshot();
180 let agent_filter = query.get("agent_id").cloned();
181 let area_filter = query.get("cortical_id").cloned();
182
183 let mut areas: Vec<MotorTapArea> = snap
184 .areas
185 .into_iter()
186 .map(|a| MotorTapArea {
187 cortical_id: a.cortical_id,
188 cortical_idx: a.cortical_idx,
189 neuron_count: a.neuron_count,
190 samples: a
191 .samples
192 .into_iter()
193 .map(|s| MotorTapSample {
194 x: s.x,
195 y: s.y,
196 z: s.z,
197 potential: s.potential,
198 })
199 .collect(),
200 })
201 .collect();
202
203 if let Some(ref cid) = area_filter {
204 if !cid.is_empty() {
205 areas.retain(|a| a.cortical_id == *cid);
206 }
207 }
208
209 let total_areas = areas.len();
210 let total_neurons: usize = areas.iter().map(|a| a.neuron_count).sum();
211 let has_data = total_areas > 0 && snap.burst_num > 0;
212
213 let mut agents: Vec<MotorTapAgent> = snap
214 .per_agent
215 .into_iter()
216 .filter(|(id, _)| match &agent_filter {
217 Some(filter) => filter == id,
218 None => true,
219 })
220 .map(|(agent_id, stats)| MotorTapAgent {
221 agent_id,
222 burst_num: stats.burst_num,
223 timestamp_ms: stats.timestamp_ms,
224 byte_count: stats.byte_count,
225 published: stats.published,
226 last_error: stats.last_error,
227 subscribed_cortical_ids: stats.subscribed_cortical_ids,
228 })
229 .collect();
230 agents.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
231
232 Ok(Json(MotorSnapshotResponse {
233 burst_num: snap.burst_num,
234 timestamp_ms: snap.timestamp_ms,
235 has_data,
236 total_areas,
237 total_neurons,
238 areas,
239 agents,
240 }))
241}