1#![allow(non_snake_case)]
4
5use super::*;
6
7use crate::engine::ingestion;
8use crate::engine::q42_compiler;
9use crate::state::*;
10use futures_util::StreamExt;
11use qualia_core_db::ilp_dispatcher::{DispatchResult, HttpIlpTransport, IlpDispatcher};
12use qualia_core_db::rpc::{route_tax_payment, TaxRecipientSuite};
13use serde::Serialize;
14use std::io::Write;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18use sysinfo::System;
19
20pub use crate::setup::{SetupProfile, SetupState};
21
22pub fn get_setup_state() -> Result<SetupState, String> {
23 crate::setup::get_setup_state()
24}
25
26pub fn complete_setup_step(step: String) -> Result<SetupState, String> {
27 crate::setup::complete_setup_step(step)
28}
29
30pub fn update_setup_profile(profile: SetupProfile) -> Result<SetupState, String> {
31 crate::setup::update_setup_profile(profile)
32}
33
34pub fn finish_setup() -> Result<SetupState, String> {
35 crate::setup::finish_setup()
36}
37
38pub fn get_identity_plane() -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
41 crate::identity_plane::get_identity_plane()
42}
43
44pub fn list_apparatus_devices() -> Result<Vec<crate::identity_plane::DeviceRecordPublic>, String> {
45 crate::identity_plane::list_devices()
46}
47
48pub fn export_person_public() -> Result<crate::identity_plane::PersonPublic, String> {
49 crate::identity_plane::export_person_public()
50}
51
52pub fn export_person_transfer_bundle() -> Result<crate::identity_plane::PersonTransferBundle, String>
54{
55 crate::identity_plane::export_person_transfer_bundle()
56}
57
58pub fn import_person_transfer_bundle(
59 bundle: crate::identity_plane::PersonTransferBundle,
60) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
61 crate::identity_plane::import_person_transfer_bundle(bundle)
62}
63
64pub fn register_remote_apparatus_device(
65 device: crate::identity_plane::DeviceRecordPublic,
66) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
67 crate::identity_plane::register_remote_device(device)
68}
69
70pub fn resolve_job_device_placement(
71 target_device_id: Option<String>,
72) -> Result<crate::identity_plane::JobPlacement, String> {
73 crate::identity_plane::resolve_job_placement(target_device_id.as_deref())
74}
75
76pub fn set_local_control_base_url(
77 url: String,
78) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
79 crate::identity_plane::set_local_control_base_url(url)
80}
81
82pub fn list_remote_job_outbox() -> Result<Vec<crate::identity_plane::RemoteOutboxEntry>, String> {
83 crate::identity_plane::list_remote_outbox()
84}
85
86pub fn retry_remote_job_outbox() -> Result<usize, String> {
87 crate::identity_plane::fleet_jobs::retry_remote_outbox()
88}
89
90pub fn mint_person_webid_tls_cert() -> Result<serde_json::Value, String> {
92 let person = crate::identity_plane::PersonPrincipal::load_or_create(None)?;
93 let (cert_pem, key_pem) = qualia_core_db::key_vault::generate_webid_tls_cert_for_seed(
94 &person.ed25519_secret,
95 &person.person_id,
96 )?;
97 let dir = crate::state::app_meta_dir().join("certs");
99 std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
100 std::fs::write(dir.join("person-webid.crt.pem"), &cert_pem).map_err(|e| e.to_string())?;
101 std::fs::write(dir.join("person-webid.key.pem"), &key_pem).map_err(|e| e.to_string())?;
102 Ok(serde_json::json!({
103 "person_id": person.person_id,
104 "cert_pem": cert_pem,
105 "key_pem_path": dir.join("person-webid.key.pem").display().to_string(),
106 "cert_path": dir.join("person-webid.crt.pem").display().to_string(),
107 "note": "Self-signed WebID-TLS style cert; DID is in SAN URI. Treat key as recovery material."
108 }))
109}
110
111pub fn accept_fleet_job_envelope(
112 envelope: crate::identity_plane::FleetJobEnvelope,
113) -> Result<serde_json::Value, String> {
114 let job = crate::identity_plane::accept_fleet_job_envelope(envelope)?;
115 serde_json::to_value(job).map_err(|e| e.to_string())
116}
117
118pub use crate::qpu_oracle::{QpuChatCommandResult, QpuOracleSettings, QpuOracleSettingsInput};
119
120pub fn get_qpu_settings() -> QpuOracleSettings {
121 crate::qpu_oracle::get_qpu_settings()
122}
123
124pub fn is_qpu_feature_unlocked() -> bool {
125 crate::qpu_oracle::is_qpu_feature_unlocked()
126}
127
128pub fn save_qpu_settings(input: QpuOracleSettingsInput) -> Result<QpuOracleSettings, String> {
129 crate::qpu_oracle::save_qpu_settings(input)
130}
131
132pub fn handle_qpu_chat_command(text: String) -> QpuChatCommandResult {
133 crate::qpu_oracle::handle_qpu_chat_command(&text)
134}
135
136pub fn handle_engine_chat_command(text: String) -> QpuChatCommandResult {
137 crate::qpu_pipeline::handle_engine_chat_command(&text)
138}
139
140pub fn profile_energy_circumstance() -> String {
141 let mut sys = System::new_all();
142 sys.refresh_all();
143 let total_mem = sys.total_memory() / 1024 / 1024;
144 let used_mem = sys.used_memory() / 1024 / 1024;
145 format!(
146 "Energy: AC_POWER\nTotal RAM: {} MB\nUsed RAM: {} MB\nSwarm Auth: GRANTED",
147 total_mem, used_mem
148 )
149}
150
151pub fn check_ollama_status() -> bool {
152 std::process::Command::new("ollama")
153 .arg("-v")
154 .output()
155 .map(|o| o.status.success())
156 .unwrap_or(false)
157}
158
159#[derive(Serialize)]
160pub struct HardwareStatus {
161 pub ram_total_gb: f64,
162 pub ram_used_gb: f64,
163 pub vram_estimated_gb: f64,
164}
165
166pub fn get_hardware_status() -> HardwareStatus {
167 let mut sys = System::new_all();
168 sys.refresh_all();
169 let vram_available_gb = {
170 #[cfg(target_os = "windows")]
171 {
172 qualia_core_db::directml_bridge::probe_best_adapter_memory()
173 .map(|memory| {
174 let free = memory.available_local_bytes();
175 let fallback = memory.dedicated_vram_bytes;
176 let bytes = if free > 0 { free } else { fallback };
177 bytes as f64 / 1024.0 / 1024.0 / 1024.0
178 })
179 .unwrap_or(0.0)
180 }
181 #[cfg(not(target_os = "windows"))]
182 {
183 0.0
184 }
185 };
186 HardwareStatus {
187 ram_total_gb: sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0,
188 ram_used_gb: sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0,
189 vram_estimated_gb: vram_available_gb,
190 }
191}
192
193#[derive(Debug, Clone, Serialize)]
195pub struct EngineTelemetryFields {
196 pub thermal_state: String,
197 pub llm_memory_bytes: u64,
198 pub memory_floor_mb: u32,
199 pub model_lifecycle: String,
200 pub kv_cache_used_mb: u32,
201 pub vram_used_mb: u32,
202 pub vram_total_mb: u32,
203 pub npu_used_mb: u32,
204 pub npu_total_mb: u32,
205}
206
207pub fn get_engine_telemetry_fields() -> EngineTelemetryFields {
208 let (vram_used_mb, vram_total_mb) = probe_vram_usage_mb();
209 let (npu_used_mb, npu_total_mb) = probe_npu_usage_mb();
210 EngineTelemetryFields {
211 thermal_state: crate::model_lifecycle::get_thermal_state_label().to_string(),
212 llm_memory_bytes: crate::model_lifecycle::get_llm_memory_bytes(),
213 memory_floor_mb: crate::model_lifecycle::MEMORY_FLOOR_MB,
214 model_lifecycle: crate::model_lifecycle::lifecycle_label(
215 crate::model_lifecycle::get_model_lifecycle_state(),
216 )
217 .to_string(),
218 kv_cache_used_mb: crate::model_lifecycle::get_kv_cache_used_mb(),
219 vram_used_mb,
220 vram_total_mb,
221 npu_used_mb,
222 npu_total_mb,
223 }
224}
225
226fn probe_vram_usage_mb() -> (u32, u32) {
227 #[cfg(target_os = "windows")]
228 {
229 if let Ok(memory) = qualia_core_db::directml_bridge::probe_best_adapter_memory() {
230 let used = memory.local_usage_bytes / (1024 * 1024);
231 let total = memory.local_budget_bytes / (1024 * 1024);
232 return (used as u32, total as u32);
233 }
234 }
235 (0, 0)
236}
237
238fn probe_npu_usage_mb() -> (u32, u32) {
239 #[cfg(target_os = "windows")]
240 {
241 if let Ok(memory) = qualia_core_db::directml_bridge::probe_npu_adapter_memory() {
242 let used = memory.shared_usage_bytes / (1024 * 1024); let total = memory.shared_budget_bytes / (1024 * 1024);
244 return (used as u32, total as u32);
245 }
246 }
247 (0, 0)
248}
249
250pub async fn download_and_vectorize(
251 url: String,
252 filename: String,
253 item_id: String,
254) -> Result<String, String> {
255 let state = crate::state::APP_STATE.get().unwrap();
256 let storage_path = state.config.lock().unwrap().storage_path.clone();
257 let handles = state.download_handles.clone();
258 let active_dl = state.active_downloads.clone();
259
260 let index_dir = PathBuf::from(&storage_path).join("Index");
261 std::fs::create_dir_all(&index_dir).map_err(|e| e.to_string())?;
262 let dest_path = index_dir.join(&filename);
263
264 let cancelled = Arc::new(AtomicBool::new(false));
265 handles
266 .lock()
267 .unwrap()
268 .insert(item_id.clone(), cancelled.clone());
269
270 let response = reqwest::get(&url).await.map_err(|e| {
271 handles.lock().unwrap().remove(&item_id);
272 active_dl.lock().unwrap().remove(&item_id);
273 e.to_string()
274 })?;
275 let total_bytes = response.content_length().unwrap_or(0);
276 let mut dest = std::fs::File::create(&dest_path).map_err(|e| e.to_string())?;
277 let mut stream = response.bytes_stream();
278 let mut downloaded: u64 = 0;
279 let mut last_report = std::time::Instant::now();
280 let mut last_downloaded: u64 = 0;
281
282 while let Some(chunk) = stream.next().await {
283 if cancelled.load(Ordering::Relaxed) {
284 let _ = std::fs::remove_file(&dest_path);
285 let payload = ProgressPayload {
286 id: item_id.clone(),
287 progress: 0.0,
288 downloaded_bytes: downloaded,
289 total_bytes,
290 speed_kbps: 0.0,
291 status: "cancelled".to_string(),
292 };
293 let _ = state.download_events.send(payload.clone());
294 handles.lock().unwrap().remove(&item_id);
295 active_dl.lock().unwrap().remove(&item_id);
296 return Err("Cancelled".to_string());
297 }
298 let chunk = chunk.map_err(|e| e.to_string())?;
299 dest.write_all(&chunk).map_err(|e| e.to_string())?;
300 downloaded += chunk.len() as u64;
301
302 let now = std::time::Instant::now();
303 if now.duration_since(last_report).as_millis() >= 200 {
304 let elapsed = now.duration_since(last_report).as_secs_f64().max(0.001);
305 let speed_kbps = ((downloaded - last_downloaded) as f64 / 1024.0) / elapsed;
306 let progress = if total_bytes > 0 {
307 (downloaded as f64 / total_bytes as f64) * 100.0
308 } else {
309 0.0
310 };
311 let payload = ProgressPayload {
312 id: item_id.clone(),
313 progress,
314 downloaded_bytes: downloaded,
315 total_bytes,
316 speed_kbps,
317 status: "downloading".to_string(),
318 };
319 let _ = state.download_events.send(payload.clone());
320 active_dl.lock().unwrap().insert(item_id.clone(), payload);
321 last_report = now;
322 last_downloaded = downloaded;
323 }
324 }
325
326 let processing_payload = ProgressPayload {
327 id: item_id.clone(),
328 progress: 100.0,
329 downloaded_bytes: downloaded,
330 total_bytes,
331 speed_kbps: 0.0,
332 status: "processing".to_string(),
333 };
334 let _ = state.download_events.send(processing_payload.clone());
335 active_dl
336 .lock()
337 .unwrap()
338 .insert(item_id.clone(), processing_payload);
339
340 let _quin_count = crate::resource_import::ingest_local_rdf(
341 &dest_path,
342 &item_id,
343 Path::new(&storage_path),
344 None,
345 )
346 .map_err(|e| e.to_string())?;
347
348 let _ = std::fs::remove_file(&dest_path);
349
350 let done_payload = ProgressPayload {
351 id: item_id.clone(),
352 progress: 100.0,
353 downloaded_bytes: downloaded,
354 total_bytes,
355 speed_kbps: 0.0,
356 status: "complete".to_string(),
357 };
358 let _ = state.download_events.send(done_payload.clone());
359 handles.lock().unwrap().remove(&item_id);
360 active_dl.lock().unwrap().remove(&item_id);
361 Ok("Download and vectorization complete".to_string())
362}
363
364pub async fn download_model(
365 url: String,
366 filename: String,
367 model_id: String,
368) -> Result<String, String> {
369 let state = crate::state::APP_STATE.get().unwrap();
370 let storage_path = state.config.lock().unwrap().storage_path.clone();
371 let handles = state.download_handles.clone();
372 let active_dl = state.active_downloads.clone();
373
374 let models_dir = PathBuf::from(&storage_path).join("Models");
375 std::fs::create_dir_all(&models_dir).map_err(|e| e.to_string())?;
376 let dest_path = models_dir.join(&filename);
377
378 let cancelled = Arc::new(AtomicBool::new(false));
379 handles
380 .lock()
381 .unwrap()
382 .insert(model_id.clone(), cancelled.clone());
383
384 let response = reqwest::get(&url)
385 .await
386 .and_then(reqwest::Response::error_for_status)
387 .map_err(|e| {
388 handles.lock().unwrap().remove(&model_id);
389 active_dl.lock().unwrap().remove(&model_id);
390 e.to_string()
391 })?;
392 let total_bytes = response.content_length().unwrap_or(0);
393 let mut dest = std::fs::File::create(&dest_path).map_err(|e| {
394 handles.lock().unwrap().remove(&model_id);
395 active_dl.lock().unwrap().remove(&model_id);
396 format!("create {}: {e}", dest_path.display())
397 })?;
398 let mut stream = response.bytes_stream();
399 let mut downloaded: u64 = 0;
400 let mut last_report = std::time::Instant::now();
401 let mut last_downloaded: u64 = 0;
402
403 while let Some(chunk) = stream.next().await {
404 if cancelled.load(Ordering::Relaxed) {
405 let _ = std::fs::remove_file(&dest_path);
406 let payload = ProgressPayload {
407 id: model_id.clone(),
408 progress: 0.0,
409 downloaded_bytes: downloaded,
410 total_bytes,
411 speed_kbps: 0.0,
412 status: "cancelled".to_string(),
413 };
414 let _ = state.download_events.send(payload.clone());
415 handles.lock().unwrap().remove(&model_id);
416 active_dl.lock().unwrap().remove(&model_id);
417 return Err("Cancelled".to_string());
418 }
419 let chunk = match chunk {
420 Ok(chunk) => chunk,
421 Err(error) => {
422 drop(dest);
423 let _ = std::fs::remove_file(&dest_path);
424 handles.lock().unwrap().remove(&model_id);
425 active_dl.lock().unwrap().remove(&model_id);
426 return Err(format!("download stream failed: {error}"));
427 }
428 };
429 if let Err(error) = dest.write_all(&chunk) {
430 drop(dest);
431 let _ = std::fs::remove_file(&dest_path);
432 handles.lock().unwrap().remove(&model_id);
433 active_dl.lock().unwrap().remove(&model_id);
434 return Err(format!("write {}: {error}", dest_path.display()));
435 }
436 downloaded += chunk.len() as u64;
437
438 let now = std::time::Instant::now();
439 if now.duration_since(last_report).as_millis() >= 200 {
440 let elapsed = now.duration_since(last_report).as_secs_f64().max(0.001);
441 let speed_kbps = ((downloaded - last_downloaded) as f64 / 1024.0) / elapsed;
442 let progress = if total_bytes > 0 {
443 (downloaded as f64 / total_bytes as f64) * 100.0
444 } else {
445 0.0
446 };
447 let payload = ProgressPayload {
448 id: model_id.clone(),
449 progress,
450 downloaded_bytes: downloaded,
451 total_bytes,
452 speed_kbps,
453 status: "downloading".to_string(),
454 };
455 let _ = state.download_events.send(payload.clone());
456 active_dl.lock().unwrap().insert(model_id.clone(), payload);
457 last_report = now;
458 last_downloaded = downloaded;
459 }
460 }
461
462 let done_payload = ProgressPayload {
463 id: model_id.clone(),
464 progress: 100.0,
465 downloaded_bytes: downloaded,
466 total_bytes,
467 speed_kbps: 0.0,
468 status: "complete".to_string(),
469 };
470 let _ = state.download_events.send(done_payload.clone());
471 handles.lock().unwrap().remove(&model_id);
472 active_dl.lock().unwrap().remove(&model_id);
473 Ok(dest_path.to_string_lossy().to_string())
474}
475
476pub fn cancel_download(id: String) -> Result<(), String> {
477 let state = crate::state::APP_STATE.get().unwrap();
478 if let Some(flag) = state.download_handles.lock().unwrap().get(&id) {
479 flag.store(true, Ordering::Relaxed);
480 }
481 Ok(())
482}
483
484pub fn start_daemon() -> String {
485 "Daemon Started".to_string()
486}
487
488pub fn daemon_status() -> String {
489 let state = crate::state::APP_STATE.get().unwrap();
490 if *state.daemon_running.lock().unwrap() {
491 "running".to_string()
492 } else {
493 "stopped".to_string()
494 }
495}
496
497pub fn get_tax_suite() -> TaxRecipientSuite {
498 let state = crate::state::APP_STATE.get().unwrap();
499 state.tax_suite.lock().unwrap().clone()
500}
501
502pub fn save_tax_suite(suite: TaxRecipientSuite) -> Result<(), String> {
503 let state = crate::state::APP_STATE.get().unwrap();
504 suite.validate()?;
505 let data_dir = state.config.lock().unwrap().storage_path.clone();
506 let path = suite_file_path(&data_dir);
507 if let Some(p) = path.parent() {
508 std::fs::create_dir_all(p).map_err(|e| e.to_string())?;
509 }
510 let json = serde_json::to_string_pretty(&suite).map_err(|e| e.to_string())?;
511 std::fs::write(&path, json).map_err(|e| e.to_string())?;
512 *state.tax_suite.lock().unwrap() = suite;
513 Ok(())
514}
515
516pub fn dispatch_tax_payment(gross_amount_micro_cents: u64) -> Result<DispatchResult, String> {
517 let state = crate::state::APP_STATE.get().unwrap();
518 let suite = state.tax_suite.lock().unwrap().clone();
519 let plan = route_tax_payment(gross_amount_micro_cents, &suite)?;
520 let disp = IlpDispatcher::new(HttpIlpTransport {
521 connector_url: "http://localhost:7770".to_string(),
522 });
523 Ok(disp.dispatch(&plan))
524}
525
526pub fn accept_vault_handshake(did_key: String, _payload: String) -> Result<String, String> {
527 println!("[VC-8] Vault handshake from: {}", did_key);
528 Ok("HANDSHAKE_SUCCESS".to_string())
529}
530
531pub fn receive_vault_job(
532 job_id: String,
533 task_type: String,
534 _data_blob_cbor_ld: Vec<u8>,
535) -> Result<String, String> {
536 println!("[VC-12] Offload job {} type {}", job_id, task_type);
537 if task_type == "LLM_INFERENCE" && check_ollama_status() {
538 Ok("INFERENCE_QUEUED".to_string())
539 } else {
540 Err("UNSUPPORTED_TASK_OR_NO_CAPACITY".to_string())
541 }
542}
543
544pub async fn ingest_pdf(file_name: String) -> Result<ingestion::IngestionResult, String> {
545 let result = ingestion::process_pdf(&file_name)?;
546 q42_compiler::compile_to_q42(&file_name, &result.bookmarks)?;
547 Ok(result)
548}
549
550pub async fn ingest_literature(file_path: String) -> Result<String, String> {
551 let state = crate::state::APP_STATE.get().unwrap();
552 let storage_path = state.config.lock().unwrap().storage_path.clone();
553 let lib_dir = PathBuf::from(&storage_path).join("SemanticLibrary");
554 if !lib_dir.exists() {
555 std::fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?;
556 }
557
558 let source_path = std::path::Path::new(&file_path);
559 let filename = source_path.file_name().unwrap_or_default();
560 let dest_path = lib_dir.join(filename);
561 std::fs::copy(&source_path, &dest_path).map_err(|e| e.to_string())?;
562
563 let text = pdf_extract::extract_text(&dest_path).map_err(|e| e.to_string())?;
564 let preview = if text.len() > 100 {
565 &text[0..100]
566 } else {
567 &text
568 };
569
570 Ok(format!(
571 "Successfully ingested literature: {}. Generated ontology nodes from preview: '{}...'",
572 filename.to_string_lossy(),
573 preview.replace("\n", " ")
574 ))
575}
576
577pub async fn upsert_cmld_definition(term: String, context_did: String) -> Result<String, String> {
578 Ok(format!(
579 "Successfully mapped '{}' to Context: {}",
580 term, context_did
581 ))
582}
583
584pub async fn ingest_ontology(file_name: String) -> Result<serde_json::Value, String> {
585 let state = crate::state::APP_STATE.get().unwrap();
586 let storage_path = state.config.lock().unwrap().storage_path.clone();
587 let index_dir = PathBuf::from(&storage_path).join("Index");
588 let source_path = index_dir.join(&file_name);
589
590 if !source_path.is_file() {
591 return Err(format!(
592 "Ontology source not found in Index/: {}",
593 source_path.display()
594 ));
595 }
596
597 let ontology_id = source_path
598 .file_stem()
599 .and_then(|s| s.to_str())
600 .unwrap_or(&file_name)
601 .to_string();
602
603 let quin_count = crate::resource_import::ingest_local_rdf(
604 &source_path,
605 &ontology_id,
606 Path::new(&storage_path),
607 None,
608 )
609 .map_err(|e| e.to_string())?;
610
611 let q42_path = index_dir.join(format!("{ontology_id}.q42"));
612
613 Ok(serde_json::json!({
614 "status": "success",
615 "file": file_name,
616 "ontology_id": ontology_id,
617 "q42_path": q42_path.to_string_lossy(),
618 "quin_count": quin_count,
619 }))
620}
621
622pub async fn import_catalog_ontology(id: String) -> Result<serde_json::Value, String> {
623 let state = crate::state::APP_STATE.get().unwrap();
624 let storage_path = state.config.lock().unwrap().storage_path.clone();
625 let catalog = load_workspace_catalog();
626
627 let cancelled = Arc::new(AtomicBool::new(false));
628 state
629 .download_handles
630 .lock()
631 .unwrap()
632 .insert(id.clone(), cancelled.clone());
633
634 let progress = crate::resource_import::ImportProgressCtx {
635 id: id.clone(),
636 handles: state.download_handles.clone(),
637 active_downloads: state.active_downloads.clone(),
638 download_events: state.download_events.clone(),
639 };
640
641 let result = crate::resource_import::import_catalog_ontology_with_options(
642 &catalog,
643 &id,
644 Path::new(&storage_path),
645 Some(&progress),
646 true,
647 )
648 .await
649 .map_err(|e| {
650 state.download_handles.lock().unwrap().remove(&id);
651 state.active_downloads.lock().unwrap().remove(&id);
652 e.to_string()
653 })?;
654
655 qualia_core_db::daemon_graph::init_daemon_graph(&storage_path);
656
657 serde_json::to_value(result).map_err(|e| e.to_string())
658}
659
660pub async fn export_to_solid(
661 input_q42_path: String,
662 output_dir_path: String,
663) -> Result<String, String> {
664 qualia_core_db::solid_ldp::SolidExporter::export_to_solid_pod(&input_q42_path, &output_dir_path)
665 .map(|_| format!("Exported to {}", output_dir_path))
666 .map_err(|e| e.to_string())
667}
668
669pub async fn fetch_from_solid_pod(
671 url: String,
672 bearer_token: Option<String>,
673) -> Result<serde_json::Value, String> {
674 let r = qualia_solid_bridge::fetch_resource(&url, bearer_token.as_deref())
675 .await
676 .map_err(|e| e.to_string())?;
677 Ok(serde_json::json!({
678 "url": r.url,
679 "status": r.status,
680 "content_type": r.content_type,
681 "quin_count": r.quin_count,
682 "body": r.body,
683 }))
684}
685
686pub async fn put_to_solid_pod(
688 url: String,
689 body: Vec<u8>,
690 content_type: Option<String>,
691 bearer_token: Option<String>,
692) -> Result<serde_json::Value, String> {
693 let ct = content_type.unwrap_or_else(|| "text/turtle".into());
694 let status = qualia_solid_bridge::put_resource(&url, &body, &ct, bearer_token.as_deref())
695 .await
696 .map_err(|e| e.to_string())?;
697 Ok(serde_json::json!({ "ok": true, "status": status, "url": url }))
698}
699
700pub async fn sync_to_solid_pod(
702 pod_url: String,
703 body_or_path: Option<String>,
704 bearer_token: Option<String>,
705) -> Result<String, String> {
706 let (bytes, ct) = if let Some(ref p) = body_or_path {
707 let path = std::path::Path::new(p);
708 if path.is_file() {
709 let b = std::fs::read(path).map_err(|e| e.to_string())?;
710 let ct = if p.ends_with(".json") || p.ends_with(".jsonld") {
711 "application/ld+json"
712 } else {
713 "text/turtle"
714 };
715 (b, ct.to_string())
716 } else {
717 (p.as_bytes().to_vec(), "text/turtle".into())
718 }
719 } else {
720 let body = format!(
722 "@prefix dcterms: <http://purl.org/dc/terms/> .\n<> dcterms:description \"Qualia sync {}\" .\n",
723 chrono::Utc::now().to_rfc3339()
724 );
725 (body.into_bytes(), "text/turtle".into())
726 };
727 let status = qualia_solid_bridge::put_resource(&pod_url, &bytes, &ct, bearer_token.as_deref())
728 .await
729 .map_err(|e| e.to_string())?;
730 Ok(format!(
731 "Synced to Solid Pod {pod_url} (HTTP {status}, {} bytes)",
732 bytes.len()
733 ))
734}
735
736pub async fn ingest_image(file_path: String) -> Result<serde_json::Value, String> {
737 ingest_image_typed(file_path, "Generic Asset".to_string()).await
738}
739
740pub async fn ingest_image_typed(
741 file_path: String,
742 typology: String,
743) -> Result<serde_json::Value, String> {
744 let state = crate::state::APP_STATE.get().unwrap();
745 let storage = state.config.lock().unwrap().storage_path.clone();
746 let active = load_active_model_record_from_disk();
747 let result = crate::vision_ingest::ingest_image_with_active_record(
748 Path::new(&storage),
749 active,
750 Path::new(&file_path),
751 &typology,
752 )
753 .map_err(|e| e.to_string())?;
754 serde_json::to_value(result).map_err(|e| e.to_string())
755}
756
757pub async fn ingest_image_async(file_path: String, typology: String) -> Result<(), String> {
758 let state = crate::state::APP_STATE.get().unwrap();
759 let storage = state.config.lock().unwrap().storage_path.clone();
760 let active = load_active_model_record_from_disk();
761 tokio::spawn(async move {
762 let _ = crate::vision_ingest::ingest_image_with_active_record(
763 Path::new(&storage),
764 active,
765 Path::new(&file_path),
766 &typology,
767 );
768 });
769 Ok(())
770}