Skip to main content

feagi_api/endpoints/
outputs.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5 * FEAGI v1 Outputs API
6 *
7 * Endpoints for output/motor target configuration
8 * Maps to Python: feagi/api/v1/outputs.py
9 */
10
11use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, State};
13// Removed - using crate::common::State instead
14use serde_json::{json, Value};
15use std::collections::HashMap;
16
17// ============================================================================
18// OUTPUT TARGETS
19// ============================================================================
20
21/// Get available output targets from connected motor/output agents.
22#[utoipa::path(
23    get,
24    path = "/v1/output/targets",
25    tag = "outputs",
26    responses(
27        (status = 200, description = "Output targets", body = HashMap<String, serde_json::Value>),
28        (status = 500, description = "Internal server error")
29    )
30)]
31pub async fn get_targets(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, Value>>> {
32    // Get motor/output capable agents from PNS
33    let agent_service = state
34        .agent_service
35        .as_ref()
36        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
37
38    let agent_ids = agent_service
39        .list_agents()
40        .await
41        .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
42
43    // Filter for agents with motor/output capabilities
44    let mut motor_agents = Vec::new();
45    for agent_id in agent_ids {
46        // Get agent properties to check capabilities
47        if let Ok(props) = agent_service.get_agent_properties(&agent_id).await {
48            // Check if agent has motor capabilities
49            if props.capabilities.contains_key("motor")
50                || props.capabilities.contains_key("output")
51                || props.agent_type.to_lowercase().contains("motor")
52            {
53                motor_agents.push(agent_id);
54            }
55        }
56    }
57
58    let mut response = HashMap::new();
59    response.insert("targets".to_string(), json!(motor_agents));
60
61    Ok(Json(response))
62}
63
64/// Configure output targets and motor agent connections.
65#[utoipa::path(
66    post,
67    path = "/v1/output/configure",
68    tag = "outputs",
69    responses(
70        (status = 200, description = "Outputs configured", body = HashMap<String, String>),
71        (status = 500, description = "Internal server error")
72    )
73)]
74pub async fn post_configure(
75    State(_state): State<ApiState>,
76    Json(request): Json<HashMap<String, Value>>,
77) -> ApiResult<Json<HashMap<String, String>>> {
78    // Extract configuration from request
79    let config = request
80        .get("config")
81        .ok_or_else(|| ApiError::invalid_input("Missing 'config' field"))?;
82
83    // TODO: Store output configuration in runtime state
84    // For now, just validate the structure
85    if !config.is_object() {
86        return Err(ApiError::invalid_input("'config' must be an object"));
87    }
88
89    tracing::info!(target: "feagi-api", "Output configuration updated: {} targets",
90        config.as_object().map(|o| o.len()).unwrap_or(0));
91
92    Ok(Json(HashMap::from([(
93        "message".to_string(),
94        "Outputs configured successfully".to_string(),
95    )])))
96}