1pub mod confidence;
7pub mod crud;
8pub mod dedup;
9pub mod embed;
10pub mod error;
11pub mod extract;
12pub mod gc;
13pub mod ingest;
14pub mod llm;
15pub mod pipeline;
16pub mod pipeline_sync;
17pub mod query;
18pub mod search;
19pub mod store;
20pub mod traverse;
21pub mod types;
22pub mod util;
23pub mod utility;
24pub mod vigil_sync;
25
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28
29pub use confidence::{Provenance, ProvenanceWeights};
30use embed::{FastEmbedder, LazyEmbedder};
31use error::GraphError;
32pub use ingest::{IngestContext, ProvenancePolicy};
33use store::Db;
34pub use store::ServerConfig;
35#[allow(unused_imports)] use surrealdb::types::SurrealValue;
37use surrealdb::Surreal;
38use types::*;
39
40pub(crate) fn deserialize_take<T: serde::de::DeserializeOwned>(
43 response: &mut surrealdb::IndexedResults,
44 index: usize,
45) -> Result<Vec<T>, GraphError> {
46 let values: Vec<serde_json::Value> = response.take(index)?;
47 values
48 .into_iter()
49 .map(|v| serde_json::from_value(v).map_err(GraphError::from))
50 .collect()
51}
52
53pub(crate) fn deserialize_take_opt<T: serde::de::DeserializeOwned>(
54 response: &mut surrealdb::IndexedResults,
55 index: usize,
56) -> Result<Option<T>, GraphError> {
57 let values: Vec<T> = deserialize_take(response, index)?;
58 Ok(values.into_iter().next())
59}
60
61pub struct GraphMemory {
63 db: Surreal<Db>,
64 embedder: LazyEmbedder,
65 path: PathBuf,
66 scoring: crate::config::GraphScoringConfig,
67 provenance: confidence::ProvenanceWeights,
68}
69
70impl GraphMemory {
71 pub async fn open(path: &Path) -> Result<Self, GraphError> {
79 let memory_dir = path.parent().unwrap_or(path);
80 let config = crate::config::load_from_dir(memory_dir);
81 let mode = config
82 .graph
83 .as_ref()
84 .map(|g| g.mode.clone())
85 .unwrap_or_else(|| "embedded".to_string());
86
87 match mode.as_str() {
88 "server" => Self::open_server(path).await,
89 _ => Self::open_embedded(path).await,
90 }
91 }
92
93 pub async fn open_embedded(path: &Path) -> Result<Self, GraphError> {
95 std::fs::create_dir_all(path)?;
96
97 let db = store::open(path).await?;
98 store::init_schema(&db).await?;
99
100 let models_dir = path.join("models");
101 std::fs::create_dir_all(&models_dir)?;
102 let embedder = LazyEmbedder::new(&models_dir);
103
104 let graph_config = load_graph_section(path);
105
106 Ok(Self {
107 db,
108 embedder,
109 path: path.to_path_buf(),
110 scoring: graph_config.scoring,
111 provenance: graph_config.provenance,
112 })
113 }
114
115 pub async fn open_server(path: &Path) -> Result<Self, GraphError> {
119 let memory_dir = path.parent().unwrap_or(path);
120 let config = crate::config::load_from_dir(memory_dir);
121
122 let graph_section = config.graph.unwrap_or_default();
123 let password = if graph_section.password_file.is_empty() {
124 String::new()
125 } else {
126 let pw_path = if graph_section.password_file.starts_with('/') {
127 std::path::PathBuf::from(&graph_section.password_file)
128 } else {
129 let entity_root = memory_dir.parent().unwrap_or(memory_dir);
131 entity_root.join(&graph_section.password_file)
132 };
133 std::fs::read_to_string(&pw_path)
134 .map(|s| s.trim().to_string())
135 .map_err(|e| {
136 GraphError::Io(std::io::Error::new(
137 e.kind(),
138 format!(
139 "failed to read graph password file {}: {e}",
140 pw_path.display()
141 ),
142 ))
143 })?
144 };
145
146 let scoring = graph_section.scoring.clone();
147 let provenance = graph_section.provenance;
148 let server_config = store::ServerConfig {
149 url: graph_section.url,
150 username: graph_section.username,
151 password,
152 namespace: graph_section.namespace,
153 database: graph_section.database,
154 };
155
156 let models_dir = path.join("models");
157 let mut gm = Self::connect(&server_config, &models_dir).await?;
158 gm.scoring = scoring;
159 gm.provenance = provenance;
160 Ok(gm)
161 }
162
163 pub async fn connect(
165 config: &store::ServerConfig,
166 models_dir: &Path,
167 ) -> Result<Self, GraphError> {
168 let db = store::connect(config).await?;
169 store::init_schema(&db).await?;
170
171 std::fs::create_dir_all(models_dir)?;
172 let embedder = LazyEmbedder::new(models_dir);
173
174 Ok(Self {
175 db,
176 embedder,
177 path: models_dir.to_path_buf(),
178 scoring: crate::config::GraphScoringConfig::default(),
179 provenance: confidence::ProvenanceWeights::default(),
180 })
181 }
182
183 pub fn path(&self) -> &Path {
185 &self.path
186 }
187
188 #[must_use]
191 pub fn provenance_weights(&self) -> &confidence::ProvenanceWeights {
192 &self.provenance
193 }
194
195 #[allow(dead_code)]
197 pub(crate) fn db(&self) -> &Surreal<Db> {
198 &self.db
199 }
200
201 #[allow(dead_code)]
203 pub(crate) fn embedder(&self) -> Result<&FastEmbedder, GraphError> {
204 self.embedder.get()
205 }
206
207 pub async fn add_entity(&self, entity: NewEntity) -> Result<Entity, GraphError> {
211 crud::add_entity(&self.db, self.embedder.get()?, entity).await
212 }
213
214 pub async fn get_entity(&self, name: &str) -> Result<Option<Entity>, GraphError> {
216 crud::get_entity_by_name(&self.db, name).await
217 }
218
219 pub async fn get_entity_by_id(&self, id: &str) -> Result<Option<Entity>, GraphError> {
221 crud::get_entity_by_id(&self.db, id).await
222 }
223
224 pub async fn update_entity(
226 &self,
227 id: &str,
228 updates: EntityUpdate,
229 ) -> Result<Entity, GraphError> {
230 crud::update_entity(&self.db, self.embedder.get()?, id, updates).await
231 }
232
233 pub async fn delete_entity(&self, id: &str) -> Result<(), GraphError> {
235 crud::delete_entity(&self.db, id).await
236 }
237
238 pub async fn list_entities(
240 &self,
241 entity_type: Option<&str>,
242 ) -> Result<Vec<Entity>, GraphError> {
243 crud::list_entities(&self.db, entity_type).await
244 }
245
246 pub async fn add_relationship(&self, rel: NewRelationship) -> Result<Relationship, GraphError> {
250 crud::add_relationship(&self.db, rel).await
251 }
252
253 pub async fn get_relationships(
255 &self,
256 entity_name: &str,
257 direction: Direction,
258 ) -> Result<Vec<Relationship>, GraphError> {
259 crud::get_relationships(&self.db, entity_name, direction).await
260 }
261
262 pub async fn supersede_relationship(
264 &self,
265 old_id: &str,
266 new: NewRelationship,
267 ) -> Result<Relationship, GraphError> {
268 crud::supersede_relationship(&self.db, old_id, new).await
269 }
270
271 pub async fn update_relationship_confidence(
274 &self,
275 rel_id: &str,
276 confidence: f64,
277 ) -> Result<(), GraphError> {
278 crud::update_relationship_confidence(&self.db, rel_id, confidence).await
279 }
280
281 pub async fn reinforce_relationship(
288 &self,
289 rel_id: &str,
290 evidence: confidence::EdgeEvidence,
291 ) -> Result<(), GraphError> {
292 crud::reinforce_relationship(&self.db, rel_id, evidence).await
293 }
294
295 pub async fn add_episode(&self, episode: NewEpisode) -> Result<Episode, GraphError> {
303 crud::add_episode(&self.db, self.embedder.get()?, episode).await
304 }
305
306 pub async fn add_episode_from(
308 &self,
309 episode: NewEpisode,
310 provenance: Provenance,
311 ) -> Result<Episode, GraphError> {
312 crud::add_episode_from(&self.db, self.embedder.get()?, episode, provenance).await
313 }
314
315 pub async fn get_episodes_by_session(
317 &self,
318 session_id: &str,
319 ) -> Result<Vec<Episode>, GraphError> {
320 crud::get_episodes_by_session(&self.db, session_id).await
321 }
322
323 pub async fn get_episode_by_log_number(
325 &self,
326 log_number: u32,
327 ) -> Result<Option<Episode>, GraphError> {
328 crud::get_episode_by_log_number(&self.db, log_number).await
329 }
330
331 pub async fn ingest_archive(
339 &self,
340 archive_text: &str,
341 context: &IngestContext,
342 llm: Option<&dyn llm::LlmProvider>,
343 ) -> Result<IngestionReport, GraphError> {
344 ingest::ingest_archive(self, archive_text, context, llm).await
345 }
346
347 pub async fn extract_from_archive(
349 &self,
350 archive_text: &str,
351 context: &IngestContext,
352 llm: &dyn llm::LlmProvider,
353 ) -> Result<IngestionReport, GraphError> {
354 ingest::extract_from_archive(self, archive_text, context, llm).await
355 }
356
357 pub async fn mark_extracted(&self, log_number: u32) -> Result<(), GraphError> {
359 crud::mark_episodes_extracted(&self.db, log_number).await
360 }
361
362 pub async fn unextracted_log_numbers(&self) -> Result<Vec<i64>, GraphError> {
364 crud::get_unextracted_log_numbers(&self.db).await
365 }
366
367 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchResult>, GraphError> {
371 search::search(&self.db, self.embedder.get()?, &self.scoring, query, limit).await
372 }
373
374 pub async fn search_with_options(
376 &self,
377 query: &str,
378 options: &SearchOptions,
379 ) -> Result<Vec<ScoredEntity>, GraphError> {
380 search::search_with_options(
381 &self.db,
382 self.embedder.get()?,
383 &self.scoring,
384 query,
385 options,
386 )
387 .await
388 }
389
390 pub async fn search_episodes(
392 &self,
393 query: &str,
394 limit: usize,
395 ) -> Result<Vec<EpisodeSearchResult>, GraphError> {
396 search::search_episodes(&self.db, self.embedder.get()?, query, limit).await
397 }
398
399 pub async fn query(
403 &self,
404 query_text: &str,
405 options: &QueryOptions,
406 ) -> Result<QueryResult, GraphError> {
407 query::query(
408 &self.db,
409 self.embedder.get()?,
410 &self.scoring,
411 query_text,
412 options,
413 )
414 .await
415 }
416
417 pub async fn traverse(
421 &self,
422 entity_name: &str,
423 depth: u32,
424 ) -> Result<TraversalNode, GraphError> {
425 traverse::traverse(&self.db, entity_name, depth).await
426 }
427
428 pub async fn traverse_filtered(
430 &self,
431 entity_name: &str,
432 depth: u32,
433 type_filter: Option<&str>,
434 ) -> Result<TraversalNode, GraphError> {
435 traverse::traverse_filtered(&self.db, entity_name, depth, type_filter).await
436 }
437
438 pub async fn sync_pipeline(
442 &self,
443 docs: &PipelineDocuments,
444 ) -> Result<PipelineSyncReport, GraphError> {
445 pipeline_sync::sync_pipeline(self, docs).await
446 }
447
448 pub async fn pipeline_stats(
450 &self,
451 staleness_days: u32,
452 ) -> Result<PipelineGraphStats, GraphError> {
453 query::pipeline_stats(&self.db, staleness_days).await
454 }
455
456 pub async fn pipeline_entities(
458 &self,
459 stage: &str,
460 status: Option<&str>,
461 ) -> Result<Vec<EntityDetail>, GraphError> {
462 query::pipeline_entities(&self.db, stage, status).await
463 }
464
465 pub async fn pipeline_flow(
467 &self,
468 entity_name: &str,
469 ) -> Result<Vec<(EntityDetail, String, EntityDetail)>, GraphError> {
470 query::pipeline_flow(&self.db, entity_name).await
471 }
472
473 pub async fn sync_vigil_signals(
477 &self,
478 signals_path: &std::path::Path,
479 ) -> Result<VigilSyncReport, GraphError> {
480 vigil_sync::sync_vigil_signals(self, signals_path).await
481 }
482
483 pub async fn sync_outcomes(
485 &self,
486 outcomes_path: &std::path::Path,
487 ) -> Result<VigilSyncReport, GraphError> {
488 vigil_sync::sync_outcomes(self, outcomes_path).await
489 }
490
491 pub async fn sync_vigil(
493 &self,
494 signals_path: &std::path::Path,
495 outcomes_path: &std::path::Path,
496 ) -> Result<VigilSyncReport, GraphError> {
497 vigil_sync::sync_vigil(self, signals_path, outcomes_path).await
498 }
499
500 pub async fn record_outcome_feedback(
505 &self,
506 session_id: &str,
507 outcome: utility::OutcomeKind,
508 retrieved_entity_ids: &[String],
509 used_entity_ids: Option<&[String]>,
510 ) -> Result<utility::FeedbackReport, GraphError> {
511 utility::record_outcome_feedback(
512 &self.db,
513 session_id,
514 outcome,
515 retrieved_entity_ids,
516 used_entity_ids,
517 )
518 .await
519 }
520
521 pub async fn record_session_outcome(
528 &self,
529 session_id: &str,
530 outcome: utility::OutcomeKind,
531 ) -> Result<utility::FeedbackReport, GraphError> {
532 let session = utility::session_entities(&self.db, session_id).await?;
533 if session.is_empty() {
534 return Ok(utility::FeedbackReport::default());
535 }
536
537 utility::record_outcome_feedback(
538 &self.db,
539 session_id,
540 outcome,
541 &session.retrieved,
542 Some(&session.used),
543 )
544 .await
545 }
546
547 pub async fn record_session_use(
549 &self,
550 session_id: &str,
551 entity_ids: &[String],
552 ) -> Result<u32, GraphError> {
553 utility::record_session_use(&self.db, session_id, entity_ids).await
554 }
555
556 pub async fn run_gc(&self, config: &gc::GcConfig) -> Result<gc::GcReport, GraphError> {
560 gc::run_gc(&self.db, config).await
561 }
562
563 pub async fn gc_stats(&self) -> Result<gc::GcStatsReport, GraphError> {
565 gc::stats_only(&self.db).await
566 }
567
568 pub async fn delete_relationship(&self, id: &str) -> Result<(), GraphError> {
570 crud::delete_relationship(&self.db, id).await
571 }
572
573 pub async fn stats(&self) -> Result<GraphStats, GraphError> {
577 let entity_count = db_count(&self.db, "entity").await?;
578 let relationship_count = db_count(&self.db, "relates_to").await?;
579 let episode_count = db_count(&self.db, "episode").await?;
580
581 let mut type_response = self
583 .db
584 .query("SELECT entity_type, count() AS count FROM entity GROUP BY entity_type")
585 .await?;
586
587 let type_rows: Vec<TypeCount> = type_response.take(0)?;
588 let entity_type_counts: HashMap<String, u64> = type_rows
589 .into_iter()
590 .map(|r| (r.entity_type, r.count))
591 .collect();
592
593 Ok(GraphStats {
594 entity_count,
595 relationship_count,
596 episode_count,
597 entity_type_counts,
598 })
599 }
600}
601
602fn load_graph_section(graph_path: &Path) -> crate::config::GraphSection {
606 let memory_dir = graph_path.parent().unwrap_or(graph_path);
607 crate::config::load_from_dir(memory_dir)
608 .graph
609 .unwrap_or_default()
610}
611
612async fn db_count(db: &Surreal<Db>, table: &str) -> Result<u64, GraphError> {
613 let query = format!("SELECT count() AS count FROM {table} GROUP ALL");
614 let mut response = db.query(&query).await?;
615 let rows: Vec<CountRow> = response.take(0)?;
616 Ok(rows.first().map(|r| r.count).unwrap_or(0))
617}
618
619#[derive(serde::Deserialize, surrealdb::types::SurrealValue)]
620struct CountRow {
621 count: u64,
622}
623
624#[derive(serde::Deserialize, surrealdb::types::SurrealValue)]
625struct TypeCount {
626 entity_type: String,
627 count: u64,
628}