1use crate::path::{PathHop, RetrievalPath};
9use crate::traits::KnowledgeGraph;
10use crate::types::EntityRef;
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use klieo_core::error::MemoryError;
14use klieo_core::ids::FactId;
15use klieo_core::memory::Scope;
16use petgraph::stable_graph::{NodeIndex, StableGraph};
17use petgraph::Undirected;
18use std::collections::{HashMap, HashSet};
19use std::sync::Mutex;
20
21#[derive(Debug, Clone)]
26enum Node {
27 Entity,
28 FactRef(FactId),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32struct ScopeKey {
33 kind: &'static str,
34 value: String,
35}
36
37impl From<&Scope> for ScopeKey {
38 fn from(scope: &Scope) -> Self {
39 match scope {
40 Scope::Workspace(s) => Self {
41 kind: "workspace",
42 value: s.clone(),
43 },
44 Scope::Agent(s) => Self {
45 kind: "agent",
46 value: s.clone(),
47 },
48 Scope::Global => Self {
49 kind: "global",
50 value: String::new(),
51 },
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
57enum Edge {
58 MentionedIn,
59 CoOccurs { count: u32 },
60}
61
62struct Inner {
63 graph: StableGraph<Node, Edge, Undirected>,
64 entity_idx: HashMap<(String, String, ScopeKey), NodeIndex>,
65 factref_idx: HashMap<(String, ScopeKey), NodeIndex>,
69}
70
71pub struct InMemoryGraph {
78 inner: Mutex<Inner>,
79}
80
81impl Default for InMemoryGraph {
82 fn default() -> Self {
83 Self {
84 inner: Mutex::new(Inner {
85 graph: StableGraph::default(),
86 entity_idx: HashMap::new(),
87 factref_idx: HashMap::new(),
88 }),
89 }
90 }
91}
92
93#[async_trait]
94impl KnowledgeGraph for InMemoryGraph {
95 async fn index(
96 &self,
97 scope: Scope,
98 fact_id: &FactId,
99 entities: &[EntityRef],
100 _text: &str,
101 _valid_from: Option<DateTime<Utc>>,
102 ) -> Result<(), MemoryError> {
103 if entities.is_empty() {
104 tracing::debug!(%fact_id, "index called with empty entities; no-op");
105 return Ok(());
106 }
107 let sk = ScopeKey::from(&scope);
108 let mut guard = self.inner.lock().map_err(|_| {
110 tracing::error!(operation = "InMemoryGraph::index", "mutex poisoned");
111 MemoryError::Store("InMemoryGraph mutex poisoned".into())
112 })?;
113 let Inner {
115 graph,
116 entity_idx,
117 factref_idx,
118 } = &mut *guard;
119
120 let fact_node = *factref_idx
121 .entry((fact_id.to_string(), sk.clone()))
122 .or_insert_with(|| graph.add_node(Node::FactRef(fact_id.clone())));
123
124 let mut entity_nodes: Vec<NodeIndex> = Vec::with_capacity(entities.len());
125 for entity in entities {
126 let key = (
127 entity.entity_type.as_str().to_owned(),
128 entity.name.clone(),
129 sk.clone(),
130 );
131 let ent_node = *entity_idx
132 .entry(key)
133 .or_insert_with(|| graph.add_node(Node::Entity));
134 entity_nodes.push(ent_node);
135 if !graph.contains_edge(ent_node, fact_node) {
136 graph.add_edge(ent_node, fact_node, Edge::MentionedIn);
137 }
138 }
139
140 for i in 0..entity_nodes.len() {
141 for j in (i + 1)..entity_nodes.len() {
142 let (a, b) = (entity_nodes[i], entity_nodes[j]);
143 if let Some(edge) = graph.find_edge(a, b) {
144 if let Some(Edge::CoOccurs { count }) = graph.edge_weight_mut(edge) {
145 *count += 1;
146 }
147 } else {
148 graph.add_edge(a, b, Edge::CoOccurs { count: 1 });
149 }
150 }
151 }
152 Ok(())
153 }
154
155 async fn neighbors(
156 &self,
157 scope: &Scope,
158 entities: &[EntityRef],
159 ) -> Result<Vec<FactId>, MemoryError> {
160 if entities.is_empty() {
161 tracing::debug!("neighbors called with empty entities; no-op");
162 return Ok(Vec::new());
163 }
164 let sk = ScopeKey::from(scope);
165 let guard = self.inner.lock().map_err(|_| {
166 tracing::error!(operation = "InMemoryGraph::neighbors", "mutex poisoned");
167 MemoryError::Store("InMemoryGraph mutex poisoned".into())
168 })?;
169 let Inner {
170 graph, entity_idx, ..
171 } = &*guard;
172 let mut seen: HashSet<FactId> = HashSet::new();
173
174 for entity in entities {
175 let key = (
176 entity.entity_type.as_str().to_owned(),
177 entity.name.clone(),
178 sk.clone(),
179 );
180 let Some(&ent_node) = entity_idx.get(&key) else {
181 continue;
182 };
183 collect_reachable_fact_ids(graph, ent_node, &mut seen);
184 }
185
186 Ok(seen.into_iter().collect())
187 }
188
189 async fn recall_paths(
190 &self,
191 scope: &Scope,
192 entities: &[EntityRef],
193 ) -> Result<Vec<RetrievalPath>, MemoryError> {
194 if entities.is_empty() {
195 return Ok(Vec::new());
196 }
197 let sk = ScopeKey::from(scope);
198 let guard = self.inner.lock().map_err(|_| {
199 tracing::error!(operation = "InMemoryGraph::recall_paths", "mutex poisoned");
200 MemoryError::Store("InMemoryGraph mutex poisoned".into())
201 })?;
202 let Inner {
203 graph, entity_idx, ..
204 } = &*guard;
205
206 let mut paths: Vec<RetrievalPath> = Vec::new();
207 let mut seen: HashSet<(String, String, FactId)> = HashSet::new();
208
209 for entity in entities {
210 let key = (
211 entity.entity_type.as_str().to_owned(),
212 entity.name.clone(),
213 sk.clone(),
214 );
215 let Some(&ent_node) = entity_idx.get(&key) else {
216 continue;
217 };
218 let mut fact_ids: HashSet<FactId> = HashSet::new();
219 collect_reachable_fact_ids(graph, ent_node, &mut fact_ids);
220 for fid in fact_ids {
221 let dedupe_key = (
222 entity.entity_type.as_str().to_owned(),
223 entity.name.clone(),
224 fid.clone(),
225 );
226 if !seen.insert(dedupe_key) {
227 continue;
228 }
229 paths.push(RetrievalPath {
230 hops: vec![PathHop::new(entity.clone(), fid)],
231 });
232 }
233 }
234 Ok(paths)
235 }
236
237 async fn forget(&self, scope: &Scope, fact_id: &FactId) -> Result<(), MemoryError> {
238 let sk = ScopeKey::from(scope);
239 let mut guard = self.inner.lock().map_err(|_| {
240 tracing::error!(operation = "InMemoryGraph::forget", "mutex poisoned");
241 MemoryError::Store("InMemoryGraph mutex poisoned".into())
242 })?;
243 let Inner {
244 graph, factref_idx, ..
245 } = &mut *guard;
246
247 let Some(fact_node) = factref_idx.remove(&(fact_id.to_string(), sk)) else {
248 return Ok(());
249 };
250
251 let fact_edges: Vec<_> = graph
254 .edges(fact_node)
255 .map(|e| petgraph::visit::EdgeRef::id(&e))
256 .collect();
257 for edge in fact_edges {
258 graph.remove_edge(edge);
259 }
260 graph.remove_node(fact_node);
261 Ok(())
262 }
263}
264
265fn collect_reachable_fact_ids(
268 graph: &StableGraph<Node, Edge, Undirected>,
269 entry_entity: NodeIndex,
270 seen: &mut HashSet<FactId>,
271) {
272 for neighbor in graph.neighbors(entry_entity) {
273 match &graph[neighbor] {
274 Node::FactRef(fid) => {
275 seen.insert(fid.clone());
276 }
277 Node::Entity => {
278 for deeper in graph.neighbors(neighbor) {
279 if let Node::FactRef(fid) = &graph[deeper] {
280 seen.insert(fid.clone());
281 }
282 }
283 }
284 }
285 }
286}