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::{
15 AgentRegistration, HeartbeatRequest as ServiceHeartbeatRequest,
16};
17use tracing::{error, info, warn};
18
19#[cfg(feature = "feagi-agent")]
20use feagi_agent::sdk::ConnectorAgent;
21#[cfg(feature = "feagi-agent")]
22use feagi_services::types::CreateCorticalAreaParams;
23#[cfg(feature = "feagi-agent")]
24use feagi_structures::genomic::cortical_area::descriptors::{
25 CorticalSubUnitIndex, CorticalUnitIndex,
26};
27#[cfg(feature = "feagi-agent")]
28use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
29 FrameChangeHandling, PercentageNeuronPositioning,
30};
31#[cfg(feature = "feagi-agent")]
32use feagi_structures::genomic::MotorCorticalUnit;
33#[cfg(feature = "feagi-agent")]
34use feagi_structures::genomic::SensoryCorticalUnit;
35#[cfg(feature = "feagi-agent")]
36use std::sync::{Arc, Mutex};
37
38#[cfg(feature = "feagi-agent")]
39async fn auto_create_cortical_areas_from_device_registrations(
40 state: &ApiState,
41 device_registrations: &serde_json::Value,
42) {
43 let connectome_service = state.connectome_service.as_ref();
44 let genome_service = state.genome_service.as_ref();
45
46 let Some(output_units) = device_registrations
47 .get("output_units_and_decoder_properties")
48 .and_then(|v| v.as_object())
49 else {
50 return;
51 };
52 let input_units = device_registrations
53 .get("input_units_and_encoder_properties")
54 .and_then(|v| v.as_object());
55
56 let mut to_create: Vec<CreateCorticalAreaParams> = Vec::new();
58
59 for (motor_unit_key, unit_defs) in output_units {
60 let motor_unit: MotorCorticalUnit = match serde_json::from_value::<MotorCorticalUnit>(
62 serde_json::Value::String(motor_unit_key.clone()),
63 ) {
64 Ok(v) => v,
65 Err(e) => {
66 warn!(
67 "⚠️ [API] Unable to parse MotorCorticalUnit key '{}' from device_registrations: {}",
68 motor_unit_key, e
69 );
70 continue;
71 }
72 };
73
74 let Some(unit_defs_arr) = unit_defs.as_array() else {
75 continue;
76 };
77
78 for entry in unit_defs_arr {
79 let Some(pair) = entry.as_array() else {
81 continue;
82 };
83 let Some(unit_def) = pair.first() else {
84 continue;
85 };
86 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
87 else {
88 continue;
89 };
90 let group_u8: u8 = match group_u64.try_into() {
91 Ok(v) => v,
92 Err(_) => continue,
93 };
94 let group: CorticalUnitIndex = group_u8.into();
95
96 let device_count = unit_def
97 .get("device_grouping")
98 .and_then(|v| v.as_array())
99 .map(|a| a.len())
100 .unwrap_or(0);
101 if device_count == 0 {
102 warn!(
103 "⚠️ [API] device_grouping is empty for motor unit '{}' group {}; skipping auto-create",
104 motor_unit_key, group_u8
105 );
106 continue;
107 }
108
109 let frame_change_handling = FrameChangeHandling::Absolute;
111 let percentage_neuron_positioning = PercentageNeuronPositioning::Linear;
112
113 let cortical_ids = match motor_unit {
114 MotorCorticalUnit::RotaryMotor => MotorCorticalUnit::get_cortical_ids_array_for_rotary_motor_with_parameters(
115 frame_change_handling,
116 percentage_neuron_positioning,
117 group,
118 )
119 .to_vec(),
120 MotorCorticalUnit::PositionalServo => MotorCorticalUnit::get_cortical_ids_array_for_positional_servo_with_parameters(
121 frame_change_handling,
122 percentage_neuron_positioning,
123 group,
124 )
125 .to_vec(),
126 MotorCorticalUnit::Gaze => MotorCorticalUnit::get_cortical_ids_array_for_gaze_with_parameters(
127 frame_change_handling,
128 percentage_neuron_positioning,
129 group,
130 )
131 .to_vec(),
132 MotorCorticalUnit::MiscData => MotorCorticalUnit::get_cortical_ids_array_for_misc_data_with_parameters(
133 frame_change_handling,
134 group,
135 )
136 .to_vec(),
137 MotorCorticalUnit::TextEnglishOutput => MotorCorticalUnit::get_cortical_ids_array_for_text_english_output_with_parameters(
138 frame_change_handling,
139 group,
140 )
141 .to_vec(),
142 MotorCorticalUnit::CountOutput => MotorCorticalUnit::get_cortical_ids_array_for_count_output_with_parameters(
143 frame_change_handling,
144 percentage_neuron_positioning,
145 group,
146 )
147 .to_vec(),
148 MotorCorticalUnit::ObjectSegmentation => MotorCorticalUnit::get_cortical_ids_array_for_object_segmentation_with_parameters(
149 frame_change_handling,
150 group,
151 )
152 .to_vec(),
153 MotorCorticalUnit::SimpleVisionOutput => MotorCorticalUnit::get_cortical_ids_array_for_simple_vision_output_with_parameters(
154 frame_change_handling,
155 group,
156 )
157 .to_vec(),
158 MotorCorticalUnit::DynamicImageProcessing => MotorCorticalUnit::get_cortical_ids_array_for_dynamic_image_processing_with_parameters(
159 frame_change_handling,
160 percentage_neuron_positioning,
161 group,
162 )
163 .to_vec(),
164 };
165
166 let topology = motor_unit.get_unit_default_topology();
167
168 for (i, cortical_id) in cortical_ids.iter().enumerate() {
169 let cortical_id_b64 = cortical_id.as_base_64();
170 let exists = match connectome_service
171 .cortical_area_exists(&cortical_id_b64)
172 .await
173 {
174 Ok(v) => v,
175 Err(e) => {
176 warn!(
177 "⚠️ [API] Failed to check cortical area existence for '{}': {}",
178 cortical_id_b64, e
179 );
180 continue;
181 }
182 };
183
184 if exists {
185 let desired_name = if matches!(motor_unit, MotorCorticalUnit::Gaze) {
191 let subunit_name = match i {
192 0 => "Eccentricity",
193 1 => "Modulation",
194 _ => "Subunit",
195 };
196 format!(
197 "{} ({}) Unit {}",
198 motor_unit.get_friendly_name(),
199 subunit_name,
200 group_u8
201 )
202 } else if cortical_ids.len() > 1 {
203 format!(
204 "{} Subunit {} Unit {}",
205 motor_unit.get_friendly_name(),
206 i,
207 group_u8
208 )
209 } else {
210 format!("{} Unit {}", motor_unit.get_friendly_name(), group_u8)
211 };
212
213 match connectome_service.get_cortical_area(&cortical_id_b64).await {
214 Ok(existing_area) => {
215 if existing_area.name.is_empty()
216 || existing_area.name == existing_area.cortical_id
217 {
218 let mut changes = std::collections::HashMap::new();
219 changes.insert(
220 "cortical_name".to_string(),
221 serde_json::json!(desired_name),
222 );
223 if let Err(e) = genome_service
224 .update_cortical_area(&cortical_id_b64, changes)
225 .await
226 {
227 warn!(
228 "⚠️ [API] Failed to auto-rename existing motor cortical area '{}': {}",
229 cortical_id_b64, e
230 );
231 }
232 }
233 }
234 Err(e) => {
235 warn!(
236 "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
237 cortical_id_b64, e
238 );
239 }
240 }
241 continue;
242 }
243
244 let Some(unit_topology) = topology.get(&CorticalSubUnitIndex::from(i as u8)) else {
245 warn!(
246 "⚠️ [API] Missing unit topology for motor unit '{}' subunit {} (agent device_registrations); cannot auto-create '{}'",
247 motor_unit.get_snake_case_name(),
248 i,
249 cortical_id_b64
250 );
251 continue;
252 };
253 let dims = unit_topology.channel_dimensions_default;
254 let pos = unit_topology.relative_position;
255 let total_x = (dims[0] as usize).saturating_mul(device_count);
256 let dimensions = (total_x, dims[1] as usize, dims[2] as usize);
257 let position = (pos[0], pos[1], pos[2]);
258
259 let name = if matches!(motor_unit, MotorCorticalUnit::Gaze) {
264 let subunit_name = match i {
265 0 => "Eccentricity",
266 1 => "Modulation",
267 _ => "Subunit",
268 };
269 format!(
270 "{} ({}) Unit {}",
271 motor_unit.get_friendly_name(),
272 subunit_name,
273 group_u8
274 )
275 } else if cortical_ids.len() > 1 {
276 format!(
277 "{} Subunit {} Unit {}",
278 motor_unit.get_friendly_name(),
279 i,
280 group_u8
281 )
282 } else {
283 format!("{} Unit {}", motor_unit.get_friendly_name(), group_u8)
284 };
285
286 to_create.push(CreateCorticalAreaParams {
287 cortical_id: cortical_id_b64.clone(),
288 name,
289 dimensions,
290 position,
291 area_type: "Motor".to_string(),
292 visible: Some(true),
293 sub_group: None,
294 neurons_per_voxel: Some(1),
295 postsynaptic_current: None,
296 plasticity_constant: None,
297 degeneration: None,
298 psp_uniform_distribution: None,
299 firing_threshold_increment: None,
300 firing_threshold_limit: None,
301 consecutive_fire_count: None,
302 snooze_period: None,
303 refractory_period: None,
304 leak_coefficient: None,
305 leak_variability: None,
306 burst_engine_active: None,
307 properties: None,
308 });
309 }
310 }
311 }
312
313 if let Some(input_units) = input_units {
314 for (sensory_unit_key, unit_defs) in input_units {
316 let sensory_unit: SensoryCorticalUnit = match serde_json::from_value::<
317 SensoryCorticalUnit,
318 >(serde_json::Value::String(
319 sensory_unit_key.clone(),
320 )) {
321 Ok(v) => v,
322 Err(e) => {
323 warn!(
324 "⚠️ [API] Unable to parse SensoryCorticalUnit key '{}' from device_registrations: {}",
325 sensory_unit_key, e
326 );
327 continue;
328 }
329 };
330
331 let Some(unit_defs_arr) = unit_defs.as_array() else {
332 continue;
333 };
334
335 for entry in unit_defs_arr {
336 let Some(pair) = entry.as_array() else {
337 continue;
338 };
339 let Some(unit_def) = pair.first() else {
340 continue;
341 };
342 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
343 else {
344 continue;
345 };
346 let group_u8: u8 = match group_u64.try_into() {
347 Ok(v) => v,
348 Err(_) => continue,
349 };
350 let group: CorticalUnitIndex = group_u8.into();
351
352 let device_count = unit_def
353 .get("device_grouping")
354 .and_then(|v| v.as_array())
355 .map(|a| a.len())
356 .unwrap_or(0);
357 if device_count == 0 {
358 continue;
359 }
360
361 let frame_change_handling = FrameChangeHandling::Absolute;
362 let percentage_neuron_positioning = PercentageNeuronPositioning::Linear;
363 let cortical_ids = match sensory_unit {
364 SensoryCorticalUnit::Infrared => {
365 SensoryCorticalUnit::get_cortical_ids_array_for_infrared_with_parameters(
366 frame_change_handling,
367 percentage_neuron_positioning,
368 group,
369 )
370 .to_vec()
371 }
372 SensoryCorticalUnit::Proximity => {
373 SensoryCorticalUnit::get_cortical_ids_array_for_proximity_with_parameters(
374 frame_change_handling,
375 percentage_neuron_positioning,
376 group,
377 )
378 .to_vec()
379 }
380 SensoryCorticalUnit::Shock => {
381 SensoryCorticalUnit::get_cortical_ids_array_for_shock_with_parameters(
382 frame_change_handling,
383 percentage_neuron_positioning,
384 group,
385 )
386 .to_vec()
387 }
388 SensoryCorticalUnit::Battery => {
389 SensoryCorticalUnit::get_cortical_ids_array_for_battery_with_parameters(
390 frame_change_handling,
391 percentage_neuron_positioning,
392 group,
393 )
394 .to_vec()
395 }
396 SensoryCorticalUnit::Servo => {
397 SensoryCorticalUnit::get_cortical_ids_array_for_servo_with_parameters(
398 frame_change_handling,
399 percentage_neuron_positioning,
400 group,
401 )
402 .to_vec()
403 }
404 SensoryCorticalUnit::AnalogGPIO => {
405 SensoryCorticalUnit::get_cortical_ids_array_for_analog_g_p_i_o_with_parameters(
406 frame_change_handling,
407 percentage_neuron_positioning,
408 group,
409 )
410 .to_vec()
411 }
412 SensoryCorticalUnit::DigitalGPIO => {
413 SensoryCorticalUnit::get_cortical_ids_array_for_digital_g_p_i_o_with_parameters(
414 group,
415 )
416 .to_vec()
417 }
418 SensoryCorticalUnit::MiscData => {
419 SensoryCorticalUnit::get_cortical_ids_array_for_misc_data_with_parameters(
420 frame_change_handling,
421 group,
422 )
423 .to_vec()
424 }
425 SensoryCorticalUnit::TextEnglishInput => {
426 SensoryCorticalUnit::get_cortical_ids_array_for_text_english_input_with_parameters(
427 frame_change_handling,
428 group,
429 )
430 .to_vec()
431 }
432 SensoryCorticalUnit::CountInput => {
433 SensoryCorticalUnit::get_cortical_ids_array_for_count_input_with_parameters(
434 frame_change_handling,
435 percentage_neuron_positioning,
436 group,
437 )
438 .to_vec()
439 }
440 SensoryCorticalUnit::Vision => {
441 SensoryCorticalUnit::get_cortical_ids_array_for_vision_with_parameters(
442 frame_change_handling,
443 group,
444 )
445 .to_vec()
446 }
447 SensoryCorticalUnit::SegmentedVision => {
448 SensoryCorticalUnit::get_cortical_ids_array_for_segmented_vision_with_parameters(
449 frame_change_handling,
450 group,
451 )
452 .to_vec()
453 }
454 SensoryCorticalUnit::Accelerometer => {
455 SensoryCorticalUnit::get_cortical_ids_array_for_accelerometer_with_parameters(
456 frame_change_handling,
457 percentage_neuron_positioning,
458 group,
459 )
460 .to_vec()
461 }
462 SensoryCorticalUnit::Gyroscope => {
463 SensoryCorticalUnit::get_cortical_ids_array_for_gyroscope_with_parameters(
464 frame_change_handling,
465 percentage_neuron_positioning,
466 group,
467 )
468 .to_vec()
469 }
470 };
471
472 for (i, cortical_id) in cortical_ids.iter().enumerate() {
473 let cortical_id_b64 = cortical_id.as_base_64();
474 match connectome_service.get_cortical_area(&cortical_id_b64).await {
475 Ok(existing_area) => {
476 if existing_area.name.is_empty()
477 || existing_area.name == existing_area.cortical_id
478 {
479 let desired_name = if cortical_ids.len() > 1 {
480 format!(
481 "{} Subunit {} Unit {}",
482 sensory_unit.get_friendly_name(),
483 i,
484 group_u8
485 )
486 } else {
487 format!(
488 "{} Unit {}",
489 sensory_unit.get_friendly_name(),
490 group_u8
491 )
492 };
493 let mut changes = std::collections::HashMap::new();
494 changes.insert(
495 "cortical_name".to_string(),
496 serde_json::json!(desired_name),
497 );
498 if let Err(e) = genome_service
499 .update_cortical_area(&cortical_id_b64, changes)
500 .await
501 {
502 warn!(
503 "⚠️ [API] Failed to auto-rename existing sensory cortical area '{}': {}",
504 cortical_id_b64, e
505 );
506 }
507 }
508 }
509 Err(e) => {
510 warn!(
511 "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
512 cortical_id_b64, e
513 );
514 }
515 }
516 }
517 }
518 }
519 }
520
521 if to_create.is_empty() {
522 return;
523 }
524
525 info!(
526 "🦀 [API] Auto-creating {} missing cortical areas from device registrations",
527 to_create.len()
528 );
529
530 if let Err(e) = genome_service.create_cortical_areas(to_create).await {
531 warn!(
532 "⚠️ [API] Failed to auto-create cortical areas from device registrations: {}",
533 e
534 );
535 }
536}
537
538#[utoipa::path(
540 post,
541 path = "/v1/agent/register",
542 request_body = AgentRegistrationRequest,
543 responses(
544 (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
545 (status = 500, description = "Registration failed", body = String)
546 ),
547 tag = "agent"
548)]
549pub async fn register_agent(
550 State(state): State<ApiState>,
551 Json(request): Json<AgentRegistrationRequest>,
552) -> ApiResult<Json<AgentRegistrationResponse>> {
553 info!(
554 "🦀 [API] Registration request received for agent '{}' (type: {})",
555 request.agent_id, request.agent_type
556 );
557
558 let agent_service = state
559 .agent_service
560 .as_ref()
561 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
562
563 #[cfg(feature = "feagi-agent")]
565 let device_registrations_opt = request
566 .capabilities
567 .get("device_registrations")
568 .and_then(|v| {
569 if let Some(obj) = v.as_object() {
571 if obj.contains_key("input_units_and_encoder_properties")
572 && obj.contains_key("output_units_and_decoder_properties")
573 && obj.contains_key("feedbacks")
574 {
575 Some(v.clone())
576 } else {
577 None
578 }
579 } else {
580 None
581 }
582 });
583
584 let registration = AgentRegistration {
585 agent_id: request.agent_id.clone(),
586 agent_type: request.agent_type,
587 agent_data_port: request.agent_data_port,
588 agent_version: request.agent_version,
589 controller_version: request.controller_version,
590 agent_ip: request.agent_ip,
591 capabilities: request.capabilities,
592 metadata: request.metadata,
593 chosen_transport: request.chosen_transport,
594 };
595
596 match agent_service.register_agent(registration).await {
597 Ok(response) => {
598 info!(
599 "✅ [API] Agent '{}' registration succeeded (status: {})",
600 request.agent_id, response.status
601 );
602
603 #[cfg(feature = "feagi-agent")]
606 if let Some(device_registrations_value) = device_registrations_opt {
607 let device_registrations_for_autocreate = device_registrations_value.clone();
608 let connector = {
610 let mut connectors = state.agent_connectors.write();
611 connectors
612 .entry(request.agent_id.clone())
613 .or_insert_with(|| Arc::new(Mutex::new(ConnectorAgent::new())))
614 .clone()
615 };
616
617 let import_result = {
619 let mut connector_guard = connector.lock().unwrap();
620 connector_guard
621 .import_device_registrations_as_config_json(device_registrations_value)
622 };
623
624 match import_result {
625 Err(e) => {
626 warn!(
627 "⚠️ [API] Failed to import device registrations from capabilities for agent '{}': {}",
628 request.agent_id, e
629 );
630 }
631 Ok(()) => {
632 info!(
633 "✅ [API] Imported device registrations from capabilities for agent '{}'",
634 request.agent_id
635 );
636 auto_create_cortical_areas_from_device_registrations(
637 &state,
638 &device_registrations_for_autocreate,
639 )
640 .await;
641 }
642 }
643 } else {
644 let mut connectors = state.agent_connectors.write();
646 connectors
647 .entry(request.agent_id.clone())
648 .or_insert_with(|| Arc::new(Mutex::new(ConnectorAgent::new())));
649 }
650
651 let transports = response.transports.map(|ts| {
653 ts.into_iter()
654 .map(|t| crate::v1::agent_dtos::TransportConfig {
655 transport_type: t.transport_type,
656 enabled: t.enabled,
657 ports: t.ports,
658 host: t.host,
659 })
660 .collect()
661 });
662
663 Ok(Json(AgentRegistrationResponse {
664 status: response.status,
665 message: response.message,
666 success: response.success,
667 transport: response.transport,
668 rates: response.rates,
669 transports,
670 recommended_transport: response.recommended_transport,
671 zmq_ports: response.zmq_ports,
672 shm_paths: response.shm_paths,
673 cortical_areas: response.cortical_areas,
674 }))
675 }
676 Err(e) => {
677 let error_msg = e.to_string();
679 warn!(
680 "❌ [API] Agent '{}' registration FAILED: {}",
681 request.agent_id, error_msg
682 );
683 if error_msg.contains("not supported") || error_msg.contains("disabled") {
684 Err(ApiError::invalid_input(error_msg))
685 } else {
686 Err(ApiError::internal(format!("Registration failed: {}", e)))
687 }
688 }
689 }
690}
691
692#[utoipa::path(
694 post,
695 path = "/v1/agent/heartbeat",
696 request_body = HeartbeatRequest,
697 responses(
698 (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
699 (status = 404, description = "Agent not found"),
700 (status = 500, description = "Heartbeat failed")
701 ),
702 tag = "agent"
703)]
704pub async fn heartbeat(
705 State(state): State<ApiState>,
706 Json(request): Json<HeartbeatRequest>,
707) -> ApiResult<Json<HeartbeatResponse>> {
708 let agent_service = state
709 .agent_service
710 .as_ref()
711 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
712
713 let service_request = ServiceHeartbeatRequest {
714 agent_id: request.agent_id.clone(),
715 };
716
717 match agent_service.heartbeat(service_request).await {
718 Ok(_) => Ok(Json(HeartbeatResponse {
719 message: "heartbeat_ok".to_string(),
720 success: true,
721 })),
722 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
723 }
724}
725
726#[utoipa::path(
728 get,
729 path = "/v1/agent/list",
730 responses(
731 (status = 200, description = "List of agent IDs", body = Vec<String>),
732 (status = 503, description = "Registration service unavailable")
733 ),
734 tag = "agent"
735)]
736pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
737 let agent_service = state
738 .agent_service
739 .as_ref()
740 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
741
742 match agent_service.list_agents().await {
743 Ok(agent_ids) => Ok(Json(agent_ids)),
744 Err(e) => Err(ApiError::internal(format!("Failed to list agents: {}", e))),
745 }
746}
747
748#[utoipa::path(
750 get,
751 path = "/v1/agent/properties",
752 params(
753 ("agent_id" = String, Query, description = "Agent ID to get properties for")
754 ),
755 responses(
756 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
757 (status = 404, description = "Agent not found"),
758 (status = 500, description = "Failed to get agent properties")
759 ),
760 tag = "agent"
761)]
762pub async fn get_agent_properties(
763 State(state): State<ApiState>,
764 Query(params): Query<HashMap<String, String>>,
765) -> ApiResult<Json<AgentPropertiesResponse>> {
766 let agent_id = params
767 .get("agent_id")
768 .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
769
770 let agent_service = state
771 .agent_service
772 .as_ref()
773 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
774
775 match agent_service.get_agent_properties(agent_id).await {
776 Ok(properties) => Ok(Json(AgentPropertiesResponse {
777 agent_type: properties.agent_type,
778 agent_ip: properties.agent_ip,
779 agent_data_port: properties.agent_data_port,
780 agent_router_address: properties.agent_router_address,
781 agent_version: properties.agent_version,
782 controller_version: properties.controller_version,
783 capabilities: properties.capabilities,
784 chosen_transport: properties.chosen_transport,
785 })),
786 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
787 }
788}
789
790#[utoipa::path(
792 get,
793 path = "/v1/agent/shared_mem",
794 responses(
795 (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
796 (status = 500, description = "Failed to get shared memory info")
797 ),
798 tag = "agent"
799)]
800pub async fn get_shared_memory(
801 State(state): State<ApiState>,
802) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
803 let agent_service = state
804 .agent_service
805 .as_ref()
806 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
807
808 match agent_service.get_shared_memory_info().await {
809 Ok(shm_info) => Ok(Json(shm_info)),
810 Err(e) => Err(ApiError::internal(format!(
811 "Failed to get shared memory info: {}",
812 e
813 ))),
814 }
815}
816
817#[utoipa::path(
819 delete,
820 path = "/v1/agent/deregister",
821 request_body = AgentDeregistrationRequest,
822 responses(
823 (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
824 (status = 500, description = "Deregistration failed")
825 ),
826 tag = "agent"
827)]
828pub async fn deregister_agent(
829 State(state): State<ApiState>,
830 Json(request): Json<AgentDeregistrationRequest>,
831) -> ApiResult<Json<SuccessResponse>> {
832 let agent_service = state
833 .agent_service
834 .as_ref()
835 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
836
837 match agent_service.deregister_agent(&request.agent_id).await {
838 Ok(_) => Ok(Json(SuccessResponse {
839 message: format!("Agent '{}' deregistered successfully", request.agent_id),
840 success: Some(true),
841 })),
842 Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
843 }
844}
845
846#[utoipa::path(
848 post,
849 path = "/v1/agent/manual_stimulation",
850 request_body = ManualStimulationRequest,
851 responses(
852 (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
853 (status = 500, description = "Stimulation failed")
854 ),
855 tag = "agent"
856)]
857pub async fn manual_stimulation(
858 State(state): State<ApiState>,
859 Json(request): Json<ManualStimulationRequest>,
860) -> ApiResult<Json<ManualStimulationResponse>> {
861 let agent_service = state
862 .agent_service
863 .as_ref()
864 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
865
866 agent_service.try_set_runtime_service(state.runtime_service.clone());
869
870 match agent_service
871 .manual_stimulation(request.stimulation_payload)
872 .await
873 {
874 Ok(result) => {
875 let success = result
876 .get("success")
877 .and_then(|v| v.as_bool())
878 .unwrap_or(false);
879 let total_coordinates = result
880 .get("total_coordinates")
881 .and_then(|v| v.as_u64())
882 .unwrap_or(0) as usize;
883 let successful_areas = result
884 .get("successful_areas")
885 .and_then(|v| v.as_u64())
886 .unwrap_or(0) as usize;
887 let failed_areas = result
888 .get("failed_areas")
889 .and_then(|v| v.as_array())
890 .map(|arr| {
891 arr.iter()
892 .filter_map(|v| v.as_str().map(String::from))
893 .collect()
894 })
895 .unwrap_or_default();
896 let error = result
897 .get("error")
898 .and_then(|v| v.as_str())
899 .map(String::from);
900
901 Ok(Json(ManualStimulationResponse {
902 success,
903 total_coordinates,
904 successful_areas,
905 failed_areas,
906 error,
907 }))
908 }
909 Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
910 }
911}
912
913#[utoipa::path(
915 get,
916 path = "/v1/agent/fq_sampler_status",
917 responses(
918 (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
919 (status = 500, description = "Failed to get FQ sampler status")
920 ),
921 tag = "agent"
922)]
923pub async fn get_fq_sampler_status(
924 State(state): State<ApiState>,
925) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
926 let agent_service = state
927 .agent_service
928 .as_ref()
929 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
930
931 let runtime_service = state.runtime_service.as_ref();
932
933 let agent_ids = agent_service
935 .list_agents()
936 .await
937 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
938
939 let (fcl_frequency, fcl_consumer) = runtime_service
941 .get_fcl_sampler_config()
942 .await
943 .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
944
945 let mut visualization_agents = Vec::new();
947 let mut motor_agents = Vec::new();
948
949 for agent_id in &agent_ids {
950 if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
951 if props.capabilities.contains_key("visualization") {
952 visualization_agents.push(agent_id.clone());
953 }
954 if props.capabilities.contains_key("motor") {
955 motor_agents.push(agent_id.clone());
956 }
957 }
958 }
959
960 let mut fq_coordination = HashMap::new();
961
962 let mut viz_sampler = HashMap::new();
963 viz_sampler.insert(
964 "enabled".to_string(),
965 serde_json::json!(!visualization_agents.is_empty()),
966 );
967 viz_sampler.insert(
968 "reason".to_string(),
969 serde_json::json!(if visualization_agents.is_empty() {
970 "No visualization agents connected".to_string()
971 } else {
972 format!(
973 "{} visualization agent(s) connected",
974 visualization_agents.len()
975 )
976 }),
977 );
978 viz_sampler.insert(
979 "agents_requiring".to_string(),
980 serde_json::json!(visualization_agents),
981 );
982 viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
983 fq_coordination.insert(
984 "visualization_fq_sampler".to_string(),
985 serde_json::json!(viz_sampler),
986 );
987
988 let mut motor_sampler = HashMap::new();
989 motor_sampler.insert(
990 "enabled".to_string(),
991 serde_json::json!(!motor_agents.is_empty()),
992 );
993 motor_sampler.insert(
994 "reason".to_string(),
995 serde_json::json!(if motor_agents.is_empty() {
996 "No motor agents connected".to_string()
997 } else {
998 format!("{} motor agent(s) connected", motor_agents.len())
999 }),
1000 );
1001 motor_sampler.insert(
1002 "agents_requiring".to_string(),
1003 serde_json::json!(motor_agents),
1004 );
1005 motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
1006 fq_coordination.insert(
1007 "motor_fq_sampler".to_string(),
1008 serde_json::json!(motor_sampler),
1009 );
1010
1011 let mut response = HashMap::new();
1012 response.insert(
1013 "fq_sampler_coordination".to_string(),
1014 serde_json::json!(fq_coordination),
1015 );
1016 response.insert(
1017 "agent_registry".to_string(),
1018 serde_json::json!({
1019 "total_agents": agent_ids.len(),
1020 "agent_ids": agent_ids
1021 }),
1022 );
1023 response.insert(
1024 "system_status".to_string(),
1025 serde_json::json!("coordinated_via_registration_manager"),
1026 );
1027 response.insert(
1028 "fcl_sampler_consumer".to_string(),
1029 serde_json::json!(fcl_consumer),
1030 );
1031
1032 Ok(Json(response))
1033}
1034
1035#[utoipa::path(
1037 get,
1038 path = "/v1/agent/capabilities",
1039 responses(
1040 (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
1041 (status = 500, description = "Failed to get capabilities")
1042 ),
1043 tag = "agent"
1044)]
1045pub async fn get_capabilities(
1046 State(_state): State<ApiState>,
1047) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
1048 let mut response = HashMap::new();
1049 response.insert(
1050 "agent_types".to_string(),
1051 vec![
1052 "sensory".to_string(),
1053 "motor".to_string(),
1054 "both".to_string(),
1055 "visualization".to_string(),
1056 "infrastructure".to_string(),
1057 ],
1058 );
1059 response.insert(
1060 "capability_types".to_string(),
1061 vec![
1062 "vision".to_string(),
1063 "motor".to_string(),
1064 "visualization".to_string(),
1065 "sensory".to_string(),
1066 ],
1067 );
1068
1069 Ok(Json(response))
1070}
1071
1072#[utoipa::path(
1074 get,
1075 path = "/v1/agent/info/{agent_id}",
1076 params(
1077 ("agent_id" = String, Path, description = "Agent ID")
1078 ),
1079 responses(
1080 (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
1081 (status = 404, description = "Agent not found"),
1082 (status = 500, description = "Failed to get agent info")
1083 ),
1084 tag = "agent"
1085)]
1086pub async fn get_agent_info(
1087 State(state): State<ApiState>,
1088 Path(agent_id): Path<String>,
1089) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1090 let agent_service = state
1091 .agent_service
1092 .as_ref()
1093 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1094
1095 let properties = agent_service
1096 .get_agent_properties(&agent_id)
1097 .await
1098 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1099
1100 let mut response = HashMap::new();
1101 response.insert("agent_id".to_string(), serde_json::json!(agent_id));
1102 response.insert(
1103 "agent_type".to_string(),
1104 serde_json::json!(properties.agent_type),
1105 );
1106 response.insert(
1107 "agent_ip".to_string(),
1108 serde_json::json!(properties.agent_ip),
1109 );
1110 response.insert(
1111 "agent_data_port".to_string(),
1112 serde_json::json!(properties.agent_data_port),
1113 );
1114 response.insert(
1115 "capabilities".to_string(),
1116 serde_json::json!(properties.capabilities),
1117 );
1118 response.insert(
1119 "agent_version".to_string(),
1120 serde_json::json!(properties.agent_version),
1121 );
1122 response.insert(
1123 "controller_version".to_string(),
1124 serde_json::json!(properties.controller_version),
1125 );
1126 response.insert("status".to_string(), serde_json::json!("active"));
1127 if let Some(ref transport) = properties.chosen_transport {
1128 response.insert("chosen_transport".to_string(), serde_json::json!(transport));
1129 }
1130
1131 Ok(Json(response))
1132}
1133
1134#[utoipa::path(
1136 get,
1137 path = "/v1/agent/properties/{agent_id}",
1138 params(
1139 ("agent_id" = String, Path, description = "Agent ID")
1140 ),
1141 responses(
1142 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
1143 (status = 404, description = "Agent not found"),
1144 (status = 500, description = "Failed to get agent properties")
1145 ),
1146 tag = "agent"
1147)]
1148pub async fn get_agent_properties_path(
1149 State(state): State<ApiState>,
1150 Path(agent_id): Path<String>,
1151) -> ApiResult<Json<AgentPropertiesResponse>> {
1152 let agent_service = state
1153 .agent_service
1154 .as_ref()
1155 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
1156
1157 match agent_service.get_agent_properties(&agent_id).await {
1158 Ok(properties) => Ok(Json(AgentPropertiesResponse {
1159 agent_type: properties.agent_type,
1160 agent_ip: properties.agent_ip,
1161 agent_data_port: properties.agent_data_port,
1162 agent_router_address: properties.agent_router_address,
1163 agent_version: properties.agent_version,
1164 controller_version: properties.controller_version,
1165 capabilities: properties.capabilities,
1166 chosen_transport: properties.chosen_transport,
1167 })),
1168 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
1169 }
1170}
1171
1172#[utoipa::path(
1174 post,
1175 path = "/v1/agent/configure",
1176 responses(
1177 (status = 200, description = "Agent configured", body = HashMap<String, String>),
1178 (status = 400, description = "Invalid input"),
1179 (status = 500, description = "Failed to configure agent")
1180 ),
1181 tag = "agent"
1182)]
1183pub async fn post_configure(
1184 State(_state): State<ApiState>,
1185 Json(config): Json<HashMap<String, serde_json::Value>>,
1186) -> ApiResult<Json<HashMap<String, String>>> {
1187 tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
1188
1189 Ok(Json(HashMap::from([
1190 (
1191 "message".to_string(),
1192 "Agent configuration updated".to_string(),
1193 ),
1194 ("status".to_string(), "not_yet_implemented".to_string()),
1195 ])))
1196}
1197
1198#[utoipa::path(
1205 get,
1206 path = "/v1/agent/{agent_id}/device_registrations",
1207 params(
1208 ("agent_id" = String, Path, description = "Agent ID")
1209 ),
1210 responses(
1211 (status = 200, description = "Device registrations exported successfully", body = DeviceRegistrationExportResponse),
1212 (status = 404, description = "Agent not found"),
1213 (status = 500, description = "Failed to export device registrations")
1214 ),
1215 tag = "agent"
1216)]
1217pub async fn export_device_registrations(
1218 State(state): State<ApiState>,
1219 Path(agent_id): Path<String>,
1220) -> ApiResult<Json<DeviceRegistrationExportResponse>> {
1221 info!(
1222 "🦀 [API] Device registration export requested for agent '{}'",
1223 agent_id
1224 );
1225
1226 if let Some(agent_service) = state.agent_service.as_ref() {
1232 let _properties = agent_service
1233 .get_agent_properties(&agent_id)
1234 .await
1235 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1236 } else {
1237 info!(
1238 "ℹ️ [API] Agent service not available; skipping agent existence check for export (agent '{}')",
1239 agent_id
1240 );
1241 }
1242
1243 #[cfg(feature = "feagi-agent")]
1245 let device_registrations = {
1246 let connector = {
1249 let connectors = state.agent_connectors.read();
1250 connectors.get(&agent_id).cloned()
1251 };
1252
1253 let connector = match connector {
1254 Some(c) => {
1255 info!(
1256 "🔍 [API] Found existing ConnectorAgent for agent '{}'",
1257 agent_id
1258 );
1259 c
1260 }
1261 None => {
1262 warn!(
1263 "⚠️ [API] No ConnectorAgent found for agent '{}' - device registrations may not have been imported yet. Total agents in registry: {}",
1264 agent_id,
1265 {
1266 let connectors = state.agent_connectors.read();
1267 connectors.len()
1268 }
1269 );
1270 return Ok(Json(DeviceRegistrationExportResponse {
1273 device_registrations: serde_json::json!({
1274 "input_units_and_encoder_properties": {},
1275 "output_units_and_decoder_properties": {},
1276 "feedbacks": []
1277 }),
1278 agent_id,
1279 }));
1280 }
1281 };
1282
1283 let connector_guard = connector.lock().unwrap();
1285 match connector_guard.export_device_registrations_as_config_json() {
1286 Ok(registrations) => {
1287 let input_count = registrations
1289 .get("input_units_and_encoder_properties")
1290 .and_then(|v| v.as_object())
1291 .map(|m| m.len())
1292 .unwrap_or(0);
1293 let output_count = registrations
1294 .get("output_units_and_decoder_properties")
1295 .and_then(|v| v.as_object())
1296 .map(|m| m.len())
1297 .unwrap_or(0);
1298 let feedback_count = registrations
1299 .get("feedbacks")
1300 .and_then(|v| v.as_array())
1301 .map(|a| a.len())
1302 .unwrap_or(0);
1303
1304 info!(
1305 "📤 [API] Exporting device registrations for agent '{}': {} input units, {} output units, {} feedbacks",
1306 agent_id, input_count, output_count, feedback_count
1307 );
1308
1309 if input_count == 0 && output_count == 0 && feedback_count == 0 {
1310 warn!(
1311 "⚠️ [API] Exported device registrations for agent '{}' are empty - agent may not have synced device registrations yet",
1312 agent_id
1313 );
1314 }
1315
1316 registrations
1317 }
1318 Err(e) => {
1319 warn!(
1320 "⚠️ [API] Failed to export device registrations for agent '{}': {}",
1321 agent_id, e
1322 );
1323 serde_json::json!({
1326 "input_units_and_encoder_properties": {},
1327 "output_units_and_decoder_properties": {},
1328 "feedbacks": []
1329 })
1330 }
1331 }
1332 };
1333
1334 #[cfg(not(feature = "feagi-agent"))]
1335 let device_registrations = serde_json::json!({
1338 "input_units_and_encoder_properties": {},
1339 "output_units_and_decoder_properties": {},
1340 "feedbacks": []
1341 });
1342
1343 info!(
1344 "✅ [API] Device registration export succeeded for agent '{}'",
1345 agent_id
1346 );
1347
1348 Ok(Json(DeviceRegistrationExportResponse {
1349 device_registrations,
1350 agent_id,
1351 }))
1352}
1353
1354#[utoipa::path(
1364 post,
1365 path = "/v1/agent/{agent_id}/device_registrations",
1366 params(
1367 ("agent_id" = String, Path, description = "Agent ID")
1368 ),
1369 request_body = DeviceRegistrationImportRequest,
1370 responses(
1371 (status = 200, description = "Device registrations imported successfully", body = DeviceRegistrationImportResponse),
1372 (status = 400, description = "Invalid device registration configuration"),
1373 (status = 404, description = "Agent not found"),
1374 (status = 500, description = "Failed to import device registrations")
1375 ),
1376 tag = "agent"
1377)]
1378pub async fn import_device_registrations(
1379 State(state): State<ApiState>,
1380 Path(agent_id): Path<String>,
1381 Json(request): Json<DeviceRegistrationImportRequest>,
1382) -> ApiResult<Json<DeviceRegistrationImportResponse>> {
1383 info!(
1384 "🦀 [API] Device registration import requested for agent '{}'",
1385 agent_id
1386 );
1387
1388 if let Some(agent_service) = state.agent_service.as_ref() {
1390 let _properties = agent_service
1391 .get_agent_properties(&agent_id)
1392 .await
1393 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1394 } else {
1395 info!(
1396 "ℹ️ [API] Agent service not available; skipping agent existence check for import (agent '{}')",
1397 agent_id
1398 );
1399 }
1400
1401 if !request.device_registrations.is_object() {
1404 return Err(ApiError::invalid_input(
1405 "Device registrations must be a JSON object",
1406 ));
1407 }
1408
1409 let obj = request.device_registrations.as_object().unwrap();
1411 if !obj.contains_key("input_units_and_encoder_properties")
1412 || !obj.contains_key("output_units_and_decoder_properties")
1413 || !obj.contains_key("feedbacks")
1414 {
1415 return Err(ApiError::invalid_input(
1416 "Device registrations must contain: input_units_and_encoder_properties, output_units_and_decoder_properties, and feedbacks",
1417 ));
1418 }
1419
1420 #[cfg(feature = "feagi-agent")]
1422 {
1423 let connector = {
1425 let mut connectors = state.agent_connectors.write();
1426 let was_existing = connectors.contains_key(&agent_id);
1427 let connector = connectors
1428 .entry(agent_id.clone())
1429 .or_insert_with(|| {
1430 info!(
1431 "🔧 [API] Creating new ConnectorAgent for agent '{}'",
1432 agent_id
1433 );
1434 Arc::new(Mutex::new(ConnectorAgent::new()))
1435 })
1436 .clone();
1437 if was_existing {
1438 info!(
1439 "🔧 [API] Using existing ConnectorAgent for agent '{}'",
1440 agent_id
1441 );
1442 }
1443 connector
1444 };
1445
1446 let input_count = request
1448 .device_registrations
1449 .get("input_units_and_encoder_properties")
1450 .and_then(|v| v.as_object())
1451 .map(|m| m.len())
1452 .unwrap_or(0);
1453 let output_count = request
1454 .device_registrations
1455 .get("output_units_and_decoder_properties")
1456 .and_then(|v| v.as_object())
1457 .map(|m| m.len())
1458 .unwrap_or(0);
1459 let feedback_count = request
1460 .device_registrations
1461 .get("feedbacks")
1462 .and_then(|v| v.as_array())
1463 .map(|a| a.len())
1464 .unwrap_or(0);
1465
1466 info!(
1467 "📥 [API] Importing device registrations for agent '{}': {} input units, {} output units, {} feedbacks",
1468 agent_id, input_count, output_count, feedback_count
1469 );
1470
1471 let import_result: Result<(), String> = (|| {
1473 let mut connector_guard = connector
1474 .lock()
1475 .map_err(|e| format!("ConnectorAgent lock poisoned: {e}"))?;
1476
1477 connector_guard
1478 .import_device_registrations_as_config_json(request.device_registrations.clone())
1479 .map_err(|e| format!("import failed: {e}"))?;
1480
1481 match connector_guard.export_device_registrations_as_config_json() {
1483 Ok(exported) => {
1484 let exported_input_count = exported
1485 .get("input_units_and_encoder_properties")
1486 .and_then(|v| v.as_object())
1487 .map(|m| m.len())
1488 .unwrap_or(0);
1489 let exported_output_count = exported
1490 .get("output_units_and_decoder_properties")
1491 .and_then(|v| v.as_object())
1492 .map(|m| m.len())
1493 .unwrap_or(0);
1494
1495 info!(
1496 "✅ [API] Device registration import succeeded for agent '{}' (verified: {} input, {} output)",
1497 agent_id, exported_input_count, exported_output_count
1498 );
1499 }
1500 Err(e) => {
1501 warn!(
1502 "⚠️ [API] Import succeeded but verification export failed for agent '{}': {}",
1503 agent_id, e
1504 );
1505 }
1506 }
1507
1508 Ok(())
1509 })();
1510
1511 if let Err(msg) = import_result {
1512 error!(
1513 "❌ [API] Failed to import device registrations for agent '{}': {}",
1514 agent_id, msg
1515 );
1516 return Err(ApiError::invalid_input(format!(
1517 "Failed to import device registrations: {msg}"
1518 )));
1519 }
1520
1521 auto_create_cortical_areas_from_device_registrations(&state, &request.device_registrations)
1522 .await;
1523
1524 Ok(Json(DeviceRegistrationImportResponse {
1525 success: true,
1526 message: format!(
1527 "Device registrations imported successfully for agent '{}'",
1528 agent_id
1529 ),
1530 agent_id,
1531 }))
1532 }
1533
1534 #[cfg(not(feature = "feagi-agent"))]
1535 {
1536 info!(
1537 "✅ [API] Device registration import succeeded for agent '{}' (feagi-agent feature not enabled)",
1538 agent_id
1539 );
1540 Ok(Json(DeviceRegistrationImportResponse {
1541 success: true,
1542 message: format!(
1543 "Device registrations imported successfully for agent '{}' (feature not enabled)",
1544 agent_id
1545 ),
1546 agent_id,
1547 }))
1548 }
1549}