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 }));
569 }
570
571 Ok(Json(HashMap::from([
572 (
573 "message".to_string(),
574 serde_json::Value::String("Amalgamation confirmed".to_string()),
575 ),
576 (
577 "brain_regions".to_string(),
578 serde_json::Value::Array(brain_regions),
579 ),
580 (
581 "skipped_existing_areas".to_string(),
582 serde_json::json!(skipped_existing),
583 ),
584 ])))
585}
586
587#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
589pub async fn delete_amalgamation_cancellation(
590 State(state): State<ApiState>,
591) -> ApiResult<Json<HashMap<String, String>>> {
592 let mut lock = state.amalgamation_state.write();
593 if let Some(pending) = lock.pending.take() {
594 let now_ms = std::time::SystemTime::now()
595 .duration_since(std::time::UNIX_EPOCH)
596 .map(|d| d.as_millis() as i64)
597 .unwrap_or(0);
598 lock.history.push(amalgamation::AmalgamationHistoryEntry {
599 amalgamation_id: pending.summary.amalgamation_id,
600 genome_title: pending.summary.genome_title,
601 circuit_size: pending.summary.circuit_size,
602 status: "cancelled".to_string(),
603 timestamp_ms: now_ms,
604 });
605
606 tracing::info!(
607 target: "feagi-api",
608 "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
609 lock.history
610 .last()
611 .map(|e| e.amalgamation_id.clone())
612 .unwrap_or_else(|| "<unknown>".to_string())
613 );
614 }
615 Ok(Json(HashMap::from([(
616 "message".to_string(),
617 "Amalgamation cancelled".to_string(),
618 )])))
619}
620
621#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
623pub async fn post_genome_append(
624 State(_state): State<ApiState>,
625 Json(_req): Json<HashMap<String, serde_json::Value>>,
626) -> ApiResult<Json<HashMap<String, String>>> {
627 Err(ApiError::internal("Not yet implemented"))
628}
629
630#[utoipa::path(
632 post,
633 path = "/v1/genome/upload/barebones",
634 responses(
635 (status = 200, description = "Barebones genome loaded successfully"),
636 (status = 500, description = "Failed to load genome")
637 ),
638 tag = "genome"
639)]
640pub async fn post_upload_barebones_genome(
641 State(state): State<ApiState>,
642) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
643 tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
644 let result = load_default_genome(state, "barebones").await;
645 match &result {
646 Ok(_) => {
647 tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
648 }
649 Err(e) => {
650 tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
651 }
652 }
653 result
654}
655
656#[utoipa::path(
658 post,
659 path = "/v1/genome/upload/essential",
660 responses(
661 (status = 200, description = "Essential genome loaded successfully"),
662 (status = 500, description = "Failed to load genome")
663 ),
664 tag = "genome"
665)]
666pub async fn post_upload_essential_genome(
667 State(state): State<ApiState>,
668) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
669 load_default_genome(state, "essential").await
670}
671
672async fn load_default_genome(
674 state: ApiState,
675 genome_name: &str,
676) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
677 tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
678 tracing::debug!(target: "feagi-api", " State components available: genome_service=true, runtime_service=true");
679 let genome_json = match genome_name {
681 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
682 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
683 "test" => feagi_evolutionary::TEST_GENOME_JSON,
684 "vision" => feagi_evolutionary::VISION_GENOME_JSON,
685 _ => {
686 return Err(ApiError::invalid_input(format!(
687 "Unknown genome name '{}'. Available: barebones, essential, test, vision",
688 genome_name
689 )))
690 }
691 };
692
693 tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
694 genome_name, genome_json.len());
695
696 let params = LoadGenomeParams {
697 json_str: genome_json.to_string(),
698 };
699
700 tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
701 let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
702
703 tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
704 genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
705
706 let mut response = HashMap::new();
708 response.insert("success".to_string(), serde_json::Value::Bool(true));
709 response.insert(
710 "message".to_string(),
711 serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
712 );
713 response.insert(
714 "cortical_area_count".to_string(),
715 serde_json::Value::Number(genome_info.cortical_area_count.into()),
716 );
717 response.insert(
718 "brain_region_count".to_string(),
719 serde_json::Value::Number(genome_info.brain_region_count.into()),
720 );
721 response.insert(
722 "genome_id".to_string(),
723 serde_json::Value::String(genome_info.genome_id),
724 );
725 response.insert(
726 "genome_title".to_string(),
727 serde_json::Value::String(genome_info.genome_title),
728 );
729
730 Ok(Json(response))
731}
732
733#[utoipa::path(
735 get,
736 path = "/v1/genome/name",
737 tag = "genome",
738 responses(
739 (status = 200, description = "Genome name", body = String)
740 )
741)]
742pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
743 Ok(Json("default_genome".to_string()))
746}
747
748#[utoipa::path(
750 get,
751 path = "/v1/genome/timestamp",
752 tag = "genome",
753 responses(
754 (status = 200, description = "Genome timestamp", body = i64)
755 )
756)]
757pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
758 Ok(Json(0))
760}
761
762#[utoipa::path(
764 post,
765 path = "/v1/genome/save",
766 tag = "genome",
767 responses(
768 (status = 200, description = "Genome saved", body = HashMap<String, String>)
769 )
770)]
771pub async fn post_save(
772 State(state): State<ApiState>,
773 Json(request): Json<HashMap<String, String>>,
774) -> ApiResult<Json<HashMap<String, String>>> {
775 use std::fs;
776 use std::path::Path;
777
778 info!("Saving genome to file");
779
780 let genome_id = request.get("genome_id").cloned();
782 let genome_title = request.get("genome_title").cloned();
783 let file_path = request.get("file_path").cloned();
784
785 let params = feagi_services::SaveGenomeParams {
787 genome_id,
788 genome_title,
789 };
790
791 let genome_service = state.genome_service.as_ref();
793 let genome_json = genome_service
794 .save_genome(params)
795 .await
796 .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
797
798 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
800 let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
801 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
802 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
803 let genome_json = serde_json::to_string_pretty(&genome_value)
804 .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
805
806 let save_path = if let Some(path) = file_path {
808 std::path::PathBuf::from(path)
809 } else {
810 let timestamp = std::time::SystemTime::now()
812 .duration_since(std::time::UNIX_EPOCH)
813 .unwrap()
814 .as_secs();
815 std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
816 };
817
818 if let Some(parent) = Path::new(&save_path).parent() {
820 fs::create_dir_all(parent)
821 .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
822 }
823
824 fs::write(&save_path, genome_json)
826 .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
827
828 info!("✅ Genome saved successfully to: {}", save_path.display());
829
830 Ok(Json(HashMap::from([
831 (
832 "message".to_string(),
833 "Genome saved successfully".to_string(),
834 ),
835 ("file_path".to_string(), save_path.display().to_string()),
836 ])))
837}
838
839#[utoipa::path(
841 post,
842 path = "/v1/genome/load",
843 tag = "genome",
844 responses(
845 (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
846 )
847)]
848pub async fn post_load(
849 State(state): State<ApiState>,
850 Json(request): Json<HashMap<String, String>>,
851) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
852 let genome_name = request
853 .get("genome_name")
854 .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
855
856 let params = feagi_services::LoadGenomeParams {
858 json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
859 };
860
861 let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
862
863 let mut response = HashMap::new();
864 response.insert(
865 "message".to_string(),
866 serde_json::json!("Genome loaded successfully"),
867 );
868 response.insert(
869 "genome_title".to_string(),
870 serde_json::json!(genome_info.genome_title),
871 );
872
873 Ok(Json(response))
874}
875
876#[utoipa::path(
878 post,
879 path = "/v1/genome/upload",
880 tag = "genome",
881 responses(
882 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
883 )
884)]
885pub async fn post_upload(
886 State(state): State<ApiState>,
887 Json(genome_json): Json<serde_json::Value>,
888) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
889 let json_str = serde_json::to_string(&genome_json)
891 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
892
893 let params = LoadGenomeParams { json_str };
894 let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
895
896 let mut response = HashMap::new();
897 response.insert("success".to_string(), serde_json::json!(true));
898 response.insert(
899 "message".to_string(),
900 serde_json::json!("Genome uploaded successfully"),
901 );
902 response.insert(
903 "cortical_area_count".to_string(),
904 serde_json::json!(genome_info.cortical_area_count),
905 );
906 response.insert(
907 "brain_region_count".to_string(),
908 serde_json::json!(genome_info.brain_region_count),
909 );
910
911 Ok(Json(response))
912}
913
914#[utoipa::path(
916 get,
917 path = "/v1/genome/download",
918 tag = "genome",
919 responses(
920 (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
921 )
922)]
923pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
924 info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
925 let genome_service = state.genome_service.as_ref();
926
927 let genome_json_str = genome_service
929 .save_genome(feagi_services::types::SaveGenomeParams {
930 genome_id: None,
931 genome_title: None,
932 })
933 .await
934 .map_err(|e| {
935 tracing::error!("Failed to export genome: {}", e);
936 ApiError::internal(format!("Failed to export genome: {}", e))
937 })?;
938
939 let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
941 .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
942
943 let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
945 let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
946
947 info!(
948 "✅ Genome download complete, {} bytes",
949 genome_json_str.len()
950 );
951 Ok(Json(genome_value))
952}
953
954#[cfg(test)]
955mod tests {
956 use super::*;
957 use serde_json::json;
958
959 #[test]
960 fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
961 let genome = json!({
962 "version": "3.0",
963 "physiology": {
964 "simulation_timestep": 0.025,
965 "max_age": 10000000
966 }
967 });
968
969 let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
970 assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
971 assert_eq!(updated["physiology"]["max_age"], json!(10000000));
972 }
973
974 #[test]
975 fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
976 let genome = json!({ "version": "3.0" });
977 let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
978 assert!(format!("{err:?}").contains("physiology"));
979 }
980}
981
982#[utoipa::path(
984 get,
985 path = "/v1/genome/properties",
986 tag = "genome",
987 responses(
988 (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
989 )
990)]
991pub async fn get_properties(
992 State(_state): State<ApiState>,
993) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
994 Ok(Json(HashMap::new()))
996}
997
998#[utoipa::path(
1000 post,
1001 path = "/v1/genome/validate",
1002 tag = "genome",
1003 responses(
1004 (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1005 )
1006)]
1007pub async fn post_validate(
1008 State(_state): State<ApiState>,
1009 Json(_genome): Json<serde_json::Value>,
1010) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1011 let mut response = HashMap::new();
1013 response.insert("valid".to_string(), serde_json::json!(true));
1014 response.insert("errors".to_string(), serde_json::json!([]));
1015 response.insert("warnings".to_string(), serde_json::json!([]));
1016
1017 Ok(Json(response))
1018}
1019
1020#[utoipa::path(
1022 post,
1023 path = "/v1/genome/transform",
1024 tag = "genome",
1025 responses(
1026 (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1027 )
1028)]
1029pub async fn post_transform(
1030 State(_state): State<ApiState>,
1031 Json(_request): Json<HashMap<String, serde_json::Value>>,
1032) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1033 let mut response = HashMap::new();
1035 response.insert(
1036 "message".to_string(),
1037 serde_json::json!("Genome transformation not yet implemented"),
1038 );
1039
1040 Ok(Json(response))
1041}
1042
1043#[utoipa::path(
1045 post,
1046 path = "/v1/genome/clone",
1047 tag = "genome",
1048 responses(
1049 (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1050 )
1051)]
1052pub async fn post_clone(
1053 State(_state): State<ApiState>,
1054 Json(_request): Json<HashMap<String, String>>,
1055) -> ApiResult<Json<HashMap<String, String>>> {
1056 Ok(Json(HashMap::from([(
1058 "message".to_string(),
1059 "Genome cloning not yet implemented".to_string(),
1060 )])))
1061}
1062
1063#[utoipa::path(
1066 post,
1067 path = "/v1/genome/reset",
1068 tag = "genome",
1069 responses(
1070 (status = 200, description = "Genome reset", body = HashMap<String, String>),
1071 (status = 409, description = "Genome transition in progress"),
1072 (status = 500, description = "Reset failed")
1073 )
1074)]
1075pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1076 let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1077 ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1078 })?;
1079
1080 let genome_service = state.genome_service.as_ref();
1081 genome_service.reset_connectome().await.map_err(|e| {
1082 tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1083 ApiError::internal(format!("Genome reset failed: {}", e))
1084 })?;
1085
1086 info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1087 Ok(Json(HashMap::from([(
1088 "message".to_string(),
1089 "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1090 )])))
1091}
1092
1093#[utoipa::path(
1095 get,
1096 path = "/v1/genome/metadata",
1097 tag = "genome",
1098 responses(
1099 (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1100 )
1101)]
1102pub async fn get_metadata(
1103 State(state): State<ApiState>,
1104) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1105 get_properties(State(state)).await
1106}
1107
1108#[utoipa::path(
1110 post,
1111 path = "/v1/genome/merge",
1112 tag = "genome",
1113 responses(
1114 (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1115 )
1116)]
1117pub async fn post_merge(
1118 State(_state): State<ApiState>,
1119 Json(_request): Json<HashMap<String, serde_json::Value>>,
1120) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1121 let mut response = HashMap::new();
1123 response.insert(
1124 "message".to_string(),
1125 serde_json::json!("Genome merging not yet implemented"),
1126 );
1127
1128 Ok(Json(response))
1129}
1130
1131#[utoipa::path(
1133 get,
1134 path = "/v1/genome/diff",
1135 tag = "genome",
1136 params(
1137 ("genome_a" = String, Query, description = "First genome name"),
1138 ("genome_b" = String, Query, description = "Second genome name")
1139 ),
1140 responses(
1141 (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1142 )
1143)]
1144pub async fn get_diff(
1145 State(_state): State<ApiState>,
1146 Query(_params): Query<HashMap<String, String>>,
1147) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1148 let mut response = HashMap::new();
1150 response.insert("differences".to_string(), serde_json::json!([]));
1151
1152 Ok(Json(response))
1153}
1154
1155#[utoipa::path(
1157 post,
1158 path = "/v1/genome/export_format",
1159 tag = "genome",
1160 responses(
1161 (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1162 )
1163)]
1164pub async fn post_export_format(
1165 State(_state): State<ApiState>,
1166 Json(_request): Json<HashMap<String, String>>,
1167) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1168 let mut response = HashMap::new();
1170 response.insert(
1171 "message".to_string(),
1172 serde_json::json!("Format export not yet implemented"),
1173 );
1174
1175 Ok(Json(response))
1176}
1177
1178#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1181pub async fn get_amalgamation(
1182 State(state): State<ApiState>,
1183) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1184 let lock = state.amalgamation_state.read();
1185 let mut response = HashMap::new();
1186 if let Some(p) = lock.pending.as_ref() {
1187 response.insert(
1188 "pending".to_string(),
1189 amalgamation::pending_summary_to_health_json(&p.summary),
1190 );
1191 } else {
1192 response.insert("pending".to_string(), serde_json::Value::Null);
1193 }
1194 Ok(Json(response))
1195}
1196
1197#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1199pub async fn get_amalgamation_history_exact(
1200 State(state): State<ApiState>,
1201) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1202 let lock = state.amalgamation_state.read();
1203 let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1204 for entry in &lock.history {
1205 out.push(HashMap::from([
1206 (
1207 "amalgamation_id".to_string(),
1208 serde_json::json!(entry.amalgamation_id),
1209 ),
1210 (
1211 "genome_title".to_string(),
1212 serde_json::json!(entry.genome_title),
1213 ),
1214 (
1215 "circuit_size".to_string(),
1216 serde_json::json!(entry.circuit_size),
1217 ),
1218 ("status".to_string(), serde_json::json!(entry.status)),
1219 (
1220 "timestamp_ms".to_string(),
1221 serde_json::json!(entry.timestamp_ms),
1222 ),
1223 ]));
1224 }
1225 Ok(Json(out))
1226}
1227
1228#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1230pub async fn get_cortical_template(
1231 State(_state): State<ApiState>,
1232) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1233 use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1234 FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1235 };
1236 use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1237 use serde_json::json;
1238
1239 let mut templates = HashMap::new();
1240
1241 let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1246 let (variant, frame, positioning) = match dt {
1247 IOCorticalAreaConfigurationFlag::Boolean => {
1248 ("Boolean", FrameChangeHandling::Absolute, None)
1249 }
1250 IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1251 IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1252 IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1253 IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1254 IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1255 ("SignedPercentage", f, Some(p))
1256 }
1257 IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1258 ("SignedPercentage2D", f, Some(p))
1259 }
1260 IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1261 ("SignedPercentage3D", f, Some(p))
1262 }
1263 IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1264 ("SignedPercentage4D", f, Some(p))
1265 }
1266 IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1267 IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1268 };
1269
1270 let frame_str = match frame {
1271 FrameChangeHandling::Absolute => "Absolute",
1272 FrameChangeHandling::Incremental => "Incremental",
1273 };
1274
1275 let positioning_str = positioning.map(|p| match p {
1276 PercentageNeuronPositioning::Linear => "Linear",
1277 PercentageNeuronPositioning::Fractional => "Fractional",
1278 });
1279
1280 json!({
1281 "variant": variant,
1282 "frame_change_handling": frame_str,
1283 "percentage_positioning": positioning_str,
1284 "config_value": dt.to_data_type_configuration_flag()
1285 })
1286 };
1287
1288 for motor_unit in MotorCorticalUnit::list_all() {
1290 let friendly_name = motor_unit.get_friendly_name();
1291 let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1292 let num_areas = motor_unit.get_number_cortical_areas();
1293 let topology = motor_unit.get_unit_default_topology();
1294
1295 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1306 use serde_json::{Map, Value};
1307 use std::collections::HashMap as StdHashMap;
1308
1309 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1310
1311 for (sub_idx, topo) in topology {
1313 subunits.insert(
1314 sub_idx.get().to_string(),
1315 json!({
1316 "relative_position": topo.relative_position,
1317 "channel_dimensions_default": topo.channel_dimensions_default,
1318 "channel_dimensions_min": topo.channel_dimensions_min,
1319 "channel_dimensions_max": topo.channel_dimensions_max,
1320 "supported_data_types": Vec::<serde_json::Value>::new(),
1321 }),
1322 );
1323 }
1324
1325 let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1327 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1328 Some(allowed) => allowed.to_vec(),
1329 None => vec![
1330 FrameChangeHandling::Absolute,
1331 FrameChangeHandling::Incremental,
1332 ],
1333 };
1334
1335 let positionings = [
1336 PercentageNeuronPositioning::Linear,
1337 PercentageNeuronPositioning::Fractional,
1338 ];
1339
1340 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1341 StdHashMap::new();
1342
1343 for frame in frames {
1344 for positioning in positionings {
1345 let mut map: Map<String, Value> = Map::new();
1346 map.insert(
1347 "frame_change_handling".to_string(),
1348 serde_json::to_value(frame).unwrap_or(Value::Null),
1349 );
1350 map.insert(
1351 "percentage_neuron_positioning".to_string(),
1352 serde_json::to_value(positioning).unwrap_or(Value::Null),
1353 );
1354
1355 let cortical_ids = motor_unit
1357 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1358 CorticalUnitIndex::from(0u8),
1359 map,
1360 );
1361
1362 if let Ok(ids) = cortical_ids {
1363 for (i, id) in ids.into_iter().enumerate() {
1364 if let Ok(flag) = id.extract_io_data_flag() {
1365 let dt_json = data_type_to_json(flag);
1366 let subunit_key = i.to_string();
1367
1368 let dedup_key = format!(
1369 "{}|{}|{}",
1370 dt_json
1371 .get("variant")
1372 .and_then(|v| v.as_str())
1373 .unwrap_or(""),
1374 dt_json
1375 .get("frame_change_handling")
1376 .and_then(|v| v.as_str())
1377 .unwrap_or(""),
1378 dt_json
1379 .get("percentage_positioning")
1380 .and_then(|v| v.as_str())
1381 .unwrap_or("")
1382 );
1383
1384 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1385 if !seen.insert(dedup_key) {
1386 continue;
1387 }
1388
1389 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1390 if let Some(arr) = subunit_obj
1391 .get_mut("supported_data_types")
1392 .and_then(|v| v.as_array_mut())
1393 {
1394 arr.push(dt_json);
1395 }
1396 }
1397 }
1398 }
1399 }
1400 }
1401 }
1402
1403 templates.insert(
1404 format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1405 json!({
1406 "type": "motor",
1407 "friendly_name": friendly_name,
1408 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1409 "number_of_cortical_areas": num_areas,
1410 "subunits": subunits,
1411 "description": format!("Motor output: {}", friendly_name)
1412 }),
1413 );
1414 }
1415
1416 for sensory_unit in SensoryCorticalUnit::list_all() {
1418 let friendly_name = sensory_unit.get_friendly_name();
1419 let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1420 let num_areas = sensory_unit.get_number_cortical_areas();
1421 let topology = sensory_unit.get_unit_default_topology();
1422
1423 use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1424 use serde_json::{Map, Value};
1425 use std::collections::HashMap as StdHashMap;
1426
1427 let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1428
1429 for (sub_idx, topo) in topology {
1430 subunits.insert(
1431 sub_idx.get().to_string(),
1432 json!({
1433 "relative_position": topo.relative_position,
1434 "channel_dimensions_default": topo.channel_dimensions_default,
1435 "channel_dimensions_min": topo.channel_dimensions_min,
1436 "channel_dimensions_max": topo.channel_dimensions_max,
1437 "supported_data_types": Vec::<serde_json::Value>::new(),
1438 }),
1439 );
1440 }
1441
1442 let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1443 let frames: Vec<FrameChangeHandling> = match allowed_frames {
1444 Some(allowed) => allowed.to_vec(),
1445 None => vec![
1446 FrameChangeHandling::Absolute,
1447 FrameChangeHandling::Incremental,
1448 ],
1449 };
1450
1451 let positionings = [
1452 PercentageNeuronPositioning::Linear,
1453 PercentageNeuronPositioning::Fractional,
1454 ];
1455
1456 let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1457 StdHashMap::new();
1458
1459 for frame in frames {
1460 for positioning in positionings {
1461 let mut map: Map<String, Value> = Map::new();
1462 map.insert(
1463 "frame_change_handling".to_string(),
1464 serde_json::to_value(frame).unwrap_or(Value::Null),
1465 );
1466 map.insert(
1467 "percentage_neuron_positioning".to_string(),
1468 serde_json::to_value(positioning).unwrap_or(Value::Null),
1469 );
1470
1471 let cortical_ids = sensory_unit
1472 .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1473 CorticalUnitIndex::from(0u8),
1474 map,
1475 );
1476
1477 if let Ok(ids) = cortical_ids {
1478 for (i, id) in ids.into_iter().enumerate() {
1479 if let Ok(flag) = id.extract_io_data_flag() {
1480 let dt_json = data_type_to_json(flag);
1481 let subunit_key = i.to_string();
1482
1483 let dedup_key = format!(
1484 "{}|{}|{}",
1485 dt_json
1486 .get("variant")
1487 .and_then(|v| v.as_str())
1488 .unwrap_or(""),
1489 dt_json
1490 .get("frame_change_handling")
1491 .and_then(|v| v.as_str())
1492 .unwrap_or(""),
1493 dt_json
1494 .get("percentage_positioning")
1495 .and_then(|v| v.as_str())
1496 .unwrap_or("")
1497 );
1498
1499 let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1500 if !seen.insert(dedup_key) {
1501 continue;
1502 }
1503
1504 if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1505 if let Some(arr) = subunit_obj
1506 .get_mut("supported_data_types")
1507 .and_then(|v| v.as_array_mut())
1508 {
1509 arr.push(dt_json);
1510 }
1511 }
1512 }
1513 }
1514 }
1515 }
1516 }
1517
1518 templates.insert(
1519 format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1520 json!({
1521 "type": "sensory",
1522 "friendly_name": friendly_name,
1523 "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1524 "number_of_cortical_areas": num_areas,
1525 "subunits": subunits,
1526 "description": format!("Sensory input: {}", friendly_name)
1527 }),
1528 );
1529 }
1530
1531 Ok(Json(templates))
1532}
1533
1534#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1536pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1537 Ok(Json(vec![
1538 "barebones".to_string(),
1539 "essential".to_string(),
1540 "test".to_string(),
1541 "vision".to_string(),
1542 ]))
1543}
1544
1545#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
1547pub async fn get_download_region(
1548 State(_state): State<ApiState>,
1549 Query(_params): Query<HashMap<String, String>>,
1550) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1551 Ok(Json(HashMap::new()))
1552}
1553
1554#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
1556pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
1557 Ok(Json(0))
1558}
1559
1560#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
1562pub async fn post_amalgamation_by_filename(
1563 State(state): State<ApiState>,
1564 Json(req): Json<HashMap<String, String>>,
1565) -> ApiResult<Json<HashMap<String, String>>> {
1566 let file_name = req
1570 .get("file_name")
1571 .or_else(|| req.get("filename"))
1572 .or_else(|| req.get("genome_file_name"))
1573 .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
1574
1575 let genome_json = match file_name.as_str() {
1576 "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
1577 "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
1578 "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
1579 "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
1580 other => {
1581 return Err(ApiError::invalid_input(format!(
1582 "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
1583 other
1584 )))
1585 }
1586 };
1587
1588 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
1589
1590 Ok(Json(HashMap::from([
1591 ("message".to_string(), "Amalgamation queued".to_string()),
1592 ("amalgamation_id".to_string(), amalgamation_id),
1593 ])))
1594}
1595
1596#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
1598pub async fn post_amalgamation_by_payload(
1599 State(state): State<ApiState>,
1600 Json(req): Json<serde_json::Value>,
1601) -> ApiResult<Json<HashMap<String, String>>> {
1602 let json_str = serde_json::to_string(&req)
1603 .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1604 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1605
1606 Ok(Json(HashMap::from([
1607 ("message".to_string(), "Amalgamation queued".to_string()),
1608 ("amalgamation_id".to_string(), amalgamation_id),
1609 ])))
1610}
1611
1612#[cfg(feature = "http")]
1614#[utoipa::path(
1615 post,
1616 path = "/v1/genome/amalgamation_by_upload",
1617 tag = "genome",
1618 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1619 responses(
1620 (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
1621 (status = 400, description = "Invalid request"),
1622 (status = 500, description = "Internal server error")
1623 )
1624)]
1625pub async fn post_amalgamation_by_upload(
1626 State(state): State<ApiState>,
1627 mut multipart: Multipart,
1628) -> ApiResult<Json<HashMap<String, String>>> {
1629 let mut genome_json: Option<String> = None;
1630
1631 while let Some(field) = multipart
1632 .next_field()
1633 .await
1634 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1635 {
1636 if field.name() == Some("file") {
1637 let bytes = field.bytes().await.map_err(|e| {
1638 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1639 })?;
1640
1641 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1642 ApiError::invalid_input(format!(
1643 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1644 e
1645 ))
1646 })?;
1647 genome_json = Some(json_str.to_string());
1648 break;
1649 }
1650 }
1651
1652 let json_str =
1653 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1654 let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1655
1656 Ok(Json(HashMap::from([
1657 ("message".to_string(), "Amalgamation queued".to_string()),
1658 ("amalgamation_id".to_string(), amalgamation_id),
1659 ])))
1660}
1661
1662#[cfg(feature = "http")]
1664#[utoipa::path(
1665 post,
1666 path = "/v1/genome/append-file",
1667 tag = "genome",
1668 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1669 responses(
1670 (status = 200, description = "Append processed", body = HashMap<String, String>)
1671 )
1672)]
1673pub async fn post_append_file(
1674 State(_state): State<ApiState>,
1675 mut _multipart: Multipart,
1676) -> ApiResult<Json<HashMap<String, String>>> {
1677 Ok(Json(HashMap::from([(
1678 "message".to_string(),
1679 "Not yet implemented".to_string(),
1680 )])))
1681}
1682
1683#[cfg(feature = "http")]
1685#[utoipa::path(
1686 post,
1687 path = "/v1/genome/upload/file",
1688 tag = "genome",
1689 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1690 responses(
1691 (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
1692 (status = 400, description = "Invalid request"),
1693 (status = 500, description = "Internal server error")
1694 )
1695)]
1696pub async fn post_upload_file(
1697 State(state): State<ApiState>,
1698 mut multipart: Multipart,
1699) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1700 let mut genome_json: Option<String> = None;
1701
1702 while let Some(field) = multipart
1703 .next_field()
1704 .await
1705 .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1706 {
1707 if field.name() == Some("file") {
1708 let bytes = field.bytes().await.map_err(|e| {
1709 ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1710 })?;
1711
1712 let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1713 ApiError::invalid_input(format!(
1714 "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1715 e
1716 ))
1717 })?;
1718 genome_json = Some(json_str.to_string());
1719 break;
1720 }
1721 }
1722
1723 let json_str =
1724 genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1725
1726 let genome_info =
1727 load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
1728 .await?;
1729
1730 let mut response = HashMap::new();
1731 response.insert("success".to_string(), serde_json::json!(true));
1732 response.insert(
1733 "message".to_string(),
1734 serde_json::json!("Genome uploaded successfully"),
1735 );
1736 response.insert(
1737 "cortical_area_count".to_string(),
1738 serde_json::json!(genome_info.cortical_area_count),
1739 );
1740 response.insert(
1741 "brain_region_count".to_string(),
1742 serde_json::json!(genome_info.brain_region_count),
1743 );
1744
1745 Ok(Json(response))
1746}
1747
1748#[cfg(feature = "http")]
1750#[utoipa::path(
1751 post,
1752 path = "/v1/genome/upload/file/edit",
1753 tag = "genome",
1754 request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1755 responses(
1756 (status = 200, description = "Upload processed", body = HashMap<String, String>)
1757 )
1758)]
1759pub async fn post_upload_file_edit(
1760 State(_state): State<ApiState>,
1761 mut _multipart: Multipart,
1762) -> ApiResult<Json<HashMap<String, String>>> {
1763 Ok(Json(HashMap::from([(
1764 "message".to_string(),
1765 "Not yet implemented".to_string(),
1766 )])))
1767}
1768
1769#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
1771pub async fn post_upload_string(
1772 State(_state): State<ApiState>,
1773 Json(_req): Json<String>,
1774) -> ApiResult<Json<HashMap<String, String>>> {
1775 Ok(Json(HashMap::from([(
1776 "message".to_string(),
1777 "Not yet implemented".to_string(),
1778 )])))
1779}