Skip to main content

feagi_api/endpoints/
agent.rs.disabled

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Agent endpoints - exact port from Python `/v1/agent/*` routes
5//!
6//! These endpoints match the Python implementation at:
7//! feagi-py/feagi/api/v1/feagi_agent.py
8
9use std::collections::HashMap;
10
11use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
13use crate::v1::agent_dtos::*;
14use feagi_services::traits::agent_service::HeartbeatRequest as ServiceHeartbeatRequest;
15use tracing::{info, warn};
16
17#[cfg(feature = "feagi-agent")]
18use crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations as auto_create_cortical_areas_shared;
19#[cfg(feature = "feagi-agent")]
20use feagi_agent::{AgentCapabilities as RegistrationCapabilities, AuthToken};
21#[cfg(feature = "feagi-agent")]
22use feagi_io::AgentID;
23#[cfg(feature = "feagi-agent")]
24#[allow(dead_code)]
25fn parse_auth_token(request: &AgentRegistrationRequest) -> ApiResult<AuthToken> {
26    let token_b64 = request
27        .auth_token
28        .as_deref()
29        .ok_or_else(|| ApiError::invalid_input("Missing auth_token for registration"))?;
30    AuthToken::from_base64(token_b64).ok_or_else(|| {
31        ApiError::invalid_input("Invalid auth_token (expected base64 32-byte token)")
32    })
33}
34
35#[cfg(feature = "feagi-agent")]
36#[allow(dead_code)]
37fn derive_capabilities_from_device_registrations(
38    device_registrations: &serde_json::Value,
39) -> ApiResult<Vec<RegistrationCapabilities>> {
40    let obj = device_registrations
41        .as_object()
42        .ok_or_else(|| ApiError::invalid_input("device_registrations must be a JSON object"))?;
43
44    let input_units = obj
45        .get("input_units_and_encoder_properties")
46        .and_then(|v| v.as_object());
47    let output_units = obj
48        .get("output_units_and_decoder_properties")
49        .and_then(|v| v.as_object());
50    let feedbacks = obj.get("feedbacks").and_then(|v| v.as_object());
51
52    let mut capabilities = Vec::new();
53    if input_units.map(|m| !m.is_empty()).unwrap_or(false) {
54        capabilities.push(RegistrationCapabilities::SendSensorData);
55    }
56    if output_units.map(|m| !m.is_empty()).unwrap_or(false) {
57        capabilities.push(RegistrationCapabilities::ReceiveMotorData);
58    }
59    if feedbacks.map(|m| !m.is_empty()).unwrap_or(false) {
60        capabilities.push(RegistrationCapabilities::ReceiveNeuronVisualizations);
61    }
62
63    if capabilities.is_empty() {
64        return Err(ApiError::invalid_input(
65            "device_registrations does not declare any input/output/feedback units",
66        ));
67    }
68
69    Ok(capabilities)
70}
71
72/// Derive capabilities for visualization-only agents (no device_registrations).
73/// Requires `capabilities.visualization` with valid `rate_hz`. Auth is still required by caller.
74#[cfg(feature = "feagi-agent")]
75#[allow(dead_code)]
76fn derive_capabilities_from_visualization_capability(
77    request: &AgentRegistrationRequest,
78) -> ApiResult<Vec<RegistrationCapabilities>> {
79    let viz = request
80        .capabilities
81        .get("visualization")
82        .and_then(|v| v.as_object())
83        .ok_or_else(|| {
84            ApiError::invalid_input(
85                "visualization-only registration requires capabilities.visualization object",
86            )
87        })?;
88    let rate_hz = viz.get("rate_hz").and_then(|v| v.as_f64()).ok_or_else(|| {
89        ApiError::invalid_input("capabilities.visualization must include rate_hz (number > 0)")
90    })?;
91    if rate_hz <= 0.0 {
92        return Err(ApiError::invalid_input(
93            "capabilities.visualization.rate_hz must be > 0",
94        ));
95    }
96    Ok(vec![RegistrationCapabilities::ReceiveNeuronVisualizations])
97}
98
99#[cfg(feature = "feagi-agent")]
100#[allow(dead_code)]
101fn parse_capability_rate_hz(
102    capabilities: &HashMap<String, serde_json::Value>,
103    capability_key: &str,
104) -> ApiResult<Option<f64>> {
105    let Some(capability_value) = capabilities.get(capability_key) else {
106        return Ok(None);
107    };
108
109    let Some(rate_value) = capability_value.get("rate_hz") else {
110        return Ok(None);
111    };
112
113    let rate_hz = rate_value.as_f64().ok_or_else(|| {
114        ApiError::invalid_input(format!(
115            "Invalid rate_hz for capability '{}': expected number",
116            capability_key
117        ))
118    })?;
119
120    if rate_hz <= 0.0 {
121        return Err(ApiError::invalid_input(format!(
122            "Invalid rate_hz for capability '{}': must be > 0",
123            capability_key
124        )));
125    }
126
127    Ok(Some(rate_hz))
128}
129
130#[cfg(feature = "feagi-agent")]
131#[allow(dead_code)]
132fn capability_key(capability: &RegistrationCapabilities) -> &'static str {
133    match capability {
134        RegistrationCapabilities::SendSensorData => "send_sensor_data",
135        RegistrationCapabilities::ReceiveMotorData => "receive_motor_data",
136        RegistrationCapabilities::ReceiveNeuronVisualizations => "receive_neuron_visualizations",
137        RegistrationCapabilities::ReceiveSystemMessages => "receive_system_messages",
138    }
139}
140
141fn get_agent_name_from_id(agent_id: &str) -> ApiResult<String> {
142    Ok(agent_id.to_string())
143}
144
145#[cfg(feature = "feagi-agent")]
146fn parse_agent_id_base64(agent_id: &str) -> ApiResult<AgentID> {
147    AgentID::try_from_base64(agent_id).map_err(|e| {
148        ApiError::invalid_input(format!("Invalid agent_id (expected AgentID base64): {}", e))
149    })
150}
151
152#[cfg(feature = "feagi-agent")]
153async fn auto_create_cortical_areas_from_device_registrations(
154    state: &ApiState,
155    device_registrations: &serde_json::Value,
156) {
157    auto_create_cortical_areas_shared(state, device_registrations).await;
158}
159
160#[cfg(not(feature = "feagi-agent"))]
161async fn auto_create_cortical_areas_from_device_registrations(
162    _state: &ApiState,
163    _device_registrations: &serde_json::Value,
164) {
165    // No-op when feature is disabled
166}
167
168/// Register a new agent with FEAGI and receive connection details including transport configuration and ports.
169#[utoipa::path(
170    post,
171    path = "/v1/agent/register",
172    request_body = AgentRegistrationRequest,
173    responses(
174        (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
175        (status = 500, description = "Registration failed", body = String)
176    ),
177    tag = "agent"
178)]
179pub async fn register_agent(
180    State(state): State<ApiState>,
181    Json(request): Json<AgentRegistrationRequest>,
182) -> ApiResult<Json<AgentRegistrationResponse>> {
183    info!(
184        "đŸĻ€ [API] Registration request received for agent '{}' (type: {})",
185        request.agent_id, request.agent_type
186    );
187
188    // Extract device_registrations from capabilities
189    let device_registrations_opt = request
190        .capabilities
191        .get("device_registrations")
192        .and_then(|v| v.as_object().map(|_| v.clone()));
193
194    // Clone device_regs before async operations to avoid borrow issues
195    let device_regs_for_autocreate = device_registrations_opt.clone();
196
197    // Store device registrations in handler if provided (no await here)
198    let handler_available = if let Some(device_regs) = &device_registrations_opt {
199        if let Some(handler) = &state.agent_handler {
200            let agent_id = parse_agent_id_base64(&request.agent_id)?;
201            {
202                let mut handler_guard = handler.lock().unwrap();
203                handler_guard.set_device_registrations_by_agent(agent_id, device_regs.clone());
204            } // Drop guard before any await
205            info!(
206                "✅ [API] Stored device registrations for agent '{}'",
207                request.agent_id
208            );
209            true
210        } else {
211            false
212        }
213    } else {
214        state.agent_handler.is_some()
215    };
216
217    // Now safe to await
218    if let Some(device_regs) = device_regs_for_autocreate {
219        auto_create_cortical_areas_from_device_registrations(&state, &device_regs).await;
220    }
221
222    // Register visualization subscription if visualization capability is present
223    if let Some(viz) = request.capabilities.get("visualization") {
224        if let Some(rate_hz) = viz.get("rate_hz").and_then(|v| v.as_f64()) {
225            if rate_hz > 0.0 {
226                match state
227                    .runtime_service
228                    .register_visualization_subscriptions(&request.agent_id, rate_hz)
229                    .await
230                {
231                    Ok(_) => {
232                        info!(
233                            "✅ [API] Registered visualization subscription for agent '{}' at {}Hz",
234                            request.agent_id, rate_hz
235                        );
236                    }
237                    Err(e) => {
238                        warn!(
239                            "âš ī¸  [API] Failed to register visualization subscription for agent '{}': {}",
240                            request.agent_id, e
241                        );
242                    }
243                }
244            }
245        }
246    }
247
248    // Get available endpoints from handler and build TransportConfig objects
249    use crate::v1::TransportConfig;
250    let transports_array = if state.agent_handler.is_some() {
251        // Load config to get port numbers
252        use feagi_config::load_config;
253        let config = load_config(None, None)
254            .map_err(|e| ApiError::internal(format!("Failed to load config: {}", e)))?;
255
256        let mut transport_configs = Vec::new();
257        for transport in &config.transports.available {
258            let transport_type = transport.to_lowercase();
259            if transport_type != "zmq" && transport_type != "websocket" && transport_type != "ws" {
260                continue;
261            }
262
263            // Build ports map from config
264            let mut ports = HashMap::new();
265            if transport_type == "websocket" || transport_type == "ws" {
266                ports.insert(
267                    "registration".to_string(),
268                    config.websocket.registration_port,
269                );
270                ports.insert("sensory".to_string(), config.websocket.sensory_port);
271                ports.insert("motor".to_string(), config.websocket.motor_port);
272                ports.insert(
273                    "visualization".to_string(),
274                    config.websocket.visualization_port,
275                );
276            } else {
277                ports.insert("registration".to_string(), config.agent.registration_port);
278                ports.insert("sensory".to_string(), config.ports.zmq_sensory_port);
279                ports.insert("motor".to_string(), config.ports.zmq_motor_port);
280                ports.insert(
281                    "visualization".to_string(),
282                    config.ports.zmq_visualization_port,
283                );
284            }
285
286            transport_configs.push(TransportConfig {
287                transport_type: if transport_type == "ws" {
288                    "websocket".to_string()
289                } else {
290                    transport_type
291                },
292                enabled: true,
293                ports,
294                host: if transport == "websocket" || transport == "ws" {
295                    config.websocket.advertised_host.clone()
296                } else {
297                    config.zmq.advertised_host.clone()
298                },
299            });
300        }
301
302        transport_configs
303    } else {
304        if !handler_available {
305            return Err(ApiError::internal("Agent handler not available"));
306        }
307        Vec::new()
308    };
309
310    Ok(Json(AgentRegistrationResponse {
311        status: "success".to_string(),
312        message: "Agent configuration stored. Connect via ZMQ/WebSocket for full registration"
313            .to_string(),
314        success: true,
315        transport: None, // Legacy field - deprecated
316        rates: None,
317        transports: Some(transports_array),
318        recommended_transport: Some("websocket".to_string()),
319        shm_paths: None,
320        cortical_areas: serde_json::json!({}),
321    }))
322}
323
324/// Send a heartbeat to keep the agent registered and prevent timeout disconnection.
325#[utoipa::path(
326    post,
327    path = "/v1/agent/heartbeat",
328    request_body = HeartbeatRequest,
329    responses(
330        (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
331        (status = 404, description = "Agent not found"),
332        (status = 500, description = "Heartbeat failed")
333    ),
334    tag = "agent"
335)]
336pub async fn heartbeat(
337    State(state): State<ApiState>,
338    Json(request): Json<HeartbeatRequest>,
339) -> ApiResult<Json<HeartbeatResponse>> {
340    let agent_service = state
341        .agent_service
342        .as_ref()
343        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
344
345    let service_request = ServiceHeartbeatRequest {
346        agent_id: request.agent_id.clone(),
347    };
348
349    match agent_service.heartbeat(service_request).await {
350        Ok(_) => Ok(Json(HeartbeatResponse {
351            message: "heartbeat_ok".to_string(),
352            success: true,
353        })),
354        Err(_) => Err(ApiError::not_found(
355            "agent",
356            &format!("Agent {} not in registry", request.agent_id),
357        )),
358    }
359}
360
361/// Get a list of all currently registered agent IDs.
362#[utoipa::path(
363    get,
364    path = "/v1/agent/list",
365    responses(
366        (status = 200, description = "List of agent IDs", body = Vec<String>),
367        (status = 503, description = "Registration service unavailable")
368    ),
369    tag = "agent"
370)]
371pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
372    let agent_service = state
373        .agent_service
374        .as_ref()
375        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
376
377    match agent_service.list_agents().await {
378        Ok(agent_ids) => Ok(Json(agent_ids)),
379        Err(e) => Err(ApiError::internal(format!("Failed to list agents: {}", e))),
380    }
381}
382
383/// Get agent properties including type, capabilities, version, and connection details. Uses query parameter ?agent_id=xxx.
384#[utoipa::path(
385    get,
386    path = "/v1/agent/properties",
387    params(
388        ("agent_id" = String, Query, description = "Agent ID to get properties for")
389    ),
390    responses(
391        (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
392        (status = 404, description = "Agent not found"),
393        (status = 500, description = "Failed to get agent properties")
394    ),
395    tag = "agent"
396)]
397pub async fn get_agent_properties(
398    State(state): State<ApiState>,
399    Query(params): Query<HashMap<String, String>>,
400) -> ApiResult<Json<AgentPropertiesResponse>> {
401    let agent_id = params
402        .get("agent_id")
403        .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
404
405    let agent_service = state
406        .agent_service
407        .as_ref()
408        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
409
410    let agent_name = get_agent_name_from_id(agent_id)?;
411    match agent_service.get_agent_properties(agent_id).await {
412        Ok(properties) => Ok(Json(AgentPropertiesResponse {
413            agent_name,
414            agent_type: properties.agent_type,
415            agent_ip: properties.agent_ip,
416            agent_data_port: properties.agent_data_port,
417            agent_router_address: properties.agent_router_address,
418            agent_version: properties.agent_version,
419            controller_version: properties.controller_version,
420            capabilities: properties.capabilities,
421            chosen_transport: properties.chosen_transport,
422        })),
423        Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
424    }
425}
426
427/// Get shared memory configuration and paths for all registered agents using shared memory transport.
428#[utoipa::path(
429    get,
430    path = "/v1/agent/shared_mem",
431    responses(
432        (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
433        (status = 500, description = "Failed to get shared memory info")
434    ),
435    tag = "agent"
436)]
437pub async fn get_shared_memory(
438    State(state): State<ApiState>,
439) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
440    let agent_service = state
441        .agent_service
442        .as_ref()
443        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
444
445    match agent_service.get_shared_memory_info().await {
446        Ok(shm_info) => Ok(Json(shm_info)),
447        Err(e) => Err(ApiError::internal(format!(
448            "Failed to get shared memory info: {}",
449            e
450        ))),
451    }
452}
453
454/// Deregister an agent from FEAGI and clean up its resources.
455#[utoipa::path(
456    delete,
457    path = "/v1/agent/deregister",
458    request_body = AgentDeregistrationRequest,
459    responses(
460        (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
461        (status = 500, description = "Deregistration failed")
462    ),
463    tag = "agent"
464)]
465pub async fn deregister_agent(
466    State(state): State<ApiState>,
467    Json(request): Json<AgentDeregistrationRequest>,
468) -> ApiResult<Json<SuccessResponse>> {
469    let agent_service = state
470        .agent_service
471        .as_ref()
472        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
473
474    match agent_service.deregister_agent(&request.agent_id).await {
475        Ok(_) => Ok(Json(SuccessResponse {
476            message: format!("Agent '{}' deregistered successfully", request.agent_id),
477            success: Some(true),
478        })),
479        Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
480    }
481}
482
483/// Manually stimulate neurons at specific coordinates across multiple cortical areas for testing and debugging.
484#[utoipa::path(
485    post,
486    path = "/v1/agent/manual_stimulation",
487    request_body = ManualStimulationRequest,
488    responses(
489        (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
490        (status = 500, description = "Stimulation failed")
491    ),
492    tag = "agent"
493)]
494pub async fn manual_stimulation(
495    State(state): State<ApiState>,
496    Json(request): Json<ManualStimulationRequest>,
497) -> ApiResult<Json<ManualStimulationResponse>> {
498    let agent_service = state
499        .agent_service
500        .as_ref()
501        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
502
503    // Ensure runtime service is connected to agent service (if not already connected)
504    // This allows runtime_service to be set after AgentServiceImpl is wrapped in Arc
505    agent_service.try_set_runtime_service(state.runtime_service.clone());
506
507    match agent_service
508        .manual_stimulation(request.stimulation_payload)
509        .await
510    {
511        Ok(result) => {
512            let success = result
513                .get("success")
514                .and_then(|v| v.as_bool())
515                .unwrap_or(false);
516            let total_coordinates = result
517                .get("total_coordinates")
518                .and_then(|v| v.as_u64())
519                .unwrap_or(0) as usize;
520            let successful_areas = result
521                .get("successful_areas")
522                .and_then(|v| v.as_u64())
523                .unwrap_or(0) as usize;
524            let failed_areas = result
525                .get("failed_areas")
526                .and_then(|v| v.as_array())
527                .map(|arr| {
528                    arr.iter()
529                        .filter_map(|v| v.as_str().map(String::from))
530                        .collect()
531                })
532                .unwrap_or_default();
533            let error = result
534                .get("error")
535                .and_then(|v| v.as_str())
536                .map(String::from);
537
538            Ok(Json(ManualStimulationResponse {
539                success,
540                total_coordinates,
541                successful_areas,
542                failed_areas,
543                error,
544            }))
545        }
546        Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
547    }
548}
549
550/// Get Fire Queue (FQ) sampler coordination status including visualization and motor sampling configuration.
551#[utoipa::path(
552    get,
553    path = "/v1/agent/fq_sampler_status",
554    responses(
555        (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
556        (status = 500, description = "Failed to get FQ sampler status")
557    ),
558    tag = "agent"
559)]
560pub async fn get_fq_sampler_status(
561    State(state): State<ApiState>,
562) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
563    let agent_service = state
564        .agent_service
565        .as_ref()
566        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
567
568    let runtime_service = state.runtime_service.as_ref();
569
570    // Get all agents
571    let agent_ids = agent_service
572        .list_agents()
573        .await
574        .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
575
576    // Get FCL sampler config from RuntimeService
577    let (fcl_frequency, fcl_consumer) = runtime_service
578        .get_fcl_sampler_config()
579        .await
580        .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
581
582    // Build response matching Python structure
583    let mut visualization_agents = Vec::new();
584    let mut motor_agents = Vec::new();
585
586    for agent_id in &agent_ids {
587        if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
588            if props.capabilities.contains_key("visualization") {
589                visualization_agents.push(agent_id.clone());
590            }
591            if props.capabilities.contains_key("motor") {
592                motor_agents.push(agent_id.clone());
593            }
594        }
595    }
596
597    let mut fq_coordination = HashMap::new();
598
599    let mut viz_sampler = HashMap::new();
600    viz_sampler.insert(
601        "enabled".to_string(),
602        serde_json::json!(!visualization_agents.is_empty()),
603    );
604    viz_sampler.insert(
605        "reason".to_string(),
606        serde_json::json!(if visualization_agents.is_empty() {
607            "No visualization agents connected".to_string()
608        } else {
609            format!(
610                "{} visualization agent(s) connected",
611                visualization_agents.len()
612            )
613        }),
614    );
615    viz_sampler.insert(
616        "agents_requiring".to_string(),
617        serde_json::json!(visualization_agents),
618    );
619    viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
620    fq_coordination.insert(
621        "visualization_fq_sampler".to_string(),
622        serde_json::json!(viz_sampler),
623    );
624
625    let mut motor_sampler = HashMap::new();
626    motor_sampler.insert(
627        "enabled".to_string(),
628        serde_json::json!(!motor_agents.is_empty()),
629    );
630    motor_sampler.insert(
631        "reason".to_string(),
632        serde_json::json!(if motor_agents.is_empty() {
633            "No motor agents connected".to_string()
634        } else {
635            format!("{} motor agent(s) connected", motor_agents.len())
636        }),
637    );
638    motor_sampler.insert(
639        "agents_requiring".to_string(),
640        serde_json::json!(motor_agents),
641    );
642    motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
643    fq_coordination.insert(
644        "motor_fq_sampler".to_string(),
645        serde_json::json!(motor_sampler),
646    );
647
648    let mut response = HashMap::new();
649    response.insert(
650        "fq_sampler_coordination".to_string(),
651        serde_json::json!(fq_coordination),
652    );
653    response.insert(
654        "agent_registry".to_string(),
655        serde_json::json!({
656            "total_agents": agent_ids.len(),
657            "agent_ids": agent_ids
658        }),
659    );
660    response.insert(
661        "system_status".to_string(),
662        serde_json::json!("coordinated_via_registration_manager"),
663    );
664    response.insert(
665        "fcl_sampler_consumer".to_string(),
666        serde_json::json!(fcl_consumer),
667    );
668
669    Ok(Json(response))
670}
671
672/// Get list of all supported agent types and capability types (sensory, motor, visualization, etc.).
673#[utoipa::path(
674    get,
675    path = "/v1/agent/capabilities",
676    responses(
677        (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
678        (status = 500, description = "Failed to get capabilities")
679    ),
680    tag = "agent"
681)]
682pub async fn get_capabilities(
683    State(_state): State<ApiState>,
684) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
685    let mut response = HashMap::new();
686    response.insert(
687        "agent_types".to_string(),
688        vec![
689            "sensory".to_string(),
690            "motor".to_string(),
691            "both".to_string(),
692            "visualization".to_string(),
693            "infrastructure".to_string(),
694        ],
695    );
696    response.insert(
697        "capability_types".to_string(),
698        vec![
699            "vision".to_string(),
700            "motor".to_string(),
701            "visualization".to_string(),
702            "sensory".to_string(),
703        ],
704    );
705
706    Ok(Json(response))
707}
708
709/// Get capabilities for all agents with optional filtering and payload includes.
710#[utoipa::path(
711    get,
712    path = "/v1/agent/capabilities/all",
713    params(
714        ("agent_type" = Option<String>, Query, description = "Filter by agent type (exact match)"),
715        ("capability" = Option<String>, Query, description = "Filter by capability key(s), comma-separated"),
716        ("include_device_registrations" = Option<bool>, Query, description = "Include device registration payloads per agent")
717    ),
718    responses(
719        (status = 200, description = "Agent capabilities map", body = HashMap<String, AgentCapabilitiesSummary>),
720        (status = 400, description = "Invalid query"),
721        (status = 500, description = "Failed to get agent capabilities")
722    ),
723    tag = "agent"
724)]
725pub async fn get_all_agent_capabilities(
726    State(state): State<ApiState>,
727    Query(params): Query<AgentCapabilitiesAllQuery>,
728) -> ApiResult<Json<HashMap<String, AgentCapabilitiesSummary>>> {
729    let agent_service = state
730        .agent_service
731        .as_ref()
732        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
733
734    let include_device_registrations = params.include_device_registrations.unwrap_or(false);
735    let capability_filters: Option<Vec<String>> = params.capability.as_ref().and_then(|value| {
736        let filters: Vec<String> = value
737            .split(',')
738            .map(|item| item.trim())
739            .filter(|item| !item.is_empty())
740            .map(String::from)
741            .collect();
742        if filters.is_empty() {
743            None
744        } else {
745            Some(filters)
746        }
747    });
748
749    let agent_ids = agent_service
750        .list_agents()
751        .await
752        .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
753
754    let mut response: HashMap<String, AgentCapabilitiesSummary> = HashMap::new();
755
756    for agent_id in agent_ids {
757        let agent_name = get_agent_name_from_id(&agent_id)?;
758        let properties = match agent_service.get_agent_properties(&agent_id).await {
759            Ok(props) => props,
760            Err(_) => continue,
761        };
762
763        if let Some(ref agent_type_filter) = params.agent_type {
764            if properties.agent_type != *agent_type_filter {
765                continue;
766            }
767        }
768
769        if let Some(ref filters) = capability_filters {
770            let has_match = filters
771                .iter()
772                .any(|capability| properties.capabilities.contains_key(capability));
773            if !has_match {
774                continue;
775            }
776        }
777
778        let device_registrations = if include_device_registrations {
779            #[cfg(feature = "feagi-agent")]
780            {
781                Some(export_device_registrations_from_connector(
782                    &state, &agent_id,
783                )?)
784            }
785            #[cfg(not(feature = "feagi-agent"))]
786            {
787                None
788            }
789        } else {
790            None
791        };
792        response.insert(
793            agent_id,
794            AgentCapabilitiesSummary {
795                agent_name,
796                capabilities: properties.capabilities,
797                device_registrations,
798            },
799        );
800    }
801
802    Ok(Json(response))
803}
804
805#[cfg(feature = "feagi-agent")]
806fn export_device_registrations_from_connector(
807    state: &ApiState,
808    agent_id: &str,
809) -> ApiResult<serde_json::Value> {
810    let parsed_agent_id = parse_agent_id_base64(agent_id)?;
811    if let Some(handler) = &state.agent_handler {
812        let handler_guard = handler.lock().unwrap();
813        if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
814            return Ok(regs.clone());
815        }
816    }
817
818    Err(ApiError::not_found(
819        "device_registrations",
820        &format!("No device registrations found for agent '{}'", agent_id),
821    ))
822}
823
824/// Get comprehensive agent information including status, capabilities, version, and connection details.
825#[utoipa::path(
826    get,
827    path = "/v1/agent/info/{agent_id}",
828    params(
829        ("agent_id" = String, Path, description = "Agent ID")
830    ),
831    responses(
832        (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
833        (status = 404, description = "Agent not found"),
834        (status = 500, description = "Failed to get agent info")
835    ),
836    tag = "agent"
837)]
838pub async fn get_agent_info(
839    State(state): State<ApiState>,
840    Path(agent_id): Path<String>,
841) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
842    let agent_service = state
843        .agent_service
844        .as_ref()
845        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
846
847    let properties = agent_service
848        .get_agent_properties(&agent_id)
849        .await
850        .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
851
852    let mut response = HashMap::new();
853    response.insert("agent_id".to_string(), serde_json::json!(agent_id));
854    response.insert(
855        "agent_name".to_string(),
856        serde_json::json!(get_agent_name_from_id(&agent_id)?),
857    );
858    response.insert(
859        "agent_type".to_string(),
860        serde_json::json!(properties.agent_type),
861    );
862    response.insert(
863        "agent_ip".to_string(),
864        serde_json::json!(properties.agent_ip),
865    );
866    response.insert(
867        "agent_data_port".to_string(),
868        serde_json::json!(properties.agent_data_port),
869    );
870    response.insert(
871        "capabilities".to_string(),
872        serde_json::json!(properties.capabilities),
873    );
874    response.insert(
875        "agent_version".to_string(),
876        serde_json::json!(properties.agent_version),
877    );
878    response.insert(
879        "controller_version".to_string(),
880        serde_json::json!(properties.controller_version),
881    );
882    response.insert("status".to_string(), serde_json::json!("active"));
883    if let Some(ref transport) = properties.chosen_transport {
884        response.insert("chosen_transport".to_string(), serde_json::json!(transport));
885    }
886
887    Ok(Json(response))
888}
889
890/// Get agent properties using path parameter. Same as /v1/agent/properties but with agent_id in the URL path.
891#[utoipa::path(
892    get,
893    path = "/v1/agent/properties/{agent_id}",
894    params(
895        ("agent_id" = String, Path, description = "Agent ID")
896    ),
897    responses(
898        (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
899        (status = 404, description = "Agent not found"),
900        (status = 500, description = "Failed to get agent properties")
901    ),
902    tag = "agent"
903)]
904pub async fn get_agent_properties_path(
905    State(state): State<ApiState>,
906    Path(agent_id): Path<String>,
907) -> ApiResult<Json<AgentPropertiesResponse>> {
908    let agent_service = state
909        .agent_service
910        .as_ref()
911        .ok_or_else(|| ApiError::internal("Agent service not available"))?;
912
913    match agent_service.get_agent_properties(&agent_id).await {
914        Ok(properties) => Ok(Json(AgentPropertiesResponse {
915            agent_name: get_agent_name_from_id(&agent_id)?,
916            agent_type: properties.agent_type,
917            agent_ip: properties.agent_ip,
918            agent_data_port: properties.agent_data_port,
919            agent_router_address: properties.agent_router_address,
920            agent_version: properties.agent_version,
921            controller_version: properties.controller_version,
922            capabilities: properties.capabilities,
923            chosen_transport: properties.chosen_transport,
924        })),
925        Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
926    }
927}
928
929/// Configure agent parameters and settings. (Not yet implemented)
930#[utoipa::path(
931    post,
932    path = "/v1/agent/configure",
933    responses(
934        (status = 200, description = "Agent configured", body = HashMap<String, String>),
935        (status = 400, description = "Invalid input"),
936        (status = 500, description = "Failed to configure agent")
937    ),
938    tag = "agent"
939)]
940pub async fn post_configure(
941    State(_state): State<ApiState>,
942    Json(config): Json<HashMap<String, serde_json::Value>>,
943) -> ApiResult<Json<HashMap<String, String>>> {
944    tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
945
946    Ok(Json(HashMap::from([
947        (
948            "message".to_string(),
949            "Agent configuration updated".to_string(),
950        ),
951        ("status".to_string(), "not_yet_implemented".to_string()),
952    ])))
953}
954
955/// Export device registrations for an agent
956///
957/// Returns the complete device registration configuration including
958/// sensor and motor device registrations, encoder/decoder properties,
959/// and feedback configurations in the format compatible with
960/// ConnectorAgent::get_device_registration_json.
961#[utoipa::path(
962    get,
963    path = "/v1/agent/{agent_id}/device_registrations",
964    params(
965        ("agent_id" = String, Path, description = "Agent ID")
966    ),
967    responses(
968        (status = 200, description = "Device registrations exported successfully", body = DeviceRegistrationExportResponse),
969        (status = 404, description = "Agent not found"),
970        (status = 500, description = "Failed to export device registrations")
971    ),
972    tag = "agent"
973)]
974pub async fn export_device_registrations(
975    State(state): State<ApiState>,
976    Path(agent_id): Path<String>,
977) -> ApiResult<Json<DeviceRegistrationExportResponse>> {
978    info!(
979        "đŸĻ€ [API] Device registration export requested for agent '{}'",
980        agent_id
981    );
982
983    // Verify agent exists only if AgentService is available.
984    //
985    // Rationale: live contract tests and minimal deployments can run without an AgentService.
986    // In that case, device registration import/export should still work as a pure per-agent
987    // configuration store (ConnectorAgent-backed).
988    if let Some(agent_service) = state.agent_service.as_ref() {
989        let _properties = agent_service
990            .get_agent_properties(&agent_id)
991            .await
992            .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
993    } else {
994        info!(
995            "â„šī¸ [API] Agent service not available; skipping agent existence check for export (agent '{}')",
996            agent_id
997        );
998    }
999
1000    // Get device registrations from agent_handler
1001    #[cfg(feature = "feagi-agent")]
1002    let device_registrations = {
1003        let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1004        if let Some(handler) = &state.agent_handler {
1005            let handler_guard = handler.lock().unwrap();
1006            if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
1007                info!(
1008                    "📤 [API] Found device registrations for agent '{}'",
1009                    agent_id
1010                );
1011                regs.clone()
1012            } else {
1013                warn!(
1014                    "âš ī¸ [API] No device registrations found for agent '{}'",
1015                    agent_id
1016                );
1017                serde_json::json!({
1018                    "input_units_and_encoder_properties": {},
1019                    "output_units_and_decoder_properties": {},
1020                    "feedbacks": []
1021                })
1022            }
1023        } else {
1024            warn!("âš ī¸ [API] No agent_handler available");
1025            serde_json::json!({
1026                "input_units_and_encoder_properties": {},
1027                "output_units_and_decoder_properties": {},
1028                "feedbacks": []
1029            })
1030        }
1031    };
1032
1033    #[cfg(not(feature = "feagi-agent"))]
1034    // @architecture:acceptable - fallback when feature is disabled
1035    // Returns empty structure when feagi-agent feature is not compiled in
1036    let device_registrations = serde_json::json!({
1037        "input_units_and_encoder_properties": {},
1038        "output_units_and_decoder_properties": {},
1039        "feedbacks": []
1040    });
1041
1042    info!(
1043        "✅ [API] Device registration export succeeded for agent '{}'",
1044        agent_id
1045    );
1046
1047    Ok(Json(DeviceRegistrationExportResponse {
1048        device_registrations,
1049        agent_id,
1050    }))
1051}
1052
1053/// Import device registrations for an agent
1054///
1055/// Imports a device registration configuration, replacing all existing
1056/// device registrations for the agent. The configuration must be in
1057/// the format compatible with ConnectorAgent::set_device_registrations_from_json.
1058///
1059/// # Warning
1060/// This operation **wipes all existing registered devices** before importing
1061/// the new configuration.
1062#[utoipa::path(
1063    post,
1064    path = "/v1/agent/{agent_id}/device_registrations",
1065    params(
1066        ("agent_id" = String, Path, description = "Agent ID")
1067    ),
1068    request_body = DeviceRegistrationImportRequest,
1069    responses(
1070        (status = 200, description = "Device registrations imported successfully", body = DeviceRegistrationImportResponse),
1071        (status = 400, description = "Invalid device registration configuration"),
1072        (status = 404, description = "Agent not found"),
1073        (status = 500, description = "Failed to import device registrations")
1074    ),
1075    tag = "agent"
1076)]
1077pub async fn import_device_registrations(
1078    State(state): State<ApiState>,
1079    Path(agent_id): Path<String>,
1080    Json(request): Json<DeviceRegistrationImportRequest>,
1081) -> ApiResult<Json<DeviceRegistrationImportResponse>> {
1082    info!(
1083        "đŸĻ€ [API] Device registration import requested for agent '{}'",
1084        agent_id
1085    );
1086
1087    // Verify agent exists only if AgentService is available (see export_device_registrations).
1088    if let Some(agent_service) = state.agent_service.as_ref() {
1089        let _properties = agent_service
1090            .get_agent_properties(&agent_id)
1091            .await
1092            .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1093    } else {
1094        info!(
1095            "â„šī¸ [API] Agent service not available; skipping agent existence check for import (agent '{}')",
1096            agent_id
1097        );
1098    }
1099
1100    // Validate the device registration JSON structure
1101    // Check that it has the expected fields
1102    if !request.device_registrations.is_object() {
1103        return Err(ApiError::invalid_input(
1104            "Device registrations must be a JSON object",
1105        ));
1106    }
1107
1108    // Validate required fields exist
1109    let obj = request.device_registrations.as_object().unwrap();
1110    if !obj.contains_key("input_units_and_encoder_properties")
1111        || !obj.contains_key("output_units_and_decoder_properties")
1112        || !obj.contains_key("feedbacks")
1113    {
1114        return Err(ApiError::invalid_input(
1115            "Device registrations must contain: input_units_and_encoder_properties, output_units_and_decoder_properties, and feedbacks",
1116        ));
1117    }
1118
1119    // Store device registrations in agent_handler
1120    #[cfg(feature = "feagi-agent")]
1121    {
1122        let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1123        if let Some(handler) = &state.agent_handler {
1124            let mut handler_guard = handler.lock().unwrap();
1125            handler_guard.set_device_registrations_by_agent(
1126                parsed_agent_id,
1127                request.device_registrations.clone(),
1128            );
1129            info!(
1130                "đŸ“Ĩ [API] Imported device registrations for agent '{}'",
1131                agent_id
1132            );
1133        } else {
1134            warn!("âš ī¸ [API] No agent_handler available to store device registrations");
1135        }
1136
1137        auto_create_cortical_areas_from_device_registrations(&state, &request.device_registrations)
1138            .await;
1139
1140        Ok(Json(DeviceRegistrationImportResponse {
1141            success: true,
1142            message: format!(
1143                "Device registrations imported successfully for agent '{}'",
1144                agent_id
1145            ),
1146            agent_id,
1147        }))
1148    }
1149
1150    #[cfg(not(feature = "feagi-agent"))]
1151    {
1152        info!(
1153            "✅ [API] Device registration import succeeded for agent '{}' (feagi-agent feature not enabled)",
1154            agent_id
1155        );
1156        Ok(Json(DeviceRegistrationImportResponse {
1157            success: true,
1158            message: format!(
1159                "Device registrations imported successfully for agent '{}' (feature not enabled)",
1160                agent_id
1161            ),
1162            agent_id,
1163        }))
1164    }
1165}