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