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