1use crate::amalgamation;
8use crate::common::ApiState;
9use crate::common::{ApiError, ApiResult, Json, Query, State};
10use feagi_services::types::{GenomeInfo, LoadGenomeParams};
11use std::collections::HashMap;
12use std::sync::atomic::Ordering;
13use std::sync::Arc;
14use tracing::info;
15use uuid::Uuid;
16
17#[cfg(feature = "http")]
18use axum::extract::Multipart;
19
20#[derive(Debug, Clone, utoipa::ToSchema)]
24pub struct GenomeFileUploadForm {
25 #[schema(value_type = String, format = Binary)]
27 pub file: String,
28}
29
30fn queue_amalgamation_from_genome_json_str(
31 state: &ApiState,
32 genome_json: String,
33) -> Result<String, ApiError> {
34 {
36 let lock = state.amalgamation_state.read();
37 if lock.pending.is_some() {
38 return Err(ApiError::invalid_input(
39 "Amalgamation already pending; cancel it first via /v1/genome/amalgamation_cancellation",
40 ));
41 }
42 }
43
44 let genome = feagi_evolutionary::load_genome_from_json(&genome_json)
45 .map_err(|e| ApiError::invalid_input(format!("Invalid genome payload: {}", e)))?;
46
47 let circuit_size = amalgamation::compute_circuit_size_from_runtime_genome(&genome);
48
49 let amalgamation_id = Uuid::new_v4().to_string();
50 let genome_title = genome.metadata.genome_title.clone();
51
52 let summary = amalgamation::AmalgamationPendingSummary {
53 amalgamation_id: amalgamation_id.clone(),
54 genome_title,
55 circuit_size,
56 };
57
58 let pending = amalgamation::AmalgamationPending {
59 summary: summary.clone(),
60 genome_json,
61 };
62
63 {
64 let mut lock = state.amalgamation_state.write();
65 let now_ms = std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .map(|d| d.as_millis() as i64)
68 .unwrap_or(0);
69
70 lock.history.push(amalgamation::AmalgamationHistoryEntry {
71 amalgamation_id: summary.amalgamation_id.clone(),
72 genome_title: summary.genome_title.clone(),
73 circuit_size: summary.circuit_size,
74 status: "pending".to_string(),
75 timestamp_ms: now_ms,
76 });
77 lock.pending = Some(pending);
78 }
79
80 tracing::info!(
81 target: "feagi-api",
82 "🧬 [AMALGAMATION] Queued pending amalgamation id={} title='{}' circuit_size={:?}",
83 summary.amalgamation_id,
84 summary.genome_title,
85 summary.circuit_size
86 );
87
88 Ok(amalgamation_id)
89}
90
91struct GenomeTransitionFlagGuard {
92 in_progress: Arc<std::sync::atomic::AtomicBool>,
93}
94
95impl Drop for GenomeTransitionFlagGuard {
96 fn drop(&mut self) {
97 self.in_progress.store(false, Ordering::SeqCst);
98 }
99}
100
101async fn load_genome_with_priority(
109 state: &ApiState,
110 params: LoadGenomeParams,
111 source: &str,
112) -> ApiResult<GenomeInfo> {
113 let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
114 ApiError::conflict(
115 "Another genome transition is already in progress; wait for it to finish",
116 )
117 })?;
118 state
119 .genome_transition_in_progress
120 .store(true, Ordering::SeqCst);
121 let _guard = GenomeTransitionFlagGuard {
122 in_progress: Arc::clone(&state.genome_transition_in_progress),
123 };
124
125 tracing::info!(
126 target: "feagi-api",
127 "🛑 Entering prioritized genome transition from {}",
128 source
129 );
130
131 let runtime_service = state.runtime_service.as_ref();
132 let runtime_status = runtime_service
133 .get_status()
134 .await
135 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
136 let runtime_was_running = runtime_status.is_running;
137
138 if runtime_was_running {
139 tracing::info!(
140 target: "feagi-api",
141 "Stopping burst engine before prioritized genome transition"
142 );
143 runtime_service.stop().await.map_err(|e| {
144 ApiError::internal(format!(
145 "Failed to stop burst engine before genome transition: {}",
146 e
147 ))
148 })?;
149 }
150
151 let genome_service = state.genome_service.as_ref();
152 let load_result = genome_service.load_genome(params).await;
153 let genome_info = match load_result {
154 Ok(info) => info,
155 Err(e) => {
156 if runtime_was_running {
157 if let Err(restart_err) = runtime_service.start().await {
158 tracing::warn!(
159 target: "feagi-api",
160 "Failed to restore runtime after failed genome load (source={}): {}",
161 source,
162 restart_err
163 );
164 }
165 }
166 return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
167 }
168 };
169
170 let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
171 runtime_service
172 .set_frequency(burst_frequency_hz)
173 .await
174 .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
175
176 if runtime_was_running {
177 runtime_service.start().await.map_err(|e| {
178 ApiError::internal(format!(
179 "Failed to restart burst engine after genome transition: {}",
180 e
181 ))
182 })?;
183 }
184
185 tracing::info!(
186 target: "feagi-api",
187 "✅ Prioritized genome transition completed from {}",
188 source
189 );
190 Ok(genome_info)
191}
192
193fn inject_simulation_timestep_into_genome(
199 mut genome: serde_json::Value,
200 simulation_timestep_s: f64,
201) -> Result<serde_json::Value, ApiError> {
202 let physiology = genome
203 .get_mut("physiology")
204 .and_then(|v| v.as_object_mut())
205 .ok_or_else(|| {
206 ApiError::internal(
207 "Genome JSON missing required object key 'physiology' while saving".to_string(),
208 )
209 })?;
210
211 physiology.insert(
212 "simulation_timestep".to_string(),
213 serde_json::Value::from(simulation_timestep_s),
214 );
215 Ok(genome)
216}
217
218async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
219 let runtime_service = state.runtime_service.as_ref();
220 let status = runtime_service
221 .get_status()
222 .await
223 .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
224
225 Ok(if status.frequency_hz > 0.0 {
227 1.0 / status.frequency_hz
228 } else {
229 0.0
230 })
231}
232
233#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
235pub async fn get_file_name(
236 State(_state): State<ApiState>,
237) -> ApiResult<Json<HashMap<String, String>>> {
238 Ok(Json(HashMap::from([(
240 "genome_file_name".to_string(),
241 "".to_string(),
242 )])))
243}
244
245#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
247pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
248 Ok(Json(vec![]))
250}
251
252#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
254pub async fn post_amalgamation_destination(
255 State(state): State<ApiState>,
256 Query(params): Query<HashMap<String, String>>,
257 Json(req): Json<HashMap<String, serde_json::Value>>,
258) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
259 let amalgamation_id = params
267 .get("amalgamation_id")
268 .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
269 .to_string();
270
271 let origin_x: i32 = params
272 .get("circuit_origin_x")
273 .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
274 .parse()
275 .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
276 let origin_y: i32 = params
277 .get("circuit_origin_y")
278 .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
279 .parse()
280 .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
281 let origin_z: i32 = params
282 .get("circuit_origin_z")
283 .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
284 .parse()
285 .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
286
287 let rewire_mode = params
288 .get("rewire_mode")
289 .cloned()
290 .unwrap_or_else(|| "rewire_all".to_string());
291
292 let parent_region_id = req
293 .get("brain_region_id")
294 .and_then(|v| v.as_str())
295 .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
296 .to_string();
297
298 let pending = {
300 let lock = state.amalgamation_state.write();
301 let Some(p) = lock.pending.as_ref() else {
302 return Err(ApiError::invalid_input("No amalgamation is pending"));
303 };
304 if p.summary.amalgamation_id != amalgamation_id {
305 return Err(ApiError::invalid_input(format!(
306 "Pending amalgamation_id mismatch: expected {}, got {}",
307 p.summary.amalgamation_id, amalgamation_id
308 )));
309 }
310 p.clone()
311 };
312
313 let connectome_service = state.connectome_service.as_ref();
317
318 let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
319 region_properties.insert(
320 "coordinate_3d".to_string(),
321 serde_json::json!([origin_x, origin_y, origin_z]),
322 );
323 region_properties.insert(
324 "amalgamation_id".to_string(),
325 serde_json::json!(pending.summary.amalgamation_id),
326 );
327 region_properties.insert(
328 "circuit_size".to_string(),
329 serde_json::json!(pending.summary.circuit_size),
330 );
331 region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
332
333 connectome_service
334 .create_brain_region(feagi_services::types::CreateBrainRegionParams {
335 region_id: amalgamation_id.clone(),
336 name: pending.summary.genome_title.clone(),
337 region_type: "Custom".to_string(),
338 parent_id: Some(parent_region_id.clone()),
339 properties: Some(region_properties),
340 })
341 .await
342 .map_err(|e| {
343 ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
344 })?;
345
346 let imported_genome =
355 feagi_evolutionary::load_genome_from_json(&pending.genome_json).map_err(|e| {
356 ApiError::invalid_input(format!(
357 "Pending genome payload can no longer be parsed as a genome: {}",
358 e
359 ))
360 })?;
361
362 let genome_service = state.genome_service.as_ref();
363 let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
364 let mut skipped_existing: Vec<String> = Vec::new();
365
366 for area in imported_genome.cortical_areas.values() {
367 let cortical_id = area.cortical_id.as_base_64();
368 let exists = connectome_service
369 .cortical_area_exists(&cortical_id)
370 .await
371 .map_err(|e| {
372 ApiError::internal(format!(
373 "Failed to check existing cortical area {}: {}",
374 cortical_id, e
375 ))
376 })?;
377 if exists {
378 skipped_existing.push(cortical_id);
379 continue;
380 }
381
382 let mut props = area.properties.clone();
383 props.insert(
384 "parent_region_id".to_string(),
385 serde_json::json!(amalgamation_id.clone()),
386 );
387 props.insert(
388 "amalgamation_source".to_string(),
389 serde_json::json!("amalgamation_by_payload"),
390 );
391
392 to_create.push(feagi_services::types::CreateCorticalAreaParams {
393 cortical_id,
394 name: area.name.clone(),
395 dimensions: (
396 area.dimensions.width as usize,
397 area.dimensions.height as usize,
398 area.dimensions.depth as usize,
399 ),
400 position: (
401 origin_x.saturating_add(area.position.x),
402 origin_y.saturating_add(area.position.y),
403 origin_z.saturating_add(area.position.z),
404 ),
405 area_type: "Custom".to_string(),
406 visible: Some(true),
407 sub_group: None,
408 neurons_per_voxel: area
409 .properties
410 .get("neurons_per_voxel")
411 .and_then(|v| v.as_u64())
412 .map(|v| v as u32),
413 postsynaptic_current: area
414 .properties
415 .get("postsynaptic_current")
416 .and_then(|v| v.as_f64()),
417 plasticity_constant: area
418 .properties
419 .get("plasticity_constant")
420 .and_then(|v| v.as_f64()),
421 degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
422 psp_uniform_distribution: area
423 .properties
424 .get("psp_uniform_distribution")
425 .and_then(|v| v.as_bool()),
426 firing_threshold_increment: None,
427 firing_threshold_limit: area
428 .properties
429 .get("firing_threshold_limit")
430 .and_then(|v| v.as_f64()),
431 consecutive_fire_count: area
432 .properties
433 .get("consecutive_fire_limit")
434 .and_then(|v| v.as_u64())
435 .map(|v| v as u32),
436 snooze_period: area
437 .properties
438 .get("snooze_period")
439 .and_then(|v| v.as_u64())
440 .map(|v| v as u32),
441 refractory_period: area
442 .properties
443 .get("refractory_period")
444 .and_then(|v| v.as_u64())
445 .map(|v| v as u32),
446 leak_coefficient: area
447 .properties
448 .get("leak_coefficient")
449 .and_then(|v| v.as_f64()),
450 leak_variability: area
451 .properties
452 .get("leak_variability")
453 .and_then(|v| v.as_f64()),
454 burst_engine_active: area
455 .properties
456 .get("burst_engine_active")
457 .and_then(|v| v.as_bool()),
458 properties: Some(props),
459 });
460 }
461
462 if !to_create.is_empty() {
463 genome_service
464 .create_cortical_areas(to_create)
465 .await
466 .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
467 }
468
469 {
471 let mut lock = state.amalgamation_state.write();
472 let now_ms = std::time::SystemTime::now()
473 .duration_since(std::time::UNIX_EPOCH)
474 .map(|d| d.as_millis() as i64)
475 .unwrap_or(0);
476 lock.history.push(amalgamation::AmalgamationHistoryEntry {
477 amalgamation_id: pending.summary.amalgamation_id.clone(),
478 genome_title: pending.summary.genome_title.clone(),
479 circuit_size: pending.summary.circuit_size,
480 status: "confirmed".to_string(),
481 timestamp_ms: now_ms,
482 });
483 lock.pending = None;
484 }
485
486 tracing::info!(
487 target: "feagi-api",
488 "🧬 [AMALGAMATION] Confirmed and cleared pending amalgamation id={} imported_areas={} skipped_existing_areas={}",
489 pending.summary.amalgamation_id,
490 if skipped_existing.is_empty() { "unknown".to_string() } else { "partial".to_string() },
491 skipped_existing.len()
492 );
493
494 let regions = state
496 .connectome_service
497 .list_brain_regions()
498 .await
499 .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
500
501 let mut brain_regions: Vec<serde_json::Value> = Vec::new();
502 for region in regions {
503 let coordinate_3d = region
505 .properties
506 .get("coordinate_3d")
507 .cloned()
508 .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
509 let coordinate_2d = region
510 .properties
511 .get("coordinate_2d")
512 .cloned()
513 .unwrap_or_else(|| serde_json::json!([0, 0]));
514
515 brain_regions.push(serde_json::json!({
516 "region_id": region.region_id,
517 "title": region.name,
518 "description": "",
519 "parent_region_id": region.parent_id,
520 "coordinate_2d": coordinate_2d,
521 "coordinate_3d": coordinate_3d,
522 "areas": region.cortical_areas,
523 "regions": region.child_regions,
524 "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
525 "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
526 }));
527 }
528
529 Ok(Json(HashMap::from([
530 (
531 "message".to_string(),
532 serde_json::Value::String("Amalgamation confirmed".to_string()),
533 ),
534 (
535 "brain_regions".to_string(),
536 serde_json::Value::Array(brain_regions),
537 ),
538 (
539 "skipped_existing_areas".to_string(),
540 serde_json::json!(skipped_existing),
541 ),
542 ])))
543}
544
545#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
547pub async fn delete_amalgamation_cancellation(
548 State(state): State<ApiState>,
549) -> ApiResult<Json<HashMap<String, String>>> {
550 let mut lock = state.amalgamation_state.write();
551 if let Some(pending) = lock.pending.take() {
552 let now_ms = std::time::SystemTime::now()
553 .duration_since(std::time::UNIX_EPOCH)
554 .map(|d| d.as_millis() as i64)
555 .unwrap_or(0);
556 lock.history.push(amalgamation::AmalgamationHistoryEntry {
557 amalgamation_id: pending.summary.amalgamation_id,
558 genome_title: pending.summary.genome_title,
559 circuit_size: pending.summary.circuit_size,
560 status: "cancelled".to_string(),
561 timestamp_ms: now_ms,
562 });
563
564 tracing::info!(
565 target: "feagi-api",
566 "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
567 lock.history
568 .last()
569 .map(|e| e.amalgamation_id.clone())
570 .unwrap_or_else(|| "<unknown>".to_string())
571 );
572 }
573 Ok(Json(HashMap::from([(
574 "message".to_string(),
575 "Amalgamation cancelled".to_string(),
576 )])))
577}
578
579#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
581pub async fn post_genome_append(
582 State(_state): State<ApiState>,
583 Json(_req): Json<HashMap<String, serde_json::Value>>,
584) -> ApiResult<Json<HashMap<String, String>>> {
585 Err(ApiError::internal("Not yet implemented"))
586}
587
588#[utoipa::path(
590 post,
591 path = "/v1/genome/upload/barebones",
592 responses(
593 (status = 200, description = "Barebones genome loaded successfully"),
594 (status = 500, description = "Failed to load genome")
595 ),
596 tag = "genome"
597)]
598pub async fn post_upload_barebones_genome(
599 State(state): State<ApiState>,
600) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
601 tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
602 let result = load_default_genome(state, "barebones").await;
603 match &result {
604 Ok(_) => {
605 tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
606 }
607 Err(e) => {
608 tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
609 }
610 }
611 result
612}
613
614#[utoipa::path(
616 post,
617 path = "/v1/genome/upload/essential",
618 responses(
619 (status = 200, description = "Essential genome loaded successfully"),
620 (status = 500, description = "Failed to load genome")
621 ),
622 tag = "genome"
623)]
624pub async fn post_upload_essential_genome(
625 State(state): State<ApiState>,
626) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
627 load_default_genome(state, "essential").await
628}
629
630async fn load_default_genome(
632 state: ApiState,
633 genome_name: &str,
634) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
635 tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
636 tracing::debug!(target: "feagi-api", " State components available: genome_service=true, runtime_service=true");
637 let genome_json = match genome_name {
639 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
640 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
641 "test" => feagi_evolutionary::TEST_GENOME_JSON,
642 "vision" => feagi_evolutionary::VISION_GENOME_JSON,
643 _ => {
644 return Err(ApiError::invalid_input(format!(
645 "Unknown genome name '{}'. Available: barebones, essential, test, vision",
646 genome_name
647 )))
648 }
649 };
650
651 tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
652 genome_name, genome_json.len());
653
654 let params = LoadGenomeParams {
655 json_str: genome_json.to_string(),
656 };
657
658 tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
659 let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
660
661 tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
662 genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
663
664 let mut response = HashMap::new();
666 response.insert("success".to_string(), serde_json::Value::Bool(true));
667 response.insert(
668 "message".to_string(),
669 serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
670 );
671 response.insert(
672 "cortical_area_count".to_string(),
673 serde_json::Value::Number(genome_info.cortical_area_count.into()),
674 );
675 response.insert(
676 "brain_region_count".to_string(),
677 serde_json::Value::Number(genome_info.brain_region_count.into()),
678 );
679 response.insert(
680 "genome_id".to_string(),
681 serde_json::Value::String(genome_info.genome_id),
682 );
683 response.insert(
684 "genome_title".to_string(),
685 serde_json::Value::String(genome_info.genome_title),
686 );
687
688 Ok(Json(response))
689}
690
691#[utoipa::path(
693 get,
694 path = "/v1/genome/name",
695 tag = "genome",
696 responses(
697 (status = 200, description = "Genome name", body = String)
698 )
699)]
700pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
701 Ok(Json("default_genome".to_string()))
704}
705
706#[utoipa::path(
708 get,
709 path = "/v1/genome/timestamp",
710 tag = "genome",
711 responses(
712 (status = 200, description = "Genome timestamp", body = i64)
713 )
714)]
715pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
716 Ok(Json(0))
718}
719
720#[utoipa::path(
722 post,
723 path = "/v1/genome/save",
724 tag = "genome",
725 responses(
726 (status = 200, description = "Genome saved", body = HashMap<String, String>)
727 )
728)]
729pub async fn post_save(
730 State(state): State<ApiState>,
731 Json(request): Json<HashMap<String, String>>,
732) -> ApiResult<Json<HashMap<String, String>>> {
733 use std::fs;
734 use std::path::Path;
735
736 info!("Saving genome to file");
737
738 let genome_id = request.get("genome_id").cloned();
740 let genome_title = request.get("genome_title").cloned();
741 let file_path = request.get("file_path").cloned();
742
743 let params = feagi_services::SaveGenomeParams {
745 genome_id,
746 genome_title,
747 };
748
749 let genome_service = state.genome_service.as_ref();
751 let genome_json = genome_service
752 .save_genome(params)
753 .await
754 .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
755
756 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
758 let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
759 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
760 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
761 let genome_json = serde_json::to_string_pretty(&genome_value)
762 .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
763
764 let save_path = if let Some(path) = file_path {
766 std::path::PathBuf::from(path)
767 } else {
768 let timestamp = std::time::SystemTime::now()
770 .duration_since(std::time::UNIX_EPOCH)
771 .unwrap()
772 .as_secs();
773 std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
774 };
775
776 if let Some(parent) = Path::new(&save_path).parent() {
778 fs::create_dir_all(parent)
779 .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
780 }
781
782 fs::write(&save_path, genome_json)
784 .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
785
786 info!("✅ Genome saved successfully to: {}", save_path.display());
787
788 Ok(Json(HashMap::from([
789 (
790 "message".to_string(),
791 "Genome saved successfully".to_string(),
792 ),
793 ("file_path".to_string(), save_path.display().to_string()),
794 ])))
795}
796
797#[utoipa::path(
799 post,
800 path = "/v1/genome/load",
801 tag = "genome",
802 responses(
803 (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
804 )
805)]
806pub async fn post_load(
807 State(state): State<ApiState>,
808 Json(request): Json<HashMap<String, String>>,
809) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
810 let genome_name = request
811 .get("genome_name")
812 .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
813
814 let params = feagi_services::LoadGenomeParams {
816 json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
817 };
818
819 let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
820
821 let mut response = HashMap::new();
822 response.insert(
823 "message".to_string(),
824 serde_json::json!("Genome loaded successfully"),
825 );
826 response.insert(
827 "genome_title".to_string(),
828 serde_json::json!(genome_info.genome_title),
829 );
830
831 Ok(Json(response))
832}
833
834#[utoipa::path(
836 post,
837 path = "/v1/genome/upload",
838 tag = "genome",
839 responses(
840 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
841 )
842)]
843pub async fn post_upload(
844 State(state): State<ApiState>,
845 Json(genome_json): Json<serde_json::Value>,
846) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
847 let json_str = serde_json::to_string(&genome_json)
849 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
850
851 let params = LoadGenomeParams { json_str };
852 let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
853
854 let mut response = HashMap::new();
855 response.insert("success".to_string(), serde_json::json!(true));
856 response.insert(
857 "message".to_string(),
858 serde_json::json!("Genome uploaded successfully"),
859 );
860 response.insert(
861 "cortical_area_count".to_string(),
862 serde_json::json!(genome_info.cortical_area_count),
863 );
864 response.insert(
865 "brain_region_count".to_string(),
866 serde_json::json!(genome_info.brain_region_count),
867 );
868
869 Ok(Json(response))
870}
871
872#[utoipa::path(
874 get,
875 path = "/v1/genome/download",
876 tag = "genome",
877 responses(
878 (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
879 )
880)]
881pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
882 info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
883 let genome_service = state.genome_service.as_ref();
884
885 let genome_json_str = genome_service
887 .save_genome(feagi_services::types::SaveGenomeParams {
888 genome_id: None,
889 genome_title: None,
890 })
891 .await
892 .map_err(|e| {
893 tracing::error!("Failed to export genome: {}", e);
894 ApiError::internal(format!("Failed to export genome: {}", e))
895 })?;
896
897 let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
899 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
900
901 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
903 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
904
905 info!(
906 "✅ Genome download complete, {} bytes",
907 genome_json_str.len()
908 );
909 Ok(Json(genome_value))
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915 use serde_json::json;
916
917 #[test]
918 fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
919 let genome = json!({
920 "version": "3.0",
921 "physiology": {
922 "simulation_timestep": 0.025,
923 "max_age": 10000000
924 }
925 });
926
927 let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
928 assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
929 assert_eq!(updated["physiology"]["max_age"], json!(10000000));
930 }
931
932 #[test]
933 fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
934 let genome = json!({ "version": "3.0" });
935 let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
936 assert!(format!("{err:?}").contains("physiology"));
937 }
938}
939
940#[utoipa::path(
942 get,
943 path = "/v1/genome/properties",
944 tag = "genome",
945 responses(
946 (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
947 )
948)]
949pub async fn get_properties(
950 State(_state): State<ApiState>,
951) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
952 Ok(Json(HashMap::new()))
954}
955
956#[utoipa::path(
958 post,
959 path = "/v1/genome/validate",
960 tag = "genome",
961 responses(
962 (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
963 )
964)]
965pub async fn post_validate(
966 State(_state): State<ApiState>,
967 Json(_genome): Json<serde_json::Value>,
968) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
969 let mut response = HashMap::new();
971 response.insert("valid".to_string(), serde_json::json!(true));
972 response.insert("errors".to_string(), serde_json::json!([]));
973 response.insert("warnings".to_string(), serde_json::json!([]));
974
975 Ok(Json(response))
976}
977
978#[utoipa::path(
980 post,
981 path = "/v1/genome/transform",
982 tag = "genome",
983 responses(
984 (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
985 )
986)]
987pub async fn post_transform(
988 State(_state): State<ApiState>,
989 Json(_request): Json<HashMap<String, serde_json::Value>>,
990) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
991 let mut response = HashMap::new();
993 response.insert(
994 "message".to_string(),
995 serde_json::json!("Genome transformation not yet implemented"),
996 );
997
998 Ok(Json(response))
999}
1000
1001#[utoipa::path(
1003 post,
1004 path = "/v1/genome/clone",
1005 tag = "genome",
1006 responses(
1007 (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1008 )
1009)]
1010pub async fn post_clone(
1011 State(_state): State<ApiState>,
1012 Json(_request): Json<HashMap<String, String>>,
1013) -> ApiResult<Json<HashMap<String, String>>> {
1014 Ok(Json(HashMap::from([(
1016 "message".to_string(),
1017 "Genome cloning not yet implemented".to_string(),
1018 )])))
1019}
1020
1021#[utoipa::path(
1023 post,
1024 path = "/v1/genome/reset",
1025 tag = "genome",
1026 responses(
1027 (status = 200, description = "Genome reset", body = HashMap<String, String>)
1028 )
1029)]
1030pub async fn post_reset(
1031 State(_state): State<ApiState>,
1032) -> ApiResult<Json<HashMap<String, String>>> {
1033 Ok(Json(HashMap::from([(
1035 "message".to_string(),
1036 "Genome reset not yet implemented".to_string(),
1037 )])))
1038}
1039
1040#[utoipa::path(
1042 get,
1043 path = "/v1/genome/metadata",
1044 tag = "genome",
1045 responses(
1046 (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1047 )
1048)]
1049pub async fn get_metadata(
1050 State(state): State<ApiState>,
1051) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1052 get_properties(State(state)).await
1053}
1054
1055#[utoipa::path(
1057 post,
1058 path = "/v1/genome/merge",
1059 tag = "genome",
1060 responses(
1061 (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1062 )
1063)]
1064pub async fn post_merge(
1065 State(_state): State<ApiState>,
1066 Json(_request): Json<HashMap<String, serde_json::Value>>,
1067) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1068 let mut response = HashMap::new();
1070 response.insert(
1071 "message".to_string(),
1072 serde_json::json!("Genome merging not yet implemented"),
1073 );
1074
1075 Ok(Json(response))
1076}
1077
1078#[utoipa::path(
1080 get,
1081 path = "/v1/genome/diff",
1082 tag = "genome",
1083 params(
1084 ("genome_a" = String, Query, description = "First genome name"),
1085 ("genome_b" = String, Query, description = "Second genome name")
1086 ),
1087 responses(
1088 (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1089 )
1090)]
1091pub async fn get_diff(
1092 State(_state): State<ApiState>,
1093 Query(_params): Query<HashMap<String, String>>,
1094) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1095 let mut response = HashMap::new();
1097 response.insert("differences".to_string(), serde_json::json!([]));
1098
1099 Ok(Json(response))
1100}
1101
1102#[utoipa::path(
1104 post,
1105 path = "/v1/genome/export_format",
1106 tag = "genome",
1107 responses(
1108 (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1109 )
1110)]
1111pub async fn post_export_format(
1112 State(_state): State<ApiState>,
1113 Json(_request): Json<HashMap<String, String>>,
1114) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1115 let mut response = HashMap::new();
1117 response.insert(
1118 "message".to_string(),
1119 serde_json::json!("Format export not yet implemented"),
1120 );
1121
1122 Ok(Json(response))
1123}
1124
1125#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1128pub async fn get_amalgamation(
1129 State(state): State<ApiState>,
1130) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1131 let lock = state.amalgamation_state.read();
1132 let mut response = HashMap::new();
1133 if let Some(p) = lock.pending.as_ref() {
1134 response.insert(
1135 "pending".to_string(),
1136 amalgamation::pending_summary_to_health_json(&p.summary),
1137 );
1138 } else {
1139 response.insert("pending".to_string(), serde_json::Value::Null);
1140 }
1141 Ok(Json(response))
1142}
1143
1144#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1146pub async fn get_amalgamation_history_exact(
1147 State(state): State<ApiState>,
1148) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1149 let lock = state.amalgamation_state.read();
1150 let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1151 for entry in &lock.history {
1152 out.push(HashMap::from([
1153 (
1154 "amalgamation_id".to_string(),
1155 serde_json::json!(entry.amalgamation_id),
1156 ),
1157 (
1158 "genome_title".to_string(),
1159 serde_json::json!(entry.genome_title),
1160 ),
1161 (
1162 "circuit_size".to_string(),
1163 serde_json::json!(entry.circuit_size),
1164 ),
1165 ("status".to_string(), serde_json::json!(entry.status)),
1166 (
1167 "timestamp_ms".to_string(),
1168 serde_json::json!(entry.timestamp_ms),
1169 ),
1170 ]));
1171 }
1172 Ok(Json(out))
1173}
1174
1175#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1177pub async fn get_cortical_template(
1178 State(_state): State<ApiState>,
1179) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1180 use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1181 FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1182 };
1183 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1184 use serde_json::json;
1185
1186 let mut templates = HashMap::new();
1187
1188 let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1193 let (variant, frame, positioning) = match dt {
1194 IOCorticalAreaConfigurationFlag::Boolean => {
1195 ("Boolean", FrameChangeHandling::Absolute, None)
1196 }
1197 IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1198 IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1199 IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1200 IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1201 IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1202 ("SignedPercentage", f, Some(p))
1203 }
1204 IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1205 ("SignedPercentage2D", f, Some(p))
1206 }
1207 IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1208 ("SignedPercentage3D", f, Some(p))
1209 }
1210 IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1211 ("SignedPercentage4D", f, Some(p))
1212 }
1213 IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1214 IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1215 };
1216
1217 let frame_str = match frame {
1218 FrameChangeHandling::Absolute => "Absolute",
1219 FrameChangeHandling::Incremental => "Incremental",
1220 };
1221
1222 let positioning_str = positioning.map(|p| match p {
1223 PercentageNeuronPositioning::Linear => "Linear",
1224 PercentageNeuronPositioning::Fractional => "Fractional",
1225 });
1226
1227 json!({
1228 "variant": variant,
1229 "frame_change_handling": frame_str,
1230 "percentage_positioning": positioning_str,
1231 "config_value": dt.to_data_type_configuration_flag()
1232 })
1233 };
1234
1235 for motor_unit in MotorCorticalUnit::list_all() {
1237 let friendly_name = motor_unit.get_friendly_name();
1238 let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1239 let num_areas = motor_unit.get_number_cortical_areas();
1240 let topology = motor_unit.get_unit_default_topology();
1241
1242 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1253 use serde_json::{Map, Value};
1254 use std::collections::HashMap as StdHashMap;
1255
1256 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1257
1258 for (sub_idx, topo) in topology {
1260 subunits.insert(
1261 sub_idx.get().to_string(),
1262 json!({
1263 "relative_position": topo.relative_position,
1264 "channel_dimensions_default": topo.channel_dimensions_default,
1265 "channel_dimensions_min": topo.channel_dimensions_min,
1266 "channel_dimensions_max": topo.channel_dimensions_max,
1267 "supported_data_types": Vec::<serde_json::Value>::new(),
1268 }),
1269 );
1270 }
1271
1272 let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1274 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1275 Some(allowed) => allowed.to_vec(),
1276 None => vec![
1277 FrameChangeHandling::Absolute,
1278 FrameChangeHandling::Incremental,
1279 ],
1280 };
1281
1282 let positionings = [
1283 PercentageNeuronPositioning::Linear,
1284 PercentageNeuronPositioning::Fractional,
1285 ];
1286
1287 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1288 StdHashMap::new();
1289
1290 for frame in frames {
1291 for positioning in positionings {
1292 let mut map: Map<String, Value> = Map::new();
1293 map.insert(
1294 "frame_change_handling".to_string(),
1295 serde_json::to_value(frame).unwrap_or(Value::Null),
1296 );
1297 map.insert(
1298 "percentage_neuron_positioning".to_string(),
1299 serde_json::to_value(positioning).unwrap_or(Value::Null),
1300 );
1301
1302 let cortical_ids = motor_unit
1304 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1305 CorticalUnitIndex::from(0u8),
1306 map,
1307 );
1308
1309 if let Ok(ids) = cortical_ids {
1310 for (i, id) in ids.into_iter().enumerate() {
1311 if let Ok(flag) = id.extract_io_data_flag() {
1312 let dt_json = data_type_to_json(flag);
1313 let subunit_key = i.to_string();
1314
1315 let dedup_key = format!(
1316 "{}|{}|{}",
1317 dt_json
1318 .get("variant")
1319 .and_then(|v| v.as_str())
1320 .unwrap_or(""),
1321 dt_json
1322 .get("frame_change_handling")
1323 .and_then(|v| v.as_str())
1324 .unwrap_or(""),
1325 dt_json
1326 .get("percentage_positioning")
1327 .and_then(|v| v.as_str())
1328 .unwrap_or("")
1329 );
1330
1331 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1332 if !seen.insert(dedup_key) {
1333 continue;
1334 }
1335
1336 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1337 if let Some(arr) = subunit_obj
1338 .get_mut("supported_data_types")
1339 .and_then(|v| v.as_array_mut())
1340 {
1341 arr.push(dt_json);
1342 }
1343 }
1344 }
1345 }
1346 }
1347 }
1348 }
1349
1350 templates.insert(
1351 format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1352 json!({
1353 "type": "motor",
1354 "friendly_name": friendly_name,
1355 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1356 "number_of_cortical_areas": num_areas,
1357 "subunits": subunits,
1358 "description": format!("Motor output: {}", friendly_name)
1359 }),
1360 );
1361 }
1362
1363 for sensory_unit in SensoryCorticalUnit::list_all() {
1365 let friendly_name = sensory_unit.get_friendly_name();
1366 let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1367 let num_areas = sensory_unit.get_number_cortical_areas();
1368 let topology = sensory_unit.get_unit_default_topology();
1369
1370 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1371 use serde_json::{Map, Value};
1372 use std::collections::HashMap as StdHashMap;
1373
1374 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1375
1376 for (sub_idx, topo) in topology {
1377 subunits.insert(
1378 sub_idx.get().to_string(),
1379 json!({
1380 "relative_position": topo.relative_position,
1381 "channel_dimensions_default": topo.channel_dimensions_default,
1382 "channel_dimensions_min": topo.channel_dimensions_min,
1383 "channel_dimensions_max": topo.channel_dimensions_max,
1384 "supported_data_types": Vec::<serde_json::Value>::new(),
1385 }),
1386 );
1387 }
1388
1389 let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1390 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1391 Some(allowed) => allowed.to_vec(),
1392 None => vec![
1393 FrameChangeHandling::Absolute,
1394 FrameChangeHandling::Incremental,
1395 ],
1396 };
1397
1398 let positionings = [
1399 PercentageNeuronPositioning::Linear,
1400 PercentageNeuronPositioning::Fractional,
1401 ];
1402
1403 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1404 StdHashMap::new();
1405
1406 for frame in frames {
1407 for positioning in positionings {
1408 let mut map: Map<String, Value> = Map::new();
1409 map.insert(
1410 "frame_change_handling".to_string(),
1411 serde_json::to_value(frame).unwrap_or(Value::Null),
1412 );
1413 map.insert(
1414 "percentage_neuron_positioning".to_string(),
1415 serde_json::to_value(positioning).unwrap_or(Value::Null),
1416 );
1417
1418 let cortical_ids = sensory_unit
1419 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1420 CorticalUnitIndex::from(0u8),
1421 map,
1422 );
1423
1424 if let Ok(ids) = cortical_ids {
1425 for (i, id) in ids.into_iter().enumerate() {
1426 if let Ok(flag) = id.extract_io_data_flag() {
1427 let dt_json = data_type_to_json(flag);
1428 let subunit_key = i.to_string();
1429
1430 let dedup_key = format!(
1431 "{}|{}|{}",
1432 dt_json
1433 .get("variant")
1434 .and_then(|v| v.as_str())
1435 .unwrap_or(""),
1436 dt_json
1437 .get("frame_change_handling")
1438 .and_then(|v| v.as_str())
1439 .unwrap_or(""),
1440 dt_json
1441 .get("percentage_positioning")
1442 .and_then(|v| v.as_str())
1443 .unwrap_or("")
1444 );
1445
1446 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1447 if !seen.insert(dedup_key) {
1448 continue;
1449 }
1450
1451 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1452 if let Some(arr) = subunit_obj
1453 .get_mut("supported_data_types")
1454 .and_then(|v| v.as_array_mut())
1455 {
1456 arr.push(dt_json);
1457 }
1458 }
1459 }
1460 }
1461 }
1462 }
1463 }
1464
1465 templates.insert(
1466 format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1467 json!({
1468 "type": "sensory",
1469 "friendly_name": friendly_name,
1470 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1471 "number_of_cortical_areas": num_areas,
1472 "subunits": subunits,
1473 "description": format!("Sensory input: {}", friendly_name)
1474 }),
1475 );
1476 }
1477
1478 Ok(Json(templates))
1479}
1480
1481#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1483pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1484 Ok(Json(vec![
1485 "barebones".to_string(),
1486 "essential".to_string(),
1487 "test".to_string(),
1488 "vision".to_string(),
1489 ]))
1490}
1491
1492#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
1494pub async fn get_download_region(
1495 State(_state): State<ApiState>,
1496 Query(_params): Query<HashMap<String, String>>,
1497) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1498 Ok(Json(HashMap::new()))
1499}
1500
1501#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
1503pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
1504 Ok(Json(0))
1505}
1506
1507#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
1509pub async fn post_amalgamation_by_filename(
1510 State(state): State<ApiState>,
1511 Json(req): Json<HashMap<String, String>>,
1512) -> ApiResult<Json<HashMap<String, String>>> {
1513 let file_name = req
1517 .get("file_name")
1518 .or_else(|| req.get("filename"))
1519 .or_else(|| req.get("genome_file_name"))
1520 .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
1521
1522 let genome_json = match file_name.as_str() {
1523 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
1524 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
1525 "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
1526 "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
1527 other => {
1528 return Err(ApiError::invalid_input(format!(
1529 "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
1530 other
1531 )))
1532 }
1533 };
1534
1535 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
1536
1537 Ok(Json(HashMap::from([
1538 ("message".to_string(), "Amalgamation queued".to_string()),
1539 ("amalgamation_id".to_string(), amalgamation_id),
1540 ])))
1541}
1542
1543#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
1545pub async fn post_amalgamation_by_payload(
1546 State(state): State<ApiState>,
1547 Json(req): Json<serde_json::Value>,
1548) -> ApiResult<Json<HashMap<String, String>>> {
1549 let json_str = serde_json::to_string(&req)
1550 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1551 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1552
1553 Ok(Json(HashMap::from([
1554 ("message".to_string(), "Amalgamation queued".to_string()),
1555 ("amalgamation_id".to_string(), amalgamation_id),
1556 ])))
1557}
1558
1559#[cfg(feature = "http")]
1561#[utoipa::path(
1562 post,
1563 path = "/v1/genome/amalgamation_by_upload",
1564 tag = "genome",
1565 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1566 responses(
1567 (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
1568 (status = 400, description = "Invalid request"),
1569 (status = 500, description = "Internal server error")
1570 )
1571)]
1572pub async fn post_amalgamation_by_upload(
1573 State(state): State<ApiState>,
1574 mut multipart: Multipart,
1575) -> ApiResult<Json<HashMap<String, String>>> {
1576 let mut genome_json: Option<String> = None;
1577
1578 while let Some(field) = multipart
1579 .next_field()
1580 .await
1581 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1582 {
1583 if field.name() == Some("file") {
1584 let bytes = field.bytes().await.map_err(|e| {
1585 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1586 })?;
1587
1588 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1589 ApiError::invalid_input(format!(
1590 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1591 e
1592 ))
1593 })?;
1594 genome_json = Some(json_str.to_string());
1595 break;
1596 }
1597 }
1598
1599 let json_str =
1600 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1601 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1602
1603 Ok(Json(HashMap::from([
1604 ("message".to_string(), "Amalgamation queued".to_string()),
1605 ("amalgamation_id".to_string(), amalgamation_id),
1606 ])))
1607}
1608
1609#[cfg(feature = "http")]
1611#[utoipa::path(
1612 post,
1613 path = "/v1/genome/append-file",
1614 tag = "genome",
1615 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1616 responses(
1617 (status = 200, description = "Append processed", body = HashMap<String, String>)
1618 )
1619)]
1620pub async fn post_append_file(
1621 State(_state): State<ApiState>,
1622 mut _multipart: Multipart,
1623) -> ApiResult<Json<HashMap<String, String>>> {
1624 Ok(Json(HashMap::from([(
1625 "message".to_string(),
1626 "Not yet implemented".to_string(),
1627 )])))
1628}
1629
1630#[cfg(feature = "http")]
1632#[utoipa::path(
1633 post,
1634 path = "/v1/genome/upload/file",
1635 tag = "genome",
1636 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1637 responses(
1638 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
1639 (status = 400, description = "Invalid request"),
1640 (status = 500, description = "Internal server error")
1641 )
1642)]
1643pub async fn post_upload_file(
1644 State(state): State<ApiState>,
1645 mut multipart: Multipart,
1646) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1647 let mut genome_json: Option<String> = None;
1648
1649 while let Some(field) = multipart
1650 .next_field()
1651 .await
1652 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1653 {
1654 if field.name() == Some("file") {
1655 let bytes = field.bytes().await.map_err(|e| {
1656 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1657 })?;
1658
1659 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1660 ApiError::invalid_input(format!(
1661 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1662 e
1663 ))
1664 })?;
1665 genome_json = Some(json_str.to_string());
1666 break;
1667 }
1668 }
1669
1670 let json_str =
1671 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1672
1673 let genome_info =
1674 load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
1675 .await?;
1676
1677 let mut response = HashMap::new();
1678 response.insert("success".to_string(), serde_json::json!(true));
1679 response.insert(
1680 "message".to_string(),
1681 serde_json::json!("Genome uploaded successfully"),
1682 );
1683 response.insert(
1684 "cortical_area_count".to_string(),
1685 serde_json::json!(genome_info.cortical_area_count),
1686 );
1687 response.insert(
1688 "brain_region_count".to_string(),
1689 serde_json::json!(genome_info.brain_region_count),
1690 );
1691
1692 Ok(Json(response))
1693}
1694
1695#[cfg(feature = "http")]
1697#[utoipa::path(
1698 post,
1699 path = "/v1/genome/upload/file/edit",
1700 tag = "genome",
1701 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1702 responses(
1703 (status = 200, description = "Upload processed", body = HashMap<String, String>)
1704 )
1705)]
1706pub async fn post_upload_file_edit(
1707 State(_state): State<ApiState>,
1708 mut _multipart: Multipart,
1709) -> ApiResult<Json<HashMap<String, String>>> {
1710 Ok(Json(HashMap::from([(
1711 "message".to_string(),
1712 "Not yet implemented".to_string(),
1713 )])))
1714}
1715
1716#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
1718pub async fn post_upload_string(
1719 State(_state): State<ApiState>,
1720 Json(_req): Json<String>,
1721) -> ApiResult<Json<HashMap<String, String>>> {
1722 Ok(Json(HashMap::from([(
1723 "message".to_string(),
1724 "Not yet implemented".to_string(),
1725 )])))
1726}