Skip to main content

feagi_api/common/
agent_registration.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared registration helpers used across transports.
5
6use 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    // Build creation params for missing OPU areas based on default topologies.
71    let mut to_create: Vec<CreateCorticalAreaParams> = Vec::new();
72
73    for (motor_unit_key, unit_defs) in output_units {
74        // MotorCorticalUnit is serde-deserializable from its string representation.
75        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            // Expected shape: [<unit_definition>, <decoder_properties>]
94            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                    // Area exists: ensure dimensions and dev_count match device_registrations.
184                    // If a genome was loaded with wrong dimensions (e.g. 1 channel per limb),
185                    // update to the correct channel count from device_grouping.
186                    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                        // Pass total dimensions. Do NOT pass cortical_dimensions_per_device here:
222                        // genome service would treat it as per-device and multiply depth by dev_count.
223                        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                    // Auto-rename if current name is placeholder (== cortical_id).
260                    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                // Use template-defined per-channel topology and scale by device_count.
281                // This keeps sizing fully template-driven and consistent across all unit types.
282                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    // Build creation params for missing IPU areas based on default topologies.
333    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                // Expected shape: [<unit_definition>, <encoder_properties>]
356                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                        // If the area already exists but still has a placeholder name (often equal to the cortical_id),
428                        // update it to a deterministic friendly name so UIs (e.g., Brain Visualizer) show readable labels.
429                        //
430                        // IMPORTANT: We only auto-rename if the current name is clearly a placeholder (== cortical_id).
431                        // This avoids clobbering user-defined names.
432                        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}