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 .keys()
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 state
1277 .filesystem_data_root
1278 .join("cache")
1279 .join(".genome")
1280 .join(format!("saved_genome_{}.json", timestamp))
1281 };
1282
1283 if let Some(parent) = Path::new(&save_path).parent() {
1285 fs::create_dir_all(parent)
1286 .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
1287 }
1288
1289 fs::write(&save_path, genome_json)
1291 .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
1292
1293 info!("✅ Genome saved successfully to: {}", save_path.display());
1294
1295 Ok(Json(HashMap::from([
1296 (
1297 "message".to_string(),
1298 "Genome saved successfully".to_string(),
1299 ),
1300 ("file_path".to_string(), save_path.display().to_string()),
1301 ])))
1302}
1303
1304#[utoipa::path(
1306 post,
1307 path = "/v1/genome/load",
1308 tag = "genome",
1309 responses(
1310 (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
1311 )
1312)]
1313pub async fn post_load(
1314 State(state): State<ApiState>,
1315 Json(request): Json<HashMap<String, String>>,
1316) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1317 let genome_name = request
1318 .get("genome_name")
1319 .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
1320
1321 let params = feagi_services::LoadGenomeParams {
1323 json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
1324 };
1325
1326 let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
1327
1328 let mut response = HashMap::new();
1329 response.insert(
1330 "message".to_string(),
1331 serde_json::json!("Genome loaded successfully"),
1332 );
1333 response.insert(
1334 "genome_title".to_string(),
1335 serde_json::json!(genome_info.genome_title),
1336 );
1337
1338 Ok(Json(response))
1339}
1340
1341#[utoipa::path(
1343 post,
1344 path = "/v1/genome/upload",
1345 tag = "genome",
1346 responses(
1347 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
1348 )
1349)]
1350pub async fn post_upload(
1351 State(state): State<ApiState>,
1352 Json(genome_json): Json<serde_json::Value>,
1353) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1354 let json_str = serde_json::to_string(&genome_json)
1356 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1357
1358 let params = LoadGenomeParams { json_str };
1359 let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
1360
1361 let mut response = HashMap::new();
1362 response.insert("success".to_string(), serde_json::json!(true));
1363 response.insert(
1364 "message".to_string(),
1365 serde_json::json!("Genome uploaded successfully"),
1366 );
1367 response.insert(
1368 "cortical_area_count".to_string(),
1369 serde_json::json!(genome_info.cortical_area_count),
1370 );
1371 response.insert(
1372 "brain_region_count".to_string(),
1373 serde_json::json!(genome_info.brain_region_count),
1374 );
1375
1376 Ok(Json(response))
1377}
1378
1379#[utoipa::path(
1381 get,
1382 path = "/v1/genome/download",
1383 tag = "genome",
1384 responses(
1385 (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
1386 )
1387)]
1388pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
1389 info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
1390 let genome_service = state.genome_service.as_ref();
1391
1392 let genome_json_str = genome_service
1394 .save_genome(feagi_services::types::SaveGenomeParams {
1395 genome_id: None,
1396 genome_title: None,
1397 })
1398 .await
1399 .map_err(|e| {
1400 tracing::error!("Failed to export genome: {}", e);
1401 ApiError::internal(format!("Failed to export genome: {}", e))
1402 })?;
1403
1404 let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
1406 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
1407
1408 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
1410 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
1411
1412 info!(
1413 "✅ Genome download complete, {} bytes",
1414 genome_json_str.len()
1415 );
1416 Ok(Json(genome_value))
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::*;
1422 use serde_json::json;
1423
1424 #[test]
1425 fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
1426 let genome = json!({
1427 "version": "3.0",
1428 "physiology": {
1429 "simulation_timestep": 0.025,
1430 "max_age": 10000000
1431 }
1432 });
1433
1434 let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
1435 assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
1436 assert_eq!(updated["physiology"]["max_age"], json!(10000000));
1437 }
1438
1439 #[test]
1440 fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
1441 let genome = json!({ "version": "3.0" });
1442 let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
1443 assert!(format!("{err:?}").contains("physiology"));
1444 }
1445}
1446
1447#[utoipa::path(
1449 get,
1450 path = "/v1/genome/properties",
1451 tag = "genome",
1452 responses(
1453 (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
1454 )
1455)]
1456pub async fn get_properties(
1457 State(_state): State<ApiState>,
1458) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1459 Ok(Json(HashMap::new()))
1461}
1462
1463#[utoipa::path(
1465 post,
1466 path = "/v1/genome/validate",
1467 tag = "genome",
1468 responses(
1469 (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1470 )
1471)]
1472pub async fn post_validate(
1473 State(_state): State<ApiState>,
1474 Json(_genome): Json<serde_json::Value>,
1475) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1476 let mut response = HashMap::new();
1478 response.insert("valid".to_string(), serde_json::json!(true));
1479 response.insert("errors".to_string(), serde_json::json!([]));
1480 response.insert("warnings".to_string(), serde_json::json!([]));
1481
1482 Ok(Json(response))
1483}
1484
1485#[utoipa::path(
1487 post,
1488 path = "/v1/genome/transform",
1489 tag = "genome",
1490 responses(
1491 (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1492 )
1493)]
1494pub async fn post_transform(
1495 State(_state): State<ApiState>,
1496 Json(_request): Json<HashMap<String, serde_json::Value>>,
1497) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1498 let mut response = HashMap::new();
1500 response.insert(
1501 "message".to_string(),
1502 serde_json::json!("Genome transformation not yet implemented"),
1503 );
1504
1505 Ok(Json(response))
1506}
1507
1508#[utoipa::path(
1510 post,
1511 path = "/v1/genome/clone",
1512 tag = "genome",
1513 responses(
1514 (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1515 )
1516)]
1517pub async fn post_clone(
1518 State(_state): State<ApiState>,
1519 Json(_request): Json<HashMap<String, String>>,
1520) -> ApiResult<Json<HashMap<String, String>>> {
1521 Ok(Json(HashMap::from([(
1523 "message".to_string(),
1524 "Genome cloning not yet implemented".to_string(),
1525 )])))
1526}
1527
1528#[utoipa::path(
1531 post,
1532 path = "/v1/genome/reset",
1533 tag = "genome",
1534 responses(
1535 (status = 200, description = "Genome reset", body = HashMap<String, String>),
1536 (status = 409, description = "Genome transition in progress"),
1537 (status = 500, description = "Reset failed")
1538 )
1539)]
1540pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1541 let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1542 ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1543 })?;
1544
1545 let genome_service = state.genome_service.as_ref();
1546 genome_service.reset_connectome().await.map_err(|e| {
1547 tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1548 ApiError::internal(format!("Genome reset failed: {}", e))
1549 })?;
1550
1551 info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1552 Ok(Json(HashMap::from([(
1553 "message".to_string(),
1554 "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1555 )])))
1556}
1557
1558#[utoipa::path(
1560 get,
1561 path = "/v1/genome/metadata",
1562 tag = "genome",
1563 responses(
1564 (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1565 )
1566)]
1567pub async fn get_metadata(
1568 State(state): State<ApiState>,
1569) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1570 get_properties(State(state)).await
1571}
1572
1573#[utoipa::path(
1575 post,
1576 path = "/v1/genome/merge",
1577 tag = "genome",
1578 responses(
1579 (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1580 )
1581)]
1582pub async fn post_merge(
1583 State(_state): State<ApiState>,
1584 Json(_request): Json<HashMap<String, serde_json::Value>>,
1585) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1586 let mut response = HashMap::new();
1588 response.insert(
1589 "message".to_string(),
1590 serde_json::json!("Genome merging not yet implemented"),
1591 );
1592
1593 Ok(Json(response))
1594}
1595
1596#[utoipa::path(
1598 get,
1599 path = "/v1/genome/diff",
1600 tag = "genome",
1601 params(
1602 ("genome_a" = String, Query, description = "First genome name"),
1603 ("genome_b" = String, Query, description = "Second genome name")
1604 ),
1605 responses(
1606 (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1607 )
1608)]
1609pub async fn get_diff(
1610 State(_state): State<ApiState>,
1611 Query(_params): Query<HashMap<String, String>>,
1612) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1613 let mut response = HashMap::new();
1615 response.insert("differences".to_string(), serde_json::json!([]));
1616
1617 Ok(Json(response))
1618}
1619
1620#[utoipa::path(
1622 post,
1623 path = "/v1/genome/export_format",
1624 tag = "genome",
1625 responses(
1626 (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1627 )
1628)]
1629pub async fn post_export_format(
1630 State(_state): State<ApiState>,
1631 Json(_request): Json<HashMap<String, String>>,
1632) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1633 let mut response = HashMap::new();
1635 response.insert(
1636 "message".to_string(),
1637 serde_json::json!("Format export not yet implemented"),
1638 );
1639
1640 Ok(Json(response))
1641}
1642
1643#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1646pub async fn get_amalgamation(
1647 State(state): State<ApiState>,
1648) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1649 let lock = state.amalgamation_state.read();
1650 let mut response = HashMap::new();
1651 if let Some(p) = lock.pending.as_ref() {
1652 response.insert(
1653 "pending".to_string(),
1654 amalgamation::pending_summary_to_health_json(&p.summary),
1655 );
1656 } else {
1657 response.insert("pending".to_string(), serde_json::Value::Null);
1658 }
1659 Ok(Json(response))
1660}
1661
1662#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1664pub async fn get_amalgamation_history_exact(
1665 State(state): State<ApiState>,
1666) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1667 let lock = state.amalgamation_state.read();
1668 let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1669 for entry in &lock.history {
1670 out.push(HashMap::from([
1671 (
1672 "amalgamation_id".to_string(),
1673 serde_json::json!(entry.amalgamation_id),
1674 ),
1675 (
1676 "genome_title".to_string(),
1677 serde_json::json!(entry.genome_title),
1678 ),
1679 (
1680 "circuit_size".to_string(),
1681 serde_json::json!(entry.circuit_size),
1682 ),
1683 ("status".to_string(), serde_json::json!(entry.status)),
1684 (
1685 "timestamp_ms".to_string(),
1686 serde_json::json!(entry.timestamp_ms),
1687 ),
1688 ]));
1689 }
1690 Ok(Json(out))
1691}
1692
1693#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1695pub async fn get_cortical_template(
1696 State(_state): State<ApiState>,
1697) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1698 use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1699 FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1700 };
1701 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1702 use serde_json::json;
1703
1704 let mut templates = HashMap::new();
1705
1706 let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1711 let (variant, frame, positioning) = match dt {
1712 IOCorticalAreaConfigurationFlag::Boolean => {
1713 ("Boolean", FrameChangeHandling::Absolute, None)
1714 }
1715 IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1716 IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1717 IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1718 IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1719 IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1720 ("SignedPercentage", f, Some(p))
1721 }
1722 IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1723 ("SignedPercentage2D", f, Some(p))
1724 }
1725 IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1726 ("SignedPercentage3D", f, Some(p))
1727 }
1728 IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1729 ("SignedPercentage4D", f, Some(p))
1730 }
1731 IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1732 IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1733 IOCorticalAreaConfigurationFlag::PoseEstimation(f, _) => ("PoseEstimation", f, None),
1734 };
1735
1736 let frame_str = match frame {
1737 FrameChangeHandling::Absolute => "Absolute",
1738 FrameChangeHandling::Incremental => "Incremental",
1739 };
1740
1741 let positioning_str = positioning.map(|p| match p {
1742 PercentageNeuronPositioning::Linear => "Linear",
1743 PercentageNeuronPositioning::Fractional => "Fractional",
1744 });
1745
1746 json!({
1747 "variant": variant,
1748 "frame_change_handling": frame_str,
1749 "percentage_positioning": positioning_str,
1750 "config_value": dt.to_data_type_configuration_flag()
1751 })
1752 };
1753
1754 for motor_unit in MotorCorticalUnit::list_all() {
1756 let friendly_name = motor_unit.get_friendly_name();
1757 let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1758 let num_areas = motor_unit.get_number_cortical_areas();
1759 let topology = motor_unit.get_unit_default_topology();
1760
1761 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1772 use serde_json::{Map, Value};
1773 use std::collections::HashMap as StdHashMap;
1774
1775 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1776
1777 for (sub_idx, topo) in topology {
1779 subunits.insert(
1780 sub_idx.get().to_string(),
1781 json!({
1782 "relative_position": topo.relative_position,
1783 "channel_dimensions_default": topo.channel_dimensions_default,
1784 "channel_dimensions_min": topo.channel_dimensions_min,
1785 "channel_dimensions_max": topo.channel_dimensions_max,
1786 "supported_data_types": Vec::<serde_json::Value>::new(),
1787 }),
1788 );
1789 }
1790
1791 let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1793 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1794 Some(allowed) => allowed.to_vec(),
1795 None => vec![
1796 FrameChangeHandling::Absolute,
1797 FrameChangeHandling::Incremental,
1798 ],
1799 };
1800
1801 let positionings = [
1802 PercentageNeuronPositioning::Linear,
1803 PercentageNeuronPositioning::Fractional,
1804 ];
1805
1806 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1807 StdHashMap::new();
1808
1809 for frame in frames {
1810 for positioning in positionings {
1811 let mut map: Map<String, Value> = Map::new();
1812 map.insert(
1813 "frame_change_handling".to_string(),
1814 serde_json::to_value(frame).unwrap_or(Value::Null),
1815 );
1816 map.insert(
1817 "percentage_neuron_positioning".to_string(),
1818 serde_json::to_value(positioning).unwrap_or(Value::Null),
1819 );
1820
1821 let cortical_ids = motor_unit
1823 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1824 CorticalUnitIndex::from(0u8),
1825 map,
1826 );
1827
1828 if let Ok(ids) = cortical_ids {
1829 for (i, id) in ids.into_iter().enumerate() {
1830 if let Ok(flag) = id.extract_io_data_flag() {
1831 let dt_json = data_type_to_json(flag);
1832 let subunit_key = i.to_string();
1833
1834 let dedup_key = format!(
1835 "{}|{}|{}",
1836 dt_json
1837 .get("variant")
1838 .and_then(|v| v.as_str())
1839 .unwrap_or(""),
1840 dt_json
1841 .get("frame_change_handling")
1842 .and_then(|v| v.as_str())
1843 .unwrap_or(""),
1844 dt_json
1845 .get("percentage_positioning")
1846 .and_then(|v| v.as_str())
1847 .unwrap_or("")
1848 );
1849
1850 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1851 if !seen.insert(dedup_key) {
1852 continue;
1853 }
1854
1855 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1856 if let Some(arr) = subunit_obj
1857 .get_mut("supported_data_types")
1858 .and_then(|v| v.as_array_mut())
1859 {
1860 arr.push(dt_json);
1861 }
1862 }
1863 }
1864 }
1865 }
1866 }
1867 }
1868
1869 templates.insert(
1870 format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1871 json!({
1872 "type": "motor",
1873 "friendly_name": friendly_name,
1874 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1875 "number_of_cortical_areas": num_areas,
1876 "subunits": subunits,
1877 "description": format!("Motor output: {}", friendly_name)
1878 }),
1879 );
1880 }
1881
1882 for sensory_unit in SensoryCorticalUnit::list_all() {
1884 let friendly_name = sensory_unit.get_friendly_name();
1885 let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1886 let num_areas = sensory_unit.get_number_cortical_areas();
1887 let topology = sensory_unit.get_unit_default_topology();
1888
1889 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1890 use serde_json::{Map, Value};
1891 use std::collections::HashMap as StdHashMap;
1892
1893 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1894
1895 for (sub_idx, topo) in topology {
1896 subunits.insert(
1897 sub_idx.get().to_string(),
1898 json!({
1899 "relative_position": topo.relative_position,
1900 "channel_dimensions_default": topo.channel_dimensions_default,
1901 "channel_dimensions_min": topo.channel_dimensions_min,
1902 "channel_dimensions_max": topo.channel_dimensions_max,
1903 "supported_data_types": Vec::<serde_json::Value>::new(),
1904 }),
1905 );
1906 }
1907
1908 let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1909 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1910 Some(allowed) => allowed.to_vec(),
1911 None => vec![
1912 FrameChangeHandling::Absolute,
1913 FrameChangeHandling::Incremental,
1914 ],
1915 };
1916
1917 let positionings = [
1918 PercentageNeuronPositioning::Linear,
1919 PercentageNeuronPositioning::Fractional,
1920 ];
1921
1922 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1923 StdHashMap::new();
1924
1925 for frame in frames {
1926 for positioning in positionings {
1927 let mut map: Map<String, Value> = Map::new();
1928 map.insert(
1929 "frame_change_handling".to_string(),
1930 serde_json::to_value(frame).unwrap_or(Value::Null),
1931 );
1932 map.insert(
1933 "percentage_neuron_positioning".to_string(),
1934 serde_json::to_value(positioning).unwrap_or(Value::Null),
1935 );
1936
1937 let cortical_ids = sensory_unit
1938 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1939 CorticalUnitIndex::from(0u8),
1940 map,
1941 );
1942
1943 if let Ok(ids) = cortical_ids {
1944 for (i, id) in ids.into_iter().enumerate() {
1945 if let Ok(flag) = id.extract_io_data_flag() {
1946 let dt_json = data_type_to_json(flag);
1947 let subunit_key = i.to_string();
1948
1949 let dedup_key = format!(
1950 "{}|{}|{}",
1951 dt_json
1952 .get("variant")
1953 .and_then(|v| v.as_str())
1954 .unwrap_or(""),
1955 dt_json
1956 .get("frame_change_handling")
1957 .and_then(|v| v.as_str())
1958 .unwrap_or(""),
1959 dt_json
1960 .get("percentage_positioning")
1961 .and_then(|v| v.as_str())
1962 .unwrap_or("")
1963 );
1964
1965 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1966 if !seen.insert(dedup_key) {
1967 continue;
1968 }
1969
1970 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1971 if let Some(arr) = subunit_obj
1972 .get_mut("supported_data_types")
1973 .and_then(|v| v.as_array_mut())
1974 {
1975 arr.push(dt_json);
1976 }
1977 }
1978 }
1979 }
1980 }
1981 }
1982 }
1983
1984 templates.insert(
1985 format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1986 json!({
1987 "type": "sensory",
1988 "friendly_name": friendly_name,
1989 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1990 "number_of_cortical_areas": num_areas,
1991 "subunits": subunits,
1992 "description": format!("Sensory input: {}", friendly_name)
1993 }),
1994 );
1995 }
1996
1997 Ok(Json(templates))
1998}
1999
2000#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
2002pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
2003 Ok(Json(vec![
2004 "barebones".to_string(),
2005 "essential".to_string(),
2006 "test".to_string(),
2007 "vision".to_string(),
2008 ]))
2009}
2010
2011#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
2013pub async fn get_download_region(
2014 State(state): State<ApiState>,
2015 Query(params): Query<HashMap<String, String>>,
2016) -> ApiResult<Json<serde_json::Value>> {
2017 let region_id = params
2018 .get("region_id")
2019 .cloned()
2020 .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
2021 let json_str = state
2022 .genome_service
2023 .export_region_genome(region_id)
2024 .await
2025 .map_err(ApiError::from)?;
2026 let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
2027 ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
2028 })?;
2029 Ok(Json(value))
2030}
2031
2032#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
2034pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
2035 Ok(Json(0))
2036}
2037
2038#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
2040pub async fn post_amalgamation_by_filename(
2041 State(state): State<ApiState>,
2042 Json(req): Json<HashMap<String, String>>,
2043) -> ApiResult<Json<HashMap<String, String>>> {
2044 let file_name = req
2048 .get("file_name")
2049 .or_else(|| req.get("filename"))
2050 .or_else(|| req.get("genome_file_name"))
2051 .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
2052
2053 let genome_json = match file_name.as_str() {
2054 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
2055 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
2056 "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
2057 "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
2058 other => {
2059 return Err(ApiError::invalid_input(format!(
2060 "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
2061 other
2062 )))
2063 }
2064 };
2065
2066 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
2067
2068 Ok(Json(HashMap::from([
2069 ("message".to_string(), "Amalgamation queued".to_string()),
2070 ("amalgamation_id".to_string(), amalgamation_id),
2071 ])))
2072}
2073
2074#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
2076pub async fn post_amalgamation_by_payload(
2077 State(state): State<ApiState>,
2078 Json(req): Json<serde_json::Value>,
2079) -> ApiResult<Json<HashMap<String, String>>> {
2080 let json_str = serde_json::to_string(&req)
2081 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
2082 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2083
2084 Ok(Json(HashMap::from([
2085 ("message".to_string(), "Amalgamation queued".to_string()),
2086 ("amalgamation_id".to_string(), amalgamation_id),
2087 ])))
2088}
2089
2090#[cfg(feature = "http")]
2092#[utoipa::path(
2093 post,
2094 path = "/v1/genome/amalgamation_by_upload",
2095 tag = "genome",
2096 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2097 responses(
2098 (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
2099 (status = 400, description = "Invalid request"),
2100 (status = 500, description = "Internal server error")
2101 )
2102)]
2103pub async fn post_amalgamation_by_upload(
2104 State(state): State<ApiState>,
2105 mut multipart: Multipart,
2106) -> ApiResult<Json<HashMap<String, String>>> {
2107 let mut genome_json: Option<String> = None;
2108
2109 while let Some(field) = multipart
2110 .next_field()
2111 .await
2112 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2113 {
2114 if field.name() == Some("file") {
2115 let bytes = field.bytes().await.map_err(|e| {
2116 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2117 })?;
2118
2119 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2120 ApiError::invalid_input(format!(
2121 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2122 e
2123 ))
2124 })?;
2125 genome_json = Some(json_str.to_string());
2126 break;
2127 }
2128 }
2129
2130 let json_str =
2131 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2132 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
2133
2134 Ok(Json(HashMap::from([
2135 ("message".to_string(), "Amalgamation queued".to_string()),
2136 ("amalgamation_id".to_string(), amalgamation_id),
2137 ])))
2138}
2139
2140#[cfg(feature = "http")]
2142#[utoipa::path(
2143 post,
2144 path = "/v1/genome/append-file",
2145 tag = "genome",
2146 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2147 responses(
2148 (status = 200, description = "Append processed", body = HashMap<String, String>)
2149 )
2150)]
2151pub async fn post_append_file(
2152 State(_state): State<ApiState>,
2153 mut _multipart: Multipart,
2154) -> ApiResult<Json<HashMap<String, String>>> {
2155 Ok(Json(HashMap::from([(
2156 "message".to_string(),
2157 "Not yet implemented".to_string(),
2158 )])))
2159}
2160
2161#[cfg(feature = "http")]
2163#[utoipa::path(
2164 post,
2165 path = "/v1/genome/upload/file",
2166 tag = "genome",
2167 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2168 responses(
2169 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
2170 (status = 400, description = "Invalid request"),
2171 (status = 500, description = "Internal server error")
2172 )
2173)]
2174pub async fn post_upload_file(
2175 State(state): State<ApiState>,
2176 mut multipart: Multipart,
2177) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
2178 let mut genome_json: Option<String> = None;
2179
2180 while let Some(field) = multipart
2181 .next_field()
2182 .await
2183 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
2184 {
2185 if field.name() == Some("file") {
2186 let bytes = field.bytes().await.map_err(|e| {
2187 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
2188 })?;
2189
2190 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
2191 ApiError::invalid_input(format!(
2192 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
2193 e
2194 ))
2195 })?;
2196 genome_json = Some(json_str.to_string());
2197 break;
2198 }
2199 }
2200
2201 let json_str =
2202 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
2203
2204 let genome_info =
2205 load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
2206 .await?;
2207
2208 let mut response = HashMap::new();
2209 response.insert("success".to_string(), serde_json::json!(true));
2210 response.insert(
2211 "message".to_string(),
2212 serde_json::json!("Genome uploaded successfully"),
2213 );
2214 response.insert(
2215 "cortical_area_count".to_string(),
2216 serde_json::json!(genome_info.cortical_area_count),
2217 );
2218 response.insert(
2219 "brain_region_count".to_string(),
2220 serde_json::json!(genome_info.brain_region_count),
2221 );
2222
2223 Ok(Json(response))
2224}
2225
2226#[cfg(feature = "http")]
2228#[utoipa::path(
2229 post,
2230 path = "/v1/genome/upload/file/edit",
2231 tag = "genome",
2232 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
2233 responses(
2234 (status = 200, description = "Upload processed", body = HashMap<String, String>)
2235 )
2236)]
2237pub async fn post_upload_file_edit(
2238 State(_state): State<ApiState>,
2239 mut _multipart: Multipart,
2240) -> ApiResult<Json<HashMap<String, String>>> {
2241 Ok(Json(HashMap::from([(
2242 "message".to_string(),
2243 "Not yet implemented".to_string(),
2244 )])))
2245}
2246
2247#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
2249pub async fn post_upload_string(
2250 State(_state): State<ApiState>,
2251 Json(_req): Json<String>,
2252) -> ApiResult<Json<HashMap<String, String>>> {
2253 Ok(Json(HashMap::from([(
2254 "message".to_string(),
2255 "Not yet implemented".to_string(),
2256 )])))
2257}