1use crate::llm_agent::AgentBackend;
27use crate::profiles::CapabilityProfile;
28use crate::{q_hash, NQuin};
29use serde::{Deserialize, Serialize};
30
31const INLINE_TAG_INTEGER: u64 = 0x1u64 << 60;
33
34const CTX_LLM: u64 = q_hash("catalog:llm");
36const CTX_ONT: u64 = q_hash("catalog:ontology");
37const CTX_PROV: u64 = q_hash("catalog:provenance");
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct DownloadInfo {
44 #[serde(rename = "type")]
46 pub download_type: String,
47 pub repo: Option<String>,
49 pub file: Option<String>,
51 pub url: Option<String>,
53 pub path: Option<String>,
55}
56
57impl DownloadInfo {
58 pub fn resolved_url(&self) -> Option<String> {
60 match self.download_type.as_str() {
61 "huggingface" => {
62 let repo = self.repo.as_deref()?;
63 let file = self.file.as_deref()?;
64 Some(format!(
65 "https://huggingface.co/{}/resolve/main/{}",
66 repo, file
67 ))
68 }
69 "github" => {
70 let repo = self.repo.as_deref()?;
71 let path = self.path.as_deref()?;
72 let path = path.trim_start_matches('/');
73 Some(format!(
74 "https://raw.githubusercontent.com/{}/main/{}",
75 repo, path
76 ))
77 }
78 _ => self.url.clone(),
79 }
80 }
81
82 pub fn local_filename(&self) -> Option<String> {
84 if let Some(ref f) = self.file {
85 return Some(f.clone());
86 }
87 self.url
88 .as_ref()
89 .and_then(|u| u.split('/').last().map(|s| s.to_string()))
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct LLMResource {
98 pub id: String,
99 pub name: String,
100 pub provider: Option<String>,
101 pub format: String,
103 pub quantization: Option<String>,
105 pub size_mb: Option<u32>,
107 pub download: DownloadInfo,
108 pub license: Option<String>,
109 pub recommended_for: Option<Vec<String>>,
110 pub ram_estimate_mb: Option<u32>,
112 pub tags: Option<Vec<String>>,
113 pub last_verified: Option<String>,
114 pub notes: Option<String>,
115 pub modality: Option<String>,
117 pub vision_projector: Option<DownloadInfo>,
119 pub architecture: Option<String>,
121 pub context_window: Option<u32>,
123}
124
125impl LLMResource {
126 pub fn is_multimodal(&self) -> bool {
127 matches!(self.modality.as_deref(), Some("multimodal" | "vision"))
128 || self.vision_projector.is_some()
129 }
130
131 pub fn effective_context_window(&self) -> u32 {
132 self.context_window
133 .unwrap_or(if self.is_multimodal() { 8192 } else { 4096 })
134 }
135 pub fn to_quins(&self) -> Vec<NQuin> {
140 let subject = q_hash(&format!("llm:{}", self.id));
141 let mut out = Vec::with_capacity(8);
142
143 out.push(Self::quin(
145 subject,
146 q_hash("llm:hasFormat"),
147 q_hash(&self.format),
148 CTX_LLM,
149 ));
150
151 if let Some(ref q) = self.quantization {
153 out.push(Self::quin(
154 subject,
155 q_hash("llm:hasQuantization"),
156 q_hash(q),
157 CTX_LLM,
158 ));
159 }
160
161 if let Some(sz) = self.size_mb {
163 out.push(Self::quin(
164 subject,
165 q_hash("llm:hasSizeMb"),
166 INLINE_TAG_INTEGER | sz as u64,
167 CTX_LLM,
168 ));
169 }
170
171 if let Some(ram) = self.ram_estimate_mb {
173 out.push(Self::quin(
174 subject,
175 q_hash("llm:hasRamEstimateMb"),
176 INLINE_TAG_INTEGER | ram as u64,
177 CTX_LLM,
178 ));
179 }
180
181 if let Some(ref repo) = self.download.repo {
183 out.push(Self::quin(
184 subject,
185 q_hash("llm:hasSourceRepo"),
186 q_hash(&format!("hf:{}", repo)),
187 CTX_LLM,
188 ));
189 }
190
191 if let Some(ref lic) = self.license {
193 out.push(Self::quin(
194 subject,
195 q_hash("llm:hasLicense"),
196 q_hash(&format!("license:{}", lic)),
197 CTX_LLM,
198 ));
199 }
200
201 if let Some(ref prov) = self.provider {
203 out.push(Self::quin(
204 subject,
205 q_hash("llm:hasProvider"),
206 q_hash(prov),
207 CTX_LLM,
208 ));
209 }
210
211 if self.is_multimodal() {
212 out.push(Self::quin(
213 subject,
214 q_hash("llm:hasModality"),
215 q_hash("multimodal"),
216 CTX_LLM,
217 ));
218 }
219
220 if let Some(ref arch) = self.architecture {
221 out.push(Self::quin(
222 subject,
223 q_hash("llm:hasArchitecture"),
224 q_hash(arch),
225 CTX_LLM,
226 ));
227 }
228
229 out
230 }
231
232 pub fn provenance_quin(&self, timestamp_unix: u64, local_path: &str) -> NQuin {
236 let subject = q_hash(&format!("download:{}", self.id));
237 let predicate = q_hash("prov:wasGeneratedBy");
238 let object = q_hash(local_path);
239 let context = CTX_PROV;
240 let metadata = timestamp_unix & 0xFFFF_FFFF;
242 let parity = subject ^ predicate ^ object ^ context ^ metadata;
243 NQuin {
244 subject,
245 predicate,
246 object,
247 context,
248 metadata,
249 parity,
250 }
251 }
252
253 pub fn source_url_quin(&self) -> Option<NQuin> {
255 let url = self.download.resolved_url()?;
256 let subject = q_hash(&format!("download:{}", self.id));
257 let predicate = q_hash("prov:hadPrimarySource");
258 let object = q_hash(&url);
259 let parity = subject ^ predicate ^ object ^ CTX_PROV;
260 Some(NQuin {
261 subject,
262 predicate,
263 object,
264 context: CTX_PROV,
265 metadata: 0,
266 parity,
267 })
268 }
269
270 pub fn to_capability_profile(&self, local_path: &str) -> CapabilityProfile {
275 self.to_capability_profile_with_projector(local_path, None)
276 }
277
278 pub fn to_capability_profile_with_projector(
279 &self,
280 local_path: &str,
281 vision_projector_path: Option<&str>,
282 ) -> CapabilityProfile {
283 let modality = if self.is_multimodal() {
284 "multimodal".to_string()
285 } else {
286 "text".to_string()
287 };
288 CapabilityProfile {
289 profile_id: q_hash(&format!("profile:{}", self.id)),
290 active_engines: vec![],
291 loaded_ontologies: vec![],
292 preferred_backend: AgentBackend::Local {
293 model_path: local_path.to_string(),
294 context_window: self.effective_context_window(),
295 quantization: self
296 .quantization
297 .clone()
298 .unwrap_or_else(|| "Q4_K_M".to_string()),
299 vision_projector_path: vision_projector_path.map(|s| s.to_string()),
300 modality,
301 architecture: self.architecture.clone(),
302 },
303 permitted_intent_frames: vec![],
304 }
305 }
306
307 fn quin(subject: u64, predicate: u64, object: u64, context: u64) -> NQuin {
308 let parity = subject ^ predicate ^ object ^ context;
309 NQuin {
310 subject,
311 predicate,
312 object,
313 context,
314 metadata: 0,
315 parity,
316 }
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct OntologyResource {
325 pub id: String,
326 pub name: String,
327 pub acronym: Option<String>,
328 pub source: Option<String>,
329 pub format: String,
330 pub size_estimate_mb: Option<f64>,
332 pub download: DownloadInfo,
333 pub license: Option<String>,
334 pub domain: Option<String>,
335 pub tags: Option<Vec<String>>,
336 pub import_strategy: Option<String>,
337 pub last_verified: Option<String>,
338 pub notes: Option<String>,
339}
340
341impl OntologyResource {
342 pub fn to_quins(&self) -> Vec<NQuin> {
344 let subject = q_hash(&format!("ont:{}", self.id));
345 let mut out = Vec::with_capacity(5);
346
347 out.push(Self::quin(
348 subject,
349 q_hash("ont:hasFormat"),
350 q_hash(&self.format),
351 CTX_ONT,
352 ));
353
354 if let Some(ref domain) = self.domain {
355 out.push(Self::quin(
356 subject,
357 q_hash("ont:hasDomain"),
358 q_hash(domain),
359 CTX_ONT,
360 ));
361 }
362
363 if let Some(sz) = self.size_estimate_mb {
364 let mb = sz.ceil().max(0.0) as u64;
365 out.push(Self::quin(
366 subject,
367 q_hash("ont:hasSizeMb"),
368 INLINE_TAG_INTEGER | mb,
369 CTX_ONT,
370 ));
371 }
372
373 if let Some(ref lic) = self.license {
374 out.push(Self::quin(
375 subject,
376 q_hash("ont:hasLicense"),
377 q_hash(&format!("license:{}", lic)),
378 CTX_ONT,
379 ));
380 }
381
382 if let Some(ref src) = self.source {
383 out.push(Self::quin(
384 subject,
385 q_hash("ont:hasSource"),
386 q_hash(src),
387 CTX_ONT,
388 ));
389 }
390
391 out
392 }
393
394 pub fn provenance_quin(&self, timestamp_unix: u64, local_path: &str) -> NQuin {
396 let subject = q_hash(&format!("import:{}", self.id));
397 let predicate = q_hash("prov:wasGeneratedBy");
398 let object = q_hash(local_path);
399 let metadata = timestamp_unix & 0xFFFF_FFFF;
400 let parity = subject ^ predicate ^ object ^ CTX_PROV ^ metadata;
401 NQuin {
402 subject,
403 predicate,
404 object,
405 context: CTX_PROV,
406 metadata,
407 parity,
408 }
409 }
410
411 fn quin(subject: u64, predicate: u64, object: u64, context: u64) -> NQuin {
412 let parity = subject ^ predicate ^ object ^ context;
413 NQuin {
414 subject,
415 predicate,
416 object,
417 context,
418 metadata: 0,
419 parity,
420 }
421 }
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct SPARQLResource {
429 pub id: String,
430 pub name: String,
431 pub endpoint: String,
432 pub gui: Option<String>,
433 pub maintainer: Option<String>,
434 pub reliability: Option<String>,
435 pub domains: Option<Vec<String>>,
436 pub rate_limit: Option<String>,
437 pub federation_supported: Option<bool>,
438 pub example_queries: Option<Vec<ExampleQuery>>,
439 pub last_verified: Option<String>,
440 pub notes: Option<String>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct ExampleQuery {
445 pub description: Option<String>,
446 pub query: Option<String>,
447}
448
449pub struct ResourceCatalog {
453 pub llms: Vec<LLMResource>,
454 pub ontologies: Vec<OntologyResource>,
455 pub sparql_endpoints: Vec<SPARQLResource>,
456}
457
458impl ResourceCatalog {
459 pub fn empty() -> Self {
460 Self {
461 llms: vec![],
462 ontologies: vec![],
463 sparql_endpoints: vec![],
464 }
465 }
466
467 pub fn find_llm(&self, id: &str) -> Option<&LLMResource> {
468 self.llms.iter().find(|r| r.id == id)
469 }
470
471 pub fn find_ontology(&self, id: &str) -> Option<&OntologyResource> {
472 self.ontologies.iter().find(|r| r.id == id)
473 }
474
475 pub fn find_sparql(&self, id: &str) -> Option<&SPARQLResource> {
476 self.sparql_endpoints.iter().find(|r| r.id == id)
477 }
478
479 pub fn summary_json(&self) -> String {
481 #[derive(Serialize)]
482 struct Summary {
483 llm_count: usize,
484 ontology_count: usize,
485 sparql_count: usize,
486 }
487 serde_json::to_string(&Summary {
488 llm_count: self.llms.len(),
489 ontology_count: self.ontologies.len(),
490 sparql_count: self.sparql_endpoints.len(),
491 })
492 .unwrap_or_else(|_| "{}".to_string())
493 }
494}
495
496use std::path::{Path, PathBuf};
499
500#[derive(Debug, Clone, PartialEq, Eq)]
502pub enum CatalogError {
503 Io { path: PathBuf, message: String },
504 Parse { path: PathBuf, message: String },
505 Index { message: String },
506}
507
508impl std::fmt::Display for CatalogError {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 match self {
511 CatalogError::Io { path, message } => {
512 write!(f, "cannot read {}: {}", path.display(), message)
513 }
514 CatalogError::Parse { path, message } => {
515 write!(f, "{}: {}", path.display(), message)
516 }
517 CatalogError::Index { message } => write!(f, "catalog.yaml: {}", message),
518 }
519 }
520}
521
522impl std::error::Error for CatalogError {}
523
524#[derive(Debug, Deserialize)]
525struct CatalogRoot {
526 catalog: CatalogMeta,
527}
528
529#[derive(Debug, Deserialize)]
530struct CatalogMeta {
531 sources: CatalogSources,
532}
533
534#[derive(Debug, Deserialize)]
535struct CatalogSources {
536 llms: String,
537 ontologies: String,
538 sparql_endpoints: String,
539}
540
541#[derive(Debug, Deserialize)]
542struct LlmsFile {
543 llms: Vec<LLMResource>,
544}
545
546#[derive(Debug, Deserialize)]
547struct OntologiesFile {
548 ontologies: Vec<OntologyResource>,
549}
550
551#[derive(Debug, Deserialize)]
552struct SparqlFile {
553 sparql_endpoints: Vec<SPARQLResource>,
554}
555
556fn read_yaml<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T, CatalogError> {
557 let raw = std::fs::read_to_string(path).map_err(|e| CatalogError::Io {
558 path: path.to_path_buf(),
559 message: e.to_string(),
560 })?;
561 serde_yaml::from_str(&raw).map_err(|e| CatalogError::Parse {
562 path: path.to_path_buf(),
563 message: e.to_string(),
564 })
565}
566
567pub fn resolve_resources_dir() -> PathBuf {
571 if let Ok(extra) = std::env::var("QUALIA_RESOURCES_DIR") {
572 return PathBuf::from(extra);
573 }
574
575 if let Ok(exe) = std::env::current_exe() {
576 if let Some(root) = exe.parent() {
577 for rel in ["bundled/resources", "resources", "bundled"] {
578 let candidate = root.join(rel);
579 if candidate.join("catalog.yaml").is_file() {
580 return candidate;
581 }
582 }
583 }
584 }
585
586 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../resources")
587}
588
589pub fn load_from_dir(dir: &Path) -> Result<ResourceCatalog, CatalogError> {
591 let index_path = dir.join("catalog.yaml");
592 let index: CatalogRoot = read_yaml(&index_path)?;
593
594 let llms_path = dir.join(&index.catalog.sources.llms);
595 let ont_path = dir.join(&index.catalog.sources.ontologies);
596 let sparql_path = dir.join(&index.catalog.sources.sparql_endpoints);
597
598 let llms_file: LlmsFile = read_yaml(&llms_path)?;
599 let ont_file: OntologiesFile = read_yaml(&ont_path)?;
600 let sparql_file: SparqlFile = read_yaml(&sparql_path)?;
601
602 Ok(ResourceCatalog {
603 llms: llms_file.llms,
604 ontologies: ont_file.ontologies,
605 sparql_endpoints: sparql_file.sparql_endpoints,
606 })
607}
608
609pub fn load_default() -> Result<ResourceCatalog, CatalogError> {
611 load_from_dir(&resolve_resources_dir())
612}
613
614#[cfg(test)]
615mod load_tests {
616 use super::*;
617
618 fn resources_fixture_dir() -> PathBuf {
619 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../resources")
620 }
621
622 #[test]
623 fn loads_all_entries_from_committed_yaml() {
624 let dir = resources_fixture_dir();
625 let cat = load_from_dir(&dir).expect("catalog should load");
626 assert!(
627 cat.llms.len() >= 10,
628 "expected >=10 LLMs, got {}",
629 cat.llms.len()
630 );
631 let multimodal = cat.llms.iter().filter(|m| m.is_multimodal()).count();
632 assert!(
633 multimodal >= 3,
634 "expected >=3 multimodal LLMs, got {multimodal}"
635 );
636 assert!(
637 cat.ontologies.len() >= 12,
638 "expected >=12 ontologies, got {}",
639 cat.ontologies.len()
640 );
641 assert!(
642 cat.sparql_endpoints.len() >= 3,
643 "expected >=3 SPARQL endpoints, got {}",
644 cat.sparql_endpoints.len()
645 );
646 }
647
648 #[test]
649 fn github_download_resolves_raw_url() {
650 let info = DownloadInfo {
651 download_type: "github".to_string(),
652 repo: Some("schemaorg/schemaorg".to_string()),
653 file: None,
654 url: None,
655 path: Some("data/releases/27.0/schemaorg-all-https.rdf".to_string()),
656 };
657 let url = info.resolved_url().expect("github url");
658 assert!(url.contains("raw.githubusercontent.com/schemaorg/schemaorg"));
659 assert!(url.contains("schemaorg-all-https.rdf"));
660 }
661
662 #[test]
663 fn find_llm_by_id() {
664 let cat = load_from_dir(&resources_fixture_dir()).unwrap();
665 assert!(cat.find_llm("phi-3-mini-4k-instruct-q4km").is_some());
666 }
667}