1use chrono::Local;
18
19use crate::{
20 db::{AimemDb, DbError},
21 types::{Entity, Triple},
22};
23
24#[derive(Debug, Clone)]
25pub struct KnowledgeGraph {
26 db: AimemDb,
27}
28
29impl KnowledgeGraph {
30 pub fn new(db: AimemDb) -> Self {
31 Self { db }
32 }
33
34 pub async fn add_entity(
38 &self,
39 name: &str,
40 entity_type: &str,
41 properties: Option<serde_json::Value>,
42 ) -> Result<String, DbError> {
43 let id = entity_id(name);
44 let props = properties
45 .map(|p| serde_json::to_string(&p).unwrap_or_else(|_| "{}".to_string()))
46 .unwrap_or_else(|| "{}".to_string());
47 let entity = Entity {
48 id: id.clone(),
49 name: name.to_string(),
50 entity_type: entity_type.to_string(),
51 properties: props,
52 created_at: chrono::Utc::now().to_rfc3339(),
53 };
54 self.db.upsert_entity(&entity).await?;
55 Ok(id)
56 }
57
58 pub async fn add_triple(
65 &self,
66 subject: &str,
67 predicate: &str,
68 object: &str,
69 valid_from: Option<&str>,
70 valid_to: Option<&str>,
71 ) -> Result<String, DbError> {
72 self.add_entity(subject, "unknown", None).await?;
74 self.add_entity(object, "unknown", None).await?;
75
76 let sub_id = entity_id(subject);
77 let obj_id = entity_id(object);
78 let pred = normalize_predicate(predicate);
79
80 let conn = self.db.conn()?;
82 let mut existing = conn
83 .query(
84 "SELECT id FROM triples \
85 WHERE subject = ?1 AND predicate = ?2 AND object = ?3 AND valid_to IS NULL",
86 [sub_id.as_str(), pred.as_str(), obj_id.as_str()],
87 )
88 .await?;
89 if let Some(row) = existing.next().await? {
90 if let Ok(turso::Value::Text(id)) = row.get_value(0) {
91 return Ok(id);
92 }
93 }
94
95 let now = chrono::Utc::now().to_rfc3339();
96 let triple_id = {
97 let input = format!("{sub_id}{pred}{obj_id}{now}");
98 let digest = md5::compute(input.as_bytes());
99 format!("t_{sub_id}_{pred}_{obj_id}_{digest:x}")
100 };
101
102 let triple = Triple {
103 id: triple_id.clone(),
104 subject: sub_id,
105 predicate: pred,
106 object: obj_id,
107 valid_from: valid_from.map(str::to_string),
108 valid_to: valid_to.map(str::to_string),
109 confidence: 1.0,
110 source_closet: None,
111 source_file: None,
112 extracted_at: now,
113 current: true,
114 };
115 self.db.insert_triple(&triple).await?;
116 Ok(triple_id)
117 }
118
119 pub async fn invalidate(
121 &self,
122 subject: &str,
123 predicate: &str,
124 object: &str,
125 ended: Option<&str>,
126 ) -> Result<u64, DbError> {
127 let sub_id = entity_id(subject);
128 let obj_id = entity_id(object);
129 let pred = normalize_predicate(predicate);
130 let ended = ended
131 .map(str::to_string)
132 .unwrap_or_else(|| Local::now().date_naive().to_string());
133 self.db
134 .invalidate_triple(&sub_id, &pred, &obj_id, &ended)
135 .await
136 }
137
138 pub async fn query_entity(
145 &self,
146 name: &str,
147 as_of: Option<&str>,
148 direction: &str,
149 ) -> Result<Vec<Triple>, DbError> {
150 let eid = entity_id(name);
151 let conn = self.db.conn()?;
152 let mut results = Vec::new();
153
154 if direction == "outgoing" || direction == "both" {
155 let sql = build_entity_query("subject", as_of);
156 let mut rows = conn.query(&sql, [eid.as_str()]).await?;
157 while let Some(row) = rows.next().await? {
158 results.push(row_to_triple(&row)?);
159 }
160 }
161
162 if direction == "incoming" || direction == "both" {
163 let sql = build_entity_query("object", as_of);
164 let mut rows = conn.query(&sql, [eid.as_str()]).await?;
165 while let Some(row) = rows.next().await? {
166 results.push(row_to_triple(&row)?);
167 }
168 }
169
170 Ok(results)
171 }
172
173 pub async fn timeline(&self, entity: Option<&str>) -> Result<Vec<Triple>, DbError> {
175 let conn = self.db.conn()?;
176 let mut results = Vec::new();
177
178 if let Some(name) = entity {
179 let eid = entity_id(name);
180 let mut rows = conn
181 .query(
182 "SELECT id, subject, predicate, object, valid_from, valid_to, \
183 confidence, source_closet, source_file, extracted_at \
184 FROM triples \
185 WHERE subject = ?1 OR object = ?1 \
186 ORDER BY valid_from ASC LIMIT 100",
187 [eid.as_str()],
188 )
189 .await?;
190 while let Some(row) = rows.next().await? {
191 results.push(row_to_triple(&row)?);
192 }
193 } else {
194 let mut rows = conn
195 .query(
196 "SELECT id, subject, predicate, object, valid_from, valid_to, \
197 confidence, source_closet, source_file, extracted_at \
198 FROM triples \
199 ORDER BY valid_from ASC LIMIT 100",
200 (),
201 )
202 .await?;
203 while let Some(row) = rows.next().await? {
204 results.push(row_to_triple(&row)?);
205 }
206 }
207
208 Ok(results)
209 }
210
211 pub async fn stats(&self) -> Result<serde_json::Value, DbError> {
213 let conn = self.db.conn()?;
214
215 let entities = query_count(&conn, "SELECT COUNT(*) FROM entities").await?;
216 let triples = query_count(&conn, "SELECT COUNT(*) FROM triples").await?;
217 let current =
218 query_count(&conn, "SELECT COUNT(*) FROM triples WHERE valid_to IS NULL").await?;
219
220 let mut pred_rows = conn
221 .query(
222 "SELECT DISTINCT predicate FROM triples ORDER BY predicate",
223 (),
224 )
225 .await?;
226 let mut predicates = Vec::new();
227 while let Some(row) = pred_rows.next().await? {
228 if let Ok(turso::Value::Text(p)) = row.get_value(0) {
229 predicates.push(p);
230 }
231 }
232
233 Ok(serde_json::json!({
234 "entities": entities,
235 "triples": triples,
236 "current_facts": current,
237 "expired_facts": triples - current,
238 "relationship_types": predicates,
239 }))
240 }
241}
242
243fn entity_id(name: &str) -> String {
246 name.to_lowercase().replace(' ', "_").replace('\'', "")
247}
248
249fn normalize_predicate(pred: &str) -> String {
250 pred.to_lowercase().replace(' ', "_")
251}
252
253fn build_entity_query(col: &str, as_of: Option<&str>) -> String {
254 let mut q = format!(
255 "SELECT id, subject, predicate, object, valid_from, valid_to, \
256 confidence, source_closet, source_file, extracted_at \
257 FROM triples WHERE {col} = ?1"
258 );
259 if let Some(date) = as_of {
260 q.push_str(&format!(
261 " AND (valid_from IS NULL OR valid_from <= '{date}') \
262 AND (valid_to IS NULL OR valid_to >= '{date}')"
263 ));
264 }
265 q
266}
267
268fn row_to_triple(row: &turso::Row) -> Result<Triple, DbError> {
269 let valid_to_str = val_opt_str(row, 5);
270 Ok(Triple {
271 id: val_str(row, 0),
272 subject: val_str(row, 1),
273 predicate: val_str(row, 2),
274 object: val_str(row, 3),
275 valid_from: val_opt_str(row, 4),
276 valid_to: valid_to_str.clone(),
277 confidence: match row.get_value(6)? {
278 turso::Value::Real(f) => f,
279 turso::Value::Integer(i) => i as f64,
280 _ => 1.0,
281 },
282 source_closet: val_opt_str(row, 7),
283 source_file: val_opt_str(row, 8),
284 extracted_at: val_str(row, 9),
285 current: valid_to_str.is_none(),
286 })
287}
288
289fn val_str(row: &turso::Row, idx: usize) -> String {
290 match row.get_value(idx) {
291 Ok(turso::Value::Text(s)) => s,
292 Ok(turso::Value::Null) | Err(_) => String::new(),
293 Ok(v) => format!("{v:?}"),
294 }
295}
296
297fn val_opt_str(row: &turso::Row, idx: usize) -> Option<String> {
298 match row.get_value(idx) {
299 Ok(turso::Value::Text(s)) if !s.is_empty() => Some(s),
300 _ => None,
301 }
302}
303
304async fn query_count(conn: &turso::Connection, sql: &str) -> Result<i64, DbError> {
305 let mut rows = conn.query(sql, ()).await?;
306 let row = rows.next().await?;
307 Ok(row
308 .and_then(|r| r.get_value(0).ok())
309 .and_then(|v| v.as_integer().copied())
310 .unwrap_or(0))
311}