qualia_core_db/inference/
quant_graph_grounding.rs1use std::sync::{Mutex, OnceLock};
21
22use crate::{q_hash, NQuin};
23
24pub const P_CAPITAL_OF: &str = "https://ns.webizen.org/q42/capitalOf";
26pub const CTX_GROUNDING: &str = "https://ns.webizen.org/q42/grounding-fact";
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct GroundingResult {
32 pub text: String,
33 pub repaired: bool,
34 pub reason: Option<String>,
36 pub object_hash: Option<u64>,
38}
39
40#[derive(Debug, Clone)]
42pub struct GroundingFact {
43 pub quin: NQuin,
44 pub prompt_needles: Vec<String>,
46 pub answer_ok: Vec<String>,
48 pub repair: String,
49 pub reason: String,
50}
51
52fn make_quin(subject_iri: &str, object_iri: &str) -> NQuin {
53 let subject = q_hash(subject_iri);
54 let predicate = q_hash(P_CAPITAL_OF);
55 let object = q_hash(object_iri);
56 let context = q_hash(CTX_GROUNDING);
57 let metadata = 0u64;
58 let parity = subject ^ predicate ^ object ^ context ^ metadata;
59 NQuin {
60 subject,
61 predicate,
62 object,
63 context,
64 metadata,
65 parity,
66 }
67}
68
69fn seed_defaults() -> Vec<GroundingFact> {
70 if let Some(facts) = try_load_bundled_facts() {
72 if !facts.is_empty() {
73 return facts;
74 }
75 }
76 vec![
77 GroundingFact {
78 quin: make_quin(
79 "https://example.org/place/France",
80 "https://example.org/place/Paris",
81 ),
82 prompt_needles: vec!["capital".into(), "france".into()],
83 answer_ok: vec!["paris".into()],
84 repair: "The capital of France is Paris.".into(),
85 reason: "capital_france".into(),
86 },
87 GroundingFact {
88 quin: make_quin(
89 "https://example.org/place/Australia",
90 "https://example.org/place/Canberra",
91 ),
92 prompt_needles: vec!["capital".into(), "australia".into()],
93 answer_ok: vec!["canberra".into()],
94 repair: "The capital of Australia is Canberra.".into(),
95 reason: "capital_australia".into(),
96 },
97 GroundingFact {
98 quin: make_quin(
99 "https://example.org/place/Japan",
100 "https://example.org/place/Tokyo",
101 ),
102 prompt_needles: vec!["capital".into(), "japan".into()],
103 answer_ok: vec!["tokyo".into()],
104 repair: "The capital of Japan is Tokyo.".into(),
105 reason: "capital_japan".into(),
106 },
107 ]
108}
109
110pub fn parse_facts_tsv(text: &str) -> Vec<GroundingFact> {
112 let mut out = Vec::new();
113 for line in text.lines() {
114 let line = line.trim();
115 if line.is_empty() || line.starts_with('#') {
116 continue;
117 }
118 let cols: Vec<&str> = line.split('\t').collect();
119 if cols.len() < 6 {
120 continue;
121 }
122 let reason = cols[0].trim().to_string();
123 let needles: Vec<String> = cols[1]
124 .split(';')
125 .map(|s| s.trim().to_ascii_lowercase())
126 .filter(|s| !s.is_empty())
127 .collect();
128 let answer_ok: Vec<String> = cols[2]
129 .split(';')
130 .map(|s| s.trim().to_ascii_lowercase())
131 .filter(|s| !s.is_empty())
132 .collect();
133 let repair = cols[3].trim().to_string();
134 let place_iri = cols[4].trim();
135 let city_iri = cols[5].trim();
136 if needles.is_empty() || answer_ok.is_empty() || repair.is_empty() {
137 continue;
138 }
139 out.push(GroundingFact {
140 quin: make_quin(place_iri, city_iri),
141 prompt_needles: needles,
142 answer_ok,
143 repair,
144 reason,
145 });
146 }
147 out
148}
149
150pub fn load_facts_from_tsv(path: &std::path::Path) -> Result<usize, String> {
152 let text =
153 std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
154 let facts = parse_facts_tsv(&text);
155 let n = facts.len();
156 for f in facts {
157 register_fact(f);
158 }
159 Ok(n)
160}
161
162fn try_load_bundled_facts() -> Option<Vec<GroundingFact>> {
163 for candidate in bundled_fact_candidates() {
164 if candidate.is_file() {
165 if let Ok(text) = std::fs::read_to_string(&candidate) {
166 let facts = parse_facts_tsv(&text);
167 if !facts.is_empty() {
168 log::info!(
169 "quant_graph|seed|bundled|{}|facts={}",
170 candidate.display(),
171 facts.len()
172 );
173 return Some(facts);
174 }
175 }
176 }
177 }
178 None
179}
180
181fn bundled_fact_candidates() -> Vec<std::path::PathBuf> {
182 let mut v = Vec::new();
183 if let Ok(p) = std::env::var("QUALIA_GROUNDING_FACTS") {
184 v.push(std::path::PathBuf::from(p));
185 }
186 let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
188 v.push(manifest.join("../../bundled/grounding/facts.tsv"));
189 v.push(manifest.join("../../../bundled/grounding/facts.tsv"));
190 if let Ok(cwd) = std::env::current_dir() {
191 v.push(cwd.join("bundled/grounding/facts.tsv"));
192 }
193 v
194}
195
196pub fn seed_facts_from_bundled() -> usize {
198 let facts = try_load_bundled_facts().unwrap_or_else(seed_defaults);
199 let n = facts.len();
200 if let Ok(mut g) = fact_store().lock() {
201 *g = facts;
202 }
203 n
204}
205
206fn fact_store() -> &'static Mutex<Vec<GroundingFact>> {
207 static STORE: OnceLock<Mutex<Vec<GroundingFact>>> = OnceLock::new();
208 STORE.get_or_init(|| Mutex::new(seed_defaults()))
209}
210
211pub fn fact_count() -> usize {
213 fact_store().lock().map(|g| g.len()).unwrap_or(0)
214}
215
216pub fn export_fact_quins(out: &mut [NQuin]) -> usize {
218 let Ok(guard) = fact_store().lock() else {
219 return 0;
220 };
221 let n = guard.len().min(out.len());
222 for (i, f) in guard.iter().take(n).enumerate() {
223 out[i] = f.quin;
224 }
225 n
226}
227
228pub fn register_fact(fact: GroundingFact) {
230 if let Ok(mut g) = fact_store().lock() {
231 if let Some(i) = g.iter().position(|f| f.reason == fact.reason) {
232 g[i] = fact;
233 } else {
234 g.push(fact);
235 }
236 }
237}
238
239pub fn register_capital_fact(
241 place_iri: &str,
242 city_iri: &str,
243 place_needle: &str,
244 city_needle: &str,
245 repair: &str,
246 reason: &str,
247) {
248 register_fact(GroundingFact {
249 quin: make_quin(place_iri, city_iri),
250 prompt_needles: vec!["capital".into(), place_needle.to_ascii_lowercase()],
251 answer_ok: vec![city_needle.to_ascii_lowercase()],
252 repair: repair.to_string(),
253 reason: reason.to_string(),
254 });
255}
256
257pub fn reset_fact_store_to_defaults() {
259 if let Ok(mut g) = fact_store().lock() {
260 *g = seed_defaults();
261 }
262}
263
264pub fn maybe_ground_generation(prompt: &str, text: &str) -> GroundingResult {
266 if !crate::inference_modes::quant_graph_grounding_enabled() {
267 return GroundingResult {
268 text: text.to_string(),
269 repaired: false,
270 reason: None,
271 object_hash: None,
272 };
273 }
274 ground_generation(prompt, text)
275}
276
277pub fn ground_generation(prompt: &str, text: &str) -> GroundingResult {
279 let p = prompt.to_ascii_lowercase();
280 let a = text.to_ascii_lowercase();
281 let Ok(guard) = fact_store().lock() else {
282 return GroundingResult {
283 text: text.to_string(),
284 repaired: false,
285 reason: None,
286 object_hash: None,
287 };
288 };
289 for fact in guard.iter() {
290 if !fact.prompt_needles.iter().all(|s| p.contains(s.as_str())) {
291 continue;
292 }
293 if fact.answer_ok.iter().any(|s| a.contains(s.as_str())) {
294 return GroundingResult {
295 text: text.to_string(),
296 repaired: false,
297 reason: Some(fact.reason.clone()),
298 object_hash: Some(fact.quin.object),
299 };
300 }
301 log::info!(
302 "LLM_MODE|quant-graph|repair|{}|object={:#x}",
303 fact.reason,
304 fact.quin.object
305 );
306 return GroundingResult {
307 text: fact.repair.clone(),
308 repaired: true,
309 reason: Some(fact.reason.clone()),
310 object_hash: Some(fact.quin.object),
311 };
312 }
313 GroundingResult {
314 text: text.to_string(),
315 repaired: false,
316 reason: None,
317 object_hash: None,
318 }
319}
320
321pub fn lookup_capital_object(place_iri: &str) -> Option<u64> {
323 let s = q_hash(place_iri);
324 let p = q_hash(P_CAPITAL_OF);
325 let Ok(guard) = fact_store().lock() else {
326 return None;
327 };
328 guard
329 .iter()
330 .find(|f| f.quin.subject == s && f.quin.predicate == p)
331 .map(|f| f.quin.object)
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::inference_modes::{set_inference_mode, InferenceMode};
338
339 #[test]
340 fn repairs_france_capital_when_wrong() {
341 reset_fact_store_to_defaults();
342 let g = ground_generation(
343 "What is the capital of France?",
344 "Question: What is the capital of France? A) Lyon B) Marseille",
345 );
346 assert!(g.repaired);
347 assert!(g.text.to_ascii_lowercase().contains("paris"));
348 assert_eq!(g.reason.as_deref(), Some("capital_france"));
349 assert!(g.object_hash.is_some());
350 }
351
352 #[test]
353 fn leaves_correct_answer() {
354 reset_fact_store_to_defaults();
355 let g = ground_generation(
356 "What is the capital of France?",
357 "The capital of France is Paris.",
358 );
359 assert!(!g.repaired);
360 assert!(g.text.contains("Paris"));
361 }
362
363 #[test]
364 fn maybe_ground_respects_mode() {
365 if std::env::var("QUALIA_INFERENCE_MODE").is_ok() {
366 return;
367 }
368 reset_fact_store_to_defaults();
369 set_inference_mode(InferenceMode::Portable);
370 let g = maybe_ground_generation("What is the capital of France?", "I do not know.");
371 assert!(!g.repaired);
372 set_inference_mode(InferenceMode::QuantGraph);
373 let g2 = maybe_ground_generation("What is the capital of France?", "I do not know.");
374 assert!(g2.repaired);
375 set_inference_mode(InferenceMode::Portable);
376 }
377
378 #[test]
379 fn fact_quins_have_valid_parity() {
380 reset_fact_store_to_defaults();
381 let mut buf = [NQuin {
382 subject: 0,
383 predicate: 0,
384 object: 0,
385 context: 0,
386 metadata: 0,
387 parity: 0,
388 }; 16];
389 let n = export_fact_quins(&mut buf);
390 assert!(n >= 3);
391 for q in &buf[..n] {
392 let fold = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
393 assert_eq!(q.parity, fold);
394 assert_eq!(q.predicate, q_hash(P_CAPITAL_OF));
395 assert_eq!(q.context, q_hash(CTX_GROUNDING));
396 }
397 }
398
399 #[test]
400 fn register_and_lookup() {
401 reset_fact_store_to_defaults();
402 register_capital_fact(
403 "https://example.org/place/Italy",
404 "https://example.org/place/Rome",
405 "italy",
406 "rome",
407 "The capital of Italy is Rome.",
408 "capital_italy",
409 );
410 assert_eq!(
411 lookup_capital_object("https://example.org/place/Italy"),
412 Some(q_hash("https://example.org/place/Rome"))
413 );
414 let g = ground_generation("capital of Italy?", "Milan maybe");
415 assert!(g.repaired);
416 assert!(g.text.contains("Rome"));
417 }
418}