1use serde::{Deserialize, Serialize};
24use surrealdb::Surreal;
25
26use super::edge_view::{self, EdgeView, NameCache};
27use super::embed::Embedder;
28use super::error::GraphError;
29use super::store::Db;
30use super::types::{EntityDetail, GraphStats, MatchSource, QueryOptions};
31use crate::config::GraphScoringConfig;
32
33pub const STRONG_CONFIDENCE: f64 = 0.8;
35pub const DOUBTFUL_CONFIDENCE: f64 = 0.5;
37
38const HIGHLIGHT_EDGES: usize = 5;
40const MAX_EDGES_PER_ENTITY: usize = 8;
43const TOPIC_GRAPH_DEPTH: u32 = 1;
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct MemoryOverview {
49 pub stats: GraphStats,
51 pub groups: Vec<TypeGroup>,
53 pub confidence: ConfidenceSummary,
55 pub uncertain: Vec<EdgeView>,
57 pub self_reinforced: Vec<EdgeView>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct TypeGroup {
65 pub entity_type: String,
66 pub count: u64,
68 pub top: Vec<KnownEntity>,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct KnownEntity {
74 pub id: String,
75 pub name: String,
76 pub entity_type: String,
77 #[serde(rename = "abstract")]
78 pub abstract_text: String,
79 #[serde(default)]
80 pub access_count: i64,
81 #[serde(default)]
82 pub utility_score: f64,
83}
84
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
87pub struct ConfidenceSummary {
88 pub strong: u64,
90 pub uncertain: u64,
92 pub doubtful: u64,
94}
95
96impl ConfidenceSummary {
97 #[must_use]
99 pub fn total(&self) -> u64 {
100 self.strong + self.uncertain + self.doubtful
101 }
102
103 fn record(&mut self, confidence: f64) {
105 if confidence >= STRONG_CONFIDENCE {
106 self.strong += 1;
107 } else if confidence >= DOUBTFUL_CONFIDENCE {
108 self.uncertain += 1;
109 } else {
110 self.doubtful += 1;
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct TopicReport {
118 pub topic: String,
119 pub entities: Vec<TopicEntity>,
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct TopicEntity {
125 pub entity: EntityDetail,
126 pub score: f64,
128 pub source: MatchSource,
130 pub edges: Vec<EdgeView>,
132 #[serde(default)]
134 pub edges_omitted: usize,
135}
136
137pub async fn overview(
139 db: &Surreal<Db>,
140 stats: GraphStats,
141 per_type: usize,
142) -> Result<MemoryOverview, GraphError> {
143 let mut groups = Vec::with_capacity(stats.entity_type_counts.len());
144 for (entity_type, count) in &stats.entity_type_counts {
145 groups.push(TypeGroup {
146 entity_type: entity_type.clone(),
147 count: *count,
148 top: strongest_of_type(db, entity_type, per_type).await?,
149 });
150 }
151 groups.sort_by(|left, right| {
152 right
153 .count
154 .cmp(&left.count)
155 .then_with(|| left.entity_type.cmp(&right.entity_type))
156 });
157
158 let mut cache = NameCache::new();
159 let uncertain = edge_view::views(db, &mut cache, &least_certain(db).await?).await?;
160 let self_reinforced =
161 edge_view::views(db, &mut cache, &most_self_reinforced(db).await?).await?;
162
163 Ok(MemoryOverview {
164 stats,
165 groups,
166 confidence: confidence_summary(db).await?,
167 uncertain,
168 self_reinforced,
169 })
170}
171
172pub async fn about(
175 db: &Surreal<Db>,
176 embedder: &dyn Embedder,
177 scoring: &GraphScoringConfig,
178 topic: &str,
179 limit: usize,
180) -> Result<TopicReport, GraphError> {
181 let options = QueryOptions {
182 limit,
183 entity_type: None,
184 keyword: None,
185 graph_depth: TOPIC_GRAPH_DEPTH,
186 include_episodes: false,
187 };
188 let result = super::query::query(db, embedder, scoring, topic, &options).await?;
189
190 let mut cache = NameCache::new();
191 let mut entities = Vec::with_capacity(result.entities.len());
192 for scored in result.entities {
193 let all = edge_view::live_edges_of(db, &scored.entity.id_string()).await?;
194 let edges_omitted = all.len().saturating_sub(MAX_EDGES_PER_ENTITY);
195 let shown = &all[..all.len().min(MAX_EDGES_PER_ENTITY)];
196 entities.push(TopicEntity {
197 entity: scored.entity,
198 score: scored.score,
199 source: scored.source,
200 edges: edge_view::views(db, &mut cache, shown).await?,
201 edges_omitted,
202 });
203 }
204
205 Ok(TopicReport {
206 topic: topic.to_string(),
207 entities,
208 })
209}
210
211async fn strongest_of_type(
218 db: &Surreal<Db>,
219 entity_type: &str,
220 limit: usize,
221) -> Result<Vec<KnownEntity>, GraphError> {
222 #[derive(serde::Deserialize)]
223 struct Row {
224 id: serde_json::Value,
225 name: String,
226 entity_type: String,
227 #[serde(rename = "abstract")]
228 abstract_text: String,
229 #[serde(default, deserialize_with = "super::util::count_or_zero")]
230 access_count: i64,
231 #[serde(default)]
232 utility_score: Option<f64>,
233 }
234
235 let query = format!(
238 r#"SELECT id, name, entity_type, abstract, access_count, utility_score, updated_at
239 FROM entity WHERE entity_type = $entity_type
240 ORDER BY utility_score DESC, access_count DESC, updated_at DESC
241 LIMIT {}"#,
242 limit.clamp(1, 100)
243 );
244 let mut response = db
245 .query(&query)
246 .bind(("entity_type", entity_type.to_string()))
247 .await?;
248
249 let rows: Vec<Row> = super::deserialize_take(&mut response, 0)?;
250 Ok(rows
251 .into_iter()
252 .map(|row| KnownEntity {
253 id: edge_view::record_id(&row.id),
254 name: row.name,
255 entity_type: row.entity_type,
256 abstract_text: row.abstract_text,
257 access_count: row.access_count,
258 utility_score: row.utility_score.unwrap_or(0.5),
259 })
260 .collect())
261}
262
263async fn confidence_summary(db: &Surreal<Db>) -> Result<ConfidenceSummary, GraphError> {
265 let mut response = db
266 .query("SELECT VALUE confidence FROM relates_to WHERE valid_until IS NONE")
267 .await?;
268 let confidences: Vec<f64> = super::deserialize_take(&mut response, 0)?;
269
270 let mut summary = ConfidenceSummary::default();
271 for confidence in confidences {
272 summary.record(confidence);
273 }
274 Ok(summary)
275}
276
277async fn least_certain(db: &Surreal<Db>) -> Result<Vec<super::types::Relationship>, GraphError> {
279 let query = format!(
280 r#"SELECT * FROM relates_to
281 WHERE valid_until IS NONE AND confidence < {STRONG_CONFIDENCE}
282 ORDER BY confidence ASC
283 LIMIT {HIGHLIGHT_EDGES}"#
284 );
285 let mut response = db.query(&query).await?;
286 super::deserialize_take(&mut response, 0)
287}
288
289async fn most_self_reinforced(
291 db: &Surreal<Db>,
292) -> Result<Vec<super::types::Relationship>, GraphError> {
293 let query = format!(
294 r#"SELECT * FROM relates_to
295 WHERE self_reinforcements IS NOT NONE AND self_reinforcements > 0
296 ORDER BY self_reinforcements DESC
297 LIMIT {HIGHLIGHT_EDGES}"#
298 );
299 let mut response = db.query(&query).await?;
300 super::deserialize_take(&mut response, 0)
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn confidence_bands_split_at_the_documented_thresholds() {
309 let mut summary = ConfidenceSummary::default();
310 for confidence in [1.0, STRONG_CONFIDENCE, 0.79, DOUBTFUL_CONFIDENCE, 0.49, 0.0] {
311 summary.record(confidence);
312 }
313 assert_eq!(summary.strong, 2);
314 assert_eq!(summary.uncertain, 2);
315 assert_eq!(summary.doubtful, 2);
316 assert_eq!(summary.total(), 6);
317 }
318
319 #[test]
320 fn an_empty_graph_has_nothing_to_be_sure_of() {
321 let summary = ConfidenceSummary::default();
322 assert_eq!(summary.total(), 0);
323 }
324}