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
101struct GenomeTransitionStateLifecycle;
106
107impl GenomeTransitionStateLifecycle {
108 fn enter() -> Self {
109 #[cfg(feature = "services")]
110 {
111 feagi_state_manager::StateManager::instance()
112 .read()
113 .set_genome_state(feagi_state_manager::GenomeState::Loading);
114 }
115 Self
116 }
117
118 fn succeed(self) {
119 #[cfg(feature = "services")]
120 {
121 feagi_state_manager::StateManager::instance()
122 .read()
123 .set_genome_state(feagi_state_manager::GenomeState::Loaded);
124 }
125 std::mem::forget(self);
126 }
127}
128
129impl Drop for GenomeTransitionStateLifecycle {
130 fn drop(&mut self) {
131 #[cfg(feature = "services")]
132 {
133 feagi_state_manager::StateManager::instance()
134 .read()
135 .set_genome_state(feagi_state_manager::GenomeState::Error);
136 }
137 }
138}
139
140async fn load_genome_with_priority(
148 state: &ApiState,
149 params: LoadGenomeParams,
150 source: &str,
151) -> ApiResult<GenomeInfo> {
152 let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
153 ApiError::conflict(
154 "Another genome transition is already in progress; wait for it to finish",
155 )
156 })?;
157 state
158 .genome_transition_in_progress
159 .store(true, Ordering::SeqCst);
160 let _guard = GenomeTransitionFlagGuard {
161 in_progress: Arc::clone(&state.genome_transition_in_progress),
162 };
163 let genome_sm_lifecycle = GenomeTransitionStateLifecycle::enter();
164
165 tracing::info!(
166 target: "feagi-api",
167 "🛑 Entering prioritized genome transition from {}",
168 source
169 );
170
171 let runtime_service = state.runtime_service.as_ref();
172 #[cfg(feature = "feagi-agent")]
173 if let Some(handler) = &state.agent_handler {
174 let deregistered_ids = {
175 let mut guard = handler.lock().unwrap();
176 guard.force_deregister_all_agents("forced by genome transition")
177 };
178 for agent_id in &deregistered_ids {
179 runtime_service.unregister_motor_subscriptions(agent_id);
180 runtime_service.unregister_visualization_subscriptions(agent_id);
181 }
182 tracing::info!(
183 target: "feagi-api",
184 "🔌 Forced deregistration for {} agents before genome transition",
185 deregistered_ids.len()
186 );
187 }
188 runtime_service.clear_all_motor_subscriptions();
190 runtime_service.clear_all_visualization_subscriptions();
191
192 let runtime_status = runtime_service
193 .get_status()
194 .await
195 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
196 let runtime_was_running = runtime_status.is_running;
197
198 if runtime_was_running {
199 tracing::info!(
200 target: "feagi-api",
201 "Stopping burst engine before prioritized genome transition"
202 );
203 runtime_service.stop().await.map_err(|e| {
204 ApiError::internal(format!(
205 "Failed to stop burst engine before genome transition: {}",
206 e
207 ))
208 })?;
209 }
210
211 let genome_service = state.genome_service.as_ref();
212 let load_result = genome_service.load_genome(params).await;
213 let genome_info = match load_result {
214 Ok(info) => info,
215 Err(e) => {
216 if runtime_was_running {
217 if let Err(restart_err) = runtime_service.start().await {
218 tracing::warn!(
219 target: "feagi-api",
220 "Failed to restore runtime after failed genome load (source={}): {}",
221 source,
222 restart_err
223 );
224 }
225 }
226 return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
227 }
228 };
229
230 let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
231 runtime_service
232 .set_frequency(burst_frequency_hz)
233 .await
234 .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
235
236 if runtime_was_running {
237 runtime_service.start().await.map_err(|e| {
238 ApiError::internal(format!(
239 "Failed to restart burst engine after genome transition: {}",
240 e
241 ))
242 })?;
243 }
244
245 tracing::info!(
246 target: "feagi-api",
247 "✅ Prioritized genome transition completed from {}",
248 source
249 );
250
251 #[cfg(feature = "feagi-agent")]
254 if let Some(handler) = &state.agent_handler {
255 let device_regs_list: Vec<serde_json::Value> = {
256 let guard = handler.lock().unwrap();
257 guard
258 .get_all_registered_agents()
259 .iter()
260 .filter_map(|(sid, _)| guard.get_device_registrations_by_agent(*sid).cloned())
261 .collect()
262 };
263 for device_regs in device_regs_list {
264 crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations(
265 state,
266 &device_regs,
267 )
268 .await;
269 }
270 }
271
272 genome_sm_lifecycle.succeed();
273 Ok(genome_info)
274}
275
276fn inject_simulation_timestep_into_genome(
282 mut genome: serde_json::Value,
283 simulation_timestep_s: f64,
284) -> Result<serde_json::Value, ApiError> {
285 let physiology = genome
286 .get_mut("physiology")
287 .and_then(|v| v.as_object_mut())
288 .ok_or_else(|| {
289 ApiError::internal(
290 "Genome JSON missing required object key 'physiology' while saving".to_string(),
291 )
292 })?;
293
294 physiology.insert(
295 "simulation_timestep".to_string(),
296 serde_json::Value::from(simulation_timestep_s),
297 );
298 Ok(genome)
299}
300
301async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
302 let runtime_service = state.runtime_service.as_ref();
303 let status = runtime_service
304 .get_status()
305 .await
306 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
307
308 Ok(if status.frequency_hz > 0.0 {
310 1.0 / status.frequency_hz
311 } else {
312 0.0
313 })
314}
315
316#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
318pub async fn get_file_name(
319 State(_state): State<ApiState>,
320) -> ApiResult<Json<HashMap<String, String>>> {
321 Ok(Json(HashMap::from([(
323 "genome_file_name".to_string(),
324 "".to_string(),
325 )])))
326}
327
328#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
330pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
331 Ok(Json(vec![]))
333}
334
335#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
337pub async fn post_amalgamation_destination(
338 State(state): State<ApiState>,
339 Query(params): Query<HashMap<String, String>>,
340 Json(req): Json<HashMap<String, serde_json::Value>>,
341) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
342 let amalgamation_id = params
350 .get("amalgamation_id")
351 .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
352 .to_string();
353
354 let origin_x: i32 = params
355 .get("circuit_origin_x")
356 .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
357 .parse()
358 .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
359 let origin_y: i32 = params
360 .get("circuit_origin_y")
361 .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
362 .parse()
363 .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
364 let origin_z: i32 = params
365 .get("circuit_origin_z")
366 .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
367 .parse()
368 .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
369
370 let rewire_mode = params
371 .get("rewire_mode")
372 .cloned()
373 .unwrap_or_else(|| "rewire_all".to_string());
374
375 let parent_region_id = req
376 .get("brain_region_id")
377 .and_then(|v| v.as_str())
378 .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
379 .to_string();
380
381 let pending = {
383 let lock = state.amalgamation_state.write();
384 let Some(p) = lock.pending.as_ref() else {
385 return Err(ApiError::invalid_input("No amalgamation is pending"));
386 };
387 if p.summary.amalgamation_id != amalgamation_id {
388 return Err(ApiError::invalid_input(format!(
389 "Pending amalgamation_id mismatch: expected {}, got {}",
390 p.summary.amalgamation_id, amalgamation_id
391 )));
392 }
393 p.clone()
394 };
395
396 let connectome_service = state.connectome_service.as_ref();
400
401 let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
402 region_properties.insert(
403 "coordinate_3d".to_string(),
404 serde_json::json!([origin_x, origin_y, origin_z]),
405 );
406 region_properties.insert(
407 "amalgamation_id".to_string(),
408 serde_json::json!(pending.summary.amalgamation_id),
409 );
410 region_properties.insert(
411 "circuit_size".to_string(),
412 serde_json::json!(pending.summary.circuit_size),
413 );
414 region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
415
416 connectome_service
417 .create_brain_region(feagi_services::types::CreateBrainRegionParams {
418 region_id: amalgamation_id.clone(),
419 name: pending.summary.genome_title.clone(),
420 region_type: "Custom".to_string(),
421 parent_id: Some(parent_region_id.clone()),
422 properties: Some(region_properties),
423 })
424 .await
425 .map_err(|e| {
426 ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
427 })?;
428
429 let mut imported_genome = feagi_evolutionary::load_genome_from_json(&pending.genome_json)
439 .map_err(|e| {
440 ApiError::invalid_input(format!(
441 "Pending genome payload can no longer be parsed as a genome: {}",
442 e
443 ))
444 })?;
445
446 let host_cortical_ids: std::collections::HashSet<String> = connectome_service
447 .get_cortical_area_ids()
448 .await
449 .map_err(|e| ApiError::internal(format!("Failed to list cortical area IDs: {}", e)))?
450 .into_iter()
451 .collect();
452
453 let remapped_guest_custom_memory_ids =
454 feagi_evolutionary::remap_guest_custom_memory_cortical_ids_for_amalgamation(
455 &mut imported_genome,
456 &host_cortical_ids,
457 )
458 .map_err(|e| {
459 ApiError::internal(format!(
460 "Amalgamation guest cortical ID remapping failed: {}",
461 e
462 ))
463 })?;
464 let guest_custom_memory_id_remap_count = remapped_guest_custom_memory_ids.len();
465
466 if guest_custom_memory_id_remap_count > 0 {
467 tracing::info!(
468 target: "feagi-api",
469 "🧬 [AMALGAMATION] Remapped {} guest Custom/Memory cortical IDs before import",
470 guest_custom_memory_id_remap_count
471 );
472 }
473
474 let genome_service = state.genome_service.as_ref();
475 let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
476 let mut skipped_existing: Vec<String> = Vec::new();
477
478 let root_region_id = connectome_service
480 .get_root_region_id()
481 .await
482 .map_err(|e| ApiError::internal(format!("Failed to get root region ID: {}", e)))?;
483
484 for area in imported_genome.cortical_areas.values() {
485 let cortical_id = area.cortical_id.as_base_64();
486 let exists = connectome_service
487 .cortical_area_exists(&cortical_id)
488 .await
489 .map_err(|e| {
490 ApiError::internal(format!(
491 "Failed to check existing cortical area {}: {}",
492 cortical_id, e
493 ))
494 })?;
495 if exists {
496 skipped_existing.push(cortical_id);
497 continue;
498 }
499
500 let mut props = area.properties.clone();
501
502 props.remove("cortical_mapping_dst");
504
505 let area_type = area.cortical_id.as_cortical_type().map_err(|e| {
508 ApiError::internal(format!(
509 "Failed to get cortical area type for {}: {}",
510 cortical_id, e
511 ))
512 })?;
513
514 let target_parent_region_id = match area_type {
515 feagi_structures::genomic::cortical_area::CorticalAreaType::BrainInput(_)
516 | feagi_structures::genomic::cortical_area::CorticalAreaType::BrainOutput(_) => {
517 match root_region_id.as_ref() {
519 Some(root_id) => {
520 tracing::info!(
521 target: "feagi-api",
522 "🧬 [AMALGAMATION] IPU/OPU area {} will be placed in root region {}",
523 cortical_id,
524 root_id
525 );
526 root_id.clone()
527 }
528 None => {
529 tracing::warn!(
530 target: "feagi-api",
531 "🧬 [AMALGAMATION] No root region found for IPU/OPU area {}, using amalgamation region",
532 cortical_id
533 );
534 amalgamation_id.clone()
535 }
536 }
537 }
538 _ => {
539 amalgamation_id.clone()
541 }
542 };
543
544 props.insert(
545 "parent_region_id".to_string(),
546 serde_json::json!(target_parent_region_id),
547 );
548 props.insert(
549 "amalgamation_source".to_string(),
550 serde_json::json!("amalgamation_by_payload"),
551 );
552
553 to_create.push(feagi_services::types::CreateCorticalAreaParams {
554 cortical_id,
555 name: area.name.clone(),
556 dimensions: (
557 area.dimensions.width as usize,
558 area.dimensions.height as usize,
559 area.dimensions.depth as usize,
560 ),
561 position: (
562 origin_x.saturating_add(area.position.x),
563 origin_y.saturating_add(area.position.y),
564 origin_z.saturating_add(area.position.z),
565 ),
566 area_type: "Custom".to_string(),
567 visible: Some(true),
568 sub_group: None,
569 neurons_per_voxel: area
570 .properties
571 .get("neurons_per_voxel")
572 .and_then(|v| v.as_u64())
573 .map(|v| v as u32),
574 postsynaptic_current: area
575 .properties
576 .get("postsynaptic_current")
577 .and_then(|v| v.as_f64()),
578 plasticity_constant: area
579 .properties
580 .get("plasticity_constant")
581 .and_then(|v| v.as_f64()),
582 degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
583 psp_uniform_distribution: area
584 .properties
585 .get("psp_uniform_distribution")
586 .and_then(|v| v.as_bool()),
587 firing_threshold_increment: None,
588 firing_threshold_limit: area
589 .properties
590 .get("firing_threshold_limit")
591 .and_then(|v| v.as_f64()),
592 consecutive_fire_count: area
593 .properties
594 .get("consecutive_fire_limit")
595 .and_then(|v| v.as_u64())
596 .map(|v| v as u32),
597 snooze_period: area
598 .properties
599 .get("snooze_period")
600 .and_then(|v| v.as_u64())
601 .map(|v| v as u32),
602 refractory_period: area
603 .properties
604 .get("refractory_period")
605 .and_then(|v| v.as_u64())
606 .map(|v| v as u32),
607 leak_coefficient: area
608 .properties
609 .get("leak_coefficient")
610 .and_then(|v| v.as_f64()),
611 leak_variability: area
612 .properties
613 .get("leak_variability")
614 .and_then(|v| v.as_f64()),
615 burst_engine_active: area
616 .properties
617 .get("burst_engine_active")
618 .and_then(|v| v.as_bool()),
619 properties: Some(props),
620 });
621 }
622
623 let imported_new_area_count = to_create.len();
624 if !to_create.is_empty() {
625 genome_service
626 .create_cortical_areas(to_create)
627 .await
628 .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
629 }
630
631 let imported_area_ids: std::collections::HashSet<String> = imported_genome
636 .cortical_areas
637 .keys()
638 .map(|id| id.as_base_64())
639 .filter(|id| !skipped_existing.contains(id))
640 .collect();
641
642 let mut required_morphologies: std::collections::HashSet<String> =
643 std::collections::HashSet::new();
644
645 for area in imported_genome.cortical_areas.values() {
647 if !imported_area_ids.contains(&area.cortical_id.as_base_64()) {
648 continue;
649 }
650
651 let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
652 continue;
653 };
654 let Some(dst_map) = cortical_mapping_dst.as_object() else {
655 continue;
656 };
657
658 for mapping_data in dst_map.values() {
659 let Some(mapping_array) = mapping_data.as_array() else {
660 continue;
661 };
662
663 for rule in mapping_array {
664 let morphology_id = if let Some(obj) = rule.as_object() {
666 obj.get("morphology_id").and_then(|v| v.as_str())
667 } else if let Some(arr) = rule.as_array() {
668 arr.first().and_then(|v| v.as_str())
669 } else {
670 None
671 };
672
673 if let Some(morph_id) = morphology_id {
674 required_morphologies.insert(morph_id.to_string());
675 }
676 }
677 }
678 }
679
680 let mut imported_morphology_count = 0;
682 let mut skipped_morphology_count = 0;
683
684 for morphology_id in &required_morphologies {
685 let morphologies = connectome_service.get_morphologies().await.map_err(|e| {
687 ApiError::internal(format!("Failed to get existing morphologies: {}", e))
688 })?;
689
690 if morphologies.contains_key(morphology_id) {
691 skipped_morphology_count += 1;
692 continue;
693 }
694
695 let Some(morphology) = imported_genome.morphologies.get(morphology_id) else {
697 tracing::warn!(
698 target: "feagi-api",
699 "🧬 [AMALGAMATION] Morphology '{}' referenced in mappings but not found in imported genome",
700 morphology_id
701 );
702 continue;
703 };
704
705 match connectome_service
707 .create_morphology(morphology_id.clone(), morphology.clone())
708 .await
709 {
710 Ok(_) => {
711 tracing::debug!(
712 target: "feagi-api",
713 "🧬 [AMALGAMATION] Imported morphology '{}'",
714 morphology_id
715 );
716 imported_morphology_count += 1;
717 }
718 Err(e) => {
719 tracing::warn!(
720 target: "feagi-api",
721 "🧬 [AMALGAMATION] Failed to import morphology '{}': {}",
722 morphology_id,
723 e
724 );
725 }
726 }
727 }
728
729 if imported_morphology_count > 0 {
730 tracing::info!(
731 target: "feagi-api",
732 "🧬 [AMALGAMATION] Imported {} morphologies (skipped {} existing)",
733 imported_morphology_count,
734 skipped_morphology_count
735 );
736 }
737
738 let mut imported_mapping_count = 0;
746 let mut skipped_mapping_count = 0;
747
748 for area in imported_genome.cortical_areas.values() {
749 let src_area_id = area.cortical_id.as_base_64();
750
751 if !imported_area_ids.contains(&src_area_id) {
753 continue;
754 }
755
756 let Some(cortical_mapping_dst) = area.properties.get("cortical_mapping_dst") else {
758 continue;
759 };
760 let Some(dst_map) = cortical_mapping_dst.as_object() else {
761 continue;
762 };
763
764 for (dst_area_id, mapping_data) in dst_map {
766 let dst_exists = connectome_service
768 .cortical_area_exists(dst_area_id)
769 .await
770 .unwrap_or(false);
771
772 if !dst_exists {
773 skipped_mapping_count += 1;
775 continue;
776 }
777
778 let Some(mapping_array) = mapping_data.as_array() else {
779 tracing::warn!(
780 target: "feagi-api",
781 "🧬 [AMALGAMATION] Invalid mapping data from {} to {}: not an array",
782 src_area_id,
783 dst_area_id
784 );
785 continue;
786 };
787
788 match connectome_service
790 .update_cortical_mapping(
791 src_area_id.clone(),
792 dst_area_id.clone(),
793 mapping_array.clone(),
794 )
795 .await
796 {
797 Ok(synapse_count) => {
798 tracing::debug!(
799 target: "feagi-api",
800 "🧬 [AMALGAMATION] Imported mapping {} -> {} ({} synapses)",
801 src_area_id,
802 dst_area_id,
803 synapse_count
804 );
805 imported_mapping_count += 1;
806 }
807 Err(e) => {
808 tracing::warn!(
809 target: "feagi-api",
810 "🧬 [AMALGAMATION] Failed to import mapping {} -> {}: {}",
811 src_area_id,
812 dst_area_id,
813 e
814 );
815 skipped_mapping_count += 1;
816 }
817 }
818 }
819 }
820
821 if imported_mapping_count > 0 {
822 tracing::info!(
823 target: "feagi-api",
824 "🧬 [AMALGAMATION] Successfully imported {} cortical mappings (skipped {} external/missing mappings)",
825 imported_mapping_count,
826 skipped_mapping_count
827 );
828 } else if skipped_mapping_count > 0 {
829 tracing::warn!(
830 target: "feagi-api",
831 "🧬 [AMALGAMATION] No internal mappings imported! Skipped {} mappings (all external or missing)",
832 skipped_mapping_count
833 );
834 } else {
835 let mut nonempty_dst_on_new = 0_usize;
838 let mut empty_dst_on_new = 0_usize;
839 let mut missing_dst_on_new = 0_usize;
840 for area in imported_genome.cortical_areas.values() {
841 let id = area.cortical_id.as_base_64();
842 if !imported_area_ids.contains(&id) {
843 continue;
844 }
845 match area.properties.get("cortical_mapping_dst") {
846 None => missing_dst_on_new += 1,
847 Some(v) => {
848 if v.as_object().map(|o| o.is_empty()).unwrap_or(true) {
849 empty_dst_on_new += 1;
850 } else {
851 nonempty_dst_on_new += 1;
852 }
853 }
854 }
855 }
856 tracing::info!(
857 target: "feagi-api",
858 "🧬 [AMALGAMATION] No cortical mappings to import from guest (new areas: nonempty_dst={} empty_dst={} missing_dst={}; imported_new_areas={}; skipped_existing_areas={}). \
859 Areas skipped due to host ID collision do not replay guest wiring. \
860 For synapses on newly created areas, guest blueprint needs non-empty dstmap (cortical_mapping_dst) on those sources.",
861 nonempty_dst_on_new,
862 empty_dst_on_new,
863 missing_dst_on_new,
864 imported_new_area_count,
865 skipped_existing.len()
866 );
867 }
868
869 {
880 let state_manager = feagi_state_manager::StateManager::instance();
881 let state_manager = state_manager.read();
882
883 state_manager
885 .set_brain_regions_hash(state_manager.get_brain_regions_hash().wrapping_add(1));
886 state_manager
887 .set_cortical_areas_hash(state_manager.get_cortical_areas_hash().wrapping_add(1));
888 state_manager
889 .set_brain_geometry_hash(state_manager.get_brain_geometry_hash().wrapping_add(1));
890 if imported_morphology_count > 0 {
891 state_manager
892 .set_morphologies_hash(state_manager.get_morphologies_hash().wrapping_add(1));
893 }
894 if imported_mapping_count > 0 {
895 state_manager.set_cortical_mappings_hash(
896 state_manager.get_cortical_mappings_hash().wrapping_add(1),
897 );
898 }
899
900 tracing::info!(
901 target: "feagi-api",
902 "🧬 [AMALGAMATION] Invalidated health_check hashes for BV cache refresh"
903 );
904 }
905
906 {
908 let mut lock = state.amalgamation_state.write();
909 let now_ms = std::time::SystemTime::now()
910 .duration_since(std::time::UNIX_EPOCH)
911 .map(|d| d.as_millis() as i64)
912 .unwrap_or(0);
913 lock.history.push(amalgamation::AmalgamationHistoryEntry {
914 amalgamation_id: pending.summary.amalgamation_id.clone(),
915 genome_title: pending.summary.genome_title.clone(),
916 circuit_size: pending.summary.circuit_size,
917 status: "confirmed".to_string(),
918 timestamp_ms: now_ms,
919 });
920 lock.pending = None;
921 }
922
923 let guest_cortical_area_count = imported_genome.cortical_areas.len();
924
925 let regions = state
927 .connectome_service
928 .list_brain_regions()
929 .await
930 .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
931
932 let post_merge_brain_region_count = regions.len();
933 let post_merge_cortical_area_total = state
934 .connectome_service
935 .get_cortical_area_ids()
936 .await
937 .map(|ids| ids.len())
938 .map_err(|e| {
939 ApiError::internal(format!(
940 "Failed to count cortical areas after amalgamation: {}",
941 e
942 ))
943 })?;
944
945 let mut brain_regions: Vec<serde_json::Value> = Vec::new();
946 for region in regions {
947 let coordinate_3d = region
949 .properties
950 .get("coordinate_3d")
951 .cloned()
952 .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
953 let coordinate_2d = region
954 .properties
955 .get("coordinate_2d")
956 .cloned()
957 .unwrap_or_else(|| serde_json::json!([0, 0]));
958
959 brain_regions.push(serde_json::json!({
960 "region_id": region.region_id,
961 "title": region.name,
962 "description": "",
963 "parent_region_id": region.parent_id,
964 "coordinate_2d": coordinate_2d,
965 "coordinate_3d": coordinate_3d,
966 "areas": region.cortical_areas,
967 "regions": region.child_regions,
968 "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
969 "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
970 "designated_inputs": region.properties.get("designated_inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
971 "designated_outputs": region.properties.get("designated_outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
972 }));
973 }
974
975 tracing::info!(
976 target: "feagi-api",
977 "🧬 [AMALGAMATION] Complete genome_title='{}' amalgamation_id={} \
978 guest_custom_memory_ids_remapped={} guest_cortical_areas={} new_cortical_areas_created={} cortical_areas_skipped_host_collision={} \
979 morphologies_imported={} morphologies_skipped_already_present={} \
980 cortical_mapping_rules_imported={} cortical_mapping_rules_skipped_unresolved_dst={} \
981 post_merge_brain_regions={} post_merge_cortical_areas_total={}",
982 pending.summary.genome_title,
983 pending.summary.amalgamation_id,
984 guest_custom_memory_id_remap_count,
985 guest_cortical_area_count,
986 imported_new_area_count,
987 skipped_existing.len(),
988 imported_morphology_count,
989 skipped_morphology_count,
990 imported_mapping_count,
991 skipped_mapping_count,
992 post_merge_brain_region_count,
993 post_merge_cortical_area_total,
994 );
995
996 Ok(Json(HashMap::from([
997 (
998 "message".to_string(),
999 serde_json::Value::String("Amalgamation confirmed".to_string()),
1000 ),
1001 (
1002 "brain_regions".to_string(),
1003 serde_json::Value::Array(brain_regions),
1004 ),
1005 (
1006 "skipped_existing_areas".to_string(),
1007 serde_json::json!(skipped_existing),
1008 ),
1009 (
1010 "imported_new_area_count".to_string(),
1011 serde_json::json!(imported_new_area_count),
1012 ),
1013 (
1014 "guest_cortical_area_count".to_string(),
1015 serde_json::json!(guest_cortical_area_count),
1016 ),
1017 (
1018 "guest_custom_memory_id_remap_count".to_string(),
1019 serde_json::json!(guest_custom_memory_id_remap_count),
1020 ),
1021 (
1022 "imported_cortical_mappings".to_string(),
1023 serde_json::json!(imported_mapping_count),
1024 ),
1025 (
1026 "skipped_cortical_mappings".to_string(),
1027 serde_json::json!(skipped_mapping_count),
1028 ),
1029 (
1030 "imported_morphology_count".to_string(),
1031 serde_json::json!(imported_morphology_count),
1032 ),
1033 (
1034 "skipped_morphology_existing_count".to_string(),
1035 serde_json::json!(skipped_morphology_count),
1036 ),
1037 (
1038 "post_merge_brain_region_count".to_string(),
1039 serde_json::json!(post_merge_brain_region_count),
1040 ),
1041 (
1042 "post_merge_cortical_area_total".to_string(),
1043 serde_json::json!(post_merge_cortical_area_total),
1044 ),
1045 ])))
1046}
1047
1048#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
1050pub async fn delete_amalgamation_cancellation(
1051 State(state): State<ApiState>,
1052) -> ApiResult<Json<HashMap<String, String>>> {
1053 let mut lock = state.amalgamation_state.write();
1054 if let Some(pending) = lock.pending.take() {
1055 let now_ms = std::time::SystemTime::now()
1056 .duration_since(std::time::UNIX_EPOCH)
1057 .map(|d| d.as_millis() as i64)
1058 .unwrap_or(0);
1059 lock.history.push(amalgamation::AmalgamationHistoryEntry {
1060 amalgamation_id: pending.summary.amalgamation_id,
1061 genome_title: pending.summary.genome_title,
1062 circuit_size: pending.summary.circuit_size,
1063 status: "cancelled".to_string(),
1064 timestamp_ms: now_ms,
1065 });
1066
1067 tracing::info!(
1068 target: "feagi-api",
1069 "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
1070 lock.history
1071 .last()
1072 .map(|e| e.amalgamation_id.clone())
1073 .unwrap_or_else(|| "<unknown>".to_string())
1074 );
1075 }
1076 Ok(Json(HashMap::from([(
1077 "message".to_string(),
1078 "Amalgamation cancelled".to_string(),
1079 )])))
1080}
1081
1082#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
1084pub async fn post_genome_append(
1085 State(_state): State<ApiState>,
1086 Json(_req): Json<HashMap<String, serde_json::Value>>,
1087) -> ApiResult<Json<HashMap<String, String>>> {
1088 Err(ApiError::internal("Not yet implemented"))
1089}
1090
1091#[utoipa::path(
1093 post,
1094 path = "/v1/genome/upload/barebones",
1095 responses(
1096 (status = 200, description = "Barebones genome loaded successfully"),
1097 (status = 500, description = "Failed to load genome")
1098 ),
1099 tag = "genome"
1100)]
1101pub async fn post_upload_barebones_genome(
1102 State(state): State<ApiState>,
1103) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1104 tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
1105 let result = load_default_genome(state, "barebones").await;
1106 match &result {
1107 Ok(_) => {
1108 tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
1109 }
1110 Err(e) => {
1111 tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
1112 }
1113 }
1114 result
1115}
1116
1117#[utoipa::path(
1119 post,
1120 path = "/v1/genome/upload/essential",
1121 responses(
1122 (status = 200, description = "Essential genome loaded successfully"),
1123 (status = 500, description = "Failed to load genome")
1124 ),
1125 tag = "genome"
1126)]
1127pub async fn post_upload_essential_genome(
1128 State(state): State<ApiState>,
1129) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1130 load_default_genome(state, "essential").await
1131}
1132
1133async fn load_default_genome(
1135 state: ApiState,
1136 genome_name: &str,
1137) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1138 tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
1139 tracing::debug!(target: "feagi-api", " State components available: genome_service=true, runtime_service=true");
1140 let genome_json = match genome_name {
1142 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
1143 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
1144 "test" => feagi_evolutionary::TEST_GENOME_JSON,
1145 "vision" => feagi_evolutionary::VISION_GENOME_JSON,
1146 _ => {
1147 return Err(ApiError::invalid_input(format!(
1148 "Unknown genome name '{}'. Available: barebones, essential, test, vision",
1149 genome_name
1150 )))
1151 }
1152 };
1153
1154 tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
1155 genome_name, genome_json.len());
1156
1157 let params = LoadGenomeParams {
1158 json_str: genome_json.to_string(),
1159 };
1160
1161 tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
1162 let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
1163
1164 tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
1165 genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
1166
1167 let mut response = HashMap::new();
1169 response.insert("success".to_string(), serde_json::Value::Bool(true));
1170 response.insert(
1171 "message".to_string(),
1172 serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
1173 );
1174 response.insert(
1175 "cortical_area_count".to_string(),
1176 serde_json::Value::Number(genome_info.cortical_area_count.into()),
1177 );
1178 response.insert(
1179 "brain_region_count".to_string(),
1180 serde_json::Value::Number(genome_info.brain_region_count.into()),
1181 );
1182 response.insert(
1183 "genome_id".to_string(),
1184 serde_json::Value::String(genome_info.genome_id),
1185 );
1186 response.insert(
1187 "genome_title".to_string(),
1188 serde_json::Value::String(genome_info.genome_title),
1189 );
1190
1191 Ok(Json(response))
1192}
1193
1194#[utoipa::path(
1196 get,
1197 path = "/v1/genome/name",
1198 tag = "genome",
1199 responses(
1200 (status = 200, description = "Genome name", body = String)
1201 )
1202)]
1203pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
1204 Ok(Json("default_genome".to_string()))
1207}
1208
1209#[utoipa::path(
1211 get,
1212 path = "/v1/genome/timestamp",
1213 tag = "genome",
1214 responses(
1215 (status = 200, description = "Genome timestamp", body = i64)
1216 )
1217)]
1218pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
1219 Ok(Json(0))
1221}
1222
1223#[utoipa::path(
1225 post,
1226 path = "/v1/genome/save",
1227 tag = "genome",
1228 responses(
1229 (status = 200, description = "Genome saved", body = HashMap<String, String>)
1230 )
1231)]
1232pub async fn post_save(
1233 State(state): State<ApiState>,
1234 Json(request): Json<HashMap<String, String>>,
1235) -> ApiResult<Json<HashMap<String, String>>> {
1236 use std::fs;
1237 use std::path::Path;
1238
1239 info!("Saving genome to file");
1240
1241 let genome_id = request.get("genome_id").cloned();
1243 let genome_title = request.get("genome_title").cloned();
1244 let file_path = request.get("file_path").cloned();
1245
1246 let params = feagi_services::SaveGenomeParams {
1248 genome_id,
1249 genome_title,
1250 };
1251
1252 let genome_service = state.genome_service.as_ref();
1254 let genome_json = genome_service
1255 .save_genome(params)
1256 .await
1257 .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
1258
1259 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1261 let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
1262 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1263 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1264 let genome_json = serde_json::to_string_pretty(&genome_value)
1265 .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
1266
1267 let save_path = if let Some(path) = file_path {
1269 std::path::PathBuf::from(path)
1270 } else {
1271 let timestamp = std::time::SystemTime::now()
1273 .duration_since(std::time::UNIX_EPOCH)
1274 .unwrap()
1275 .as_secs();
1276 std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
1277 };
1278
1279 if let Some(parent) = Path::new(&save_path).parent() {
1281 fs::create_dir_all(parent)
1282 .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
1283 }
1284
1285 fs::write(&save_path, genome_json)
1287 .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
1288
1289 info!("✅ Genome saved successfully to: {}", save_path.display());
1290
1291 Ok(Json(HashMap::from([
1292 (
1293 "message".to_string(),
1294 "Genome saved successfully".to_string(),
1295 ),
1296 ("file_path".to_string(), save_path.display().to_string()),
1297 ])))
1298}
1299
1300#[utoipa::path(
1302 post,
1303 path = "/v1/genome/load",
1304 tag = "genome",
1305 responses(
1306 (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
1307 )
1308)]
1309pub async fn post_load(
1310 State(state): State<ApiState>,
1311 Json(request): Json<HashMap<String, String>>,
1312) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1313 let genome_name = request
1314 .get("genome_name")
1315 .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
1316
1317 let params = feagi_services::LoadGenomeParams {
1319 json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
1320 };
1321
1322 let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
1323
1324 let mut response = HashMap::new();
1325 response.insert(
1326 "message".to_string(),
1327 serde_json::json!("Genome loaded successfully"),
1328 );
1329 response.insert(
1330 "genome_title".to_string(),
1331 serde_json::json!(genome_info.genome_title),
1332 );
1333
1334 Ok(Json(response))
1335}
1336
1337#[utoipa::path(
1339 post,
1340 path = "/v1/genome/upload",
1341 tag = "genome",
1342 responses(
1343 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
1344 )
1345)]
1346pub async fn post_upload(
1347 State(state): State<ApiState>,
1348 Json(genome_json): Json<serde_json::Value>,
1349) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1350 let json_str = serde_json::to_string(&genome_json)
1352 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1353
1354 let params = LoadGenomeParams { json_str };
1355 let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
1356
1357 let mut response = HashMap::new();
1358 response.insert("success".to_string(), serde_json::json!(true));
1359 response.insert(
1360 "message".to_string(),
1361 serde_json::json!("Genome uploaded successfully"),
1362 );
1363 response.insert(
1364 "cortical_area_count".to_string(),
1365 serde_json::json!(genome_info.cortical_area_count),
1366 );
1367 response.insert(
1368 "brain_region_count".to_string(),
1369 serde_json::json!(genome_info.brain_region_count),
1370 );
1371
1372 Ok(Json(response))
1373}
1374
1375#[utoipa::path(
1377 get,
1378 path = "/v1/genome/download",
1379 tag = "genome",
1380 responses(
1381 (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
1382 )
1383)]
1384pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
1385 info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
1386 let genome_service = state.genome_service.as_ref();
1387
1388 let genome_json_str = genome_service
1390 .save_genome(feagi_services::types::SaveGenomeParams {
1391 genome_id: None,
1392 genome_title: None,
1393 })
1394 .await
1395 .map_err(|e| {
1396 tracing::error!("Failed to export genome: {}", e);
1397 ApiError::internal(format!("Failed to export genome: {}", e))
1398 })?;
1399
1400 let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
1402 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1403
1404 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1406 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1407
1408 info!(
1409 "✅ Genome download complete, {} bytes",
1410 genome_json_str.len()
1411 );
1412 Ok(Json(genome_value))
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417 use super::*;
1418 use serde_json::json;
1419
1420 #[test]
1421 fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
1422 let genome = json!({
1423 "version": "3.0",
1424 "physiology": {
1425 "simulation_timestep": 0.025,
1426 "max_age": 10000000
1427 }
1428 });
1429
1430 let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
1431 assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
1432 assert_eq!(updated["physiology"]["max_age"], json!(10000000));
1433 }
1434
1435 #[test]
1436 fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
1437 let genome = json!({ "version": "3.0" });
1438 let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
1439 assert!(format!("{err:?}").contains("physiology"));
1440 }
1441}
1442
1443#[utoipa::path(
1445 get,
1446 path = "/v1/genome/properties",
1447 tag = "genome",
1448 responses(
1449 (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
1450 )
1451)]
1452pub async fn get_properties(
1453 State(_state): State<ApiState>,
1454) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1455 Ok(Json(HashMap::new()))
1457}
1458
1459#[utoipa::path(
1461 post,
1462 path = "/v1/genome/validate",
1463 tag = "genome",
1464 responses(
1465 (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1466 )
1467)]
1468pub async fn post_validate(
1469 State(_state): State<ApiState>,
1470 Json(_genome): Json<serde_json::Value>,
1471) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1472 let mut response = HashMap::new();
1474 response.insert("valid".to_string(), serde_json::json!(true));
1475 response.insert("errors".to_string(), serde_json::json!([]));
1476 response.insert("warnings".to_string(), serde_json::json!([]));
1477
1478 Ok(Json(response))
1479}
1480
1481#[utoipa::path(
1483 post,
1484 path = "/v1/genome/transform",
1485 tag = "genome",
1486 responses(
1487 (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1488 )
1489)]
1490pub async fn post_transform(
1491 State(_state): State<ApiState>,
1492 Json(_request): Json<HashMap<String, serde_json::Value>>,
1493) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1494 let mut response = HashMap::new();
1496 response.insert(
1497 "message".to_string(),
1498 serde_json::json!("Genome transformation not yet implemented"),
1499 );
1500
1501 Ok(Json(response))
1502}
1503
1504#[utoipa::path(
1506 post,
1507 path = "/v1/genome/clone",
1508 tag = "genome",
1509 responses(
1510 (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1511 )
1512)]
1513pub async fn post_clone(
1514 State(_state): State<ApiState>,
1515 Json(_request): Json<HashMap<String, String>>,
1516) -> ApiResult<Json<HashMap<String, String>>> {
1517 Ok(Json(HashMap::from([(
1519 "message".to_string(),
1520 "Genome cloning not yet implemented".to_string(),
1521 )])))
1522}
1523
1524#[utoipa::path(
1527 post,
1528 path = "/v1/genome/reset",
1529 tag = "genome",
1530 responses(
1531 (status = 200, description = "Genome reset", body = HashMap<String, String>),
1532 (status = 409, description = "Genome transition in progress"),
1533 (status = 500, description = "Reset failed")
1534 )
1535)]
1536pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1537 let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1538 ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1539 })?;
1540
1541 let genome_service = state.genome_service.as_ref();
1542 genome_service.reset_connectome().await.map_err(|e| {
1543 tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1544 ApiError::internal(format!("Genome reset failed: {}", e))
1545 })?;
1546
1547 info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1548 Ok(Json(HashMap::from([(
1549 "message".to_string(),
1550 "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1551 )])))
1552}
1553
1554#[utoipa::path(
1556 get,
1557 path = "/v1/genome/metadata",
1558 tag = "genome",
1559 responses(
1560 (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1561 )
1562)]
1563pub async fn get_metadata(
1564 State(state): State<ApiState>,
1565) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1566 get_properties(State(state)).await
1567}
1568
1569#[utoipa::path(
1571 post,
1572 path = "/v1/genome/merge",
1573 tag = "genome",
1574 responses(
1575 (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1576 )
1577)]
1578pub async fn post_merge(
1579 State(_state): State<ApiState>,
1580 Json(_request): Json<HashMap<String, serde_json::Value>>,
1581) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1582 let mut response = HashMap::new();
1584 response.insert(
1585 "message".to_string(),
1586 serde_json::json!("Genome merging not yet implemented"),
1587 );
1588
1589 Ok(Json(response))
1590}
1591
1592#[utoipa::path(
1594 get,
1595 path = "/v1/genome/diff",
1596 tag = "genome",
1597 params(
1598 ("genome_a" = String, Query, description = "First genome name"),
1599 ("genome_b" = String, Query, description = "Second genome name")
1600 ),
1601 responses(
1602 (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1603 )
1604)]
1605pub async fn get_diff(
1606 State(_state): State<ApiState>,
1607 Query(_params): Query<HashMap<String, String>>,
1608) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1609 let mut response = HashMap::new();
1611 response.insert("differences".to_string(), serde_json::json!([]));
1612
1613 Ok(Json(response))
1614}
1615
1616#[utoipa::path(
1618 post,
1619 path = "/v1/genome/export_format",
1620 tag = "genome",
1621 responses(
1622 (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1623 )
1624)]
1625pub async fn post_export_format(
1626 State(_state): State<ApiState>,
1627 Json(_request): Json<HashMap<String, String>>,
1628) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1629 let mut response = HashMap::new();
1631 response.insert(
1632 "message".to_string(),
1633 serde_json::json!("Format export not yet implemented"),
1634 );
1635
1636 Ok(Json(response))
1637}
1638
1639#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1642pub async fn get_amalgamation(
1643 State(state): State<ApiState>,
1644) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1645 let lock = state.amalgamation_state.read();
1646 let mut response = HashMap::new();
1647 if let Some(p) = lock.pending.as_ref() {
1648 response.insert(
1649 "pending".to_string(),
1650 amalgamation::pending_summary_to_health_json(&p.summary),
1651 );
1652 } else {
1653 response.insert("pending".to_string(), serde_json::Value::Null);
1654 }
1655 Ok(Json(response))
1656}
1657
1658#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1660pub async fn get_amalgamation_history_exact(
1661 State(state): State<ApiState>,
1662) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1663 let lock = state.amalgamation_state.read();
1664 let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1665 for entry in &lock.history {
1666 out.push(HashMap::from([
1667 (
1668 "amalgamation_id".to_string(),
1669 serde_json::json!(entry.amalgamation_id),
1670 ),
1671 (
1672 "genome_title".to_string(),
1673 serde_json::json!(entry.genome_title),
1674 ),
1675 (
1676 "circuit_size".to_string(),
1677 serde_json::json!(entry.circuit_size),
1678 ),
1679 ("status".to_string(), serde_json::json!(entry.status)),
1680 (
1681 "timestamp_ms".to_string(),
1682 serde_json::json!(entry.timestamp_ms),
1683 ),
1684 ]));
1685 }
1686 Ok(Json(out))
1687}
1688
1689#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1691pub async fn get_cortical_template(
1692 State(_state): State<ApiState>,
1693) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1694 use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1695 FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1696 };
1697 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1698 use serde_json::json;
1699
1700 let mut templates = HashMap::new();
1701
1702 let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1707 let (variant, frame, positioning) = match dt {
1708 IOCorticalAreaConfigurationFlag::Boolean => {
1709 ("Boolean", FrameChangeHandling::Absolute, None)
1710 }
1711 IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1712 IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1713 IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1714 IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1715 IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1716 ("SignedPercentage", f, Some(p))
1717 }
1718 IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1719 ("SignedPercentage2D", f, Some(p))
1720 }
1721 IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1722 ("SignedPercentage3D", f, Some(p))
1723 }
1724 IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1725 ("SignedPercentage4D", f, Some(p))
1726 }
1727 IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1728 IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1729 };
1730
1731 let frame_str = match frame {
1732 FrameChangeHandling::Absolute => "Absolute",
1733 FrameChangeHandling::Incremental => "Incremental",
1734 };
1735
1736 let positioning_str = positioning.map(|p| match p {
1737 PercentageNeuronPositioning::Linear => "Linear",
1738 PercentageNeuronPositioning::Fractional => "Fractional",
1739 });
1740
1741 json!({
1742 "variant": variant,
1743 "frame_change_handling": frame_str,
1744 "percentage_positioning": positioning_str,
1745 "config_value": dt.to_data_type_configuration_flag()
1746 })
1747 };
1748
1749 for motor_unit in MotorCorticalUnit::list_all() {
1751 let friendly_name = motor_unit.get_friendly_name();
1752 let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1753 let num_areas = motor_unit.get_number_cortical_areas();
1754 let topology = motor_unit.get_unit_default_topology();
1755
1756 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1767 use serde_json::{Map, Value};
1768 use std::collections::HashMap as StdHashMap;
1769
1770 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1771
1772 for (sub_idx, topo) in topology {
1774 subunits.insert(
1775 sub_idx.get().to_string(),
1776 json!({
1777 "relative_position": topo.relative_position,
1778 "channel_dimensions_default": topo.channel_dimensions_default,
1779 "channel_dimensions_min": topo.channel_dimensions_min,
1780 "channel_dimensions_max": topo.channel_dimensions_max,
1781 "supported_data_types": Vec::<serde_json::Value>::new(),
1782 }),
1783 );
1784 }
1785
1786 let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1788 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1789 Some(allowed) => allowed.to_vec(),
1790 None => vec![
1791 FrameChangeHandling::Absolute,
1792 FrameChangeHandling::Incremental,
1793 ],
1794 };
1795
1796 let positionings = [
1797 PercentageNeuronPositioning::Linear,
1798 PercentageNeuronPositioning::Fractional,
1799 ];
1800
1801 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1802 StdHashMap::new();
1803
1804 for frame in frames {
1805 for positioning in positionings {
1806 let mut map: Map<String, Value> = Map::new();
1807 map.insert(
1808 "frame_change_handling".to_string(),
1809 serde_json::to_value(frame).unwrap_or(Value::Null),
1810 );
1811 map.insert(
1812 "percentage_neuron_positioning".to_string(),
1813 serde_json::to_value(positioning).unwrap_or(Value::Null),
1814 );
1815
1816 let cortical_ids = motor_unit
1818 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1819 CorticalUnitIndex::from(0u8),
1820 map,
1821 );
1822
1823 if let Ok(ids) = cortical_ids {
1824 for (i, id) in ids.into_iter().enumerate() {
1825 if let Ok(flag) = id.extract_io_data_flag() {
1826 let dt_json = data_type_to_json(flag);
1827 let subunit_key = i.to_string();
1828
1829 let dedup_key = format!(
1830 "{}|{}|{}",
1831 dt_json
1832 .get("variant")
1833 .and_then(|v| v.as_str())
1834 .unwrap_or(""),
1835 dt_json
1836 .get("frame_change_handling")
1837 .and_then(|v| v.as_str())
1838 .unwrap_or(""),
1839 dt_json
1840 .get("percentage_positioning")
1841 .and_then(|v| v.as_str())
1842 .unwrap_or("")
1843 );
1844
1845 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1846 if !seen.insert(dedup_key) {
1847 continue;
1848 }
1849
1850 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1851 if let Some(arr) = subunit_obj
1852 .get_mut("supported_data_types")
1853 .and_then(|v| v.as_array_mut())
1854 {
1855 arr.push(dt_json);
1856 }
1857 }
1858 }
1859 }
1860 }
1861 }
1862 }
1863
1864 templates.insert(
1865 format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1866 json!({
1867 "type": "motor",
1868 "friendly_name": friendly_name,
1869 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1870 "number_of_cortical_areas": num_areas,
1871 "subunits": subunits,
1872 "description": format!("Motor output: {}", friendly_name)
1873 }),
1874 );
1875 }
1876
1877 for sensory_unit in SensoryCorticalUnit::list_all() {
1879 let friendly_name = sensory_unit.get_friendly_name();
1880 let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1881 let num_areas = sensory_unit.get_number_cortical_areas();
1882 let topology = sensory_unit.get_unit_default_topology();
1883
1884 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1885 use serde_json::{Map, Value};
1886 use std::collections::HashMap as StdHashMap;
1887
1888 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1889
1890 for (sub_idx, topo) in topology {
1891 subunits.insert(
1892 sub_idx.get().to_string(),
1893 json!({
1894 "relative_position": topo.relative_position,
1895 "channel_dimensions_default": topo.channel_dimensions_default,
1896 "channel_dimensions_min": topo.channel_dimensions_min,
1897 "channel_dimensions_max": topo.channel_dimensions_max,
1898 "supported_data_types": Vec::<serde_json::Value>::new(),
1899 }),
1900 );
1901 }
1902
1903 let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1904 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1905 Some(allowed) => allowed.to_vec(),
1906 None => vec![
1907 FrameChangeHandling::Absolute,
1908 FrameChangeHandling::Incremental,
1909 ],
1910 };
1911
1912 let positionings = [
1913 PercentageNeuronPositioning::Linear,
1914 PercentageNeuronPositioning::Fractional,
1915 ];
1916
1917 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1918 StdHashMap::new();
1919
1920 for frame in frames {
1921 for positioning in positionings {
1922 let mut map: Map<String, Value> = Map::new();
1923 map.insert(
1924 "frame_change_handling".to_string(),
1925 serde_json::to_value(frame).unwrap_or(Value::Null),
1926 );
1927 map.insert(
1928 "percentage_neuron_positioning".to_string(),
1929 serde_json::to_value(positioning).unwrap_or(Value::Null),
1930 );
1931
1932 let cortical_ids = sensory_unit
1933 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1934 CorticalUnitIndex::from(0u8),
1935 map,
1936 );
1937
1938 if let Ok(ids) = cortical_ids {
1939 for (i, id) in ids.into_iter().enumerate() {
1940 if let Ok(flag) = id.extract_io_data_flag() {
1941 let dt_json = data_type_to_json(flag);
1942 let subunit_key = i.to_string();
1943
1944 let dedup_key = format!(
1945 "{}|{}|{}",
1946 dt_json
1947 .get("variant")
1948 .and_then(|v| v.as_str())
1949 .unwrap_or(""),
1950 dt_json
1951 .get("frame_change_handling")
1952 .and_then(|v| v.as_str())
1953 .unwrap_or(""),
1954 dt_json
1955 .get("percentage_positioning")
1956 .and_then(|v| v.as_str())
1957 .unwrap_or("")
1958 );
1959
1960 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1961 if !seen.insert(dedup_key) {
1962 continue;
1963 }
1964
1965 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1966 if let Some(arr) = subunit_obj
1967 .get_mut("supported_data_types")
1968 .and_then(|v| v.as_array_mut())
1969 {
1970 arr.push(dt_json);
1971 }
1972 }
1973 }
1974 }
1975 }
1976 }
1977 }
1978
1979 templates.insert(
1980 format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1981 json!({
1982 "type": "sensory",
1983 "friendly_name": friendly_name,
1984 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1985 "number_of_cortical_areas": num_areas,
1986 "subunits": subunits,
1987 "description": format!("Sensory input: {}", friendly_name)
1988 }),
1989 );
1990 }
1991
1992 Ok(Json(templates))
1993}
1994
1995#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1997pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1998 Ok(Json(vec![
1999 "barebones".to_string(),
2000 "essential".to_string(),
2001 "test".to_string(),
2002 "vision".to_string(),
2003 ]))
2004}
2005
2006#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
2008pub async fn get_download_region(
2009 State(state): State<ApiState>,
2010 Query(params): Query<HashMap<String, String>>,
2011) -> ApiResult<Json<serde_json::Value>> {
2012 let region_id = params
2013 .get("region_id")
2014 .cloned()
2015 .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
2016 let json_str = state
2017 .genome_service
2018 .export_region_genome(region_id)
2019 .await
2020 .map_err(ApiError::from)?;
2021 let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
2022 ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
2023 })?;
2024 Ok(Json(value))
2025}
2026
2027#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
2029pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
2030 Ok(Json(0))
2031}
2032
2033#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
2035pub async fn post_amalgamation_by_filename(
2036 State(state): State<ApiState>,
2037 Json(req): Json<HashMap<String, String>>,
2038) -> ApiResult<Json<HashMap<String, String>>> {
2039 let file_name = req
2043 .get("file_name")
2044 .or_else(|| req.get("filename"))
2045 .or_else(|| req.get("genome_file_name"))
2046 .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
2047
2048 let genome_json = match file_name.as_str() {
2049 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
2050 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
2051 "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
2052 "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
2053 other => {
2054 return Err(ApiError::invalid_input(format!(
2055 "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
2056 other
2057 )))
2058 }
2059 };
2060
2061 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
2062
2063 Ok(Json(HashMap::from([
2064 ("message".to_string(), "Amalgamation queued".to_string()),
2065 ("amalgamation_id".to_string(), amalgamation_id),
2066 ])))
2067}
2068
2069#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
2071pub async fn post_amalgamation_by_payload(
2072 State(state): State<ApiState>,
2073 Json(req): Json<serde_json::Value>,
2074) -> ApiResult<Json<HashMap<String, String>>> {
2075 let json_str = serde_json::to_string(&req)
2076 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
2077 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2078
2079 Ok(Json(HashMap::from([
2080 ("message".to_string(), "Amalgamation queued".to_string()),
2081 ("amalgamation_id".to_string(), amalgamation_id),
2082 ])))
2083}
2084
2085#[cfg(feature = "http")]
2087#[utoipa::path(
2088 post,
2089 path = "/v1/genome/amalgamation_by_upload",
2090 tag = "genome",
2091 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2092 responses(
2093 (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
2094 (status = 400, description = "Invalid request"),
2095 (status = 500, description = "Internal server error")
2096 )
2097)]
2098pub async fn post_amalgamation_by_upload(
2099 State(state): State<ApiState>,
2100 mut multipart: Multipart,
2101) -> ApiResult<Json<HashMap<String, String>>> {
2102 let mut genome_json: Option<String> = None;
2103
2104 while let Some(field) = multipart
2105 .next_field()
2106 .await
2107 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2108 {
2109 if field.name() == Some("file") {
2110 let bytes = field.bytes().await.map_err(|e| {
2111 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2112 })?;
2113
2114 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2115 ApiError::invalid_input(format!(
2116 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2117 e
2118 ))
2119 })?;
2120 genome_json = Some(json_str.to_string());
2121 break;
2122 }
2123 }
2124
2125 let json_str =
2126 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2127 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2128
2129 Ok(Json(HashMap::from([
2130 ("message".to_string(), "Amalgamation queued".to_string()),
2131 ("amalgamation_id".to_string(), amalgamation_id),
2132 ])))
2133}
2134
2135#[cfg(feature = "http")]
2137#[utoipa::path(
2138 post,
2139 path = "/v1/genome/append-file",
2140 tag = "genome",
2141 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2142 responses(
2143 (status = 200, description = "Append processed", body = HashMap<String, String>)
2144 )
2145)]
2146pub async fn post_append_file(
2147 State(_state): State<ApiState>,
2148 mut _multipart: Multipart,
2149) -> ApiResult<Json<HashMap<String, String>>> {
2150 Ok(Json(HashMap::from([(
2151 "message".to_string(),
2152 "Not yet implemented".to_string(),
2153 )])))
2154}
2155
2156#[cfg(feature = "http")]
2158#[utoipa::path(
2159 post,
2160 path = "/v1/genome/upload/file",
2161 tag = "genome",
2162 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2163 responses(
2164 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
2165 (status = 400, description = "Invalid request"),
2166 (status = 500, description = "Internal server error")
2167 )
2168)]
2169pub async fn post_upload_file(
2170 State(state): State<ApiState>,
2171 mut multipart: Multipart,
2172) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
2173 let mut genome_json: Option<String> = None;
2174
2175 while let Some(field) = multipart
2176 .next_field()
2177 .await
2178 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2179 {
2180 if field.name() == Some("file") {
2181 let bytes = field.bytes().await.map_err(|e| {
2182 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2183 })?;
2184
2185 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2186 ApiError::invalid_input(format!(
2187 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2188 e
2189 ))
2190 })?;
2191 genome_json = Some(json_str.to_string());
2192 break;
2193 }
2194 }
2195
2196 let json_str =
2197 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2198
2199 let genome_info =
2200 load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
2201 .await?;
2202
2203 let mut response = HashMap::new();
2204 response.insert("success".to_string(), serde_json::json!(true));
2205 response.insert(
2206 "message".to_string(),
2207 serde_json::json!("Genome uploaded successfully"),
2208 );
2209 response.insert(
2210 "cortical_area_count".to_string(),
2211 serde_json::json!(genome_info.cortical_area_count),
2212 );
2213 response.insert(
2214 "brain_region_count".to_string(),
2215 serde_json::json!(genome_info.brain_region_count),
2216 );
2217
2218 Ok(Json(response))
2219}
2220
2221#[cfg(feature = "http")]
2223#[utoipa::path(
2224 post,
2225 path = "/v1/genome/upload/file/edit",
2226 tag = "genome",
2227 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2228 responses(
2229 (status = 200, description = "Upload processed", body = HashMap<String, String>)
2230 )
2231)]
2232pub async fn post_upload_file_edit(
2233 State(_state): State<ApiState>,
2234 mut _multipart: Multipart,
2235) -> ApiResult<Json<HashMap<String, String>>> {
2236 Ok(Json(HashMap::from([(
2237 "message".to_string(),
2238 "Not yet implemented".to_string(),
2239 )])))
2240}
2241
2242#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
2244pub async fn post_upload_string(
2245 State(_state): State<ApiState>,
2246 Json(_req): Json<String>,
2247) -> ApiResult<Json<HashMap<String, String>>> {
2248 Ok(Json(HashMap::from([(
2249 "message".to_string(),
2250 "Not yet implemented".to_string(),
2251 )])))
2252}