use crate::common::ApiState;
use crate::common::{ApiError, ApiResult, Json, State};
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::HashMap;
#[utoipa::path(
get,
path = "/v1/output/targets",
tag = "outputs",
responses(
(status = 200, description = "Output targets", body = HashMap<String, serde_json::Value>),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_targets(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
let agent_service = state
.agent_service
.as_ref()
.ok_or_else(|| ApiError::internal("Agent service not available"))?;
let agent_ids = agent_service
.list_agents()
.await
.map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
let mut motor_agents = Vec::new();
for agent_id in agent_ids {
if let Ok(props) = agent_service.get_agent_properties(&agent_id).await {
if props.capabilities.contains_key("motor")
|| props.capabilities.contains_key("output")
|| props.agent_type.to_lowercase().contains("motor")
{
motor_agents.push(agent_id);
}
}
}
let mut response = HashMap::new();
response.insert("targets".to_string(), json!(motor_agents));
Ok(Json(response))
}
#[utoipa::path(
post,
path = "/v1/output/configure",
tag = "outputs",
responses(
(status = 200, description = "Outputs configured", body = HashMap<String, String>),
(status = 500, description = "Internal server error")
)
)]
pub async fn post_configure(
State(_state): State<ApiState>,
Json(request): Json<HashMap<String, Value>>,
) -> ApiResult<Json<HashMap<String, String>>> {
let config = request
.get("config")
.ok_or_else(|| ApiError::invalid_input("Missing 'config' field"))?;
if !config.is_object() {
return Err(ApiError::invalid_input("'config' must be an object"));
}
tracing::info!(target: "feagi-api", "Output configuration updated: {} targets",
config.as_object().map(|o| o.len()).unwrap_or(0));
Ok(Json(HashMap::from([(
"message".to_string(),
"Outputs configured successfully".to_string(),
)])))
}
#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct MotorTapSample {
pub x: u32,
pub y: u32,
pub z: u32,
pub potential: f32,
}
#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct MotorTapArea {
pub cortical_id: String,
pub cortical_idx: u32,
pub neuron_count: usize,
pub samples: Vec<MotorTapSample>,
}
#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct MotorTapAgent {
pub agent_id: String,
pub burst_num: u64,
pub timestamp_ms: i64,
pub byte_count: usize,
pub published: bool,
pub last_error: String,
pub subscribed_cortical_ids: Vec<String>,
}
#[derive(Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct MotorSnapshotResponse {
pub burst_num: u64,
pub timestamp_ms: i64,
pub has_data: bool,
pub total_areas: usize,
pub total_neurons: usize,
pub areas: Vec<MotorTapArea>,
pub agents: Vec<MotorTapAgent>,
}
#[utoipa::path(
get,
path = "/v1/output/motor_snapshot/last",
tag = "outputs",
params(
("agent_id" = Option<String>, Query, description = "Filter agents by id"),
("cortical_id" = Option<String>, Query, description = "Filter motor areas to one cortical id (base64)")
),
responses(
(status = 200, description = "Latest motor pipeline snapshot", body = MotorSnapshotResponse),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_motor_snapshot_last(
State(_state): State<ApiState>,
axum::extract::Query(query): axum::extract::Query<HashMap<String, String>>,
) -> ApiResult<Json<MotorSnapshotResponse>> {
let snap = feagi_npu_burst_engine::BurstTaps::instance().motor_snapshot();
let agent_filter = query.get("agent_id").cloned();
let area_filter = query.get("cortical_id").cloned();
let mut areas: Vec<MotorTapArea> = snap
.areas
.into_iter()
.map(|a| MotorTapArea {
cortical_id: a.cortical_id,
cortical_idx: a.cortical_idx,
neuron_count: a.neuron_count,
samples: a
.samples
.into_iter()
.map(|s| MotorTapSample {
x: s.x,
y: s.y,
z: s.z,
potential: s.potential,
})
.collect(),
})
.collect();
if let Some(ref cid) = area_filter {
if !cid.is_empty() {
areas.retain(|a| a.cortical_id == *cid);
}
}
let total_areas = areas.len();
let total_neurons: usize = areas.iter().map(|a| a.neuron_count).sum();
let has_data = total_areas > 0 && snap.burst_num > 0;
let mut agents: Vec<MotorTapAgent> = snap
.per_agent
.into_iter()
.filter(|(id, _)| match &agent_filter {
Some(filter) => filter == id,
None => true,
})
.map(|(agent_id, stats)| MotorTapAgent {
agent_id,
burst_num: stats.burst_num,
timestamp_ms: stats.timestamp_ms,
byte_count: stats.byte_count,
published: stats.published,
last_error: stats.last_error,
subscribed_cortical_ids: stats.subscribed_cortical_ids,
})
.collect();
agents.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
Ok(Json(MotorSnapshotResponse {
burst_num: snap.burst_num,
timestamp_ms: snap.timestamp_ms,
has_data,
total_areas,
total_neurons,
areas,
agents,
}))
}