1use std::collections::HashMap;
10
11use surrealdb::Surreal;
12
13use super::confidence;
14use super::embed::Embedder;
15use super::error::GraphError;
16use super::store::Db;
17use super::types::*;
18use crate::config::GraphScoringConfig;
19
20pub async fn query(
22 db: &Surreal<Db>,
23 embedder: &dyn Embedder,
24 scoring: &GraphScoringConfig,
25 query_text: &str,
26 options: &QueryOptions,
27) -> Result<QueryResult, GraphError> {
28 let limit = if options.limit == 0 {
29 10
30 } else {
31 options.limit
32 };
33
34 let semantic_options = SearchOptions {
36 limit: limit * 2,
37 entity_type: options.entity_type.clone(),
38 keyword: options.keyword.clone(),
39 };
40 let semantic_results =
41 super::search::search_with_options(db, embedder, scoring, query_text, &semantic_options)
42 .await?;
43
44 let mut entity_map: HashMap<String, ScoredEntity> = HashMap::new();
46 for result in semantic_results {
47 entity_map.insert(result.entity.id_string(), result);
48 }
49
50 if options.graph_depth > 0 {
52 let top_n: Vec<(String, f64)> = {
53 let mut entries: Vec<_> = entity_map
54 .values()
55 .map(|e| (e.entity.id_string(), e.score))
56 .collect();
57 entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
58 entries.truncate(3); entries
60 };
61
62 for (parent_id, parent_score) in &top_n {
63 let parent_name = entity_map
64 .get(parent_id)
65 .map(|e| e.entity.name.clone())
66 .unwrap_or_default();
67
68 let neighbors = get_neighbor_details(db, parent_id).await?;
69
70 for (neighbor, rel_type, confidence) in neighbors {
71 let neighbor_id = neighbor.id_string();
72 if entity_map.contains_key(&neighbor_id) {
73 continue; }
75
76 if let Some(ref et) = options.entity_type {
78 if neighbor.entity_type.to_string() != *et {
79 continue;
80 }
81 }
82
83 let graph_score = parent_score * confidence;
84 entity_map.insert(
85 neighbor_id,
86 ScoredEntity {
87 entity: neighbor,
88 score: graph_score,
89 source: MatchSource::Graph {
90 parent: parent_name.clone(),
91 rel_type,
92 },
93 },
94 );
95 }
96 }
97 }
98
99 let mut entities: Vec<ScoredEntity> = entity_map.into_values().collect();
101 entities.sort_by(|a, b| {
102 b.score
103 .partial_cmp(&a.score)
104 .unwrap_or(std::cmp::Ordering::Equal)
105 });
106 entities.truncate(limit);
107
108 let episodes = if options.include_episodes {
110 super::search::search_episodes(db, embedder, query_text, limit).await?
111 } else {
112 vec![]
113 };
114
115 Ok(QueryResult { entities, episodes })
116}
117
118async fn get_neighbor_details(
120 db: &Surreal<Db>,
121 entity_id: &str,
122) -> Result<Vec<(EntityDetail, String, f64)>, GraphError> {
123 let now = chrono::Utc::now();
124
125 let mut response = db
127 .query(
128 r#"
129 SELECT rel_type, confidence, last_reinforced, valid_from, out AS target_id
130 FROM relates_to
131 WHERE in = type::record($id) AND valid_until IS NONE
132 "#,
133 )
134 .bind(("id", entity_id.to_string()))
135 .await?;
136
137 let outgoing: Vec<RelTarget> = super::deserialize_take(&mut response, 0)?;
138
139 let mut response = db
141 .query(
142 r#"
143 SELECT rel_type, confidence, last_reinforced, valid_from, in AS target_id
144 FROM relates_to
145 WHERE out = type::record($id) AND valid_until IS NONE
146 "#,
147 )
148 .bind(("id", entity_id.to_string()))
149 .await?;
150
151 let incoming: Vec<RelTarget> = super::deserialize_take(&mut response, 0)?;
152
153 let mut results = Vec::new();
154 let all_edges: Vec<_> = outgoing.into_iter().chain(incoming).collect();
155
156 for edge in all_edges {
157 let effective = confidence::effective_confidence(
159 edge.confidence,
160 edge.last_reinforced.as_ref(),
161 &edge.valid_from,
162 &now,
163 );
164
165 if effective < 0.1 {
167 continue;
168 }
169
170 let tid = match &edge.target_id {
171 serde_json::Value::String(s) => s.clone(),
172 other => other.to_string(),
173 };
174
175 if let Some(detail) = super::crud::get_entity_detail(db, &tid).await? {
176 results.push((detail, edge.rel_type, effective));
177 }
178 }
179
180 Ok(results)
181}
182
183fn default_rel_confidence() -> f64 {
184 1.0
185}
186
187#[derive(serde::Deserialize)]
188struct RelTarget {
189 rel_type: String,
190 target_id: serde_json::Value,
191 #[serde(default = "default_rel_confidence")]
192 confidence: f64,
193 #[serde(default)]
194 last_reinforced: Option<serde_json::Value>,
195 #[serde(default)]
196 valid_from: serde_json::Value,
197}
198
199pub async fn pipeline_entities(
203 db: &Surreal<Db>,
204 stage: &str,
205 status: Option<&str>,
206) -> Result<Vec<EntityDetail>, GraphError> {
207 let query = match status {
208 Some(_) => {
209 r#"SELECT id, name, entity_type, abstract, overview, attributes, access_count, updated_at, source
210 FROM entity
211 WHERE attributes.pipeline_stage = $stage
212 AND attributes.pipeline_status = $status
213 ORDER BY updated_at DESC"#
214 }
215 None => {
216 r#"SELECT id, name, entity_type, abstract, overview, attributes, access_count, updated_at, source
217 FROM entity
218 WHERE attributes.pipeline_stage = $stage
219 ORDER BY updated_at DESC"#
220 }
221 };
222
223 let stage_owned = stage.to_string();
224 let mut response = match status {
225 Some(s) => {
226 let status_owned = s.to_string();
227 db.query(query)
228 .bind(("stage", stage_owned))
229 .bind(("status", status_owned))
230 .await?
231 }
232 None => db.query(query).bind(("stage", stage_owned)).await?,
233 };
234
235 let entities: Vec<EntityDetail> = super::deserialize_take(&mut response, 0)?;
236 Ok(entities)
237}
238
239pub async fn pipeline_stats(
241 db: &Surreal<Db>,
242 staleness_days: u32,
243) -> Result<PipelineGraphStats, GraphError> {
244 let mut response = db
246 .query(
247 r#"SELECT
248 attributes.pipeline_stage AS stage,
249 attributes.pipeline_status AS status,
250 count() AS count
251 FROM entity
252 WHERE attributes.pipeline_stage IS NOT NONE
253 GROUP BY attributes.pipeline_stage, attributes.pipeline_status"#,
254 )
255 .await?;
256
257 let rows: Vec<StageStatusCount> = super::deserialize_take(&mut response, 0)?;
258
259 let mut by_stage: std::collections::HashMap<String, std::collections::HashMap<String, u64>> =
260 std::collections::HashMap::new();
261 let mut total = 0u64;
262
263 for row in rows {
264 total += row.count;
265 by_stage
266 .entry(row.stage)
267 .or_default()
268 .insert(row.status, row.count);
269 }
270
271 let mut stale_response = db
274 .query(
275 r#"SELECT id, name, entity_type, abstract, overview, attributes, access_count, updated_at, source
276 FROM entity
277 WHERE attributes.pipeline_stage = 'thoughts'
278 AND attributes.pipeline_status = 'active'
279 AND updated_at < time::now() - type::duration($threshold)
280 AND count(
281 SELECT * FROM relates_to
282 WHERE (in = $parent.id OR out = $parent.id)
283 AND valid_from > time::now() - type::duration($threshold)
284 ) = 0
285 ORDER BY updated_at ASC"#,
286 )
287 .bind(("threshold", format!("{staleness_days}d")))
288 .await?;
289
290 let stale_thoughts: Vec<EntityDetail> = super::deserialize_take(&mut stale_response, 0)?;
291
292 let mut stale_q_response = db
294 .query(
295 r#"SELECT id, name, entity_type, abstract, overview, attributes, access_count, updated_at, source
296 FROM entity
297 WHERE attributes.pipeline_stage = 'curiosity'
298 AND attributes.pipeline_status = 'active'
299 AND attributes.sub_type IS NONE
300 AND updated_at < time::now() - type::duration($threshold)
301 AND count(
302 SELECT * FROM relates_to
303 WHERE (in = $parent.id OR out = $parent.id)
304 AND valid_from > time::now() - type::duration($threshold)
305 ) = 0
306 ORDER BY updated_at ASC"#,
307 )
308 .bind(("threshold", format!("{}d", staleness_days * 2)))
309 .await?;
310
311 let stale_questions: Vec<EntityDetail> = super::deserialize_take(&mut stale_q_response, 0)?;
312
313 let mut orphan_response = db
315 .query(
316 r#"SELECT count() AS count FROM entity
317 WHERE attributes.pipeline_stage IS NOT NONE
318 AND attributes.pipeline_status = 'active'
319 AND count(SELECT * FROM relates_to WHERE in = $parent.id OR out = $parent.id) = 0
320 GROUP ALL"#,
321 )
322 .await?;
323
324 let orphan_rows: Vec<CountRow> = super::deserialize_take(&mut orphan_response, 0)?;
325 let orphan_count = orphan_rows.first().map(|r| r.count).unwrap_or(0);
326
327 let mut movement_response = db
329 .query(
330 r#"SELECT updated_at
331 FROM entity
332 WHERE attributes.pipeline_status IN ['graduated', 'dissolved', 'explored']
333 ORDER BY updated_at DESC
334 LIMIT 1"#,
335 )
336 .await?;
337
338 let movement_rows: Vec<UpdatedAtRow> = super::deserialize_take(&mut movement_response, 0)?;
339 let last_movement = movement_rows.first().map(|r| match &r.updated_at {
340 serde_json::Value::String(s) => s.clone(),
341 other => other.to_string(),
342 });
343
344 Ok(PipelineGraphStats {
345 by_stage,
346 stale_thoughts,
347 stale_questions,
348 orphan_count,
349 total_entities: total,
350 last_movement,
351 })
352}
353
354pub async fn pipeline_flow(
356 db: &Surreal<Db>,
357 entity_name: &str,
358) -> Result<Vec<(EntityDetail, String, EntityDetail)>, GraphError> {
359 let entity = super::crud::get_entity_by_name(db, entity_name)
361 .await?
362 .ok_or_else(|| GraphError::NotFound(format!("entity: {entity_name}")))?;
363
364 let entity_id = entity.id_string();
365 let mut chain = Vec::new();
366
367 let pipeline_rel_types = [
369 "EVOLVED_FROM",
370 "CRYSTALLIZED_FROM",
371 "INFORMED_BY",
372 "GRADUATED_TO",
373 "CONNECTED_TO",
374 "EXPLORES",
375 "ARCHIVED_FROM",
376 ];
377 let rel_types_str = pipeline_rel_types
378 .iter()
379 .map(|r| format!("'{r}'"))
380 .collect::<Vec<_>>()
381 .join(", ");
382
383 let query_out = format!(
385 r#"SELECT rel_type, out AS target_id
386 FROM relates_to
387 WHERE in = type::record($id) AND rel_type IN [{rel_types_str}] AND valid_until IS NONE"#
388 );
389 let mut response = db.query(&query_out).bind(("id", entity_id.clone())).await?;
390 let outgoing: Vec<RelTarget> = super::deserialize_take(&mut response, 0)?;
391
392 for edge in &outgoing {
393 let tid = match &edge.target_id {
394 serde_json::Value::String(s) => s.clone(),
395 other => other.to_string(),
396 };
397 if let Some(target) = super::crud::get_entity_detail(db, &tid).await? {
398 let source_detail = super::crud::get_entity_detail(db, &entity_id)
399 .await?
400 .unwrap();
401 chain.push((source_detail, edge.rel_type.clone(), target));
402 }
403 }
404
405 let query_in = format!(
407 r#"SELECT rel_type, in AS target_id
408 FROM relates_to
409 WHERE out = type::record($id) AND rel_type IN [{rel_types_str}] AND valid_until IS NONE"#
410 );
411 let mut response = db.query(&query_in).bind(("id", entity_id.clone())).await?;
412 let incoming: Vec<RelTarget> = super::deserialize_take(&mut response, 0)?;
413
414 for edge in &incoming {
415 let tid = match &edge.target_id {
416 serde_json::Value::String(s) => s.clone(),
417 other => other.to_string(),
418 };
419 if let Some(source) = super::crud::get_entity_detail(db, &tid).await? {
420 let target_detail = super::crud::get_entity_detail(db, &entity_id)
421 .await?
422 .unwrap();
423 chain.push((source, edge.rel_type.clone(), target_detail));
424 }
425 }
426
427 Ok(chain)
428}
429
430fn lenient_string<'de, D>(deserializer: D) -> Result<String, D::Error>
431where
432 D: serde::Deserializer<'de>,
433{
434 use serde::de;
435 struct Visitor;
436 impl<'de> de::Visitor<'de> for Visitor {
437 type Value = String;
438 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
439 f.write_str("a string, integer, or null")
440 }
441 fn visit_str<E: de::Error>(self, v: &str) -> Result<String, E> {
442 Ok(v.to_string())
443 }
444 fn visit_string<E: de::Error>(self, v: String) -> Result<String, E> {
445 Ok(v)
446 }
447 fn visit_i64<E: de::Error>(self, v: i64) -> Result<String, E> {
448 Ok(v.to_string())
449 }
450 fn visit_u64<E: de::Error>(self, v: u64) -> Result<String, E> {
451 Ok(v.to_string())
452 }
453 fn visit_unit<E: de::Error>(self) -> Result<String, E> {
454 Ok("unknown".to_string())
455 }
456 fn visit_none<E: de::Error>(self) -> Result<String, E> {
457 Ok("unknown".to_string())
458 }
459 fn visit_bool<E: de::Error>(self, v: bool) -> Result<String, E> {
460 Ok(v.to_string())
461 }
462 fn visit_f64<E: de::Error>(self, v: f64) -> Result<String, E> {
463 Ok(v.to_string())
464 }
465 }
466 deserializer.deserialize_any(Visitor)
467}
468
469#[derive(serde::Deserialize)]
470struct StageStatusCount {
471 #[serde(deserialize_with = "lenient_string")]
472 stage: String,
473 #[serde(deserialize_with = "lenient_string")]
474 status: String,
475 count: u64,
476}
477
478#[derive(serde::Deserialize)]
479struct UpdatedAtRow {
480 updated_at: serde_json::Value,
481}
482
483#[derive(serde::Deserialize)]
484struct CountRow {
485 count: u64,
486}