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