1use crate::common::ApiState;
7use feagi_config::load_config;
8use feagi_services::types::CreateCorticalAreaParams;
9use feagi_structures::genomic::cortical_area::descriptors::{
10 CorticalSubUnitIndex, CorticalUnitIndex,
11};
12use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
13 FrameChangeHandling, PercentageNeuronPositioning,
14};
15use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
16use std::collections::{HashMap, HashSet};
17use tracing::{info, warn};
18
19fn build_friendly_unit_name(unit_label: &str, group: u8, sub_unit_index: usize) -> String {
20 format!("{unit_label}-{}-{}", group, sub_unit_index)
21}
22
23fn build_io_config_map() -> Result<serde_json::Map<String, serde_json::Value>, String> {
24 let mut config = serde_json::Map::new();
25 config.insert(
26 "frame_change_handling".to_string(),
27 serde_json::to_value(FrameChangeHandling::Absolute)
28 .map_err(|e| format!("Failed to serialize FrameChangeHandling: {}", e))?,
29 );
30 config.insert(
31 "percentage_neuron_positioning".to_string(),
32 serde_json::to_value(PercentageNeuronPositioning::Linear)
33 .map_err(|e| format!("Failed to serialize PercentageNeuronPositioning: {}", e))?,
34 );
35 Ok(config)
36}
37
38pub async fn auto_create_cortical_areas_from_device_registrations(
39 state: &ApiState,
40 device_registrations: &serde_json::Value,
41) {
42 let config = match load_config(None, None) {
43 Ok(config) => config,
44 Err(e) => {
45 warn!(
46 "⚠️ [API] Failed to load FEAGI configuration for auto-create: {}",
47 e
48 );
49 return;
50 }
51 };
52
53 if !config.agent.auto_create_missing_cortical_areas {
54 return;
55 }
56
57 let connectome_service = state.connectome_service.as_ref();
58 let genome_service = state.genome_service.as_ref();
59
60 let Some(output_units) = device_registrations
61 .get("output_units_and_decoder_properties")
62 .and_then(|v| v.as_object())
63 else {
64 return;
65 };
66 let input_units = device_registrations
67 .get("input_units_and_encoder_properties")
68 .and_then(|v| v.as_object());
69
70 let mut to_create: Vec<CreateCorticalAreaParams> = Vec::new();
72
73 for (motor_unit_key, unit_defs) in output_units {
74 let motor_unit: MotorCorticalUnit = match serde_json::from_value::<MotorCorticalUnit>(
76 serde_json::Value::String(motor_unit_key.clone()),
77 ) {
78 Ok(v) => v,
79 Err(e) => {
80 warn!(
81 "⚠️ [API] Unable to parse MotorCorticalUnit key '{}' from device_registrations: {}",
82 motor_unit_key, e
83 );
84 continue;
85 }
86 };
87
88 let Some(unit_defs_arr) = unit_defs.as_array() else {
89 continue;
90 };
91
92 for entry in unit_defs_arr {
93 let Some(pair) = entry.as_array() else {
95 continue;
96 };
97 let Some(unit_def) = pair.first() else {
98 continue;
99 };
100 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
101 else {
102 continue;
103 };
104 let group_u8: u8 = match group_u64.try_into() {
105 Ok(v) => v,
106 Err(_) => continue,
107 };
108 let group: CorticalUnitIndex = group_u8.into();
109
110 let device_count = unit_def
111 .get("device_grouping")
112 .and_then(|v| v.as_array())
113 .map(|a| a.len())
114 .unwrap_or(0);
115 if device_count == 0 {
116 warn!(
117 "⚠️ [API] device_grouping is empty for motor unit '{}' group {}; skipping auto-create",
118 motor_unit_key, group_u8
119 );
120 continue;
121 }
122
123 let config_map = match build_io_config_map() {
124 Ok(map) => map,
125 Err(e) => {
126 warn!(
127 "⚠️ [API] Failed to build motor IO config map for '{}' group {}: {}",
128 motor_unit_key, group_u8, e
129 );
130 continue;
131 }
132 };
133 let topology = motor_unit.get_unit_default_topology();
134
135 let cortical_ids = match motor_unit
136 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
137 group, config_map,
138 ) {
139 Ok(ids) => ids,
140 Err(e) => {
141 warn!(
142 "⚠️ [API] Failed to derive motor cortical IDs for '{}' group {}: {}",
143 motor_unit_key, group_u8, e
144 );
145 continue;
146 }
147 };
148
149 for (i, cortical_id) in cortical_ids.iter().enumerate() {
150 let cortical_id_b64 = cortical_id.as_base_64();
151 let exists = match connectome_service
152 .cortical_area_exists(&cortical_id_b64)
153 .await
154 {
155 Ok(v) => v,
156 Err(e) => {
157 warn!(
158 "⚠️ [API] Failed to check cortical area existence for '{}': {}",
159 cortical_id_b64, e
160 );
161 continue;
162 }
163 };
164
165 let sub_index = CorticalSubUnitIndex::from(i as u8);
166 let unit_topology = match topology.get(&sub_index) {
167 Some(t) => t,
168 None => {
169 warn!(
170 "⚠️ [API] Missing unit topology for motor unit '{}' subunit {}; skipping",
171 motor_unit_key, i
172 );
173 continue;
174 }
175 };
176 let expected_position = (
177 unit_topology.relative_position[0],
178 unit_topology.relative_position[1] + (group_u8 as i32 * 20),
179 unit_topology.relative_position[2],
180 );
181
182 if exists {
183 let current = match connectome_service.get_cortical_area(&cortical_id_b64).await
187 {
188 Ok(v) => v,
189 Err(e) => {
190 warn!(
191 "⚠️ [API] Failed to fetch existing cortical area '{}': {}",
192 cortical_id_b64, e
193 );
194 continue;
195 }
196 };
197
198 let (per_channel_width, per_channel_height, per_channel_depth) = (
199 unit_topology.channel_dimensions_default[0] as usize,
200 unit_topology.channel_dimensions_default[1] as usize,
201 unit_topology.channel_dimensions_default[2] as usize,
202 );
203 let expected_dimensions = (
204 (per_channel_width * device_count).max(1),
205 per_channel_height,
206 per_channel_depth,
207 );
208
209 let current_dev_count = current
210 .properties
211 .get("dev_count")
212 .and_then(|v| v.as_u64())
213 .map(|u| u as usize)
214 .or(current.dev_count);
215 let dimensions_mismatch = current.dimensions != expected_dimensions;
216 let dev_count_mismatch = current_dev_count != Some(device_count);
217 let position_mismatch = current.position != expected_position;
218
219 if dimensions_mismatch || dev_count_mismatch || position_mismatch {
220 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
221 changes.insert(
224 "dimensions".to_string(),
225 serde_json::json!([
226 expected_dimensions.0,
227 expected_dimensions.1,
228 expected_dimensions.2
229 ]),
230 );
231 changes.insert(
232 "dev_count".to_string(),
233 serde_json::Value::Number(serde_json::Number::from(device_count)),
234 );
235 changes.insert(
236 "position".to_string(),
237 serde_json::json!([
238 expected_position.0,
239 expected_position.1,
240 expected_position.2
241 ]),
242 );
243 if let Err(e) = genome_service
244 .update_cortical_area(&cortical_id_b64, changes)
245 .await
246 {
247 warn!(
248 "⚠️ [API] Failed to update cortical area '{}' dimensions/dev_count/position: {}",
249 cortical_id_b64, e
250 );
251 } else {
252 info!(
253 "[API] Updated cortical area '{}' to {} channels (dimensions {:?}, position {:?})",
254 cortical_id_b64, device_count, expected_dimensions, expected_position
255 );
256 }
257 }
258
259 if current.name == cortical_id_b64 {
261 let desired_name =
262 build_friendly_unit_name(motor_unit.get_friendly_name(), group_u8, i);
263 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
264 changes.insert("name".to_string(), serde_json::Value::String(desired_name));
265 if let Err(e) = genome_service
266 .update_cortical_area(&cortical_id_b64, changes)
267 .await
268 {
269 warn!(
270 "⚠️ [API] Failed to auto-rename existing motor cortical area '{}': {}",
271 cortical_id_b64, e
272 );
273 }
274 }
275 continue;
276 }
277
278 let friendly_name =
279 build_friendly_unit_name(motor_unit.get_friendly_name(), group_u8, i);
280 let (per_channel_width, per_channel_height, per_channel_depth) = (
283 unit_topology.channel_dimensions_default[0] as usize,
284 unit_topology.channel_dimensions_default[1] as usize,
285 unit_topology.channel_dimensions_default[2] as usize,
286 );
287 let dimensions = (
288 (per_channel_width * device_count).max(1),
289 per_channel_height,
290 per_channel_depth,
291 );
292 let per_device_dims = (per_channel_width, per_channel_height, per_channel_depth);
293 let position = expected_position;
294
295 let mut properties = HashMap::new();
296 properties.insert(
297 "dev_count".to_string(),
298 serde_json::Value::Number(serde_json::Number::from(device_count)),
299 );
300 properties.insert(
301 "cortical_dimensions_per_device".to_string(),
302 serde_json::json!([per_device_dims.0, per_device_dims.1, per_device_dims.2]),
303 );
304
305 to_create.push(CreateCorticalAreaParams {
306 cortical_id: cortical_id_b64.clone(),
307 name: friendly_name,
308 dimensions,
309 position,
310 area_type: "motor".to_string(),
311 visible: None,
312 sub_group: None,
313 neurons_per_voxel: None,
314 postsynaptic_current: None,
315 plasticity_constant: None,
316 degeneration: None,
317 psp_uniform_distribution: None,
318 firing_threshold_increment: None,
319 firing_threshold_limit: None,
320 consecutive_fire_count: None,
321 snooze_period: None,
322 refractory_period: None,
323 leak_coefficient: None,
324 leak_variability: None,
325 burst_engine_active: None,
326 properties: Some(properties),
327 });
328 }
329 }
330 }
331
332 if let Some(input_units) = input_units {
334 for (sensory_unit_key, unit_defs) in input_units {
335 let sensory_unit: SensoryCorticalUnit = match serde_json::from_value::<
336 SensoryCorticalUnit,
337 >(serde_json::Value::String(
338 sensory_unit_key.clone(),
339 )) {
340 Ok(v) => v,
341 Err(e) => {
342 warn!(
343 "⚠️ [API] Unable to parse SensoryCorticalUnit key '{}' from device_registrations: {}",
344 sensory_unit_key, e
345 );
346 continue;
347 }
348 };
349
350 let Some(unit_defs_arr) = unit_defs.as_array() else {
351 continue;
352 };
353
354 for entry in unit_defs_arr {
355 let Some(pair) = entry.as_array() else {
357 continue;
358 };
359 let Some(unit_def) = pair.first() else {
360 continue;
361 };
362 let Some(group_u64) = unit_def.get("cortical_unit_index").and_then(|v| v.as_u64())
363 else {
364 continue;
365 };
366 let group_u8: u8 = match group_u64.try_into() {
367 Ok(v) => v,
368 Err(_) => continue,
369 };
370 let group: CorticalUnitIndex = group_u8.into();
371
372 let device_count = unit_def
373 .get("device_grouping")
374 .and_then(|v| v.as_array())
375 .map(|a| a.len())
376 .unwrap_or(0);
377 if device_count == 0 {
378 warn!(
379 "⚠️ [API] device_grouping is empty for sensory unit '{}' group {}; skipping auto-create",
380 sensory_unit_key, group_u8
381 );
382 continue;
383 }
384
385 let config_map = match build_io_config_map() {
386 Ok(map) => map,
387 Err(e) => {
388 warn!(
389 "⚠️ [API] Failed to build sensory IO config map for '{}' group {}: {}",
390 sensory_unit_key, group_u8, e
391 );
392 continue;
393 }
394 };
395 let cortical_ids = match sensory_unit
396 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
397 group, config_map,
398 ) {
399 Ok(ids) => ids,
400 Err(e) => {
401 warn!(
402 "⚠️ [API] Failed to derive sensory cortical IDs for '{}' group {}: {}",
403 sensory_unit_key, group_u8, e
404 );
405 continue;
406 }
407 };
408 let topology = sensory_unit.get_unit_default_topology();
409
410 for (i, cortical_id) in cortical_ids.iter().enumerate() {
411 let cortical_id_b64 = cortical_id.as_base_64();
412 let exists = match connectome_service
413 .cortical_area_exists(&cortical_id_b64)
414 .await
415 {
416 Ok(v) => v,
417 Err(e) => {
418 warn!(
419 "⚠️ [API] Failed to check cortical area existence for '{}': {}",
420 cortical_id_b64, e
421 );
422 continue;
423 }
424 };
425
426 if exists {
427 let current = match connectome_service
433 .get_cortical_area(&cortical_id_b64)
434 .await
435 {
436 Ok(v) => v,
437 Err(e) => {
438 warn!(
439 "⚠️ [API] Failed to fetch existing cortical area '{}' for potential rename: {}",
440 cortical_id_b64, e
441 );
442 continue;
443 }
444 };
445 if current.name == cortical_id_b64 {
446 let desired_name = build_friendly_unit_name(
447 sensory_unit.get_friendly_name(),
448 group_u8,
449 i,
450 );
451 let mut changes: HashMap<String, serde_json::Value> = HashMap::new();
452 changes.insert(
453 "name".to_string(),
454 serde_json::Value::String(desired_name),
455 );
456 if let Err(e) = genome_service
457 .update_cortical_area(&cortical_id_b64, changes)
458 .await
459 {
460 warn!(
461 "⚠️ [API] Failed to auto-rename existing sensory cortical area '{}': {}",
462 cortical_id_b64, e
463 );
464 }
465 }
466 continue;
467 }
468
469 let friendly_name =
470 build_friendly_unit_name(sensory_unit.get_friendly_name(), group_u8, i);
471 let sub_index = CorticalSubUnitIndex::from(i as u8);
472 let unit_topology = match topology.get(&sub_index) {
473 Some(topology) => topology,
474 None => {
475 warn!(
476 "⚠️ [API] Missing unit topology for sensory unit '{}' subunit {} (agent device_registrations); cannot auto-create '{}'",
477 sensory_unit_key, i, friendly_name
478 );
479 continue;
480 }
481 };
482
483 let dimensions = (
484 unit_topology.channel_dimensions_default[0] as usize,
485 unit_topology.channel_dimensions_default[1] as usize,
486 unit_topology.channel_dimensions_default[2] as usize,
487 );
488 let position = (
489 unit_topology.relative_position[0],
490 unit_topology.relative_position[1],
491 unit_topology.relative_position[2],
492 );
493 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
494 properties.insert(
495 "cortical_subunit_index".to_string(),
496 serde_json::Value::Number(serde_json::Number::from(sub_index.get())),
497 );
498
499 to_create.push(CreateCorticalAreaParams {
500 cortical_id: cortical_id_b64.clone(),
501 name: friendly_name,
502 dimensions,
503 position,
504 area_type: "sensory".to_string(),
505 visible: None,
506 sub_group: None,
507 neurons_per_voxel: None,
508 postsynaptic_current: None,
509 plasticity_constant: None,
510 degeneration: None,
511 psp_uniform_distribution: None,
512 firing_threshold_increment: None,
513 firing_threshold_limit: None,
514 consecutive_fire_count: None,
515 snooze_period: None,
516 refractory_period: None,
517 leak_coefficient: None,
518 leak_variability: None,
519 burst_engine_active: None,
520 properties: Some(properties),
521 });
522 }
523 }
524 }
525 }
526
527 if to_create.is_empty() {
528 return;
529 }
530
531 info!(
532 "🦀 [API] Auto-creating {} missing cortical areas from device registrations",
533 to_create.len()
534 );
535
536 if let Err(e) = genome_service.create_cortical_areas(to_create).await {
537 warn!(
538 "⚠️ [API] Failed to auto-create cortical areas from device registrations: {}",
539 e
540 );
541 }
542}
543
544pub fn derive_motor_cortical_ids_from_device_registrations(
545 device_registrations: &serde_json::Value,
546) -> Result<HashSet<String>, String> {
547 let output_units = device_registrations
548 .get("output_units_and_decoder_properties")
549 .and_then(|v| v.as_object())
550 .ok_or_else(|| {
551 "device_registrations missing output_units_and_decoder_properties".to_string()
552 })?;
553
554 let mut cortical_ids: HashSet<String> = HashSet::new();
555
556 for (motor_unit_key, unit_defs) in output_units {
557 let motor_unit: MotorCorticalUnit = serde_json::from_value::<MotorCorticalUnit>(
558 serde_json::Value::String(motor_unit_key.clone()),
559 )
560 .map_err(|e| {
561 format!(
562 "Unable to parse MotorCorticalUnit key '{}': {}",
563 motor_unit_key, e
564 )
565 })?;
566
567 let unit_defs_arr = unit_defs
568 .as_array()
569 .ok_or_else(|| "Motor unit definitions must be an array".to_string())?;
570
571 for entry in unit_defs_arr {
572 let pair = entry
573 .as_array()
574 .ok_or_else(|| "Motor unit definition entries must be arrays".to_string())?;
575 let unit_def = pair
576 .first()
577 .ok_or_else(|| "Motor unit definition entry missing unit_def".to_string())?;
578 let group_u64 = unit_def
579 .get("cortical_unit_index")
580 .and_then(|v| v.as_u64())
581 .ok_or_else(|| "Motor unit definition missing cortical_unit_index".to_string())?;
582 let group_u8: u8 = group_u64
583 .try_into()
584 .map_err(|_| "Motor unit cortical_unit_index out of range for u8".to_string())?;
585 let group: CorticalUnitIndex = group_u8.into();
586
587 let device_count = unit_def
588 .get("device_grouping")
589 .and_then(|v| v.as_array())
590 .map(|a| a.len())
591 .unwrap_or(0);
592 if device_count == 0 {
593 return Err(format!(
594 "device_grouping is empty for motor unit '{}' group {}",
595 motor_unit_key, group_u8
596 ));
597 }
598
599 let config = build_io_config_map()
600 .map_err(|e| format!("Failed to build motor IO config map: {}", e))?;
601 let unit_cortical_ids = motor_unit
602 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(group, config)
603 .map_err(|e| format!("Failed to derive cortical IDs: {}", e))?;
604 for cortical_id in unit_cortical_ids {
605 cortical_ids.insert(cortical_id.as_base_64());
606 }
607 }
608 }
609
610 Ok(cortical_ids)
611}