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(state: &ApiState, agent_id: &str) -> ApiResult<String> {
146 #[cfg(feature = "feagi-agent")]
147 {
148 if let Some(handler) = &state.agent_handler {
149 if let Ok(parsed) = parse_agent_id_base64(agent_id) {
150 let guard = handler.lock().unwrap();
151 if let Some((descriptor, _)) = guard.get_all_registered_agents().get(&parsed) {
152 return Ok(descriptor.agent_name().to_string());
153 }
154 }
155 }
156 }
157 if let Ok(name) = try_extract_agent_name_from_descriptor_bytes(agent_id) {
158 return Ok(name);
159 }
160 Ok(agent_id.to_string())
161}
162
163fn try_extract_agent_name_from_descriptor_bytes(agent_id: &str) -> Result<String, ()> {
165 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
166 const AGENT_DESC_SIZE: usize = 48;
167 const AGENT_NAME_OFFSET: usize = 4 + 20;
168 const AGENT_NAME_LEN: usize = 20;
169 let decoded = BASE64.decode(agent_id).map_err(|_| ())?;
170 if decoded.len() != AGENT_DESC_SIZE {
171 return Err(());
172 }
173 let name_bytes = &decoded[AGENT_NAME_OFFSET..AGENT_NAME_OFFSET + AGENT_NAME_LEN];
174 let name = String::from_utf8_lossy(name_bytes)
175 .trim_end_matches('\0')
176 .trim()
177 .to_string();
178 if name.is_empty() || !name.chars().all(|c| c.is_ascii() && !c.is_control()) {
179 return Err(());
180 }
181 Ok(name)
182}
183
184#[cfg(feature = "feagi-agent")]
185fn registration_capabilities_to_map(
186 capabilities: &[RegistrationCapabilities],
187) -> HashMap<String, serde_json::Value> {
188 let mut out = HashMap::new();
189 for capability in capabilities {
190 match capability {
191 RegistrationCapabilities::SendSensorData => {
192 out.insert("send_sensor_data".to_string(), serde_json::json!(true));
193 out.insert("sensory".to_string(), serde_json::json!(true));
194 }
195 RegistrationCapabilities::ReceiveMotorData => {
196 out.insert("receive_motor_data".to_string(), serde_json::json!(true));
197 out.insert("motor".to_string(), serde_json::json!(true));
198 }
199 RegistrationCapabilities::ReceiveNeuronVisualizations => {
200 out.insert(
201 "receive_neuron_visualizations".to_string(),
202 serde_json::json!(true),
203 );
204 out.insert(
205 "visualization".to_string(),
206 serde_json::json!({"rate_hz": 0.0}),
207 );
208 }
209 RegistrationCapabilities::ReceiveSystemMessages => {
210 out.insert(
211 "receive_system_messages".to_string(),
212 serde_json::json!(true),
213 );
214 }
215 }
216 }
217 out
218}
219
220#[cfg(feature = "feagi-agent")]
221fn list_agent_ids_from_handler(state: &ApiState) -> Vec<String> {
222 if let Some(handler) = &state.agent_handler {
223 let handler_guard = handler.lock().unwrap();
224 return handler_guard
225 .get_all_registered_agents()
226 .keys()
227 .map(|id| id.to_base64())
228 .collect();
229 }
230 Vec::new()
231}
232
233#[cfg(feature = "feagi-agent")]
234fn get_handler_capabilities_for_agent(
235 state: &ApiState,
236 agent_id: &str,
237) -> Option<HashMap<String, serde_json::Value>> {
238 let parsed_agent_id = parse_agent_id_base64(agent_id).ok()?;
239 let handler = state.agent_handler.as_ref()?;
240 let handler_guard = handler.lock().unwrap();
241 let (_, capabilities) = handler_guard
242 .get_all_registered_agents()
243 .get(&parsed_agent_id)?;
244 Some(registration_capabilities_to_map(capabilities))
245}
246
247#[cfg(feature = "feagi-agent")]
248fn parse_agent_id_base64(agent_id: &str) -> ApiResult<AgentID> {
249 AgentID::try_from_base64(agent_id).map_err(|e| {
250 ApiError::invalid_input(format!("Invalid agent_id (expected AgentID base64): {}", e))
251 })
252}
253
254#[cfg(feature = "feagi-agent")]
255async fn auto_create_cortical_areas_from_device_registrations(
256 state: &ApiState,
257 device_registrations: &serde_json::Value,
258) {
259 auto_create_cortical_areas_shared(state, device_registrations).await;
260}
261
262#[cfg(not(feature = "feagi-agent"))]
263async fn auto_create_cortical_areas_from_device_registrations(
264 _state: &ApiState,
265 _device_registrations: &serde_json::Value,
266) {
267 }
269
270#[utoipa::path(
272 post,
273 path = "/v1/agent/register",
274 request_body = AgentRegistrationRequest,
275 responses(
276 (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
277 (status = 500, description = "Registration failed", body = String)
278 ),
279 tag = "agent"
280)]
281pub async fn register_agent(
282 State(state): State<ApiState>,
283 Json(request): Json<AgentRegistrationRequest>,
284) -> ApiResult<Json<AgentRegistrationResponse>> {
285 info!(
286 "đĻ [API] Registration request received for agent '{}' (type: {})",
287 request.agent_id, request.agent_type
288 );
289
290 let device_registrations_opt = request
292 .capabilities
293 .get("device_registrations")
294 .and_then(|v| v.as_object().map(|_| v.clone()));
295
296 let device_regs_for_autocreate = device_registrations_opt.clone();
298
299 let handler_available = if let Some(device_regs) = &device_registrations_opt {
301 if let Some(handler) = &state.agent_handler {
302 let agent_id = parse_agent_id_base64(&request.agent_id)?;
303 {
304 let mut handler_guard = handler.lock().unwrap();
305 handler_guard.set_device_registrations_by_agent(agent_id, device_regs.clone());
306 } info!(
308 "â
[API] Stored device registrations for agent '{}'",
309 request.agent_id
310 );
311 true
312 } else {
313 false
314 }
315 } else {
316 state.agent_handler.is_some()
317 };
318
319 if let Some(device_regs) = device_regs_for_autocreate {
321 auto_create_cortical_areas_from_device_registrations(&state, &device_regs).await;
322 }
323
324 if let Some(viz) = request.capabilities.get("visualization") {
326 if let Some(rate_hz) = viz.get("rate_hz").and_then(|v| v.as_f64()) {
327 if rate_hz > 0.0 {
328 match state
329 .runtime_service
330 .register_visualization_subscriptions(&request.agent_id, rate_hz)
331 .await
332 {
333 Ok(_) => {
334 info!(
335 "â
[API] Registered visualization subscription for agent '{}' at {}Hz",
336 request.agent_id, rate_hz
337 );
338 }
339 Err(e) => {
340 warn!(
341 "â ī¸ [API] Failed to register visualization subscription for agent '{}': {}",
342 request.agent_id, e
343 );
344 }
345 }
346 }
347 }
348 }
349
350 use crate::v1::TransportConfig;
352 let transports_array = if state.agent_handler.is_some() {
353 use feagi_config::load_config;
355 let config = load_config(None, None)
356 .map_err(|e| ApiError::internal(format!("Failed to load config: {}", e)))?;
357
358 let mut transport_configs = Vec::new();
359 for transport in &config.transports.available {
360 let transport_type = transport.to_lowercase();
361 if transport_type != "zmq" && transport_type != "websocket" && transport_type != "ws" {
362 continue;
363 }
364
365 let mut ports = HashMap::new();
367 if transport_type == "websocket" || transport_type == "ws" {
368 ports.insert(
369 "registration".to_string(),
370 config.websocket.registration_port,
371 );
372 ports.insert("sensory".to_string(), config.websocket.sensory_port);
373 ports.insert("motor".to_string(), config.websocket.motor_port);
374 ports.insert(
375 "visualization".to_string(),
376 config.websocket.visualization_port,
377 );
378 } else {
379 ports.insert("registration".to_string(), config.agent.registration_port);
380 ports.insert("sensory".to_string(), config.ports.zmq_sensory_port);
381 ports.insert("motor".to_string(), config.ports.zmq_motor_port);
382 ports.insert(
383 "visualization".to_string(),
384 config.ports.zmq_visualization_port,
385 );
386 }
387
388 transport_configs.push(TransportConfig {
389 transport_type: if transport_type == "ws" {
390 "websocket".to_string()
391 } else {
392 transport_type
393 },
394 enabled: true,
395 ports,
396 host: if transport == "websocket" || transport == "ws" {
397 config.websocket.advertised_host.clone()
398 } else {
399 config.zmq.advertised_host.clone()
400 },
401 });
402 }
403
404 transport_configs
405 } else {
406 if !handler_available {
407 return Err(ApiError::internal("Agent handler not available"));
408 }
409 Vec::new()
410 };
411
412 Ok(Json(AgentRegistrationResponse {
413 status: "success".to_string(),
414 message: "Agent configuration stored. Connect via ZMQ/WebSocket for full registration"
415 .to_string(),
416 success: true,
417 transport: None, rates: None,
419 transports: Some(transports_array),
420 recommended_transport: Some("websocket".to_string()),
421 shm_paths: None,
422 cortical_areas: serde_json::json!({}),
423 }))
424}
425
426#[utoipa::path(
428 post,
429 path = "/v1/agent/heartbeat",
430 request_body = HeartbeatRequest,
431 responses(
432 (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
433 (status = 404, description = "Agent not found"),
434 (status = 500, description = "Heartbeat failed")
435 ),
436 tag = "agent"
437)]
438pub async fn heartbeat(
439 State(state): State<ApiState>,
440 Json(request): Json<HeartbeatRequest>,
441) -> ApiResult<Json<HeartbeatResponse>> {
442 let agent_service = state
443 .agent_service
444 .as_ref()
445 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
446
447 let service_request = ServiceHeartbeatRequest {
448 agent_id: request.agent_id.clone(),
449 };
450
451 match agent_service.heartbeat(service_request).await {
452 Ok(_) => Ok(Json(HeartbeatResponse {
453 message: "heartbeat_ok".to_string(),
454 success: true,
455 })),
456 Err(_) => Err(ApiError::not_found(
457 "agent",
458 &format!("Agent {} not in registry", request.agent_id),
459 )),
460 }
461}
462
463#[utoipa::path(
465 get,
466 path = "/v1/agent/list",
467 responses(
468 (status = 200, description = "List of agent IDs", body = Vec<String>),
469 (status = 503, description = "Registration service unavailable")
470 ),
471 tag = "agent"
472)]
473pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
474 if let Some(agent_service) = state.agent_service.as_ref() {
475 match agent_service.list_agents().await {
476 Ok(agent_ids) if !agent_ids.is_empty() => return Ok(Json(agent_ids)),
477 Ok(_) => {}
478 Err(e) => {
479 warn!(
480 "â ī¸ [API] AgentService list_agents failed ({}); falling back to agent_handler",
481 e
482 );
483 }
484 }
485 }
486
487 #[cfg(feature = "feagi-agent")]
488 {
489 Ok(Json(list_agent_ids_from_handler(&state)))
490 }
491
492 #[cfg(not(feature = "feagi-agent"))]
493 {
494 Err(ApiError::internal("Agent service not available"))
495 }
496}
497
498#[utoipa::path(
500 get,
501 path = "/v1/agent/properties",
502 params(
503 ("agent_id" = String, Query, description = "Agent ID to get properties for")
504 ),
505 responses(
506 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
507 (status = 404, description = "Agent not found"),
508 (status = 500, description = "Failed to get agent properties")
509 ),
510 tag = "agent"
511)]
512pub async fn get_agent_properties(
513 State(state): State<ApiState>,
514 Query(params): Query<HashMap<String, String>>,
515) -> ApiResult<Json<AgentPropertiesResponse>> {
516 let agent_id = params
517 .get("agent_id")
518 .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
519
520 let agent_service = state
521 .agent_service
522 .as_ref()
523 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
524
525 let agent_name = get_agent_name_from_id(&state, agent_id)?;
526 match agent_service.get_agent_properties(agent_id).await {
527 Ok(properties) => Ok(Json(AgentPropertiesResponse {
528 agent_name,
529 agent_type: properties.agent_type,
530 agent_ip: properties.agent_ip,
531 agent_data_port: properties.agent_data_port,
532 agent_router_address: properties.agent_router_address,
533 agent_version: properties.agent_version,
534 controller_version: properties.controller_version,
535 capabilities: properties.capabilities,
536 chosen_transport: properties.chosen_transport,
537 })),
538 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
539 }
540}
541
542#[utoipa::path(
544 get,
545 path = "/v1/agent/shared_mem",
546 responses(
547 (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
548 (status = 500, description = "Failed to get shared memory info")
549 ),
550 tag = "agent"
551)]
552pub async fn get_shared_memory(
553 State(state): State<ApiState>,
554) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
555 let agent_service = state
556 .agent_service
557 .as_ref()
558 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
559
560 match agent_service.get_shared_memory_info().await {
561 Ok(shm_info) => Ok(Json(shm_info)),
562 Err(e) => Err(ApiError::internal(format!(
563 "Failed to get shared memory info: {}",
564 e
565 ))),
566 }
567}
568
569#[utoipa::path(
571 delete,
572 path = "/v1/agent/deregister",
573 request_body = AgentDeregistrationRequest,
574 responses(
575 (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
576 (status = 500, description = "Deregistration failed")
577 ),
578 tag = "agent"
579)]
580pub async fn deregister_agent(
581 State(state): State<ApiState>,
582 Json(request): Json<AgentDeregistrationRequest>,
583) -> ApiResult<Json<SuccessResponse>> {
584 let agent_service = state
585 .agent_service
586 .as_ref()
587 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
588
589 match agent_service.deregister_agent(&request.agent_id).await {
590 Ok(_) => Ok(Json(SuccessResponse {
591 message: format!("Agent '{}' deregistered successfully", request.agent_id),
592 success: Some(true),
593 })),
594 Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
595 }
596}
597
598#[utoipa::path(
600 post,
601 path = "/v1/agent/manual_stimulation",
602 request_body = ManualStimulationRequest,
603 responses(
604 (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
605 (status = 500, description = "Stimulation failed")
606 ),
607 tag = "agent"
608)]
609pub async fn manual_stimulation(
610 State(state): State<ApiState>,
611 Json(request): Json<ManualStimulationRequest>,
612) -> ApiResult<Json<ManualStimulationResponse>> {
613 let agent_service = state
614 .agent_service
615 .as_ref()
616 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
617
618 agent_service.try_set_runtime_service(state.runtime_service.clone());
621
622 let mode = match request.mode.as_deref() {
623 Some("force_fire") => ManualStimulationMode::ForceFire,
624 Some("candidate") | None => ManualStimulationMode::Candidate,
625 Some(other) => {
626 return Err(ApiError::invalid_input(format!(
627 "Invalid stimulation mode '{}'; expected 'candidate' or 'force_fire'",
628 other
629 )));
630 }
631 };
632
633 match agent_service
634 .manual_stimulation(request.stimulation_payload, mode)
635 .await
636 {
637 Ok(result) => {
638 let success = result
639 .get("success")
640 .and_then(|v| v.as_bool())
641 .unwrap_or(false);
642 let total_coordinates = result
643 .get("total_coordinates")
644 .and_then(|v| v.as_u64())
645 .unwrap_or(0) as usize;
646 let successful_areas = result
647 .get("successful_areas")
648 .and_then(|v| v.as_u64())
649 .unwrap_or(0) as usize;
650 let requested_coordinates = result
651 .get("requested_coordinates")
652 .and_then(|v| v.as_u64())
653 .unwrap_or(0) as usize;
654 let matched_coordinates = result
655 .get("matched_coordinates")
656 .and_then(|v| v.as_u64())
657 .unwrap_or(0) as usize;
658 let unique_neuron_ids = result
659 .get("unique_neuron_ids")
660 .and_then(|v| v.as_u64())
661 .unwrap_or(0) as usize;
662 let mode = result
663 .get("mode")
664 .and_then(|v| v.as_str())
665 .unwrap_or("candidate")
666 .to_string();
667 let failed_areas = result
668 .get("failed_areas")
669 .and_then(|v| v.as_array())
670 .map(|arr| {
671 arr.iter()
672 .filter_map(|v| v.as_str().map(String::from))
673 .collect()
674 })
675 .unwrap_or_default();
676 let error = result
677 .get("error")
678 .and_then(|v| v.as_str())
679 .map(String::from);
680
681 Ok(Json(ManualStimulationResponse {
682 success,
683 total_coordinates,
684 requested_coordinates,
685 matched_coordinates,
686 unique_neuron_ids,
687 mode,
688 successful_areas,
689 failed_areas,
690 error,
691 }))
692 }
693 Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
694 }
695}
696
697#[utoipa::path(
699 get,
700 path = "/v1/agent/fq_sampler_status",
701 responses(
702 (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
703 (status = 500, description = "Failed to get FQ sampler status")
704 ),
705 tag = "agent"
706)]
707pub async fn get_fq_sampler_status(
708 State(state): State<ApiState>,
709) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
710 let agent_service = state
711 .agent_service
712 .as_ref()
713 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
714
715 let runtime_service = state.runtime_service.as_ref();
716
717 let agent_ids = agent_service
719 .list_agents()
720 .await
721 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
722
723 let (fcl_frequency, fcl_consumer) = runtime_service
725 .get_fcl_sampler_config()
726 .await
727 .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
728
729 let mut visualization_agents = Vec::new();
731 let mut motor_agents = Vec::new();
732
733 for agent_id in &agent_ids {
734 if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
735 if props.capabilities.contains_key("visualization") {
736 visualization_agents.push(agent_id.clone());
737 }
738 if props.capabilities.contains_key("motor") {
739 motor_agents.push(agent_id.clone());
740 }
741 }
742 }
743
744 let mut fq_coordination = HashMap::new();
745
746 let mut viz_sampler = HashMap::new();
747 viz_sampler.insert(
748 "enabled".to_string(),
749 serde_json::json!(!visualization_agents.is_empty()),
750 );
751 viz_sampler.insert(
752 "reason".to_string(),
753 serde_json::json!(if visualization_agents.is_empty() {
754 "No visualization agents connected".to_string()
755 } else {
756 format!(
757 "{} visualization agent(s) connected",
758 visualization_agents.len()
759 )
760 }),
761 );
762 viz_sampler.insert(
763 "agents_requiring".to_string(),
764 serde_json::json!(visualization_agents),
765 );
766 viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
767 fq_coordination.insert(
768 "visualization_fq_sampler".to_string(),
769 serde_json::json!(viz_sampler),
770 );
771
772 let mut motor_sampler = HashMap::new();
773 motor_sampler.insert(
774 "enabled".to_string(),
775 serde_json::json!(!motor_agents.is_empty()),
776 );
777 motor_sampler.insert(
778 "reason".to_string(),
779 serde_json::json!(if motor_agents.is_empty() {
780 "No motor agents connected".to_string()
781 } else {
782 format!("{} motor agent(s) connected", motor_agents.len())
783 }),
784 );
785 motor_sampler.insert(
786 "agents_requiring".to_string(),
787 serde_json::json!(motor_agents),
788 );
789 motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
790 fq_coordination.insert(
791 "motor_fq_sampler".to_string(),
792 serde_json::json!(motor_sampler),
793 );
794
795 let mut response = HashMap::new();
796 response.insert(
797 "fq_sampler_coordination".to_string(),
798 serde_json::json!(fq_coordination),
799 );
800 response.insert(
801 "agent_registry".to_string(),
802 serde_json::json!({
803 "total_agents": agent_ids.len(),
804 "agent_ids": agent_ids
805 }),
806 );
807 response.insert(
808 "system_status".to_string(),
809 serde_json::json!("coordinated_via_registration_manager"),
810 );
811 response.insert(
812 "fcl_sampler_consumer".to_string(),
813 serde_json::json!(fcl_consumer),
814 );
815
816 Ok(Json(response))
817}
818
819#[utoipa::path(
821 get,
822 path = "/v1/agent/capabilities",
823 responses(
824 (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
825 (status = 500, description = "Failed to get capabilities")
826 ),
827 tag = "agent"
828)]
829pub async fn get_capabilities(
830 State(_state): State<ApiState>,
831) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
832 let mut response = HashMap::new();
833 response.insert(
834 "agent_types".to_string(),
835 vec![
836 "sensory".to_string(),
837 "motor".to_string(),
838 "both".to_string(),
839 "visualization".to_string(),
840 "infrastructure".to_string(),
841 ],
842 );
843 response.insert(
844 "capability_types".to_string(),
845 vec![
846 "vision".to_string(),
847 "motor".to_string(),
848 "visualization".to_string(),
849 "sensory".to_string(),
850 ],
851 );
852
853 Ok(Json(response))
854}
855
856#[utoipa::path(
858 get,
859 path = "/v1/agent/capabilities/all",
860 params(
861 ("agent_type" = Option<String>, Query, description = "Filter by agent type (exact match)"),
862 ("capability" = Option<String>, Query, description = "Filter by capability key(s), comma-separated"),
863 ("include_device_registrations" = Option<bool>, Query, description = "Include device registration payloads per agent")
864 ),
865 responses(
866 (status = 200, description = "Agent capabilities map", body = HashMap<String, AgentCapabilitiesSummary>),
867 (status = 400, description = "Invalid query"),
868 (status = 500, description = "Failed to get agent capabilities")
869 ),
870 tag = "agent"
871)]
872pub async fn get_all_agent_capabilities(
873 State(state): State<ApiState>,
874 Query(params): Query<AgentCapabilitiesAllQuery>,
875) -> ApiResult<Json<HashMap<String, AgentCapabilitiesSummary>>> {
876 let agent_service = state.agent_service.as_ref();
877
878 let include_device_registrations = params.include_device_registrations.unwrap_or(false);
879 let capability_filters: Option<Vec<String>> = params.capability.as_ref().and_then(|value| {
880 let filters: Vec<String> = value
881 .split(',')
882 .map(|item| item.trim())
883 .filter(|item| !item.is_empty())
884 .map(String::from)
885 .collect();
886 if filters.is_empty() {
887 None
888 } else {
889 Some(filters)
890 }
891 });
892
893 let mut agent_ids: Vec<String> = Vec::new();
894 if let Some(service) = agent_service {
895 match service.list_agents().await {
896 Ok(ids) => agent_ids = ids,
897 Err(e) => {
898 warn!(
899 "â ī¸ [API] AgentService list_agents failed ({}); falling back to agent_handler",
900 e
901 );
902 }
903 }
904 }
905 #[cfg(feature = "feagi-agent")]
906 if agent_ids.is_empty() {
907 agent_ids = list_agent_ids_from_handler(&state);
908 }
909
910 let mut response: HashMap<String, AgentCapabilitiesSummary> = HashMap::new();
911
912 for agent_id in agent_ids {
913 let agent_name = get_agent_name_from_id(&state, &agent_id)?;
914 let (agent_type, capabilities_map) = if let Some(service) = agent_service {
915 match service.get_agent_properties(&agent_id).await {
916 Ok(props) => (props.agent_type, props.capabilities),
917 Err(_) => {
918 #[cfg(feature = "feagi-agent")]
919 {
920 if let Some(caps) = get_handler_capabilities_for_agent(&state, &agent_id) {
921 ("unknown".to_string(), caps)
922 } else {
923 continue;
924 }
925 }
926 #[cfg(not(feature = "feagi-agent"))]
927 {
928 continue;
929 }
930 }
931 }
932 } else {
933 #[cfg(feature = "feagi-agent")]
934 {
935 if let Some(caps) = get_handler_capabilities_for_agent(&state, &agent_id) {
936 ("unknown".to_string(), caps)
937 } else {
938 continue;
939 }
940 }
941 #[cfg(not(feature = "feagi-agent"))]
942 {
943 continue;
944 }
945 };
946
947 if let Some(ref agent_type_filter) = params.agent_type {
948 if agent_type != *agent_type_filter {
949 continue;
950 }
951 }
952
953 if let Some(ref filters) = capability_filters {
954 let has_match = filters
955 .iter()
956 .any(|capability| capabilities_map.contains_key(capability));
957 if !has_match {
958 continue;
959 }
960 }
961
962 let device_registrations = if include_device_registrations {
963 #[cfg(feature = "feagi-agent")]
964 {
965 match export_device_registrations_from_connector(&state, &agent_id) {
966 Ok(v) => Some(v),
967 Err(e) => {
968 warn!(
969 "â ī¸ [API] Could not export device registrations for '{}': {:?}",
970 agent_id, e
971 );
972 None
973 }
974 }
975 }
976 #[cfg(not(feature = "feagi-agent"))]
977 {
978 None
979 }
980 } else {
981 None
982 };
983 response.insert(
984 agent_id,
985 AgentCapabilitiesSummary {
986 agent_name,
987 capabilities: capabilities_map,
988 device_registrations,
989 },
990 );
991 }
992
993 Ok(Json(response))
994}
995
996#[cfg(feature = "feagi-agent")]
997fn export_device_registrations_from_connector(
998 state: &ApiState,
999 agent_id: &str,
1000) -> ApiResult<serde_json::Value> {
1001 let parsed_agent_id = parse_agent_id_base64(agent_id)?;
1002 if let Some(handler) = &state.agent_handler {
1003 let handler_guard = handler.lock().unwrap();
1004 if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
1005 return Ok(regs.clone());
1006 }
1007 }
1008
1009 Err(ApiError::not_found(
1010 "device_registrations",
1011 &format!("No device registrations found for agent '{}'", agent_id),
1012 ))
1013}
1014
1015#[utoipa::path(
1017 get,
1018 path = "/v1/agent/info/{agent_id}",
1019 params(
1020 ("agent_id" = String, Path, description = "Agent ID")
1021 ),
1022 responses(
1023 (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
1024 (status = 404, description = "Agent not found"),
1025 (status = 500, description = "Failed to get agent info")
1026 ),
1027 tag = "agent"
1028)]
1029pub async fn get_agent_info(
1030 State(state): State<ApiState>,
1031 Path(agent_id): Path<String>,
1032) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1033 let agent_service = state
1034 .agent_service
1035 .as_ref()
1036 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1037
1038 let properties = agent_service
1039 .get_agent_properties(&agent_id)
1040 .await
1041 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1042
1043 let mut response = HashMap::new();
1044 response.insert("agent_id".to_string(), serde_json::json!(agent_id));
1045 response.insert(
1046 "agent_name".to_string(),
1047 serde_json::json!(get_agent_name_from_id(&state, &agent_id)?),
1048 );
1049 response.insert(
1050 "agent_type".to_string(),
1051 serde_json::json!(properties.agent_type),
1052 );
1053 response.insert(
1054 "agent_ip".to_string(),
1055 serde_json::json!(properties.agent_ip),
1056 );
1057 response.insert(
1058 "agent_data_port".to_string(),
1059 serde_json::json!(properties.agent_data_port),
1060 );
1061 response.insert(
1062 "capabilities".to_string(),
1063 serde_json::json!(properties.capabilities),
1064 );
1065 response.insert(
1066 "agent_version".to_string(),
1067 serde_json::json!(properties.agent_version),
1068 );
1069 response.insert(
1070 "controller_version".to_string(),
1071 serde_json::json!(properties.controller_version),
1072 );
1073 response.insert("status".to_string(), serde_json::json!("active"));
1074 if let Some(ref transport) = properties.chosen_transport {
1075 response.insert("chosen_transport".to_string(), serde_json::json!(transport));
1076 }
1077
1078 Ok(Json(response))
1079}
1080
1081#[utoipa::path(
1083 get,
1084 path = "/v1/agent/properties/{agent_id}",
1085 params(
1086 ("agent_id" = String, Path, description = "Agent ID")
1087 ),
1088 responses(
1089 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
1090 (status = 404, description = "Agent not found"),
1091 (status = 500, description = "Failed to get agent properties")
1092 ),
1093 tag = "agent"
1094)]
1095pub async fn get_agent_properties_path(
1096 State(state): State<ApiState>,
1097 Path(agent_id): Path<String>,
1098) -> ApiResult<Json<AgentPropertiesResponse>> {
1099 let agent_service = state
1100 .agent_service
1101 .as_ref()
1102 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1103
1104 match agent_service.get_agent_properties(&agent_id).await {
1105 Ok(properties) => Ok(Json(AgentPropertiesResponse {
1106 agent_name: get_agent_name_from_id(&state, &agent_id)?,
1107 agent_type: properties.agent_type,
1108 agent_ip: properties.agent_ip,
1109 agent_data_port: properties.agent_data_port,
1110 agent_router_address: properties.agent_router_address,
1111 agent_version: properties.agent_version,
1112 controller_version: properties.controller_version,
1113 capabilities: properties.capabilities,
1114 chosen_transport: properties.chosen_transport,
1115 })),
1116 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
1117 }
1118}
1119
1120#[utoipa::path(
1122 post,
1123 path = "/v1/agent/configure",
1124 responses(
1125 (status = 200, description = "Agent configured", body = HashMap<String, String>),
1126 (status = 400, description = "Invalid input"),
1127 (status = 500, description = "Failed to configure agent")
1128 ),
1129 tag = "agent"
1130)]
1131pub async fn post_configure(
1132 State(_state): State<ApiState>,
1133 Json(config): Json<HashMap<String, serde_json::Value>>,
1134) -> ApiResult<Json<HashMap<String, String>>> {
1135 tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
1136
1137 Ok(Json(HashMap::from([
1138 (
1139 "message".to_string(),
1140 "Agent configuration updated".to_string(),
1141 ),
1142 ("status".to_string(), "not_yet_implemented".to_string()),
1143 ])))
1144}
1145
1146#[utoipa::path(
1153 get,
1154 path = "/v1/agent/{agent_id}/device_registrations",
1155 params(
1156 ("agent_id" = String, Path, description = "Agent ID")
1157 ),
1158 responses(
1159 (status = 200, description = "Device registrations exported successfully", body = DeviceRegistrationExportResponse),
1160 (status = 404, description = "Agent not found"),
1161 (status = 500, description = "Failed to export device registrations")
1162 ),
1163 tag = "agent"
1164)]
1165pub async fn export_device_registrations(
1166 State(state): State<ApiState>,
1167 Path(agent_id): Path<String>,
1168) -> ApiResult<Json<DeviceRegistrationExportResponse>> {
1169 info!(
1170 "đĻ [API] Device registration export requested for agent '{}'",
1171 agent_id
1172 );
1173
1174 if let Some(agent_service) = state.agent_service.as_ref() {
1178 if let Err(e) = agent_service.get_agent_properties(&agent_id).await {
1179 info!(
1180 "âšī¸ [API] Agent '{}' not found in AgentService during export ({}); falling back to agent_handler lookup",
1181 agent_id, e
1182 );
1183 }
1184 }
1185
1186 #[cfg(feature = "feagi-agent")]
1188 let device_registrations = {
1189 let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1190 if let Some(handler) = &state.agent_handler {
1191 let handler_guard = handler.lock().unwrap();
1192 if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
1193 info!(
1194 "đ¤ [API] Found device registrations for agent '{}'",
1195 agent_id
1196 );
1197 regs.clone()
1198 } else {
1199 warn!(
1200 "â ī¸ [API] No device registrations found for agent '{}'",
1201 agent_id
1202 );
1203 serde_json::json!({
1204 "input_units_and_encoder_properties": {},
1205 "output_units_and_decoder_properties": {},
1206 "feedbacks": []
1207 })
1208 }
1209 } else {
1210 warn!("â ī¸ [API] No agent_handler available");
1211 serde_json::json!({
1212 "input_units_and_encoder_properties": {},
1213 "output_units_and_decoder_properties": {},
1214 "feedbacks": []
1215 })
1216 }
1217 };
1218
1219 #[cfg(not(feature = "feagi-agent"))]
1220 let device_registrations = serde_json::json!({
1223 "input_units_and_encoder_properties": {},
1224 "output_units_and_decoder_properties": {},
1225 "feedbacks": []
1226 });
1227
1228 info!(
1229 "â
[API] Device registration export succeeded for agent '{}'",
1230 agent_id
1231 );
1232
1233 Ok(Json(DeviceRegistrationExportResponse {
1234 device_registrations,
1235 agent_id,
1236 }))
1237}
1238
1239#[utoipa::path(
1249 post,
1250 path = "/v1/agent/{agent_id}/device_registrations",
1251 params(
1252 ("agent_id" = String, Path, description = "Agent ID")
1253 ),
1254 request_body = DeviceRegistrationImportRequest,
1255 responses(
1256 (status = 200, description = "Device registrations imported successfully", body = DeviceRegistrationImportResponse),
1257 (status = 400, description = "Invalid device registration configuration"),
1258 (status = 404, description = "Agent not found"),
1259 (status = 500, description = "Failed to import device registrations")
1260 ),
1261 tag = "agent"
1262)]
1263pub async fn import_device_registrations(
1264 State(state): State<ApiState>,
1265 Path(agent_id): Path<String>,
1266 Json(request): Json<DeviceRegistrationImportRequest>,
1267) -> ApiResult<Json<DeviceRegistrationImportResponse>> {
1268 info!(
1269 "đĻ [API] Device registration import requested for agent '{}'",
1270 agent_id
1271 );
1272
1273 if let Some(agent_service) = state.agent_service.as_ref() {
1276 if let Err(e) = agent_service.get_agent_properties(&agent_id).await {
1277 info!(
1278 "âšī¸ [API] Agent '{}' not found in AgentService during import ({}); continuing with agent_handler",
1279 agent_id, e
1280 );
1281 }
1282 }
1283
1284 if !request.device_registrations.is_object() {
1287 return Err(ApiError::invalid_input(
1288 "Device registrations must be a JSON object",
1289 ));
1290 }
1291
1292 let obj = request.device_registrations.as_object().unwrap();
1294 if !obj.contains_key("input_units_and_encoder_properties")
1295 || !obj.contains_key("output_units_and_decoder_properties")
1296 || !obj.contains_key("feedbacks")
1297 {
1298 return Err(ApiError::invalid_input(
1299 "Device registrations must contain: input_units_and_encoder_properties, output_units_and_decoder_properties, and feedbacks",
1300 ));
1301 }
1302
1303 #[cfg(feature = "feagi-agent")]
1305 {
1306 let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1307 if let Some(handler) = &state.agent_handler {
1308 let mut handler_guard = handler.lock().unwrap();
1309 let device_regs = request.device_registrations.clone();
1310 handler_guard.set_device_registrations_by_agent(parsed_agent_id, device_regs.clone());
1311 let descriptor_opt = handler_guard
1313 .get_all_registered_agents()
1314 .get(&parsed_agent_id)
1315 .map(|(d, _)| d.clone());
1316 if let Some(descriptor) = descriptor_opt {
1317 handler_guard.set_device_registrations_by_descriptor(
1318 agent_id.clone(),
1319 descriptor,
1320 device_regs,
1321 );
1322 }
1323 info!(
1324 "đĨ [API] Imported device registrations for agent '{}'",
1325 agent_id
1326 );
1327 } else {
1328 warn!("â ī¸ [API] No agent_handler available to store device registrations");
1329 }
1330
1331 auto_create_cortical_areas_from_device_registrations(&state, &request.device_registrations)
1332 .await;
1333
1334 Ok(Json(DeviceRegistrationImportResponse {
1335 success: true,
1336 message: format!(
1337 "Device registrations imported successfully for agent '{}'",
1338 agent_id
1339 ),
1340 agent_id,
1341 }))
1342 }
1343
1344 #[cfg(not(feature = "feagi-agent"))]
1345 {
1346 info!(
1347 "â
[API] Device registration import succeeded for agent '{}' (feagi-agent feature not enabled)",
1348 agent_id
1349 );
1350 Ok(Json(DeviceRegistrationImportResponse {
1351 success: true,
1352 message: format!(
1353 "Device registrations imported successfully for agent '{}' (feature not enabled)",
1354 agent_id
1355 ),
1356 agent_id,
1357 }))
1358 }
1359}