1use 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#[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 }
231
232#[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 let device_registrations_opt = request
254 .capabilities
255 .get("device_registrations")
256 .and_then(|v| v.as_object().map(|_| v.clone()));
257
258 let device_regs_for_autocreate = device_registrations_opt.clone();
260
261 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 } 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 if let Some(device_regs) = device_regs_for_autocreate {
283 auto_create_cortical_areas_from_device_registrations(&state, &device_regs).await;
284 }
285
286 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 use crate::v1::TransportConfig;
314 let transports_array = if state.agent_handler.is_some() {
315 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 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, 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#[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#[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#[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#[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#[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#[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 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#[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 let agent_ids = agent_service
681 .list_agents()
682 .await
683 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
684
685 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 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#[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#[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#[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#[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#[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#[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 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 #[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 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#[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 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 if !request.device_registrations.is_object() {
1249 return Err(ApiError::invalid_input(
1250 "Device registrations must be a JSON object",
1251 ));
1252 }
1253
1254 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 #[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 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}