Skip to main content

holographic_memory/core/agency/
questions.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Template-based question generation.
5//!
6//! Given knowledge gaps or hypotheses, generates natural language questions.
7//! Questions are ranked by goal-relevance: overlap between the question's
8//! vector and the highest-priority active goal.
9
10use crate::core::agency::goals::GoalStore;
11use crate::core::atom_memory::AtomMemory;
12use crate::core::cognition::gaps::KnowledgeGap;
13use crate::core::cognition::hypothesis::Hypothesis;
14use crate::core::entangled::EntangledHVec;
15
16/// A generated question with its relevance score.
17#[derive(Clone, Debug)]
18pub struct Question {
19    pub text: String,
20    pub source: QuestionSource,
21    pub goal_relevance: f64,
22}
23
24/// What triggered the question.
25#[derive(Clone, Debug)]
26pub enum QuestionSource {
27    Gap {
28        entity: String,
29        relation: String,
30    },
31    Hypothesis {
32        entity: String,
33        relation: String,
34        proposed: String,
35    },
36    Exploration {
37        relation: String,
38    },
39}
40
41/// Generates questions from gaps, hypotheses, and exploration targets.
42pub struct QuestionGenerator;
43
44impl QuestionGenerator {
45    /// Generate questions from knowledge gaps.
46    pub fn from_gaps(
47        gaps: &[KnowledgeGap],
48        atom_memory: &AtomMemory,
49        goal_store: &GoalStore,
50    ) -> Vec<Question> {
51        gaps.iter()
52            .map(|gap| {
53                let text = format!("What is the {} of {}?", gap.missing_relation, gap.entity);
54                let relevance = Self::score_relevance(&text, atom_memory, goal_store);
55                Question {
56                    text,
57                    source: QuestionSource::Gap {
58                        entity: gap.entity.clone(),
59                        relation: gap.missing_relation.clone(),
60                    },
61                    goal_relevance: relevance,
62                }
63            })
64            .collect()
65    }
66
67    /// Generate confirmation questions from hypotheses.
68    pub fn from_hypotheses(
69        hypotheses: &[Hypothesis],
70        atom_memory: &AtomMemory,
71        goal_store: &GoalStore,
72    ) -> Vec<Question> {
73        hypotheses
74            .iter()
75            .map(|hyp| {
76                let text = format!(
77                    "Is {} the {} of {}? (confidence: {:.0}%)",
78                    hyp.proposed_filler,
79                    hyp.relation,
80                    hyp.entity,
81                    hyp.confidence * 100.0
82                );
83                let relevance = Self::score_relevance(&text, atom_memory, goal_store);
84                Question {
85                    text,
86                    source: QuestionSource::Hypothesis {
87                        entity: hyp.entity.clone(),
88                        relation: hyp.relation.clone(),
89                        proposed: hyp.proposed_filler.clone(),
90                    },
91                    goal_relevance: relevance,
92                }
93            })
94            .collect()
95    }
96
97    /// Generate exploration questions for under-represented relations.
98    pub fn from_relations(
99        relations: &[String],
100        atom_memory: &AtomMemory,
101        goal_store: &GoalStore,
102    ) -> Vec<Question> {
103        relations
104            .iter()
105            .map(|rel| {
106                let text = format!("What entities have the {} relation?", rel);
107                let relevance = Self::score_relevance(&text, atom_memory, goal_store);
108                Question {
109                    text,
110                    source: QuestionSource::Exploration {
111                        relation: rel.clone(),
112                    },
113                    goal_relevance: relevance,
114                }
115            })
116            .collect()
117    }
118
119    /// Rank questions by goal relevance (highest first).
120    pub fn prioritize(mut questions: Vec<Question>) -> Vec<Question> {
121        questions.sort_by(|a, b| {
122            b.goal_relevance
123                .partial_cmp(&a.goal_relevance)
124                .unwrap_or(std::cmp::Ordering::Equal)
125        });
126        questions
127    }
128
129    fn score_relevance(text: &str, atom_memory: &AtomMemory, goal_store: &GoalStore) -> f64 {
130        // Encode the question text as a vector by bundling its word atoms
131        let words: Vec<&str> = text.split_whitespace().collect();
132        let vecs: Vec<EntangledHVec> = words
133            .iter()
134            .filter_map(|w| atom_memory.get(&w.to_lowercase()))
135            .collect();
136        if vecs.is_empty() {
137            return 0.0;
138        }
139        let question_vec = EntangledHVec::bundle(&vecs);
140        if question_vec.indices().is_empty() {
141            return 0.0;
142        }
143        goal_store.goal_relevance(&question_vec)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::core::agency::goals::Goal;
151    use crate::core::cognition::gaps::KnowledgeGap;
152    use crate::core::cognition::hypothesis::Hypothesis;
153
154    fn make_test_env() -> (AtomMemory, GoalStore) {
155        let dim = 16384;
156        let atom_mem = AtomMemory::new(dim, 3.0);
157        let goal_store = GoalStore::new();
158
159        atom_mem.get_or_insert("located_in");
160        atom_mem.get_or_insert("tokyo");
161        atom_mem.get_or_insert("europe");
162
163        goal_store.add(Goal {
164            name: "find_locations".to_string(),
165            description: "Find where things are located".to_string(),
166            vector: atom_mem.get("located_in").unwrap(),
167            relevance: 1.0,
168            urgency: 1.0,
169            cost: 0.0,
170            active: true,
171        });
172
173        (atom_mem, goal_store)
174    }
175
176    #[test]
177    fn test_from_gaps() {
178        let (atom_mem, goal_store) = make_test_env();
179        let gaps = vec![KnowledgeGap {
180            entity: "tokyo".to_string(),
181            missing_relation: "located_in".to_string(),
182            peers_with_relation: vec!["paris".to_string()],
183            peer_coverage: 1.0,
184        }];
185
186        let questions = QuestionGenerator::from_gaps(&gaps, &atom_mem, &goal_store);
187        assert_eq!(questions.len(), 1);
188        assert!(questions[0].text.contains("located_in"));
189        assert!(questions[0].text.contains("tokyo"));
190    }
191
192    #[test]
193    fn test_from_hypotheses() {
194        let (atom_mem, goal_store) = make_test_env();
195        let hypotheses = vec![Hypothesis {
196            entity: "tokyo".to_string(),
197            relation: "located_in".to_string(),
198            proposed_filler: "asia".to_string(),
199            confidence: 0.85,
200            evidence_count: 3,
201            confirmed: false,
202        }];
203
204        let questions = QuestionGenerator::from_hypotheses(&hypotheses, &atom_mem, &goal_store);
205        assert_eq!(questions.len(), 1);
206        assert!(questions[0].text.contains("asia"));
207        assert!(questions[0].text.contains("85%"));
208    }
209
210    #[test]
211    fn test_prioritize() {
212        let q1 = Question {
213            text: "low".to_string(),
214            source: QuestionSource::Exploration {
215                relation: "r".to_string(),
216            },
217            goal_relevance: 0.1,
218        };
219        let q2 = Question {
220            text: "high".to_string(),
221            source: QuestionSource::Exploration {
222                relation: "r".to_string(),
223            },
224            goal_relevance: 0.9,
225        };
226        let ranked = QuestionGenerator::prioritize(vec![q1, q2]);
227        assert_eq!(ranked[0].text, "high");
228        assert_eq!(ranked[1].text, "low");
229    }
230
231    #[test]
232    fn test_no_goals_zero_relevance() {
233        let atom_mem = AtomMemory::new(16384, 3.0);
234        let goal_store = GoalStore::new(); // empty
235        let gaps = vec![KnowledgeGap {
236            entity: "x".to_string(),
237            missing_relation: "r".to_string(),
238            peers_with_relation: vec![],
239            peer_coverage: 1.0,
240        }];
241        let questions = QuestionGenerator::from_gaps(&gaps, &atom_mem, &goal_store);
242        assert_eq!(questions[0].goal_relevance, 0.0);
243    }
244}