1use std::collections::HashMap;
10
11use crate::common::ApiState;
12use crate::common::{ApiError, ApiResult, Json, Path, Query, State};
13use crate::v1::agent_dtos::*;
14use feagi_services::traits::agent_service::HeartbeatRequest as ServiceHeartbeatRequest;
15use tracing::{info, warn};
16
17#[cfg(feature = "feagi-agent")]
18use crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations as auto_create_cortical_areas_shared;
19#[cfg(feature = "feagi-agent")]
20use feagi_agent::{AgentCapabilities as RegistrationCapabilities, AuthToken};
21#[cfg(feature = "feagi-agent")]
22use feagi_io::AgentID;
23#[cfg(feature = "feagi-agent")]
24#[allow(dead_code)]
25fn parse_auth_token(request: &AgentRegistrationRequest) -> ApiResult<AuthToken> {
26 let token_b64 = request
27 .auth_token
28 .as_deref()
29 .ok_or_else(|| ApiError::invalid_input("Missing auth_token for registration"))?;
30 AuthToken::from_base64(token_b64).ok_or_else(|| {
31 ApiError::invalid_input("Invalid auth_token (expected base64 32-byte token)")
32 })
33}
34
35#[cfg(feature = "feagi-agent")]
36#[allow(dead_code)]
37fn derive_capabilities_from_device_registrations(
38 device_registrations: &serde_json::Value,
39) -> ApiResult<Vec<RegistrationCapabilities>> {
40 let obj = device_registrations
41 .as_object()
42 .ok_or_else(|| ApiError::invalid_input("device_registrations must be a JSON object"))?;
43
44 let input_units = obj
45 .get("input_units_and_encoder_properties")
46 .and_then(|v| v.as_object());
47 let output_units = obj
48 .get("output_units_and_decoder_properties")
49 .and_then(|v| v.as_object());
50 let feedbacks = obj.get("feedbacks").and_then(|v| v.as_object());
51
52 let mut capabilities = Vec::new();
53 if input_units.map(|m| !m.is_empty()).unwrap_or(false) {
54 capabilities.push(RegistrationCapabilities::SendSensorData);
55 }
56 if output_units.map(|m| !m.is_empty()).unwrap_or(false) {
57 capabilities.push(RegistrationCapabilities::ReceiveMotorData);
58 }
59 if feedbacks.map(|m| !m.is_empty()).unwrap_or(false) {
60 capabilities.push(RegistrationCapabilities::ReceiveNeuronVisualizations);
61 }
62
63 if capabilities.is_empty() {
64 return Err(ApiError::invalid_input(
65 "device_registrations does not declare any input/output/feedback units",
66 ));
67 }
68
69 Ok(capabilities)
70}
71
72#[cfg(feature = "feagi-agent")]
75#[allow(dead_code)]
76fn derive_capabilities_from_visualization_capability(
77 request: &AgentRegistrationRequest,
78) -> ApiResult<Vec<RegistrationCapabilities>> {
79 let viz = request
80 .capabilities
81 .get("visualization")
82 .and_then(|v| v.as_object())
83 .ok_or_else(|| {
84 ApiError::invalid_input(
85 "visualization-only registration requires capabilities.visualization object",
86 )
87 })?;
88 let rate_hz = viz.get("rate_hz").and_then(|v| v.as_f64()).ok_or_else(|| {
89 ApiError::invalid_input("capabilities.visualization must include rate_hz (number > 0)")
90 })?;
91 if rate_hz <= 0.0 {
92 return Err(ApiError::invalid_input(
93 "capabilities.visualization.rate_hz must be > 0",
94 ));
95 }
96 Ok(vec![RegistrationCapabilities::ReceiveNeuronVisualizations])
97}
98
99#[cfg(feature = "feagi-agent")]
100#[allow(dead_code)]
101fn parse_capability_rate_hz(
102 capabilities: &HashMap<String, serde_json::Value>,
103 capability_key: &str,
104) -> ApiResult<Option<f64>> {
105 let Some(capability_value) = capabilities.get(capability_key) else {
106 return Ok(None);
107 };
108
109 let Some(rate_value) = capability_value.get("rate_hz") else {
110 return Ok(None);
111 };
112
113 let rate_hz = rate_value.as_f64().ok_or_else(|| {
114 ApiError::invalid_input(format!(
115 "Invalid rate_hz for capability '{}': expected number",
116 capability_key
117 ))
118 })?;
119
120 if rate_hz <= 0.0 {
121 return Err(ApiError::invalid_input(format!(
122 "Invalid rate_hz for capability '{}': must be > 0",
123 capability_key
124 )));
125 }
126
127 Ok(Some(rate_hz))
128}
129
130#[cfg(feature = "feagi-agent")]
131#[allow(dead_code)]
132fn capability_key(capability: &RegistrationCapabilities) -> &'static str {
133 match capability {
134 RegistrationCapabilities::SendSensorData => "send_sensor_data",
135 RegistrationCapabilities::ReceiveMotorData => "receive_motor_data",
136 RegistrationCapabilities::ReceiveNeuronVisualizations => "receive_neuron_visualizations",
137 RegistrationCapabilities::ReceiveSystemMessages => "receive_system_messages",
138 }
139}
140
141fn get_agent_name_from_id(agent_id: &str) -> ApiResult<String> {
142 Ok(agent_id.to_string())
143}
144
145#[cfg(feature = "feagi-agent")]
146fn parse_agent_id_base64(agent_id: &str) -> ApiResult<AgentID> {
147 AgentID::try_from_base64(agent_id).map_err(|e| {
148 ApiError::invalid_input(format!("Invalid agent_id (expected AgentID base64): {}", e))
149 })
150}
151
152#[cfg(feature = "feagi-agent")]
153async fn auto_create_cortical_areas_from_device_registrations(
154 state: &ApiState,
155 device_registrations: &serde_json::Value,
156) {
157 auto_create_cortical_areas_shared(state, device_registrations).await;
158}
159
160#[cfg(not(feature = "feagi-agent"))]
161async fn auto_create_cortical_areas_from_device_registrations(
162 _state: &ApiState,
163 _device_registrations: &serde_json::Value,
164) {
165 }
167
168#[utoipa::path(
170 post,
171 path = "/v1/agent/register",
172 request_body = AgentRegistrationRequest,
173 responses(
174 (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
175 (status = 500, description = "Registration failed", body = String)
176 ),
177 tag = "agent"
178)]
179pub async fn register_agent(
180 State(state): State<ApiState>,
181 Json(request): Json<AgentRegistrationRequest>,
182) -> ApiResult<Json<AgentRegistrationResponse>> {
183 info!(
184 "đĻ [API] Registration request received for agent '{}' (type: {})",
185 request.agent_id, request.agent_type
186 );
187
188 let device_registrations_opt = request
190 .capabilities
191 .get("device_registrations")
192 .and_then(|v| v.as_object().map(|_| v.clone()));
193
194 let device_regs_for_autocreate = device_registrations_opt.clone();
196
197 let handler_available = if let Some(device_regs) = &device_registrations_opt {
199 if let Some(handler) = &state.agent_handler {
200 let agent_id = parse_agent_id_base64(&request.agent_id)?;
201 {
202 let mut handler_guard = handler.lock().unwrap();
203 handler_guard.set_device_registrations_by_agent(agent_id, device_regs.clone());
204 } info!(
206 "â
[API] Stored device registrations for agent '{}'",
207 request.agent_id
208 );
209 true
210 } else {
211 false
212 }
213 } else {
214 state.agent_handler.is_some()
215 };
216
217 if let Some(device_regs) = device_regs_for_autocreate {
219 auto_create_cortical_areas_from_device_registrations(&state, &device_regs).await;
220 }
221
222 if let Some(viz) = request.capabilities.get("visualization") {
224 if let Some(rate_hz) = viz.get("rate_hz").and_then(|v| v.as_f64()) {
225 if rate_hz > 0.0 {
226 match state
227 .runtime_service
228 .register_visualization_subscriptions(&request.agent_id, rate_hz)
229 .await
230 {
231 Ok(_) => {
232 info!(
233 "â
[API] Registered visualization subscription for agent '{}' at {}Hz",
234 request.agent_id, rate_hz
235 );
236 }
237 Err(e) => {
238 warn!(
239 "â ī¸ [API] Failed to register visualization subscription for agent '{}': {}",
240 request.agent_id, e
241 );
242 }
243 }
244 }
245 }
246 }
247
248 use crate::v1::TransportConfig;
250 let transports_array = if state.agent_handler.is_some() {
251 use feagi_config::load_config;
253 let config = load_config(None, None)
254 .map_err(|e| ApiError::internal(format!("Failed to load config: {}", e)))?;
255
256 let mut transport_configs = Vec::new();
257 for transport in &config.transports.available {
258 let transport_type = transport.to_lowercase();
259 if transport_type != "zmq" && transport_type != "websocket" && transport_type != "ws" {
260 continue;
261 }
262
263 let mut ports = HashMap::new();
265 if transport_type == "websocket" || transport_type == "ws" {
266 ports.insert(
267 "registration".to_string(),
268 config.websocket.registration_port,
269 );
270 ports.insert("sensory".to_string(), config.websocket.sensory_port);
271 ports.insert("motor".to_string(), config.websocket.motor_port);
272 ports.insert(
273 "visualization".to_string(),
274 config.websocket.visualization_port,
275 );
276 } else {
277 ports.insert("registration".to_string(), config.agent.registration_port);
278 ports.insert("sensory".to_string(), config.ports.zmq_sensory_port);
279 ports.insert("motor".to_string(), config.ports.zmq_motor_port);
280 ports.insert(
281 "visualization".to_string(),
282 config.ports.zmq_visualization_port,
283 );
284 }
285
286 transport_configs.push(TransportConfig {
287 transport_type: if transport_type == "ws" {
288 "websocket".to_string()
289 } else {
290 transport_type
291 },
292 enabled: true,
293 ports,
294 host: if transport == "websocket" || transport == "ws" {
295 config.websocket.advertised_host.clone()
296 } else {
297 config.zmq.advertised_host.clone()
298 },
299 });
300 }
301
302 transport_configs
303 } else {
304 if !handler_available {
305 return Err(ApiError::internal("Agent handler not available"));
306 }
307 Vec::new()
308 };
309
310 Ok(Json(AgentRegistrationResponse {
311 status: "success".to_string(),
312 message: "Agent configuration stored. Connect via ZMQ/WebSocket for full registration"
313 .to_string(),
314 success: true,
315 transport: None, rates: None,
317 transports: Some(transports_array),
318 recommended_transport: Some("websocket".to_string()),
319 shm_paths: None,
320 cortical_areas: serde_json::json!({}),
321 }))
322}
323
324#[utoipa::path(
326 post,
327 path = "/v1/agent/heartbeat",
328 request_body = HeartbeatRequest,
329 responses(
330 (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
331 (status = 404, description = "Agent not found"),
332 (status = 500, description = "Heartbeat failed")
333 ),
334 tag = "agent"
335)]
336pub async fn heartbeat(
337 State(state): State<ApiState>,
338 Json(request): Json<HeartbeatRequest>,
339) -> ApiResult<Json<HeartbeatResponse>> {
340 let agent_service = state
341 .agent_service
342 .as_ref()
343 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
344
345 let service_request = ServiceHeartbeatRequest {
346 agent_id: request.agent_id.clone(),
347 };
348
349 match agent_service.heartbeat(service_request).await {
350 Ok(_) => Ok(Json(HeartbeatResponse {
351 message: "heartbeat_ok".to_string(),
352 success: true,
353 })),
354 Err(_) => Err(ApiError::not_found(
355 "agent",
356 &format!("Agent {} not in registry", request.agent_id),
357 )),
358 }
359}
360
361#[utoipa::path(
363 get,
364 path = "/v1/agent/list",
365 responses(
366 (status = 200, description = "List of agent IDs", body = Vec<String>),
367 (status = 503, description = "Registration service unavailable")
368 ),
369 tag = "agent"
370)]
371pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
372 let agent_service = state
373 .agent_service
374 .as_ref()
375 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
376
377 match agent_service.list_agents().await {
378 Ok(agent_ids) => Ok(Json(agent_ids)),
379 Err(e) => Err(ApiError::internal(format!("Failed to list agents: {}", e))),
380 }
381}
382
383#[utoipa::path(
385 get,
386 path = "/v1/agent/properties",
387 params(
388 ("agent_id" = String, Query, description = "Agent ID to get properties for")
389 ),
390 responses(
391 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
392 (status = 404, description = "Agent not found"),
393 (status = 500, description = "Failed to get agent properties")
394 ),
395 tag = "agent"
396)]
397pub async fn get_agent_properties(
398 State(state): State<ApiState>,
399 Query(params): Query<HashMap<String, String>>,
400) -> ApiResult<Json<AgentPropertiesResponse>> {
401 let agent_id = params
402 .get("agent_id")
403 .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
404
405 let agent_service = state
406 .agent_service
407 .as_ref()
408 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
409
410 let agent_name = get_agent_name_from_id(agent_id)?;
411 match agent_service.get_agent_properties(agent_id).await {
412 Ok(properties) => Ok(Json(AgentPropertiesResponse {
413 agent_name,
414 agent_type: properties.agent_type,
415 agent_ip: properties.agent_ip,
416 agent_data_port: properties.agent_data_port,
417 agent_router_address: properties.agent_router_address,
418 agent_version: properties.agent_version,
419 controller_version: properties.controller_version,
420 capabilities: properties.capabilities,
421 chosen_transport: properties.chosen_transport,
422 })),
423 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
424 }
425}
426
427#[utoipa::path(
429 get,
430 path = "/v1/agent/shared_mem",
431 responses(
432 (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
433 (status = 500, description = "Failed to get shared memory info")
434 ),
435 tag = "agent"
436)]
437pub async fn get_shared_memory(
438 State(state): State<ApiState>,
439) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
440 let agent_service = state
441 .agent_service
442 .as_ref()
443 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
444
445 match agent_service.get_shared_memory_info().await {
446 Ok(shm_info) => Ok(Json(shm_info)),
447 Err(e) => Err(ApiError::internal(format!(
448 "Failed to get shared memory info: {}",
449 e
450 ))),
451 }
452}
453
454#[utoipa::path(
456 delete,
457 path = "/v1/agent/deregister",
458 request_body = AgentDeregistrationRequest,
459 responses(
460 (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
461 (status = 500, description = "Deregistration failed")
462 ),
463 tag = "agent"
464)]
465pub async fn deregister_agent(
466 State(state): State<ApiState>,
467 Json(request): Json<AgentDeregistrationRequest>,
468) -> ApiResult<Json<SuccessResponse>> {
469 let agent_service = state
470 .agent_service
471 .as_ref()
472 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
473
474 match agent_service.deregister_agent(&request.agent_id).await {
475 Ok(_) => Ok(Json(SuccessResponse {
476 message: format!("Agent '{}' deregistered successfully", request.agent_id),
477 success: Some(true),
478 })),
479 Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
480 }
481}
482
483#[utoipa::path(
485 post,
486 path = "/v1/agent/manual_stimulation",
487 request_body = ManualStimulationRequest,
488 responses(
489 (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
490 (status = 500, description = "Stimulation failed")
491 ),
492 tag = "agent"
493)]
494pub async fn manual_stimulation(
495 State(state): State<ApiState>,
496 Json(request): Json<ManualStimulationRequest>,
497) -> ApiResult<Json<ManualStimulationResponse>> {
498 let agent_service = state
499 .agent_service
500 .as_ref()
501 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
502
503 agent_service.try_set_runtime_service(state.runtime_service.clone());
506
507 match agent_service
508 .manual_stimulation(request.stimulation_payload)
509 .await
510 {
511 Ok(result) => {
512 let success = result
513 .get("success")
514 .and_then(|v| v.as_bool())
515 .unwrap_or(false);
516 let total_coordinates = result
517 .get("total_coordinates")
518 .and_then(|v| v.as_u64())
519 .unwrap_or(0) as usize;
520 let successful_areas = result
521 .get("successful_areas")
522 .and_then(|v| v.as_u64())
523 .unwrap_or(0) as usize;
524 let failed_areas = result
525 .get("failed_areas")
526 .and_then(|v| v.as_array())
527 .map(|arr| {
528 arr.iter()
529 .filter_map(|v| v.as_str().map(String::from))
530 .collect()
531 })
532 .unwrap_or_default();
533 let error = result
534 .get("error")
535 .and_then(|v| v.as_str())
536 .map(String::from);
537
538 Ok(Json(ManualStimulationResponse {
539 success,
540 total_coordinates,
541 successful_areas,
542 failed_areas,
543 error,
544 }))
545 }
546 Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
547 }
548}
549
550#[utoipa::path(
552 get,
553 path = "/v1/agent/fq_sampler_status",
554 responses(
555 (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
556 (status = 500, description = "Failed to get FQ sampler status")
557 ),
558 tag = "agent"
559)]
560pub async fn get_fq_sampler_status(
561 State(state): State<ApiState>,
562) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
563 let agent_service = state
564 .agent_service
565 .as_ref()
566 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
567
568 let runtime_service = state.runtime_service.as_ref();
569
570 let agent_ids = agent_service
572 .list_agents()
573 .await
574 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
575
576 let (fcl_frequency, fcl_consumer) = runtime_service
578 .get_fcl_sampler_config()
579 .await
580 .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
581
582 let mut visualization_agents = Vec::new();
584 let mut motor_agents = Vec::new();
585
586 for agent_id in &agent_ids {
587 if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
588 if props.capabilities.contains_key("visualization") {
589 visualization_agents.push(agent_id.clone());
590 }
591 if props.capabilities.contains_key("motor") {
592 motor_agents.push(agent_id.clone());
593 }
594 }
595 }
596
597 let mut fq_coordination = HashMap::new();
598
599 let mut viz_sampler = HashMap::new();
600 viz_sampler.insert(
601 "enabled".to_string(),
602 serde_json::json!(!visualization_agents.is_empty()),
603 );
604 viz_sampler.insert(
605 "reason".to_string(),
606 serde_json::json!(if visualization_agents.is_empty() {
607 "No visualization agents connected".to_string()
608 } else {
609 format!(
610 "{} visualization agent(s) connected",
611 visualization_agents.len()
612 )
613 }),
614 );
615 viz_sampler.insert(
616 "agents_requiring".to_string(),
617 serde_json::json!(visualization_agents),
618 );
619 viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
620 fq_coordination.insert(
621 "visualization_fq_sampler".to_string(),
622 serde_json::json!(viz_sampler),
623 );
624
625 let mut motor_sampler = HashMap::new();
626 motor_sampler.insert(
627 "enabled".to_string(),
628 serde_json::json!(!motor_agents.is_empty()),
629 );
630 motor_sampler.insert(
631 "reason".to_string(),
632 serde_json::json!(if motor_agents.is_empty() {
633 "No motor agents connected".to_string()
634 } else {
635 format!("{} motor agent(s) connected", motor_agents.len())
636 }),
637 );
638 motor_sampler.insert(
639 "agents_requiring".to_string(),
640 serde_json::json!(motor_agents),
641 );
642 motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
643 fq_coordination.insert(
644 "motor_fq_sampler".to_string(),
645 serde_json::json!(motor_sampler),
646 );
647
648 let mut response = HashMap::new();
649 response.insert(
650 "fq_sampler_coordination".to_string(),
651 serde_json::json!(fq_coordination),
652 );
653 response.insert(
654 "agent_registry".to_string(),
655 serde_json::json!({
656 "total_agents": agent_ids.len(),
657 "agent_ids": agent_ids
658 }),
659 );
660 response.insert(
661 "system_status".to_string(),
662 serde_json::json!("coordinated_via_registration_manager"),
663 );
664 response.insert(
665 "fcl_sampler_consumer".to_string(),
666 serde_json::json!(fcl_consumer),
667 );
668
669 Ok(Json(response))
670}
671
672#[utoipa::path(
674 get,
675 path = "/v1/agent/capabilities",
676 responses(
677 (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
678 (status = 500, description = "Failed to get capabilities")
679 ),
680 tag = "agent"
681)]
682pub async fn get_capabilities(
683 State(_state): State<ApiState>,
684) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
685 let mut response = HashMap::new();
686 response.insert(
687 "agent_types".to_string(),
688 vec![
689 "sensory".to_string(),
690 "motor".to_string(),
691 "both".to_string(),
692 "visualization".to_string(),
693 "infrastructure".to_string(),
694 ],
695 );
696 response.insert(
697 "capability_types".to_string(),
698 vec![
699 "vision".to_string(),
700 "motor".to_string(),
701 "visualization".to_string(),
702 "sensory".to_string(),
703 ],
704 );
705
706 Ok(Json(response))
707}
708
709#[utoipa::path(
711 get,
712 path = "/v1/agent/capabilities/all",
713 params(
714 ("agent_type" = Option<String>, Query, description = "Filter by agent type (exact match)"),
715 ("capability" = Option<String>, Query, description = "Filter by capability key(s), comma-separated"),
716 ("include_device_registrations" = Option<bool>, Query, description = "Include device registration payloads per agent")
717 ),
718 responses(
719 (status = 200, description = "Agent capabilities map", body = HashMap<String, AgentCapabilitiesSummary>),
720 (status = 400, description = "Invalid query"),
721 (status = 500, description = "Failed to get agent capabilities")
722 ),
723 tag = "agent"
724)]
725pub async fn get_all_agent_capabilities(
726 State(state): State<ApiState>,
727 Query(params): Query<AgentCapabilitiesAllQuery>,
728) -> ApiResult<Json<HashMap<String, AgentCapabilitiesSummary>>> {
729 let agent_service = state
730 .agent_service
731 .as_ref()
732 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
733
734 let include_device_registrations = params.include_device_registrations.unwrap_or(false);
735 let capability_filters: Option<Vec<String>> = params.capability.as_ref().and_then(|value| {
736 let filters: Vec<String> = value
737 .split(',')
738 .map(|item| item.trim())
739 .filter(|item| !item.is_empty())
740 .map(String::from)
741 .collect();
742 if filters.is_empty() {
743 None
744 } else {
745 Some(filters)
746 }
747 });
748
749 let agent_ids = agent_service
750 .list_agents()
751 .await
752 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
753
754 let mut response: HashMap<String, AgentCapabilitiesSummary> = HashMap::new();
755
756 for agent_id in agent_ids {
757 let agent_name = get_agent_name_from_id(&agent_id)?;
758 let properties = match agent_service.get_agent_properties(&agent_id).await {
759 Ok(props) => props,
760 Err(_) => continue,
761 };
762
763 if let Some(ref agent_type_filter) = params.agent_type {
764 if properties.agent_type != *agent_type_filter {
765 continue;
766 }
767 }
768
769 if let Some(ref filters) = capability_filters {
770 let has_match = filters
771 .iter()
772 .any(|capability| properties.capabilities.contains_key(capability));
773 if !has_match {
774 continue;
775 }
776 }
777
778 let device_registrations = if include_device_registrations {
779 #[cfg(feature = "feagi-agent")]
780 {
781 Some(export_device_registrations_from_connector(
782 &state, &agent_id,
783 )?)
784 }
785 #[cfg(not(feature = "feagi-agent"))]
786 {
787 None
788 }
789 } else {
790 None
791 };
792 response.insert(
793 agent_id,
794 AgentCapabilitiesSummary {
795 agent_name,
796 capabilities: properties.capabilities,
797 device_registrations,
798 },
799 );
800 }
801
802 Ok(Json(response))
803}
804
805#[cfg(feature = "feagi-agent")]
806fn export_device_registrations_from_connector(
807 state: &ApiState,
808 agent_id: &str,
809) -> ApiResult<serde_json::Value> {
810 let parsed_agent_id = parse_agent_id_base64(agent_id)?;
811 if let Some(handler) = &state.agent_handler {
812 let handler_guard = handler.lock().unwrap();
813 if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
814 return Ok(regs.clone());
815 }
816 }
817
818 Err(ApiError::not_found(
819 "device_registrations",
820 &format!("No device registrations found for agent '{}'", agent_id),
821 ))
822}
823
824#[utoipa::path(
826 get,
827 path = "/v1/agent/info/{agent_id}",
828 params(
829 ("agent_id" = String, Path, description = "Agent ID")
830 ),
831 responses(
832 (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
833 (status = 404, description = "Agent not found"),
834 (status = 500, description = "Failed to get agent info")
835 ),
836 tag = "agent"
837)]
838pub async fn get_agent_info(
839 State(state): State<ApiState>,
840 Path(agent_id): Path<String>,
841) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
842 let agent_service = state
843 .agent_service
844 .as_ref()
845 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
846
847 let properties = agent_service
848 .get_agent_properties(&agent_id)
849 .await
850 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
851
852 let mut response = HashMap::new();
853 response.insert("agent_id".to_string(), serde_json::json!(agent_id));
854 response.insert(
855 "agent_name".to_string(),
856 serde_json::json!(get_agent_name_from_id(&agent_id)?),
857 );
858 response.insert(
859 "agent_type".to_string(),
860 serde_json::json!(properties.agent_type),
861 );
862 response.insert(
863 "agent_ip".to_string(),
864 serde_json::json!(properties.agent_ip),
865 );
866 response.insert(
867 "agent_data_port".to_string(),
868 serde_json::json!(properties.agent_data_port),
869 );
870 response.insert(
871 "capabilities".to_string(),
872 serde_json::json!(properties.capabilities),
873 );
874 response.insert(
875 "agent_version".to_string(),
876 serde_json::json!(properties.agent_version),
877 );
878 response.insert(
879 "controller_version".to_string(),
880 serde_json::json!(properties.controller_version),
881 );
882 response.insert("status".to_string(), serde_json::json!("active"));
883 if let Some(ref transport) = properties.chosen_transport {
884 response.insert("chosen_transport".to_string(), serde_json::json!(transport));
885 }
886
887 Ok(Json(response))
888}
889
890#[utoipa::path(
892 get,
893 path = "/v1/agent/properties/{agent_id}",
894 params(
895 ("agent_id" = String, Path, description = "Agent ID")
896 ),
897 responses(
898 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
899 (status = 404, description = "Agent not found"),
900 (status = 500, description = "Failed to get agent properties")
901 ),
902 tag = "agent"
903)]
904pub async fn get_agent_properties_path(
905 State(state): State<ApiState>,
906 Path(agent_id): Path<String>,
907) -> ApiResult<Json<AgentPropertiesResponse>> {
908 let agent_service = state
909 .agent_service
910 .as_ref()
911 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
912
913 match agent_service.get_agent_properties(&agent_id).await {
914 Ok(properties) => Ok(Json(AgentPropertiesResponse {
915 agent_name: get_agent_name_from_id(&agent_id)?,
916 agent_type: properties.agent_type,
917 agent_ip: properties.agent_ip,
918 agent_data_port: properties.agent_data_port,
919 agent_router_address: properties.agent_router_address,
920 agent_version: properties.agent_version,
921 controller_version: properties.controller_version,
922 capabilities: properties.capabilities,
923 chosen_transport: properties.chosen_transport,
924 })),
925 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
926 }
927}
928
929#[utoipa::path(
931 post,
932 path = "/v1/agent/configure",
933 responses(
934 (status = 200, description = "Agent configured", body = HashMap<String, String>),
935 (status = 400, description = "Invalid input"),
936 (status = 500, description = "Failed to configure agent")
937 ),
938 tag = "agent"
939)]
940pub async fn post_configure(
941 State(_state): State<ApiState>,
942 Json(config): Json<HashMap<String, serde_json::Value>>,
943) -> ApiResult<Json<HashMap<String, String>>> {
944 tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
945
946 Ok(Json(HashMap::from([
947 (
948 "message".to_string(),
949 "Agent configuration updated".to_string(),
950 ),
951 ("status".to_string(), "not_yet_implemented".to_string()),
952 ])))
953}
954
955#[utoipa::path(
962 get,
963 path = "/v1/agent/{agent_id}/device_registrations",
964 params(
965 ("agent_id" = String, Path, description = "Agent ID")
966 ),
967 responses(
968 (status = 200, description = "Device registrations exported successfully", body = DeviceRegistrationExportResponse),
969 (status = 404, description = "Agent not found"),
970 (status = 500, description = "Failed to export device registrations")
971 ),
972 tag = "agent"
973)]
974pub async fn export_device_registrations(
975 State(state): State<ApiState>,
976 Path(agent_id): Path<String>,
977) -> ApiResult<Json<DeviceRegistrationExportResponse>> {
978 info!(
979 "đĻ [API] Device registration export requested for agent '{}'",
980 agent_id
981 );
982
983 if let Some(agent_service) = state.agent_service.as_ref() {
989 let _properties = agent_service
990 .get_agent_properties(&agent_id)
991 .await
992 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
993 } else {
994 info!(
995 "âšī¸ [API] Agent service not available; skipping agent existence check for export (agent '{}')",
996 agent_id
997 );
998 }
999
1000 #[cfg(feature = "feagi-agent")]
1002 let device_registrations = {
1003 let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1004 if let Some(handler) = &state.agent_handler {
1005 let handler_guard = handler.lock().unwrap();
1006 if let Some(regs) = handler_guard.get_device_registrations_by_agent(parsed_agent_id) {
1007 info!(
1008 "đ¤ [API] Found device registrations for agent '{}'",
1009 agent_id
1010 );
1011 regs.clone()
1012 } else {
1013 warn!(
1014 "â ī¸ [API] No device registrations found for agent '{}'",
1015 agent_id
1016 );
1017 serde_json::json!({
1018 "input_units_and_encoder_properties": {},
1019 "output_units_and_decoder_properties": {},
1020 "feedbacks": []
1021 })
1022 }
1023 } else {
1024 warn!("â ī¸ [API] No agent_handler available");
1025 serde_json::json!({
1026 "input_units_and_encoder_properties": {},
1027 "output_units_and_decoder_properties": {},
1028 "feedbacks": []
1029 })
1030 }
1031 };
1032
1033 #[cfg(not(feature = "feagi-agent"))]
1034 let device_registrations = serde_json::json!({
1037 "input_units_and_encoder_properties": {},
1038 "output_units_and_decoder_properties": {},
1039 "feedbacks": []
1040 });
1041
1042 info!(
1043 "â
[API] Device registration export succeeded for agent '{}'",
1044 agent_id
1045 );
1046
1047 Ok(Json(DeviceRegistrationExportResponse {
1048 device_registrations,
1049 agent_id,
1050 }))
1051}
1052
1053#[utoipa::path(
1063 post,
1064 path = "/v1/agent/{agent_id}/device_registrations",
1065 params(
1066 ("agent_id" = String, Path, description = "Agent ID")
1067 ),
1068 request_body = DeviceRegistrationImportRequest,
1069 responses(
1070 (status = 200, description = "Device registrations imported successfully", body = DeviceRegistrationImportResponse),
1071 (status = 400, description = "Invalid device registration configuration"),
1072 (status = 404, description = "Agent not found"),
1073 (status = 500, description = "Failed to import device registrations")
1074 ),
1075 tag = "agent"
1076)]
1077pub async fn import_device_registrations(
1078 State(state): State<ApiState>,
1079 Path(agent_id): Path<String>,
1080 Json(request): Json<DeviceRegistrationImportRequest>,
1081) -> ApiResult<Json<DeviceRegistrationImportResponse>> {
1082 info!(
1083 "đĻ [API] Device registration import requested for agent '{}'",
1084 agent_id
1085 );
1086
1087 if let Some(agent_service) = state.agent_service.as_ref() {
1089 let _properties = agent_service
1090 .get_agent_properties(&agent_id)
1091 .await
1092 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
1093 } else {
1094 info!(
1095 "âšī¸ [API] Agent service not available; skipping agent existence check for import (agent '{}')",
1096 agent_id
1097 );
1098 }
1099
1100 if !request.device_registrations.is_object() {
1103 return Err(ApiError::invalid_input(
1104 "Device registrations must be a JSON object",
1105 ));
1106 }
1107
1108 let obj = request.device_registrations.as_object().unwrap();
1110 if !obj.contains_key("input_units_and_encoder_properties")
1111 || !obj.contains_key("output_units_and_decoder_properties")
1112 || !obj.contains_key("feedbacks")
1113 {
1114 return Err(ApiError::invalid_input(
1115 "Device registrations must contain: input_units_and_encoder_properties, output_units_and_decoder_properties, and feedbacks",
1116 ));
1117 }
1118
1119 #[cfg(feature = "feagi-agent")]
1121 {
1122 let parsed_agent_id = parse_agent_id_base64(&agent_id)?;
1123 if let Some(handler) = &state.agent_handler {
1124 let mut handler_guard = handler.lock().unwrap();
1125 handler_guard.set_device_registrations_by_agent(
1126 parsed_agent_id,
1127 request.device_registrations.clone(),
1128 );
1129 info!(
1130 "đĨ [API] Imported device registrations for agent '{}'",
1131 agent_id
1132 );
1133 } else {
1134 warn!("â ī¸ [API] No agent_handler available to store device registrations");
1135 }
1136
1137 auto_create_cortical_areas_from_device_registrations(&state, &request.device_registrations)
1138 .await;
1139
1140 Ok(Json(DeviceRegistrationImportResponse {
1141 success: true,
1142 message: format!(
1143 "Device registrations imported successfully for agent '{}'",
1144 agent_id
1145 ),
1146 agent_id,
1147 }))
1148 }
1149
1150 #[cfg(not(feature = "feagi-agent"))]
1151 {
1152 info!(
1153 "â
[API] Device registration import succeeded for agent '{}' (feagi-agent feature not enabled)",
1154 agent_id
1155 );
1156 Ok(Json(DeviceRegistrationImportResponse {
1157 success: true,
1158 message: format!(
1159 "Device registrations imported successfully for agent '{}' (feature not enabled)",
1160 agent_id
1161 ),
1162 agent_id,
1163 }))
1164 }
1165}