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