Skip to main content

writerslogic_warm/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 vecs: Vec<EntangledHVec> = text
132            .split_whitespace()
133            .filter_map(|w| atom_memory.get(&w.to_lowercase()))
134            .collect();
135        if vecs.is_empty() {
136            return 0.0;
137        }
138        let question_vec = EntangledHVec::bundle(&vecs);
139        if question_vec.indices().is_empty() {
140            return 0.0;
141        }
142        goal_store.goal_relevance(&question_vec)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::core::agency::goals::Goal;
150    use crate::core::cognition::gaps::KnowledgeGap;
151    use crate::core::cognition::hypothesis::Hypothesis;
152
153    fn make_test_env() -> (AtomMemory, GoalStore) {
154        let dim = 16384;
155        let atom_mem = AtomMemory::new(dim, 3.0);
156        let goal_store = GoalStore::new();
157
158        atom_mem.get_or_insert("located_in");
159        atom_mem.get_or_insert("tokyo");
160        atom_mem.get_or_insert("europe");
161
162        goal_store.add(Goal {
163            name: "find_locations".to_string(),
164            description: "Find where things are located".to_string(),
165            vector: atom_mem.get("located_in").unwrap(),
166            relevance: 1.0,
167            urgency: 1.0,
168            cost: 0.0,
169            active: true,
170        });
171
172        (atom_mem, goal_store)
173    }
174
175    #[test]
176    fn test_from_gaps() {
177        let (atom_mem, goal_store) = make_test_env();
178        let gaps = vec![KnowledgeGap {
179            entity: "tokyo".to_string(),
180            missing_relation: "located_in".to_string(),
181            peers_with_relation: vec!["paris".to_string()],
182            peer_coverage: 1.0,
183        }];
184
185        let questions = QuestionGenerator::from_gaps(&gaps, &atom_mem, &goal_store);
186        assert_eq!(questions.len(), 1);
187        assert!(questions[0].text.contains("located_in"));
188        assert!(questions[0].text.contains("tokyo"));
189    }
190
191    #[test]
192    fn test_from_hypotheses() {
193        let (atom_mem, goal_store) = make_test_env();
194        let hypotheses = vec![Hypothesis {
195            entity: "tokyo".to_string(),
196            relation: "located_in".to_string(),
197            proposed_filler: "asia".to_string(),
198            confidence: 0.85,
199            evidence_count: 3,
200            confirmed: false,
201        }];
202
203        let questions = QuestionGenerator::from_hypotheses(&hypotheses, &atom_mem, &goal_store);
204        assert_eq!(questions.len(), 1);
205        assert!(questions[0].text.contains("asia"));
206        assert!(questions[0].text.contains("85%"));
207    }
208
209    #[test]
210    fn test_prioritize() {
211        let q1 = Question {
212            text: "low".to_string(),
213            source: QuestionSource::Exploration {
214                relation: "r".to_string(),
215            },
216            goal_relevance: 0.1,
217        };
218        let q2 = Question {
219            text: "high".to_string(),
220            source: QuestionSource::Exploration {
221                relation: "r".to_string(),
222            },
223            goal_relevance: 0.9,
224        };
225        let ranked = QuestionGenerator::prioritize(vec![q1, q2]);
226        assert_eq!(ranked[0].text, "high");
227        assert_eq!(ranked[1].text, "low");
228    }
229
230    #[test]
231    fn test_no_goals_zero_relevance() {
232        let atom_mem = AtomMemory::new(16384, 3.0);
233        let goal_store = GoalStore::new(); // empty
234        let gaps = vec![KnowledgeGap {
235            entity: "x".to_string(),
236            missing_relation: "r".to_string(),
237            peers_with_relation: vec![],
238            peer_coverage: 1.0,
239        }];
240        let questions = QuestionGenerator::from_gaps(&gaps, &atom_mem, &goal_store);
241        assert_eq!(questions[0].goal_relevance, 0.0);
242    }
243}