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