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
29use embed::FastEmbedder;
30use error::GraphError;
31use store::Db;
32#[cfg(feature = "server")]
33pub use store::ServerConfig;
34#[allow(unused_imports)] use surrealdb::types::SurrealValue;
36use surrealdb::Surreal;
37use types::*;
38
39pub(crate) fn deserialize_take<T: serde::de::DeserializeOwned>(
42 response: &mut surrealdb::IndexedResults,
43 index: usize,
44) -> Result<Vec<T>, GraphError> {
45 let values: Vec<serde_json::Value> = response.take(index)?;
46 values
47 .into_iter()
48 .map(|v| serde_json::from_value(v).map_err(GraphError::from))
49 .collect()
50}
51
52pub(crate) fn deserialize_take_opt<T: serde::de::DeserializeOwned>(
53 response: &mut surrealdb::IndexedResults,
54 index: usize,
55) -> Result<Option<T>, GraphError> {
56 let values: Vec<T> = deserialize_take(response, index)?;
57 Ok(values.into_iter().next())
58}
59
60pub struct GraphMemory {
62 db: Surreal<Db>,
63 embedder: FastEmbedder,
64 path: PathBuf,
65 scoring: crate::config::GraphScoringConfig,
66}
67
68impl GraphMemory {
69 #[cfg(feature = "embedded")]
76 pub async fn open(path: &Path) -> Result<Self, GraphError> {
77 std::fs::create_dir_all(path)?;
78
79 let db = store::open(path).await?;
80 store::init_schema(&db).await?;
81
82 let models_dir = path.join("models");
83 std::fs::create_dir_all(&models_dir)?;
84 let embedder = FastEmbedder::new(&models_dir)?;
85
86 let scoring = load_scoring_config(path);
87
88 Ok(Self {
89 db,
90 embedder,
91 path: path.to_path_buf(),
92 scoring,
93 })
94 }
95
96 #[cfg(feature = "server")]
102 pub async fn open(path: &Path) -> Result<Self, GraphError> {
103 let memory_dir = path.parent().unwrap_or(path);
104 let config = crate::config::load_from_dir(memory_dir);
105
106 let graph_section = config.graph.unwrap_or_default();
107 let password = if graph_section.password_file.is_empty() {
108 String::new()
109 } else {
110 let pw_path = if graph_section.password_file.starts_with('/') {
111 std::path::PathBuf::from(&graph_section.password_file)
112 } else {
113 let entity_root = memory_dir.parent().unwrap_or(memory_dir);
115 entity_root.join(&graph_section.password_file)
116 };
117 std::fs::read_to_string(&pw_path)
118 .map(|s| s.trim().to_string())
119 .map_err(|e| {
120 GraphError::Io(std::io::Error::new(
121 e.kind(),
122 format!(
123 "failed to read graph password file {}: {e}",
124 pw_path.display()
125 ),
126 ))
127 })?
128 };
129
130 let scoring = graph_section.scoring.clone();
131 let server_config = store::ServerConfig {
132 url: graph_section.url,
133 username: graph_section.username,
134 password,
135 namespace: graph_section.namespace,
136 database: graph_section.database,
137 };
138
139 let models_dir = path.join("models");
140 let mut gm = Self::connect(&server_config, &models_dir).await?;
141 gm.scoring = scoring;
142 Ok(gm)
143 }
144
145 #[cfg(feature = "server")]
147 pub async fn connect(
148 config: &store::ServerConfig,
149 models_dir: &Path,
150 ) -> Result<Self, GraphError> {
151 let db = store::connect(config).await?;
152 store::init_schema(&db).await?;
153
154 std::fs::create_dir_all(models_dir)?;
155 let embedder = FastEmbedder::new(models_dir)?;
156
157 Ok(Self {
158 db,
159 embedder,
160 path: models_dir.to_path_buf(),
161 scoring: crate::config::GraphScoringConfig::default(),
162 })
163 }
164
165 pub fn path(&self) -> &Path {
167 &self.path
168 }
169
170 #[allow(dead_code)]
172 pub(crate) fn db(&self) -> &Surreal<Db> {
173 &self.db
174 }
175
176 #[allow(dead_code)]
178 pub(crate) fn embedder(&self) -> &FastEmbedder {
179 &self.embedder
180 }
181
182 pub async fn add_entity(&self, entity: NewEntity) -> Result<Entity, GraphError> {
186 crud::add_entity(&self.db, &self.embedder, entity).await
187 }
188
189 pub async fn get_entity(&self, name: &str) -> Result<Option<Entity>, GraphError> {
191 crud::get_entity_by_name(&self.db, name).await
192 }
193
194 pub async fn get_entity_by_id(&self, id: &str) -> Result<Option<Entity>, GraphError> {
196 crud::get_entity_by_id(&self.db, id).await
197 }
198
199 pub async fn update_entity(
201 &self,
202 id: &str,
203 updates: EntityUpdate,
204 ) -> Result<Entity, GraphError> {
205 crud::update_entity(&self.db, &self.embedder, id, updates).await
206 }
207
208 pub async fn delete_entity(&self, id: &str) -> Result<(), GraphError> {
210 crud::delete_entity(&self.db, id).await
211 }
212
213 pub async fn list_entities(
215 &self,
216 entity_type: Option<&str>,
217 ) -> Result<Vec<Entity>, GraphError> {
218 crud::list_entities(&self.db, entity_type).await
219 }
220
221 pub async fn add_relationship(&self, rel: NewRelationship) -> Result<Relationship, GraphError> {
225 crud::add_relationship(&self.db, rel).await
226 }
227
228 pub async fn get_relationships(
230 &self,
231 entity_name: &str,
232 direction: Direction,
233 ) -> Result<Vec<Relationship>, GraphError> {
234 crud::get_relationships(&self.db, entity_name, direction).await
235 }
236
237 pub async fn supersede_relationship(
239 &self,
240 old_id: &str,
241 new: NewRelationship,
242 ) -> Result<Relationship, GraphError> {
243 crud::supersede_relationship(&self.db, old_id, new).await
244 }
245
246 pub async fn update_relationship_confidence(
248 &self,
249 rel_id: &str,
250 confidence: f64,
251 ) -> Result<(), GraphError> {
252 crud::update_relationship_confidence(&self.db, rel_id, confidence).await
253 }
254
255 pub async fn reinforce_relationship(
260 &self,
261 rel_id: &str,
262 new_confidence: f64,
263 ) -> Result<(), GraphError> {
264 crud::reinforce_relationship(&self.db, rel_id, new_confidence).await
265 }
266
267 pub async fn add_episode(&self, episode: NewEpisode) -> Result<Episode, GraphError> {
271 crud::add_episode(&self.db, &self.embedder, episode).await
272 }
273
274 pub async fn get_episodes_by_session(
276 &self,
277 session_id: &str,
278 ) -> Result<Vec<Episode>, GraphError> {
279 crud::get_episodes_by_session(&self.db, session_id).await
280 }
281
282 pub async fn get_episode_by_log_number(
284 &self,
285 log_number: u32,
286 ) -> Result<Option<Episode>, GraphError> {
287 crud::get_episode_by_log_number(&self.db, log_number).await
288 }
289
290 pub async fn ingest_archive(
294 &self,
295 archive_text: &str,
296 session_id: &str,
297 log_number: Option<u32>,
298 llm: Option<&dyn llm::LlmProvider>,
299 ) -> Result<IngestionReport, GraphError> {
300 ingest::ingest_archive(self, archive_text, session_id, log_number, llm).await
301 }
302
303 pub async fn extract_from_archive(
305 &self,
306 archive_text: &str,
307 session_id: &str,
308 log_number: Option<u32>,
309 llm: &dyn llm::LlmProvider,
310 ) -> Result<IngestionReport, GraphError> {
311 ingest::extract_from_archive(self, archive_text, session_id, log_number, llm).await
312 }
313
314 pub async fn mark_extracted(&self, log_number: u32) -> Result<(), GraphError> {
316 crud::mark_episodes_extracted(&self.db, log_number).await
317 }
318
319 pub async fn unextracted_log_numbers(&self) -> Result<Vec<i64>, GraphError> {
321 crud::get_unextracted_log_numbers(&self.db).await
322 }
323
324 pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<SearchResult>, GraphError> {
328 search::search(&self.db, &self.embedder, &self.scoring, query, limit).await
329 }
330
331 pub async fn search_with_options(
333 &self,
334 query: &str,
335 options: &SearchOptions,
336 ) -> Result<Vec<ScoredEntity>, GraphError> {
337 search::search_with_options(&self.db, &self.embedder, &self.scoring, query, options).await
338 }
339
340 pub async fn search_episodes(
342 &self,
343 query: &str,
344 limit: usize,
345 ) -> Result<Vec<EpisodeSearchResult>, GraphError> {
346 search::search_episodes(&self.db, &self.embedder, query, limit).await
347 }
348
349 pub async fn query(
353 &self,
354 query_text: &str,
355 options: &QueryOptions,
356 ) -> Result<QueryResult, GraphError> {
357 query::query(&self.db, &self.embedder, &self.scoring, query_text, options).await
358 }
359
360 pub async fn traverse(
364 &self,
365 entity_name: &str,
366 depth: u32,
367 ) -> Result<TraversalNode, GraphError> {
368 traverse::traverse(&self.db, entity_name, depth).await
369 }
370
371 pub async fn traverse_filtered(
373 &self,
374 entity_name: &str,
375 depth: u32,
376 type_filter: Option<&str>,
377 ) -> Result<TraversalNode, GraphError> {
378 traverse::traverse_filtered(&self.db, entity_name, depth, type_filter).await
379 }
380
381 pub async fn sync_pipeline(
385 &self,
386 docs: &PipelineDocuments,
387 ) -> Result<PipelineSyncReport, GraphError> {
388 pipeline_sync::sync_pipeline(self, docs).await
389 }
390
391 pub async fn pipeline_stats(
393 &self,
394 staleness_days: u32,
395 ) -> Result<PipelineGraphStats, GraphError> {
396 query::pipeline_stats(&self.db, staleness_days).await
397 }
398
399 pub async fn pipeline_entities(
401 &self,
402 stage: &str,
403 status: Option<&str>,
404 ) -> Result<Vec<EntityDetail>, GraphError> {
405 query::pipeline_entities(&self.db, stage, status).await
406 }
407
408 pub async fn pipeline_flow(
410 &self,
411 entity_name: &str,
412 ) -> Result<Vec<(EntityDetail, String, EntityDetail)>, GraphError> {
413 query::pipeline_flow(&self.db, entity_name).await
414 }
415
416 pub async fn sync_vigil_signals(
420 &self,
421 signals_path: &std::path::Path,
422 ) -> Result<VigilSyncReport, GraphError> {
423 vigil_sync::sync_vigil_signals(self, signals_path).await
424 }
425
426 pub async fn sync_outcomes(
428 &self,
429 outcomes_path: &std::path::Path,
430 ) -> Result<VigilSyncReport, GraphError> {
431 vigil_sync::sync_outcomes(self, outcomes_path).await
432 }
433
434 pub async fn sync_vigil(
436 &self,
437 signals_path: &std::path::Path,
438 outcomes_path: &std::path::Path,
439 ) -> Result<VigilSyncReport, GraphError> {
440 vigil_sync::sync_vigil(self, signals_path, outcomes_path).await
441 }
442
443 pub async fn record_outcome_feedback(
448 &self,
449 session_id: &str,
450 outcome: utility::OutcomeKind,
451 retrieved_entity_ids: &[String],
452 used_entity_ids: Option<&[String]>,
453 ) -> Result<utility::FeedbackReport, GraphError> {
454 utility::record_outcome_feedback(
455 &self.db,
456 session_id,
457 outcome,
458 retrieved_entity_ids,
459 used_entity_ids,
460 )
461 .await
462 }
463
464 pub async fn run_gc(&self, config: &gc::GcConfig) -> Result<gc::GcReport, GraphError> {
468 gc::run_gc(&self.db, config).await
469 }
470
471 pub async fn gc_stats(&self) -> Result<gc::GcStatsReport, GraphError> {
473 gc::stats_only(&self.db).await
474 }
475
476 pub async fn delete_relationship(&self, id: &str) -> Result<(), GraphError> {
478 crud::delete_relationship(&self.db, id).await
479 }
480
481 pub async fn stats(&self) -> Result<GraphStats, GraphError> {
485 let entity_count = db_count(&self.db, "entity").await?;
486 let relationship_count = db_count(&self.db, "relates_to").await?;
487 let episode_count = db_count(&self.db, "episode").await?;
488
489 let mut type_response = self
491 .db
492 .query("SELECT entity_type, count() AS count FROM entity GROUP BY entity_type")
493 .await?;
494
495 let type_rows: Vec<TypeCount> = type_response.take(0)?;
496 let entity_type_counts: HashMap<String, u64> = type_rows
497 .into_iter()
498 .map(|r| (r.entity_type, r.count))
499 .collect();
500
501 Ok(GraphStats {
502 entity_count,
503 relationship_count,
504 episode_count,
505 entity_type_counts,
506 })
507 }
508}
509
510#[cfg(feature = "embedded")]
514fn load_scoring_config(graph_path: &Path) -> crate::config::GraphScoringConfig {
515 let memory_dir = graph_path.parent().unwrap_or(graph_path);
516 crate::config::load_from_dir(memory_dir)
517 .graph
518 .map(|g| g.scoring)
519 .unwrap_or_default()
520}
521
522async fn db_count(db: &Surreal<Db>, table: &str) -> Result<u64, GraphError> {
523 let query = format!("SELECT count() AS count FROM {table} GROUP ALL");
524 let mut response = db.query(&query).await?;
525 let rows: Vec<CountRow> = response.take(0)?;
526 Ok(rows.first().map(|r| r.count).unwrap_or(0))
527}
528
529#[derive(serde::Deserialize, surrealdb::types::SurrealValue)]
530struct CountRow {
531 count: u64,
532}
533
534#[derive(serde::Deserialize, surrealdb::types::SurrealValue)]
535struct TypeCount {
536 entity_type: String,
537 count: u64,
538}