1use crate::amalgamation;
8use crate::common::ApiState;
9use crate::common::{ApiError, ApiResult, Json, Query, State};
10use feagi_services::types::{GenomeInfo, LoadGenomeParams};
11use std::collections::HashMap;
12use std::sync::atomic::Ordering;
13use std::sync::Arc;
14use tracing::info;
15use uuid::Uuid;
16
17#[cfg(feature = "http")]
18use axum::extract::Multipart;
19
20#[derive(Debug, Clone, utoipa::ToSchema)]
24pub struct GenomeFileUploadForm {
25 #[schema(value_type = String, format = Binary)]
27 pub file: String,
28}
29
30fn queue_amalgamation_from_genome_json_str(
31 state: &ApiState,
32 genome_json: String,
33) -> Result<String, ApiError> {
34 {
36 let lock = state.amalgamation_state.read();
37 if lock.pending.is_some() {
38 return Err(ApiError::invalid_input(
39 "Amalgamation already pending; cancel it first via /v1/genome/amalgamation_cancellation",
40 ));
41 }
42 }
43
44 let genome = feagi_evolutionary::load_genome_from_json(&genome_json)
45 .map_err(|e| ApiError::invalid_input(format!("Invalid genome payload: {}", e)))?;
46
47 let circuit_size = amalgamation::compute_circuit_size_from_runtime_genome(&genome);
48
49 let amalgamation_id = Uuid::new_v4().to_string();
50 let genome_title = genome.metadata.genome_title.clone();
51
52 let summary = amalgamation::AmalgamationPendingSummary {
53 amalgamation_id: amalgamation_id.clone(),
54 genome_title,
55 circuit_size,
56 };
57
58 let pending = amalgamation::AmalgamationPending {
59 summary: summary.clone(),
60 genome_json,
61 };
62
63 {
64 let mut lock = state.amalgamation_state.write();
65 let now_ms = std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .map(|d| d.as_millis() as i64)
68 .unwrap_or(0);
69
70 lock.history.push(amalgamation::AmalgamationHistoryEntry {
71 amalgamation_id: summary.amalgamation_id.clone(),
72 genome_title: summary.genome_title.clone(),
73 circuit_size: summary.circuit_size,
74 status: "pending".to_string(),
75 timestamp_ms: now_ms,
76 });
77 lock.pending = Some(pending);
78 }
79
80 tracing::info!(
81 target: "feagi-api",
82 "🧬 [AMALGAMATION] Queued pending amalgamation id={} title='{}' circuit_size={:?}",
83 summary.amalgamation_id,
84 summary.genome_title,
85 summary.circuit_size
86 );
87
88 Ok(amalgamation_id)
89}
90
91struct GenomeTransitionFlagGuard {
92 in_progress: Arc<std::sync::atomic::AtomicBool>,
93}
94
95impl Drop for GenomeTransitionFlagGuard {
96 fn drop(&mut self) {
97 self.in_progress.store(false, Ordering::SeqCst);
98 }
99}
100
101async fn load_genome_with_priority(
109 state: &ApiState,
110 params: LoadGenomeParams,
111 source: &str,
112) -> ApiResult<GenomeInfo> {
113 let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
114 ApiError::conflict(
115 "Another genome transition is already in progress; wait for it to finish",
116 )
117 })?;
118 state
119 .genome_transition_in_progress
120 .store(true, Ordering::SeqCst);
121 let _guard = GenomeTransitionFlagGuard {
122 in_progress: Arc::clone(&state.genome_transition_in_progress),
123 };
124
125 tracing::info!(
126 target: "feagi-api",
127 "🛑 Entering prioritized genome transition from {}",
128 source
129 );
130
131 let runtime_service = state.runtime_service.as_ref();
132 #[cfg(feature = "feagi-agent")]
133 if let Some(handler) = &state.agent_handler {
134 let deregistered_ids = {
135 let mut guard = handler.lock().unwrap();
136 guard.force_deregister_all_agents("forced by genome transition")
137 };
138 for agent_id in &deregistered_ids {
139 runtime_service.unregister_motor_subscriptions(agent_id);
140 runtime_service.unregister_visualization_subscriptions(agent_id);
141 }
142 tracing::info!(
143 target: "feagi-api",
144 "🔌 Forced deregistration for {} agents before genome transition",
145 deregistered_ids.len()
146 );
147 }
148 runtime_service.clear_all_motor_subscriptions();
150 runtime_service.clear_all_visualization_subscriptions();
151
152 let runtime_status = runtime_service
153 .get_status()
154 .await
155 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
156 let runtime_was_running = runtime_status.is_running;
157
158 if runtime_was_running {
159 tracing::info!(
160 target: "feagi-api",
161 "Stopping burst engine before prioritized genome transition"
162 );
163 runtime_service.stop().await.map_err(|e| {
164 ApiError::internal(format!(
165 "Failed to stop burst engine before genome transition: {}",
166 e
167 ))
168 })?;
169 }
170
171 let genome_service = state.genome_service.as_ref();
172 let load_result = genome_service.load_genome(params).await;
173 let genome_info = match load_result {
174 Ok(info) => info,
175 Err(e) => {
176 if runtime_was_running {
177 if let Err(restart_err) = runtime_service.start().await {
178 tracing::warn!(
179 target: "feagi-api",
180 "Failed to restore runtime after failed genome load (source={}): {}",
181 source,
182 restart_err
183 );
184 }
185 }
186 return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
187 }
188 };
189
190 let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
191 runtime_service
192 .set_frequency(burst_frequency_hz)
193 .await
194 .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
195
196 if runtime_was_running {
197 runtime_service.start().await.map_err(|e| {
198 ApiError::internal(format!(
199 "Failed to restart burst engine after genome transition: {}",
200 e
201 ))
202 })?;
203 }
204
205 tracing::info!(
206 target: "feagi-api",
207 "✅ Prioritized genome transition completed from {}",
208 source
209 );
210
211 #[cfg(feature = "feagi-agent")]
214 if let Some(handler) = &state.agent_handler {
215 let device_regs_list: Vec<serde_json::Value> = {
216 let guard = handler.lock().unwrap();
217 guard
218 .get_all_registered_agents()
219 .iter()
220 .filter_map(|(sid, _)| guard.get_device_registrations_by_agent(*sid).cloned())
221 .collect()
222 };
223 for device_regs in device_regs_list {
224 crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations(
225 state,
226 &device_regs,
227 )
228 .await;
229 }
230 }
231
232 Ok(genome_info)
233}
234
235fn inject_simulation_timestep_into_genome(
241 mut genome: serde_json::Value,
242 simulation_timestep_s: f64,
243) -> Result<serde_json::Value, ApiError> {
244 let physiology = genome
245 .get_mut("physiology")
246 .and_then(|v| v.as_object_mut())
247 .ok_or_else(|| {
248 ApiError::internal(
249 "Genome JSON missing required object key 'physiology' while saving".to_string(),
250 )
251 })?;
252
253 physiology.insert(
254 "simulation_timestep".to_string(),
255 serde_json::Value::from(simulation_timestep_s),
256 );
257 Ok(genome)
258}
259
260async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
261 let runtime_service = state.runtime_service.as_ref();
262 let status = runtime_service
263 .get_status()
264 .await
265 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
266
267 Ok(if status.frequency_hz > 0.0 {
269 1.0 / status.frequency_hz
270 } else {
271 0.0
272 })
273}
274
275#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
277pub async fn get_file_name(
278 State(_state): State<ApiState>,
279) -> ApiResult<Json<HashMap<String, String>>> {
280 Ok(Json(HashMap::from([(
282 "genome_file_name".to_string(),
283 "".to_string(),
284 )])))
285}
286
287#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
289pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
290 Ok(Json(vec![]))
292}
293
294#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
296pub async fn post_amalgamation_destination(
297 State(state): State<ApiState>,
298 Query(params): Query<HashMap<String, String>>,
299 Json(req): Json<HashMap<String, serde_json::Value>>,
300) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
301 let amalgamation_id = params
309 .get("amalgamation_id")
310 .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
311 .to_string();
312
313 let origin_x: i32 = params
314 .get("circuit_origin_x")
315 .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
316 .parse()
317 .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
318 let origin_y: i32 = params
319 .get("circuit_origin_y")
320 .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
321 .parse()
322 .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
323 let origin_z: i32 = params
324 .get("circuit_origin_z")
325 .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
326 .parse()
327 .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
328
329 let rewire_mode = params
330 .get("rewire_mode")
331 .cloned()
332 .unwrap_or_else(|| "rewire_all".to_string());
333
334 let parent_region_id = req
335 .get("brain_region_id")
336 .and_then(|v| v.as_str())
337 .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
338 .to_string();
339
340 let pending = {
342 let lock = state.amalgamation_state.write();
343 let Some(p) = lock.pending.as_ref() else {
344 return Err(ApiError::invalid_input("No amalgamation is pending"));
345 };
346 if p.summary.amalgamation_id != amalgamation_id {
347 return Err(ApiError::invalid_input(format!(
348 "Pending amalgamation_id mismatch: expected {}, got {}",
349 p.summary.amalgamation_id, amalgamation_id
350 )));
351 }
352 p.clone()
353 };
354
355 let connectome_service = state.connectome_service.as_ref();
359
360 let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
361 region_properties.insert(
362 "coordinate_3d".to_string(),
363 serde_json::json!([origin_x, origin_y, origin_z]),
364 );
365 region_properties.insert(
366 "amalgamation_id".to_string(),
367 serde_json::json!(pending.summary.amalgamation_id),
368 );
369 region_properties.insert(
370 "circuit_size".to_string(),
371 serde_json::json!(pending.summary.circuit_size),
372 );
373 region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
374
375 connectome_service
376 .create_brain_region(feagi_services::types::CreateBrainRegionParams {
377 region_id: amalgamation_id.clone(),
378 name: pending.summary.genome_title.clone(),
379 region_type: "Custom".to_string(),
380 parent_id: Some(parent_region_id.clone()),
381 properties: Some(region_properties),
382 })
383 .await
384 .map_err(|e| {
385 ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
386 })?;
387
388 let imported_genome =
397 feagi_evolutionary::load_genome_from_json(&pending.genome_json).map_err(|e| {
398 ApiError::invalid_input(format!(
399 "Pending genome payload can no longer be parsed as a genome: {}",
400 e
401 ))
402 })?;
403
404 let genome_service = state.genome_service.as_ref();
405 let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
406 let mut skipped_existing: Vec<String> = Vec::new();
407
408 let root_region_id = connectome_service
410 .get_root_region_id()
411 .await
412 .map_err(|e| ApiError::internal(format!("Failed to get root region ID: {}", e)))?;
413
414 for area in imported_genome.cortical_areas.values() {
415 let cortical_id = area.cortical_id.as_base_64();
416 let exists = connectome_service
417 .cortical_area_exists(&cortical_id)
418 .await
419 .map_err(|e| {
420 ApiError::internal(format!(
421 "Failed to check existing cortical area {}: {}",
422 cortical_id, e
423 ))
424 })?;
425 if exists {
426 skipped_existing.push(cortical_id);
427 continue;
428 }
429
430 let mut props = area.properties.clone();
431
432 props.remove("cortical_mapping_dst");
434
435 let area_type = area.cortical_id.as_cortical_type().map_err(|e| {
438 ApiError::internal(format!(
439 "Failed to get cortical area type for {}: {}",
440 cortical_id, e
441 ))
442 })?;
443
444 let target_parent_region_id = match area_type {
445 feagi_structures::genomic::cortical_area::CorticalAreaType::BrainInput(_)
446 | feagi_structures::genomic::cortical_area::CorticalAreaType::BrainOutput(_) => {
447 match root_region_id.as_ref() {
449 Some(root_id) => {
450 tracing::info!(
451 target: "feagi-api",
452 "🧬 [AMALGAMATION] IPU/OPU area {} will be placed in root region {}",
453 cortical_id,
454 root_id
455 );
456 root_id.clone()
457 }
458 None => {
459 tracing::warn!(
460 target: "feagi-api",
461 "🧬 [AMALGAMATION] No root region found for IPU/OPU area {}, using amalgamation region",
462 cortical_id
463 );
464 amalgamation_id.clone()
465 }
466 }
467 }
468 _ => {
469 amalgamation_id.clone()
471 }
472 };
473
474 props.insert(
475 "parent_region_id".to_string(),
476 serde_json::json!(target_parent_region_id),
477 );
478 props.insert(
479 "amalgamation_source".to_string(),
480 serde_json::json!("amalgamation_by_payload"),
481 );
482
483 to_create.push(feagi_services::types::CreateCorticalAreaParams {
484 cortical_id,
485 name: area.name.clone(),
486 dimensions: (
487 area.dimensions.width as usize,
488 area.dimensions.height as usize,
489 area.dimensions.depth as usize,
490 ),
491 position: (
492 origin_x.saturating_add(area.position.x),
493 origin_y.saturating_add(area.position.y),
494 origin_z.saturating_add(area.position.z),
495 ),
496 area_type: "Custom".to_string(),
497 visible: Some(true),
498 sub_group: None,
499 neurons_per_voxel: area
500 .properties
501 .get("neurons_per_voxel")
502 .and_then(|v| v.as_u64())
503 .map(|v| v as u32),
504 postsynaptic_current: area
505 .properties
506 .get("postsynaptic_current")
507 .and_then(|v| v.as_f64()),
508 plasticity_constant: area
509 .properties
510 .get("plasticity_constant")
511 .and_then(|v| v.as_f64()),
512 degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
513 psp_uniform_distribution: area
514 .properties
515 .get("psp_uniform_distribution")
516 .and_then(|v| v.as_bool()),
517 firing_threshold_increment: None,
518 firing_threshold_limit: area
519 .properties
520 .get("firing_threshold_limit")
521 .and_then(|v| v.as_f64()),
522 consecutive_fire_count: area
523 .properties
524 .get("consecutive_fire_limit")
525 .and_then(|v| v.as_u64())
526 .map(|v| v as u32),
527 snooze_period: area
528 .properties
529 .get("snooze_period")
530 .and_then(|v| v.as_u64())
531 .map(|v| v as u32),
532 refractory_period: area
533 .properties
534 .get("refractory_period")
535 .and_then(|v| v.as_u64())
536 .map(|v| v as u32),
537 leak_coefficient: area
538 .properties
539 .get("leak_coefficient")
540 .and_then(|v| v.as_f64()),
541 leak_variability: area
542 .properties
543 .get("leak_variability")
544 .and_then(|v| v.as_f64()),
545 burst_engine_active: area
546 .properties
547 .get("burst_engine_active")
548 .and_then(|v| v.as_bool()),
549 properties: Some(props),
550 });
551 }
552
553 if !to_create.is_empty() {
554 genome_service
555 .create_cortical_areas(to_create)
556 .await
557 .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
558 }
559
560 let imported_area_ids: std::collections::HashSet<String> = imported_genome
565 .cortical_areas
566 .keys()
567 .map(|id| id.as_base_64())
568 .filter(|id| !skipped_existing.contains(id))
569 .collect();
570
571 let mut required_morphologies: std::collections::HashSet<String> =
572 std::collections::HashSet::new();
573
574 for area in imported_genome.cortical_areas.values() {
576 if !imported_area_ids.contains(&area.cortical_id.as_base_64()) {
577 continue;
578 }
579
580 let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
581 continue;
582 };
583 let Some(dst_map) = cortical_mapping_dst.as_object() else {
584 continue;
585 };
586
587 for mapping_data in dst_map.values() {
588 let Some(mapping_array) = mapping_data.as_array() else {
589 continue;
590 };
591
592 for rule in mapping_array {
593 let morphology_id = if let Some(obj) = rule.as_object() {
595 obj.get("morphology_id").and_then(|v| v.as_str())
596 } else if let Some(arr) = rule.as_array() {
597 arr.first().and_then(|v| v.as_str())
598 } else {
599 None
600 };
601
602 if let Some(morph_id) = morphology_id {
603 required_morphologies.insert(morph_id.to_string());
604 }
605 }
606 }
607 }
608
609 let mut imported_morphology_count = 0;
611 let mut skipped_morphology_count = 0;
612
613 for morphology_id in &required_morphologies {
614 let morphologies = connectome_service.get_morphologies().await.map_err(|e| {
616 ApiError::internal(format!("Failed to get existing morphologies: {}", e))
617 })?;
618
619 if morphologies.contains_key(morphology_id) {
620 skipped_morphology_count += 1;
621 continue;
622 }
623
624 let Some(morphology) = imported_genome.morphologies.get(morphology_id) else {
626 tracing::warn!(
627 target: "feagi-api",
628 "🧬 [AMALGAMATION] Morphology '{}' referenced in mappings but not found in imported genome",
629 morphology_id
630 );
631 continue;
632 };
633
634 match connectome_service
636 .create_morphology(morphology_id.clone(), morphology.clone())
637 .await
638 {
639 Ok(_) => {
640 tracing::debug!(
641 target: "feagi-api",
642 "🧬 [AMALGAMATION] Imported morphology '{}'",
643 morphology_id
644 );
645 imported_morphology_count += 1;
646 }
647 Err(e) => {
648 tracing::warn!(
649 target: "feagi-api",
650 "🧬 [AMALGAMATION] Failed to import morphology '{}': {}",
651 morphology_id,
652 e
653 );
654 }
655 }
656 }
657
658 if imported_morphology_count > 0 {
659 tracing::info!(
660 target: "feagi-api",
661 "🧬 [AMALGAMATION] Imported {} morphologies (skipped {} existing)",
662 imported_morphology_count,
663 skipped_morphology_count
664 );
665 }
666
667 let mut imported_mapping_count = 0;
674 let mut skipped_mapping_count = 0;
675
676 for area in imported_genome.cortical_areas.values() {
677 let src_area_id = area.cortical_id.as_base_64();
678
679 if !imported_area_ids.contains(&src_area_id) {
681 continue;
682 }
683
684 let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
686 continue;
687 };
688 let Some(dst_map) = cortical_mapping_dst.as_object() else {
689 continue;
690 };
691
692 for (dst_area_id, mapping_data) in dst_map {
694 let dst_exists = connectome_service
696 .cortical_area_exists(dst_area_id)
697 .await
698 .unwrap_or(false);
699
700 if !dst_exists {
701 skipped_mapping_count += 1;
703 continue;
704 }
705
706 let Some(mapping_array) = mapping_data.as_array() else {
707 tracing::warn!(
708 target: "feagi-api",
709 "🧬 [AMALGAMATION] Invalid mapping data from {} to {}: not an array",
710 src_area_id,
711 dst_area_id
712 );
713 continue;
714 };
715
716 match connectome_service
718 .update_cortical_mapping(
719 src_area_id.clone(),
720 dst_area_id.clone(),
721 mapping_array.clone(),
722 )
723 .await
724 {
725 Ok(synapse_count) => {
726 tracing::debug!(
727 target: "feagi-api",
728 "🧬 [AMALGAMATION] Imported mapping {} -> {} ({} synapses)",
729 src_area_id,
730 dst_area_id,
731 synapse_count
732 );
733 imported_mapping_count += 1;
734 }
735 Err(e) => {
736 tracing::warn!(
737 target: "feagi-api",
738 "🧬 [AMALGAMATION] Failed to import mapping {} -> {}: {}",
739 src_area_id,
740 dst_area_id,
741 e
742 );
743 skipped_mapping_count += 1;
744 }
745 }
746 }
747 }
748
749 if imported_mapping_count > 0 {
750 tracing::info!(
751 target: "feagi-api",
752 "🧬 [AMALGAMATION] Successfully imported {} cortical mappings (skipped {} external/missing mappings)",
753 imported_mapping_count,
754 skipped_mapping_count
755 );
756 } else if skipped_mapping_count > 0 {
757 tracing::warn!(
758 target: "feagi-api",
759 "🧬 [AMALGAMATION] No internal mappings imported! Skipped {} mappings (all external or missing)",
760 skipped_mapping_count
761 );
762 } else {
763 tracing::info!(
764 target: "feagi-api",
765 "🧬 [AMALGAMATION] No cortical mappings found in imported genome"
766 );
767 }
768
769 {
780 let state_manager = feagi_state_manager::StateManager::instance();
781 let state_manager = state_manager.read();
782
783 state_manager
785 .set_brain_regions_hash(state_manager.get_brain_regions_hash().wrapping_add(1));
786 state_manager
787 .set_cortical_areas_hash(state_manager.get_cortical_areas_hash().wrapping_add(1));
788 state_manager
789 .set_brain_geometry_hash(state_manager.get_brain_geometry_hash().wrapping_add(1));
790 if imported_morphology_count > 0 {
791 state_manager
792 .set_morphologies_hash(state_manager.get_morphologies_hash().wrapping_add(1));
793 }
794 if imported_mapping_count > 0 {
795 state_manager.set_cortical_mappings_hash(
796 state_manager.get_cortical_mappings_hash().wrapping_add(1),
797 );
798 }
799
800 tracing::info!(
801 target: "feagi-api",
802 "🧬 [AMALGAMATION] Invalidated health_check hashes for BV cache refresh"
803 );
804 }
805
806 {
808 let mut lock = state.amalgamation_state.write();
809 let now_ms = std::time::SystemTime::now()
810 .duration_since(std::time::UNIX_EPOCH)
811 .map(|d| d.as_millis() as i64)
812 .unwrap_or(0);
813 lock.history.push(amalgamation::AmalgamationHistoryEntry {
814 amalgamation_id: pending.summary.amalgamation_id.clone(),
815 genome_title: pending.summary.genome_title.clone(),
816 circuit_size: pending.summary.circuit_size,
817 status: "confirmed".to_string(),
818 timestamp_ms: now_ms,
819 });
820 lock.pending = None;
821 }
822
823 tracing::info!(
824 target: "feagi-api",
825 "🧬 [AMALGAMATION] Confirmed and cleared pending amalgamation id={} imported_areas={} skipped_existing_areas={}",
826 pending.summary.amalgamation_id,
827 if skipped_existing.is_empty() { "unknown".to_string() } else { "partial".to_string() },
828 skipped_existing.len()
829 );
830
831 let regions = state
833 .connectome_service
834 .list_brain_regions()
835 .await
836 .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
837
838 let mut brain_regions: Vec<serde_json::Value> = Vec::new();
839 for region in regions {
840 let coordinate_3d = region
842 .properties
843 .get("coordinate_3d")
844 .cloned()
845 .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
846 let coordinate_2d = region
847 .properties
848 .get("coordinate_2d")
849 .cloned()
850 .unwrap_or_else(|| serde_json::json!([0, 0]));
851
852 brain_regions.push(serde_json::json!({
853 "region_id": region.region_id,
854 "title": region.name,
855 "description": "",
856 "parent_region_id": region.parent_id,
857 "coordinate_2d": coordinate_2d,
858 "coordinate_3d": coordinate_3d,
859 "areas": region.cortical_areas,
860 "regions": region.child_regions,
861 "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
862 "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
863 "designated_inputs": region.properties.get("designated_inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
864 "designated_outputs": region.properties.get("designated_outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
865 }));
866 }
867
868 Ok(Json(HashMap::from([
869 (
870 "message".to_string(),
871 serde_json::Value::String("Amalgamation confirmed".to_string()),
872 ),
873 (
874 "brain_regions".to_string(),
875 serde_json::Value::Array(brain_regions),
876 ),
877 (
878 "skipped_existing_areas".to_string(),
879 serde_json::json!(skipped_existing),
880 ),
881 ])))
882}
883
884#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
886pub async fn delete_amalgamation_cancellation(
887 State(state): State<ApiState>,
888) -> ApiResult<Json<HashMap<String, String>>> {
889 let mut lock = state.amalgamation_state.write();
890 if let Some(pending) = lock.pending.take() {
891 let now_ms = std::time::SystemTime::now()
892 .duration_since(std::time::UNIX_EPOCH)
893 .map(|d| d.as_millis() as i64)
894 .unwrap_or(0);
895 lock.history.push(amalgamation::AmalgamationHistoryEntry {
896 amalgamation_id: pending.summary.amalgamation_id,
897 genome_title: pending.summary.genome_title,
898 circuit_size: pending.summary.circuit_size,
899 status: "cancelled".to_string(),
900 timestamp_ms: now_ms,
901 });
902
903 tracing::info!(
904 target: "feagi-api",
905 "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
906 lock.history
907 .last()
908 .map(|e| e.amalgamation_id.clone())
909 .unwrap_or_else(|| "<unknown>".to_string())
910 );
911 }
912 Ok(Json(HashMap::from([(
913 "message".to_string(),
914 "Amalgamation cancelled".to_string(),
915 )])))
916}
917
918#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
920pub async fn post_genome_append(
921 State(_state): State<ApiState>,
922 Json(_req): Json<HashMap<String, serde_json::Value>>,
923) -> ApiResult<Json<HashMap<String, String>>> {
924 Err(ApiError::internal("Not yet implemented"))
925}
926
927#[utoipa::path(
929 post,
930 path = "/v1/genome/upload/barebones",
931 responses(
932 (status = 200, description = "Barebones genome loaded successfully"),
933 (status = 500, description = "Failed to load genome")
934 ),
935 tag = "genome"
936)]
937pub async fn post_upload_barebones_genome(
938 State(state): State<ApiState>,
939) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
940 tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
941 let result = load_default_genome(state, "barebones").await;
942 match &result {
943 Ok(_) => {
944 tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
945 }
946 Err(e) => {
947 tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
948 }
949 }
950 result
951}
952
953#[utoipa::path(
955 post,
956 path = "/v1/genome/upload/essential",
957 responses(
958 (status = 200, description = "Essential genome loaded successfully"),
959 (status = 500, description = "Failed to load genome")
960 ),
961 tag = "genome"
962)]
963pub async fn post_upload_essential_genome(
964 State(state): State<ApiState>,
965) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
966 load_default_genome(state, "essential").await
967}
968
969async fn load_default_genome(
971 state: ApiState,
972 genome_name: &str,
973) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
974 tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
975 tracing::debug!(target: "feagi-api", " State components available: genome_service=true, runtime_service=true");
976 let genome_json = match genome_name {
978 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
979 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
980 "test" => feagi_evolutionary::TEST_GENOME_JSON,
981 "vision" => feagi_evolutionary::VISION_GENOME_JSON,
982 _ => {
983 return Err(ApiError::invalid_input(format!(
984 "Unknown genome name '{}'. Available: barebones, essential, test, vision",
985 genome_name
986 )))
987 }
988 };
989
990 tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
991 genome_name, genome_json.len());
992
993 let params = LoadGenomeParams {
994 json_str: genome_json.to_string(),
995 };
996
997 tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
998 let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
999
1000 tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
1001 genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
1002
1003 let mut response = HashMap::new();
1005 response.insert("success".to_string(), serde_json::Value::Bool(true));
1006 response.insert(
1007 "message".to_string(),
1008 serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
1009 );
1010 response.insert(
1011 "cortical_area_count".to_string(),
1012 serde_json::Value::Number(genome_info.cortical_area_count.into()),
1013 );
1014 response.insert(
1015 "brain_region_count".to_string(),
1016 serde_json::Value::Number(genome_info.brain_region_count.into()),
1017 );
1018 response.insert(
1019 "genome_id".to_string(),
1020 serde_json::Value::String(genome_info.genome_id),
1021 );
1022 response.insert(
1023 "genome_title".to_string(),
1024 serde_json::Value::String(genome_info.genome_title),
1025 );
1026
1027 Ok(Json(response))
1028}
1029
1030#[utoipa::path(
1032 get,
1033 path = "/v1/genome/name",
1034 tag = "genome",
1035 responses(
1036 (status = 200, description = "Genome name", body = String)
1037 )
1038)]
1039pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
1040 Ok(Json("default_genome".to_string()))
1043}
1044
1045#[utoipa::path(
1047 get,
1048 path = "/v1/genome/timestamp",
1049 tag = "genome",
1050 responses(
1051 (status = 200, description = "Genome timestamp", body = i64)
1052 )
1053)]
1054pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
1055 Ok(Json(0))
1057}
1058
1059#[utoipa::path(
1061 post,
1062 path = "/v1/genome/save",
1063 tag = "genome",
1064 responses(
1065 (status = 200, description = "Genome saved", body = HashMap<String, String>)
1066 )
1067)]
1068pub async fn post_save(
1069 State(state): State<ApiState>,
1070 Json(request): Json<HashMap<String, String>>,
1071) -> ApiResult<Json<HashMap<String, String>>> {
1072 use std::fs;
1073 use std::path::Path;
1074
1075 info!("Saving genome to file");
1076
1077 let genome_id = request.get("genome_id").cloned();
1079 let genome_title = request.get("genome_title").cloned();
1080 let file_path = request.get("file_path").cloned();
1081
1082 let params = feagi_services::SaveGenomeParams {
1084 genome_id,
1085 genome_title,
1086 };
1087
1088 let genome_service = state.genome_service.as_ref();
1090 let genome_json = genome_service
1091 .save_genome(params)
1092 .await
1093 .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
1094
1095 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1097 let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
1098 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1099 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1100 let genome_json = serde_json::to_string_pretty(&genome_value)
1101 .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
1102
1103 let save_path = if let Some(path) = file_path {
1105 std::path::PathBuf::from(path)
1106 } else {
1107 let timestamp = std::time::SystemTime::now()
1109 .duration_since(std::time::UNIX_EPOCH)
1110 .unwrap()
1111 .as_secs();
1112 std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
1113 };
1114
1115 if let Some(parent) = Path::new(&save_path).parent() {
1117 fs::create_dir_all(parent)
1118 .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
1119 }
1120
1121 fs::write(&save_path, genome_json)
1123 .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
1124
1125 info!("✅ Genome saved successfully to: {}", save_path.display());
1126
1127 Ok(Json(HashMap::from([
1128 (
1129 "message".to_string(),
1130 "Genome saved successfully".to_string(),
1131 ),
1132 ("file_path".to_string(), save_path.display().to_string()),
1133 ])))
1134}
1135
1136#[utoipa::path(
1138 post,
1139 path = "/v1/genome/load",
1140 tag = "genome",
1141 responses(
1142 (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
1143 )
1144)]
1145pub async fn post_load(
1146 State(state): State<ApiState>,
1147 Json(request): Json<HashMap<String, String>>,
1148) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1149 let genome_name = request
1150 .get("genome_name")
1151 .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
1152
1153 let params = feagi_services::LoadGenomeParams {
1155 json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
1156 };
1157
1158 let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
1159
1160 let mut response = HashMap::new();
1161 response.insert(
1162 "message".to_string(),
1163 serde_json::json!("Genome loaded successfully"),
1164 );
1165 response.insert(
1166 "genome_title".to_string(),
1167 serde_json::json!(genome_info.genome_title),
1168 );
1169
1170 Ok(Json(response))
1171}
1172
1173#[utoipa::path(
1175 post,
1176 path = "/v1/genome/upload",
1177 tag = "genome",
1178 responses(
1179 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
1180 )
1181)]
1182pub async fn post_upload(
1183 State(state): State<ApiState>,
1184 Json(genome_json): Json<serde_json::Value>,
1185) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1186 let json_str = serde_json::to_string(&genome_json)
1188 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1189
1190 let params = LoadGenomeParams { json_str };
1191 let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
1192
1193 let mut response = HashMap::new();
1194 response.insert("success".to_string(), serde_json::json!(true));
1195 response.insert(
1196 "message".to_string(),
1197 serde_json::json!("Genome uploaded successfully"),
1198 );
1199 response.insert(
1200 "cortical_area_count".to_string(),
1201 serde_json::json!(genome_info.cortical_area_count),
1202 );
1203 response.insert(
1204 "brain_region_count".to_string(),
1205 serde_json::json!(genome_info.brain_region_count),
1206 );
1207
1208 Ok(Json(response))
1209}
1210
1211#[utoipa::path(
1213 get,
1214 path = "/v1/genome/download",
1215 tag = "genome",
1216 responses(
1217 (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
1218 )
1219)]
1220pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
1221 info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
1222 let genome_service = state.genome_service.as_ref();
1223
1224 let genome_json_str = genome_service
1226 .save_genome(feagi_services::types::SaveGenomeParams {
1227 genome_id: None,
1228 genome_title: None,
1229 })
1230 .await
1231 .map_err(|e| {
1232 tracing::error!("Failed to export genome: {}", e);
1233 ApiError::internal(format!("Failed to export genome: {}", e))
1234 })?;
1235
1236 let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
1238 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1239
1240 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1242 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1243
1244 info!(
1245 "✅ Genome download complete, {} bytes",
1246 genome_json_str.len()
1247 );
1248 Ok(Json(genome_value))
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253 use super::*;
1254 use serde_json::json;
1255
1256 #[test]
1257 fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
1258 let genome = json!({
1259 "version": "3.0",
1260 "physiology": {
1261 "simulation_timestep": 0.025,
1262 "max_age": 10000000
1263 }
1264 });
1265
1266 let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
1267 assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
1268 assert_eq!(updated["physiology"]["max_age"], json!(10000000));
1269 }
1270
1271 #[test]
1272 fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
1273 let genome = json!({ "version": "3.0" });
1274 let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
1275 assert!(format!("{err:?}").contains("physiology"));
1276 }
1277}
1278
1279#[utoipa::path(
1281 get,
1282 path = "/v1/genome/properties",
1283 tag = "genome",
1284 responses(
1285 (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
1286 )
1287)]
1288pub async fn get_properties(
1289 State(_state): State<ApiState>,
1290) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1291 Ok(Json(HashMap::new()))
1293}
1294
1295#[utoipa::path(
1297 post,
1298 path = "/v1/genome/validate",
1299 tag = "genome",
1300 responses(
1301 (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1302 )
1303)]
1304pub async fn post_validate(
1305 State(_state): State<ApiState>,
1306 Json(_genome): Json<serde_json::Value>,
1307) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1308 let mut response = HashMap::new();
1310 response.insert("valid".to_string(), serde_json::json!(true));
1311 response.insert("errors".to_string(), serde_json::json!([]));
1312 response.insert("warnings".to_string(), serde_json::json!([]));
1313
1314 Ok(Json(response))
1315}
1316
1317#[utoipa::path(
1319 post,
1320 path = "/v1/genome/transform",
1321 tag = "genome",
1322 responses(
1323 (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1324 )
1325)]
1326pub async fn post_transform(
1327 State(_state): State<ApiState>,
1328 Json(_request): Json<HashMap<String, serde_json::Value>>,
1329) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1330 let mut response = HashMap::new();
1332 response.insert(
1333 "message".to_string(),
1334 serde_json::json!("Genome transformation not yet implemented"),
1335 );
1336
1337 Ok(Json(response))
1338}
1339
1340#[utoipa::path(
1342 post,
1343 path = "/v1/genome/clone",
1344 tag = "genome",
1345 responses(
1346 (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1347 )
1348)]
1349pub async fn post_clone(
1350 State(_state): State<ApiState>,
1351 Json(_request): Json<HashMap<String, String>>,
1352) -> ApiResult<Json<HashMap<String, String>>> {
1353 Ok(Json(HashMap::from([(
1355 "message".to_string(),
1356 "Genome cloning not yet implemented".to_string(),
1357 )])))
1358}
1359
1360#[utoipa::path(
1363 post,
1364 path = "/v1/genome/reset",
1365 tag = "genome",
1366 responses(
1367 (status = 200, description = "Genome reset", body = HashMap<String, String>),
1368 (status = 409, description = "Genome transition in progress"),
1369 (status = 500, description = "Reset failed")
1370 )
1371)]
1372pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1373 let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1374 ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1375 })?;
1376
1377 let genome_service = state.genome_service.as_ref();
1378 genome_service.reset_connectome().await.map_err(|e| {
1379 tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1380 ApiError::internal(format!("Genome reset failed: {}", e))
1381 })?;
1382
1383 info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1384 Ok(Json(HashMap::from([(
1385 "message".to_string(),
1386 "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1387 )])))
1388}
1389
1390#[utoipa::path(
1392 get,
1393 path = "/v1/genome/metadata",
1394 tag = "genome",
1395 responses(
1396 (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1397 )
1398)]
1399pub async fn get_metadata(
1400 State(state): State<ApiState>,
1401) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1402 get_properties(State(state)).await
1403}
1404
1405#[utoipa::path(
1407 post,
1408 path = "/v1/genome/merge",
1409 tag = "genome",
1410 responses(
1411 (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1412 )
1413)]
1414pub async fn post_merge(
1415 State(_state): State<ApiState>,
1416 Json(_request): Json<HashMap<String, serde_json::Value>>,
1417) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1418 let mut response = HashMap::new();
1420 response.insert(
1421 "message".to_string(),
1422 serde_json::json!("Genome merging not yet implemented"),
1423 );
1424
1425 Ok(Json(response))
1426}
1427
1428#[utoipa::path(
1430 get,
1431 path = "/v1/genome/diff",
1432 tag = "genome",
1433 params(
1434 ("genome_a" = String, Query, description = "First genome name"),
1435 ("genome_b" = String, Query, description = "Second genome name")
1436 ),
1437 responses(
1438 (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1439 )
1440)]
1441pub async fn get_diff(
1442 State(_state): State<ApiState>,
1443 Query(_params): Query<HashMap<String, String>>,
1444) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1445 let mut response = HashMap::new();
1447 response.insert("differences".to_string(), serde_json::json!([]));
1448
1449 Ok(Json(response))
1450}
1451
1452#[utoipa::path(
1454 post,
1455 path = "/v1/genome/export_format",
1456 tag = "genome",
1457 responses(
1458 (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1459 )
1460)]
1461pub async fn post_export_format(
1462 State(_state): State<ApiState>,
1463 Json(_request): Json<HashMap<String, String>>,
1464) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1465 let mut response = HashMap::new();
1467 response.insert(
1468 "message".to_string(),
1469 serde_json::json!("Format export not yet implemented"),
1470 );
1471
1472 Ok(Json(response))
1473}
1474
1475#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1478pub async fn get_amalgamation(
1479 State(state): State<ApiState>,
1480) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1481 let lock = state.amalgamation_state.read();
1482 let mut response = HashMap::new();
1483 if let Some(p) = lock.pending.as_ref() {
1484 response.insert(
1485 "pending".to_string(),
1486 amalgamation::pending_summary_to_health_json(&p.summary),
1487 );
1488 } else {
1489 response.insert("pending".to_string(), serde_json::Value::Null);
1490 }
1491 Ok(Json(response))
1492}
1493
1494#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1496pub async fn get_amalgamation_history_exact(
1497 State(state): State<ApiState>,
1498) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1499 let lock = state.amalgamation_state.read();
1500 let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1501 for entry in &lock.history {
1502 out.push(HashMap::from([
1503 (
1504 "amalgamation_id".to_string(),
1505 serde_json::json!(entry.amalgamation_id),
1506 ),
1507 (
1508 "genome_title".to_string(),
1509 serde_json::json!(entry.genome_title),
1510 ),
1511 (
1512 "circuit_size".to_string(),
1513 serde_json::json!(entry.circuit_size),
1514 ),
1515 ("status".to_string(), serde_json::json!(entry.status)),
1516 (
1517 "timestamp_ms".to_string(),
1518 serde_json::json!(entry.timestamp_ms),
1519 ),
1520 ]));
1521 }
1522 Ok(Json(out))
1523}
1524
1525#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1527pub async fn get_cortical_template(
1528 State(_state): State<ApiState>,
1529) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1530 use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1531 FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1532 };
1533 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1534 use serde_json::json;
1535
1536 let mut templates = HashMap::new();
1537
1538 let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1543 let (variant, frame, positioning) = match dt {
1544 IOCorticalAreaConfigurationFlag::Boolean => {
1545 ("Boolean", FrameChangeHandling::Absolute, None)
1546 }
1547 IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1548 IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1549 IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1550 IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1551 IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1552 ("SignedPercentage", f, Some(p))
1553 }
1554 IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1555 ("SignedPercentage2D", f, Some(p))
1556 }
1557 IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1558 ("SignedPercentage3D", f, Some(p))
1559 }
1560 IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1561 ("SignedPercentage4D", f, Some(p))
1562 }
1563 IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1564 IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1565 };
1566
1567 let frame_str = match frame {
1568 FrameChangeHandling::Absolute => "Absolute",
1569 FrameChangeHandling::Incremental => "Incremental",
1570 };
1571
1572 let positioning_str = positioning.map(|p| match p {
1573 PercentageNeuronPositioning::Linear => "Linear",
1574 PercentageNeuronPositioning::Fractional => "Fractional",
1575 });
1576
1577 json!({
1578 "variant": variant,
1579 "frame_change_handling": frame_str,
1580 "percentage_positioning": positioning_str,
1581 "config_value": dt.to_data_type_configuration_flag()
1582 })
1583 };
1584
1585 for motor_unit in MotorCorticalUnit::list_all() {
1587 let friendly_name = motor_unit.get_friendly_name();
1588 let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1589 let num_areas = motor_unit.get_number_cortical_areas();
1590 let topology = motor_unit.get_unit_default_topology();
1591
1592 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1603 use serde_json::{Map, Value};
1604 use std::collections::HashMap as StdHashMap;
1605
1606 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1607
1608 for (sub_idx, topo) in topology {
1610 subunits.insert(
1611 sub_idx.get().to_string(),
1612 json!({
1613 "relative_position": topo.relative_position,
1614 "channel_dimensions_default": topo.channel_dimensions_default,
1615 "channel_dimensions_min": topo.channel_dimensions_min,
1616 "channel_dimensions_max": topo.channel_dimensions_max,
1617 "supported_data_types": Vec::<serde_json::Value>::new(),
1618 }),
1619 );
1620 }
1621
1622 let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1624 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1625 Some(allowed) => allowed.to_vec(),
1626 None => vec![
1627 FrameChangeHandling::Absolute,
1628 FrameChangeHandling::Incremental,
1629 ],
1630 };
1631
1632 let positionings = [
1633 PercentageNeuronPositioning::Linear,
1634 PercentageNeuronPositioning::Fractional,
1635 ];
1636
1637 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1638 StdHashMap::new();
1639
1640 for frame in frames {
1641 for positioning in positionings {
1642 let mut map: Map<String, Value> = Map::new();
1643 map.insert(
1644 "frame_change_handling".to_string(),
1645 serde_json::to_value(frame).unwrap_or(Value::Null),
1646 );
1647 map.insert(
1648 "percentage_neuron_positioning".to_string(),
1649 serde_json::to_value(positioning).unwrap_or(Value::Null),
1650 );
1651
1652 let cortical_ids = motor_unit
1654 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1655 CorticalUnitIndex::from(0u8),
1656 map,
1657 );
1658
1659 if let Ok(ids) = cortical_ids {
1660 for (i, id) in ids.into_iter().enumerate() {
1661 if let Ok(flag) = id.extract_io_data_flag() {
1662 let dt_json = data_type_to_json(flag);
1663 let subunit_key = i.to_string();
1664
1665 let dedup_key = format!(
1666 "{}|{}|{}",
1667 dt_json
1668 .get("variant")
1669 .and_then(|v| v.as_str())
1670 .unwrap_or(""),
1671 dt_json
1672 .get("frame_change_handling")
1673 .and_then(|v| v.as_str())
1674 .unwrap_or(""),
1675 dt_json
1676 .get("percentage_positioning")
1677 .and_then(|v| v.as_str())
1678 .unwrap_or("")
1679 );
1680
1681 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1682 if !seen.insert(dedup_key) {
1683 continue;
1684 }
1685
1686 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1687 if let Some(arr) = subunit_obj
1688 .get_mut("supported_data_types")
1689 .and_then(|v| v.as_array_mut())
1690 {
1691 arr.push(dt_json);
1692 }
1693 }
1694 }
1695 }
1696 }
1697 }
1698 }
1699
1700 templates.insert(
1701 format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1702 json!({
1703 "type": "motor",
1704 "friendly_name": friendly_name,
1705 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1706 "number_of_cortical_areas": num_areas,
1707 "subunits": subunits,
1708 "description": format!("Motor output: {}", friendly_name)
1709 }),
1710 );
1711 }
1712
1713 for sensory_unit in SensoryCorticalUnit::list_all() {
1715 let friendly_name = sensory_unit.get_friendly_name();
1716 let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1717 let num_areas = sensory_unit.get_number_cortical_areas();
1718 let topology = sensory_unit.get_unit_default_topology();
1719
1720 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1721 use serde_json::{Map, Value};
1722 use std::collections::HashMap as StdHashMap;
1723
1724 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1725
1726 for (sub_idx, topo) in topology {
1727 subunits.insert(
1728 sub_idx.get().to_string(),
1729 json!({
1730 "relative_position": topo.relative_position,
1731 "channel_dimensions_default": topo.channel_dimensions_default,
1732 "channel_dimensions_min": topo.channel_dimensions_min,
1733 "channel_dimensions_max": topo.channel_dimensions_max,
1734 "supported_data_types": Vec::<serde_json::Value>::new(),
1735 }),
1736 );
1737 }
1738
1739 let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1740 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1741 Some(allowed) => allowed.to_vec(),
1742 None => vec![
1743 FrameChangeHandling::Absolute,
1744 FrameChangeHandling::Incremental,
1745 ],
1746 };
1747
1748 let positionings = [
1749 PercentageNeuronPositioning::Linear,
1750 PercentageNeuronPositioning::Fractional,
1751 ];
1752
1753 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1754 StdHashMap::new();
1755
1756 for frame in frames {
1757 for positioning in positionings {
1758 let mut map: Map<String, Value> = Map::new();
1759 map.insert(
1760 "frame_change_handling".to_string(),
1761 serde_json::to_value(frame).unwrap_or(Value::Null),
1762 );
1763 map.insert(
1764 "percentage_neuron_positioning".to_string(),
1765 serde_json::to_value(positioning).unwrap_or(Value::Null),
1766 );
1767
1768 let cortical_ids = sensory_unit
1769 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1770 CorticalUnitIndex::from(0u8),
1771 map,
1772 );
1773
1774 if let Ok(ids) = cortical_ids {
1775 for (i, id) in ids.into_iter().enumerate() {
1776 if let Ok(flag) = id.extract_io_data_flag() {
1777 let dt_json = data_type_to_json(flag);
1778 let subunit_key = i.to_string();
1779
1780 let dedup_key = format!(
1781 "{}|{}|{}",
1782 dt_json
1783 .get("variant")
1784 .and_then(|v| v.as_str())
1785 .unwrap_or(""),
1786 dt_json
1787 .get("frame_change_handling")
1788 .and_then(|v| v.as_str())
1789 .unwrap_or(""),
1790 dt_json
1791 .get("percentage_positioning")
1792 .and_then(|v| v.as_str())
1793 .unwrap_or("")
1794 );
1795
1796 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1797 if !seen.insert(dedup_key) {
1798 continue;
1799 }
1800
1801 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1802 if let Some(arr) = subunit_obj
1803 .get_mut("supported_data_types")
1804 .and_then(|v| v.as_array_mut())
1805 {
1806 arr.push(dt_json);
1807 }
1808 }
1809 }
1810 }
1811 }
1812 }
1813 }
1814
1815 templates.insert(
1816 format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1817 json!({
1818 "type": "sensory",
1819 "friendly_name": friendly_name,
1820 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1821 "number_of_cortical_areas": num_areas,
1822 "subunits": subunits,
1823 "description": format!("Sensory input: {}", friendly_name)
1824 }),
1825 );
1826 }
1827
1828 Ok(Json(templates))
1829}
1830
1831#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1833pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1834 Ok(Json(vec![
1835 "barebones".to_string(),
1836 "essential".to_string(),
1837 "test".to_string(),
1838 "vision".to_string(),
1839 ]))
1840}
1841
1842#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
1844pub async fn get_download_region(
1845 State(state): State<ApiState>,
1846 Query(params): Query<HashMap<String, String>>,
1847) -> ApiResult<Json<serde_json::Value>> {
1848 let region_id = params
1849 .get("region_id")
1850 .cloned()
1851 .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
1852 let json_str = state
1853 .genome_service
1854 .export_region_genome(region_id)
1855 .await
1856 .map_err(ApiError::from)?;
1857 let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
1858 ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
1859 })?;
1860 Ok(Json(value))
1861}
1862
1863#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
1865pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
1866 Ok(Json(0))
1867}
1868
1869#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
1871pub async fn post_amalgamation_by_filename(
1872 State(state): State<ApiState>,
1873 Json(req): Json<HashMap<String, String>>,
1874) -> ApiResult<Json<HashMap<String, String>>> {
1875 let file_name = req
1879 .get("file_name")
1880 .or_else(|| req.get("filename"))
1881 .or_else(|| req.get("genome_file_name"))
1882 .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
1883
1884 let genome_json = match file_name.as_str() {
1885 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
1886 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
1887 "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
1888 "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
1889 other => {
1890 return Err(ApiError::invalid_input(format!(
1891 "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
1892 other
1893 )))
1894 }
1895 };
1896
1897 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
1898
1899 Ok(Json(HashMap::from([
1900 ("message".to_string(), "Amalgamation queued".to_string()),
1901 ("amalgamation_id".to_string(), amalgamation_id),
1902 ])))
1903}
1904
1905#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
1907pub async fn post_amalgamation_by_payload(
1908 State(state): State<ApiState>,
1909 Json(req): Json<serde_json::Value>,
1910) -> ApiResult<Json<HashMap<String, String>>> {
1911 let json_str = serde_json::to_string(&req)
1912 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1913 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1914
1915 Ok(Json(HashMap::from([
1916 ("message".to_string(), "Amalgamation queued".to_string()),
1917 ("amalgamation_id".to_string(), amalgamation_id),
1918 ])))
1919}
1920
1921#[cfg(feature = "http")]
1923#[utoipa::path(
1924 post,
1925 path = "/v1/genome/amalgamation_by_upload",
1926 tag = "genome",
1927 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1928 responses(
1929 (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
1930 (status = 400, description = "Invalid request"),
1931 (status = 500, description = "Internal server error")
1932 )
1933)]
1934pub async fn post_amalgamation_by_upload(
1935 State(state): State<ApiState>,
1936 mut multipart: Multipart,
1937) -> ApiResult<Json<HashMap<String, String>>> {
1938 let mut genome_json: Option<String> = None;
1939
1940 while let Some(field) = multipart
1941 .next_field()
1942 .await
1943 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1944 {
1945 if field.name() == Some("file") {
1946 let bytes = field.bytes().await.map_err(|e| {
1947 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1948 })?;
1949
1950 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1951 ApiError::invalid_input(format!(
1952 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1953 e
1954 ))
1955 })?;
1956 genome_json = Some(json_str.to_string());
1957 break;
1958 }
1959 }
1960
1961 let json_str =
1962 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1963 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1964
1965 Ok(Json(HashMap::from([
1966 ("message".to_string(), "Amalgamation queued".to_string()),
1967 ("amalgamation_id".to_string(), amalgamation_id),
1968 ])))
1969}
1970
1971#[cfg(feature = "http")]
1973#[utoipa::path(
1974 post,
1975 path = "/v1/genome/append-file",
1976 tag = "genome",
1977 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1978 responses(
1979 (status = 200, description = "Append processed", body = HashMap<String, String>)
1980 )
1981)]
1982pub async fn post_append_file(
1983 State(_state): State<ApiState>,
1984 mut _multipart: Multipart,
1985) -> ApiResult<Json<HashMap<String, String>>> {
1986 Ok(Json(HashMap::from([(
1987 "message".to_string(),
1988 "Not yet implemented".to_string(),
1989 )])))
1990}
1991
1992#[cfg(feature = "http")]
1994#[utoipa::path(
1995 post,
1996 path = "/v1/genome/upload/file",
1997 tag = "genome",
1998 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1999 responses(
2000 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
2001 (status = 400, description = "Invalid request"),
2002 (status = 500, description = "Internal server error")
2003 )
2004)]
2005pub async fn post_upload_file(
2006 State(state): State<ApiState>,
2007 mut multipart: Multipart,
2008) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
2009 let mut genome_json: Option<String> = None;
2010
2011 while let Some(field) = multipart
2012 .next_field()
2013 .await
2014 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2015 {
2016 if field.name() == Some("file") {
2017 let bytes = field.bytes().await.map_err(|e| {
2018 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2019 })?;
2020
2021 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2022 ApiError::invalid_input(format!(
2023 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2024 e
2025 ))
2026 })?;
2027 genome_json = Some(json_str.to_string());
2028 break;
2029 }
2030 }
2031
2032 let json_str =
2033 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2034
2035 let genome_info =
2036 load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
2037 .await?;
2038
2039 let mut response = HashMap::new();
2040 response.insert("success".to_string(), serde_json::json!(true));
2041 response.insert(
2042 "message".to_string(),
2043 serde_json::json!("Genome uploaded successfully"),
2044 );
2045 response.insert(
2046 "cortical_area_count".to_string(),
2047 serde_json::json!(genome_info.cortical_area_count),
2048 );
2049 response.insert(
2050 "brain_region_count".to_string(),
2051 serde_json::json!(genome_info.brain_region_count),
2052 );
2053
2054 Ok(Json(response))
2055}
2056
2057#[cfg(feature = "http")]
2059#[utoipa::path(
2060 post,
2061 path = "/v1/genome/upload/file/edit",
2062 tag = "genome",
2063 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2064 responses(
2065 (status = 200, description = "Upload processed", body = HashMap<String, String>)
2066 )
2067)]
2068pub async fn post_upload_file_edit(
2069 State(_state): State<ApiState>,
2070 mut _multipart: Multipart,
2071) -> ApiResult<Json<HashMap<String, String>>> {
2072 Ok(Json(HashMap::from([(
2073 "message".to_string(),
2074 "Not yet implemented".to_string(),
2075 )])))
2076}
2077
2078#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
2080pub async fn post_upload_string(
2081 State(_state): State<ApiState>,
2082 Json(_req): Json<String>,
2083) -> ApiResult<Json<HashMap<String, String>>> {
2084 Ok(Json(HashMap::from([(
2085 "message".to_string(),
2086 "Not yet implemented".to_string(),
2087 )])))
2088}