1use crate::scope::Scope;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16pub type FactId = Uuid;
21pub type EntityId = Uuid;
22pub type RelationshipId = Uuid;
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35pub enum MemoryTier {
36 Working,
37 #[default]
38 Conversation,
39 Knowledge,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Fact {
54 pub id: FactId,
55 pub text: String,
57 pub scope: Scope,
59 pub tier: MemoryTier,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub category: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub source: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub confidence: Option<f32>,
70 pub valid_from: DateTime<Utc>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub invalid_at: Option<DateTime<Utc>>,
75 pub created_at: DateTime<Utc>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub embedding: Vec<f32>,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub entity_refs: Vec<EntityId>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub supersedes: Option<FactId>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub superseded_by: Option<FactId>,
88 #[serde(default)]
90 pub access_count: u64,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub last_accessed: Option<DateTime<Utc>>,
94 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
96 pub metadata: serde_json::Map<String, serde_json::Value>,
97}
98
99impl Fact {
100 pub fn new(text: impl Into<String>, scope: Scope) -> Self {
102 let now = Utc::now();
103 Self {
104 id: Uuid::new_v4(),
105 text: text.into(),
106 scope,
107 tier: MemoryTier::default(),
108 category: None,
109 source: None,
110 confidence: None,
111 valid_from: now,
112 invalid_at: None,
113 created_at: now,
114 embedding: Vec::new(),
115 entity_refs: Vec::new(),
116 supersedes: None,
117 superseded_by: None,
118 access_count: 0,
119 last_accessed: None,
120 metadata: serde_json::Map::new(),
121 }
122 }
123
124 pub fn is_valid(&self) -> bool {
126 self.is_valid_at(Utc::now())
127 }
128
129 pub fn is_valid_at(&self, at: DateTime<Utc>) -> bool {
131 if at < self.valid_from {
132 return false;
133 }
134 match self.invalid_at {
135 Some(exp) => at < exp,
136 None => true,
137 }
138 }
139
140 pub fn with_tier(mut self, tier: MemoryTier) -> Self {
143 self.tier = tier;
144 self
145 }
146
147 pub fn with_category(mut self, category: impl Into<String>) -> Self {
148 self.category = Some(category.into());
149 self
150 }
151
152 pub fn with_confidence(mut self, confidence: f32) -> Self {
153 self.confidence = Some(confidence);
154 self
155 }
156
157 pub fn with_source(mut self, source: impl Into<String>) -> Self {
158 self.source = Some(source.into());
159 self
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct Entity {
170 pub id: EntityId,
171 pub name: String,
172 pub entity_type: String,
173 pub scope: Scope,
174 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
176 pub attributes: serde_json::Map<String, serde_json::Value>,
177 pub created_at: DateTime<Utc>,
178 pub updated_at: DateTime<Utc>,
179}
180
181impl Entity {
182 pub fn new(name: impl Into<String>, scope: Scope) -> Self {
183 let now = Utc::now();
184 Self {
185 id: Uuid::new_v4(),
186 name: name.into(),
187 entity_type: "unknown".to_string(),
188 scope,
189 attributes: serde_json::Map::new(),
190 created_at: now,
191 updated_at: now,
192 }
193 }
194
195 pub fn with_type(mut self, entity_type: impl Into<String>) -> Self {
196 self.entity_type = entity_type.into();
197 self
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct Relationship {
208 pub id: RelationshipId,
209 pub source_id: EntityId,
211 pub relation: String,
213 pub target_id: EntityId,
215 pub scope: Scope,
216 pub valid_from: DateTime<Utc>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub invalid_at: Option<DateTime<Utc>>,
220 pub created_at: DateTime<Utc>,
221}
222
223impl Relationship {
224 pub fn new(
225 source_id: EntityId,
226 relation: impl Into<String>,
227 target_id: EntityId,
228 scope: Scope,
229 ) -> Self {
230 let now = Utc::now();
231 Self {
232 id: Uuid::new_v4(),
233 source_id,
234 relation: relation.into(),
235 target_id,
236 scope,
237 valid_from: now,
238 invalid_at: None,
239 created_at: now,
240 }
241 }
242
243 pub fn is_valid(&self) -> bool {
245 self.is_valid_at(Utc::now())
246 }
247
248 pub fn is_valid_at(&self, at: DateTime<Utc>) -> bool {
250 if at < self.valid_from {
251 return false;
252 }
253 match self.invalid_at {
254 Some(exp) => at < exp,
255 None => true,
256 }
257 }
258}
259
260#[derive(Debug, Clone, Default, Serialize, Deserialize)]
266pub struct SubGraph {
267 pub entities: Vec<Entity>,
268 pub relationships: Vec<Relationship>,
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
279pub struct FactPatch {
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub text: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub tier: Option<MemoryTier>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub category: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub source: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub confidence: Option<f32>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub invalid_at: Option<DateTime<Utc>>,
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub embedding: Vec<f32>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub superseded_by: Option<FactId>,
296 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
297 pub metadata: serde_json::Map<String, serde_json::Value>,
298}
299
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
306pub struct FactFilter {
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub scope: Option<Scope>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub tier: Option<MemoryTier>,
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub category: Option<String>,
316 #[serde(default = "default_valid_only")]
318 pub valid_only: bool,
319 #[serde(skip_serializing_if = "Option::is_none")]
322 pub as_of: Option<DateTime<Utc>>,
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub text_contains: Option<String>,
326 #[serde(default = "default_limit")]
328 pub limit: u32,
329 #[serde(default)]
331 pub offset: u32,
332}
333
334fn default_valid_only() -> bool {
335 true
336}
337
338fn default_limit() -> u32 {
339 50
340}
341
342impl FactFilter {
343 pub fn new() -> Self {
344 Self {
345 valid_only: true,
346 limit: 50,
347 ..Default::default()
348 }
349 }
350
351 pub fn with_scope(mut self, scope: Scope) -> Self {
352 self.scope = Some(scope);
353 self
354 }
355
356 pub fn with_tier(mut self, tier: MemoryTier) -> Self {
357 self.tier = Some(tier);
358 self
359 }
360
361 pub fn include_invalid(mut self) -> Self {
363 self.valid_only = false;
364 self
365 }
366}
367
368#[cfg(test)]
373mod tests {
374 use super::*;
375
376 fn org_scope() -> Scope {
377 Scope::org("acme")
378 }
379
380 #[test]
381 fn fact_new_defaults() {
382 let f = Fact::new("Alice likes Rust", org_scope());
383 assert_eq!(f.text, "Alice likes Rust");
384 assert_eq!(f.tier, MemoryTier::Conversation);
385 assert!(f.is_valid());
386 assert!(f.embedding.is_empty());
387 assert_eq!(f.access_count, 0);
388 }
389
390 #[test]
391 fn fact_builder_methods() {
392 let f = Fact::new("test", org_scope())
393 .with_tier(MemoryTier::Knowledge)
394 .with_category("preference")
395 .with_confidence(0.9)
396 .with_source("gpt-4o");
397 assert_eq!(f.tier, MemoryTier::Knowledge);
398 assert_eq!(f.category.as_deref(), Some("preference"));
399 assert_eq!(f.confidence, Some(0.9));
400 assert_eq!(f.source.as_deref(), Some("gpt-4o"));
401 }
402
403 #[test]
404 fn fact_is_valid_at_before_valid_from() {
405 let future = Utc::now() + chrono::Duration::hours(1);
406 let mut f = Fact::new("future fact", org_scope());
407 f.valid_from = future;
408 assert!(!f.is_valid());
409 }
410
411 #[test]
412 fn fact_is_valid_at_after_invalid_at() {
413 let past = Utc::now() - chrono::Duration::hours(1);
414 let mut f = Fact::new("expired fact", org_scope());
415 f.invalid_at = Some(past);
416 assert!(!f.is_valid());
417 }
418
419 #[test]
420 fn entity_new_and_with_type() {
421 let e = Entity::new("Anthropic", org_scope()).with_type("organization");
422 assert_eq!(e.name, "Anthropic");
423 assert_eq!(e.entity_type, "organization");
424 }
425
426 #[test]
427 fn relationship_new_and_validity() {
428 let src = Uuid::new_v4();
429 let tgt = Uuid::new_v4();
430 let r = Relationship::new(src, "founded_by", tgt, org_scope());
431 assert_eq!(r.relation, "founded_by");
432 assert!(r.is_valid());
433 }
434
435 #[test]
436 fn fact_filter_defaults() {
437 let f = FactFilter::new();
438 assert!(f.valid_only);
439 assert_eq!(f.limit, 50);
440 assert_eq!(f.offset, 0);
441 }
442}