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};
17
18#[utoipa::path(
20 post,
21 path = "/v1/agent/register",
22 request_body = AgentRegistrationRequest,
23 responses(
24 (status = 200, description = "Agent registered successfully", body = AgentRegistrationResponse),
25 (status = 500, description = "Registration failed", body = String)
26 ),
27 tag = "agent"
28)]
29pub async fn register_agent(
30 State(state): State<ApiState>,
31 Json(request): Json<AgentRegistrationRequest>,
32) -> ApiResult<Json<AgentRegistrationResponse>> {
33 let agent_service = state
34 .agent_service
35 .as_ref()
36 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
37
38 let registration = AgentRegistration {
39 agent_id: request.agent_id.clone(),
40 agent_type: request.agent_type,
41 agent_data_port: request.agent_data_port,
42 agent_version: request.agent_version,
43 controller_version: request.controller_version,
44 agent_ip: request.agent_ip,
45 capabilities: request.capabilities,
46 metadata: request.metadata,
47 chosen_transport: request.chosen_transport,
48 };
49
50 match agent_service.register_agent(registration).await {
51 Ok(response) => {
52 let transports = response.transports.map(|ts| {
54 ts.into_iter()
55 .map(|t| crate::v1::agent_dtos::TransportConfig {
56 transport_type: t.transport_type,
57 enabled: t.enabled,
58 ports: t.ports,
59 host: t.host,
60 })
61 .collect()
62 });
63
64 Ok(Json(AgentRegistrationResponse {
65 status: response.status,
66 message: response.message,
67 success: response.success,
68 transport: response.transport,
69 rates: response.rates,
70 transports,
71 recommended_transport: response.recommended_transport,
72 zmq_ports: response.zmq_ports,
73 shm_paths: response.shm_paths,
74 cortical_areas: response.cortical_areas,
75 }))
76 }
77 Err(e) => {
78 let error_msg = e.to_string();
80 if error_msg.contains("not supported") || error_msg.contains("disabled") {
81 Err(ApiError::invalid_input(error_msg))
82 } else {
83 Err(ApiError::internal(format!("Registration failed: {}", e)))
84 }
85 }
86 }
87}
88
89#[utoipa::path(
91 post,
92 path = "/v1/agent/heartbeat",
93 request_body = HeartbeatRequest,
94 responses(
95 (status = 200, description = "Heartbeat recorded", body = HeartbeatResponse),
96 (status = 404, description = "Agent not found"),
97 (status = 500, description = "Heartbeat failed")
98 ),
99 tag = "agent"
100)]
101pub async fn heartbeat(
102 State(state): State<ApiState>,
103 Json(request): Json<HeartbeatRequest>,
104) -> ApiResult<Json<HeartbeatResponse>> {
105 let agent_service = state
106 .agent_service
107 .as_ref()
108 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
109
110 let service_request = ServiceHeartbeatRequest {
111 agent_id: request.agent_id.clone(),
112 };
113
114 match agent_service.heartbeat(service_request).await {
115 Ok(_) => Ok(Json(HeartbeatResponse {
116 message: "heartbeat_ok".to_string(),
117 success: true,
118 })),
119 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
120 }
121}
122
123#[utoipa::path(
125 get,
126 path = "/v1/agent/list",
127 responses(
128 (status = 200, description = "List of agent IDs", body = Vec<String>),
129 (status = 503, description = "Registration service unavailable")
130 ),
131 tag = "agent"
132)]
133pub async fn list_agents(State(state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
134 let agent_service = state
135 .agent_service
136 .as_ref()
137 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
138
139 match agent_service.list_agents().await {
140 Ok(agent_ids) => Ok(Json(agent_ids)),
141 Err(e) => Err(ApiError::internal(format!("Failed to list agents: {}", e))),
142 }
143}
144
145#[utoipa::path(
147 get,
148 path = "/v1/agent/properties",
149 params(
150 ("agent_id" = String, Query, description = "Agent ID to get properties for")
151 ),
152 responses(
153 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
154 (status = 404, description = "Agent not found"),
155 (status = 500, description = "Failed to get agent properties")
156 ),
157 tag = "agent"
158)]
159pub async fn get_agent_properties(
160 State(state): State<ApiState>,
161 Query(params): Query<HashMap<String, String>>,
162) -> ApiResult<Json<AgentPropertiesResponse>> {
163 let agent_id = params
164 .get("agent_id")
165 .ok_or_else(|| ApiError::invalid_input("Missing agent_id query parameter"))?;
166
167 let agent_service = state
168 .agent_service
169 .as_ref()
170 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
171
172 match agent_service.get_agent_properties(agent_id).await {
173 Ok(properties) => Ok(Json(AgentPropertiesResponse {
174 agent_type: properties.agent_type,
175 agent_ip: properties.agent_ip,
176 agent_data_port: properties.agent_data_port,
177 agent_router_address: properties.agent_router_address,
178 agent_version: properties.agent_version,
179 controller_version: properties.controller_version,
180 capabilities: properties.capabilities,
181 chosen_transport: properties.chosen_transport,
182 })),
183 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
184 }
185}
186
187#[utoipa::path(
189 get,
190 path = "/v1/agent/shared_mem",
191 responses(
192 (status = 200, description = "Shared memory info", body = HashMap<String, HashMap<String, serde_json::Value>>),
193 (status = 500, description = "Failed to get shared memory info")
194 ),
195 tag = "agent"
196)]
197pub async fn get_shared_memory(
198 State(state): State<ApiState>,
199) -> ApiResult<Json<HashMap<String, HashMap<String, serde_json::Value>>>> {
200 let agent_service = state
201 .agent_service
202 .as_ref()
203 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
204
205 match agent_service.get_shared_memory_info().await {
206 Ok(shm_info) => Ok(Json(shm_info)),
207 Err(e) => Err(ApiError::internal(format!(
208 "Failed to get shared memory info: {}",
209 e
210 ))),
211 }
212}
213
214#[utoipa::path(
216 delete,
217 path = "/v1/agent/deregister",
218 request_body = AgentDeregistrationRequest,
219 responses(
220 (status = 200, description = "Agent deregistered successfully", body = SuccessResponse),
221 (status = 500, description = "Deregistration failed")
222 ),
223 tag = "agent"
224)]
225pub async fn deregister_agent(
226 State(state): State<ApiState>,
227 Json(request): Json<AgentDeregistrationRequest>,
228) -> ApiResult<Json<SuccessResponse>> {
229 let agent_service = state
230 .agent_service
231 .as_ref()
232 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
233
234 match agent_service.deregister_agent(&request.agent_id).await {
235 Ok(_) => Ok(Json(SuccessResponse {
236 message: format!("Agent '{}' deregistered successfully", request.agent_id),
237 success: Some(true),
238 })),
239 Err(e) => Err(ApiError::internal(format!("Deregistration failed: {}", e))),
240 }
241}
242
243#[utoipa::path(
245 post,
246 path = "/v1/agent/manual_stimulation",
247 request_body = ManualStimulationRequest,
248 responses(
249 (status = 200, description = "Manual stimulation result", body = ManualStimulationResponse),
250 (status = 500, description = "Stimulation failed")
251 ),
252 tag = "agent"
253)]
254pub async fn manual_stimulation(
255 State(state): State<ApiState>,
256 Json(request): Json<ManualStimulationRequest>,
257) -> ApiResult<Json<ManualStimulationResponse>> {
258 let agent_service = state
259 .agent_service
260 .as_ref()
261 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
262
263 agent_service.try_set_runtime_service(state.runtime_service.clone());
266
267 match agent_service
268 .manual_stimulation(request.stimulation_payload)
269 .await
270 {
271 Ok(result) => {
272 let success = result
273 .get("success")
274 .and_then(|v| v.as_bool())
275 .unwrap_or(false);
276 let total_coordinates = result
277 .get("total_coordinates")
278 .and_then(|v| v.as_u64())
279 .unwrap_or(0) as usize;
280 let successful_areas = result
281 .get("successful_areas")
282 .and_then(|v| v.as_u64())
283 .unwrap_or(0) as usize;
284 let failed_areas = result
285 .get("failed_areas")
286 .and_then(|v| v.as_array())
287 .map(|arr| {
288 arr.iter()
289 .filter_map(|v| v.as_str().map(String::from))
290 .collect()
291 })
292 .unwrap_or_default();
293 let error = result
294 .get("error")
295 .and_then(|v| v.as_str())
296 .map(String::from);
297
298 Ok(Json(ManualStimulationResponse {
299 success,
300 total_coordinates,
301 successful_areas,
302 failed_areas,
303 error,
304 }))
305 }
306 Err(e) => Err(ApiError::internal(format!("Stimulation failed: {}", e))),
307 }
308}
309
310#[utoipa::path(
312 get,
313 path = "/v1/agent/fq_sampler_status",
314 responses(
315 (status = 200, description = "FQ sampler status", body = HashMap<String, serde_json::Value>),
316 (status = 500, description = "Failed to get FQ sampler status")
317 ),
318 tag = "agent"
319)]
320pub async fn get_fq_sampler_status(
321 State(state): State<ApiState>,
322) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
323 let agent_service = state
324 .agent_service
325 .as_ref()
326 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
327
328 let runtime_service = state.runtime_service.as_ref();
329
330 let agent_ids = agent_service
332 .list_agents()
333 .await
334 .map_err(|e| ApiError::internal(format!("Failed to list agents: {}", e)))?;
335
336 let (fcl_frequency, fcl_consumer) = runtime_service
338 .get_fcl_sampler_config()
339 .await
340 .map_err(|e| ApiError::internal(format!("Failed to get sampler config: {}", e)))?;
341
342 let mut visualization_agents = Vec::new();
344 let mut motor_agents = Vec::new();
345
346 for agent_id in &agent_ids {
347 if let Ok(props) = agent_service.get_agent_properties(agent_id).await {
348 if props.capabilities.contains_key("visualization") {
349 visualization_agents.push(agent_id.clone());
350 }
351 if props.capabilities.contains_key("motor") {
352 motor_agents.push(agent_id.clone());
353 }
354 }
355 }
356
357 let mut fq_coordination = HashMap::new();
358
359 let mut viz_sampler = HashMap::new();
360 viz_sampler.insert(
361 "enabled".to_string(),
362 serde_json::json!(!visualization_agents.is_empty()),
363 );
364 viz_sampler.insert(
365 "reason".to_string(),
366 serde_json::json!(if visualization_agents.is_empty() {
367 "No visualization agents connected".to_string()
368 } else {
369 format!(
370 "{} visualization agent(s) connected",
371 visualization_agents.len()
372 )
373 }),
374 );
375 viz_sampler.insert(
376 "agents_requiring".to_string(),
377 serde_json::json!(visualization_agents),
378 );
379 viz_sampler.insert("frequency_hz".to_string(), serde_json::json!(fcl_frequency));
380 fq_coordination.insert(
381 "visualization_fq_sampler".to_string(),
382 serde_json::json!(viz_sampler),
383 );
384
385 let mut motor_sampler = HashMap::new();
386 motor_sampler.insert(
387 "enabled".to_string(),
388 serde_json::json!(!motor_agents.is_empty()),
389 );
390 motor_sampler.insert(
391 "reason".to_string(),
392 serde_json::json!(if motor_agents.is_empty() {
393 "No motor agents connected".to_string()
394 } else {
395 format!("{} motor agent(s) connected", motor_agents.len())
396 }),
397 );
398 motor_sampler.insert(
399 "agents_requiring".to_string(),
400 serde_json::json!(motor_agents),
401 );
402 motor_sampler.insert("frequency_hz".to_string(), serde_json::json!(100.0));
403 fq_coordination.insert(
404 "motor_fq_sampler".to_string(),
405 serde_json::json!(motor_sampler),
406 );
407
408 let mut response = HashMap::new();
409 response.insert(
410 "fq_sampler_coordination".to_string(),
411 serde_json::json!(fq_coordination),
412 );
413 response.insert(
414 "agent_registry".to_string(),
415 serde_json::json!({
416 "total_agents": agent_ids.len(),
417 "agent_ids": agent_ids
418 }),
419 );
420 response.insert(
421 "system_status".to_string(),
422 serde_json::json!("coordinated_via_registration_manager"),
423 );
424 response.insert(
425 "fcl_sampler_consumer".to_string(),
426 serde_json::json!(fcl_consumer),
427 );
428
429 Ok(Json(response))
430}
431
432#[utoipa::path(
434 get,
435 path = "/v1/agent/capabilities",
436 responses(
437 (status = 200, description = "List of capabilities", body = HashMap<String, Vec<String>>),
438 (status = 500, description = "Failed to get capabilities")
439 ),
440 tag = "agent"
441)]
442pub async fn get_capabilities(
443 State(_state): State<ApiState>,
444) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
445 let mut response = HashMap::new();
446 response.insert(
447 "agent_types".to_string(),
448 vec![
449 "sensory".to_string(),
450 "motor".to_string(),
451 "both".to_string(),
452 "visualization".to_string(),
453 "infrastructure".to_string(),
454 ],
455 );
456 response.insert(
457 "capability_types".to_string(),
458 vec![
459 "vision".to_string(),
460 "motor".to_string(),
461 "visualization".to_string(),
462 "sensory".to_string(),
463 ],
464 );
465
466 Ok(Json(response))
467}
468
469#[utoipa::path(
471 get,
472 path = "/v1/agent/info/{agent_id}",
473 params(
474 ("agent_id" = String, Path, description = "Agent ID")
475 ),
476 responses(
477 (status = 200, description = "Agent detailed info", body = HashMap<String, serde_json::Value>),
478 (status = 404, description = "Agent not found"),
479 (status = 500, description = "Failed to get agent info")
480 ),
481 tag = "agent"
482)]
483pub async fn get_agent_info(
484 State(state): State<ApiState>,
485 Path(agent_id): Path<String>,
486) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
487 let agent_service = state
488 .agent_service
489 .as_ref()
490 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
491
492 let properties = agent_service
493 .get_agent_properties(&agent_id)
494 .await
495 .map_err(|e| ApiError::not_found("agent", &e.to_string()))?;
496
497 let mut response = HashMap::new();
498 response.insert("agent_id".to_string(), serde_json::json!(agent_id));
499 response.insert(
500 "agent_type".to_string(),
501 serde_json::json!(properties.agent_type),
502 );
503 response.insert(
504 "agent_ip".to_string(),
505 serde_json::json!(properties.agent_ip),
506 );
507 response.insert(
508 "agent_data_port".to_string(),
509 serde_json::json!(properties.agent_data_port),
510 );
511 response.insert(
512 "capabilities".to_string(),
513 serde_json::json!(properties.capabilities),
514 );
515 response.insert(
516 "agent_version".to_string(),
517 serde_json::json!(properties.agent_version),
518 );
519 response.insert(
520 "controller_version".to_string(),
521 serde_json::json!(properties.controller_version),
522 );
523 response.insert("status".to_string(), serde_json::json!("active"));
524 if let Some(ref transport) = properties.chosen_transport {
525 response.insert("chosen_transport".to_string(), serde_json::json!(transport));
526 }
527
528 Ok(Json(response))
529}
530
531#[utoipa::path(
533 get,
534 path = "/v1/agent/properties/{agent_id}",
535 params(
536 ("agent_id" = String, Path, description = "Agent ID")
537 ),
538 responses(
539 (status = 200, description = "Agent properties", body = AgentPropertiesResponse),
540 (status = 404, description = "Agent not found"),
541 (status = 500, description = "Failed to get agent properties")
542 ),
543 tag = "agent"
544)]
545pub async fn get_agent_properties_path(
546 State(state): State<ApiState>,
547 Path(agent_id): Path<String>,
548) -> ApiResult<Json<AgentPropertiesResponse>> {
549 let agent_service = state
550 .agent_service
551 .as_ref()
552 .ok_or_else(|| ApiError::internal("Agent service not available"))?;
553
554 match agent_service.get_agent_properties(&agent_id).await {
555 Ok(properties) => Ok(Json(AgentPropertiesResponse {
556 agent_type: properties.agent_type,
557 agent_ip: properties.agent_ip,
558 agent_data_port: properties.agent_data_port,
559 agent_router_address: properties.agent_router_address,
560 agent_version: properties.agent_version,
561 controller_version: properties.controller_version,
562 capabilities: properties.capabilities,
563 chosen_transport: properties.chosen_transport,
564 })),
565 Err(e) => Err(ApiError::not_found("agent", &format!("{}", e))),
566 }
567}
568
569#[utoipa::path(
571 post,
572 path = "/v1/agent/configure",
573 responses(
574 (status = 200, description = "Agent configured", body = HashMap<String, String>),
575 (status = 400, description = "Invalid input"),
576 (status = 500, description = "Failed to configure agent")
577 ),
578 tag = "agent"
579)]
580pub async fn post_configure(
581 State(_state): State<ApiState>,
582 Json(config): Json<HashMap<String, serde_json::Value>>,
583) -> ApiResult<Json<HashMap<String, String>>> {
584 tracing::info!(target: "feagi-api", "Agent configuration requested: {} params", config.len());
585
586 Ok(Json(HashMap::from([
587 (
588 "message".to_string(),
589 "Agent configuration updated".to_string(),
590 ),
591 ("status".to_string(), "not_yet_implemented".to_string()),
592 ])))
593}