Skip to main content

recall_echo/graph/
types.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6/// Node types in the knowledge graph.
7/// Mutable types can be merged/updated. Immutable types are historical facts.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum EntityType {
11    Person,
12    Project,
13    Tool,
14    Service,
15    Preference,
16    Decision,
17    Event,
18    Concept,
19    Case,
20    Pattern,
21    Thread,
22    Thought,
23    Question,
24    Observation,
25    Policy,
26    Measurement,
27    Outcome,
28}
29
30impl EntityType {
31    #[must_use]
32    pub fn is_mutable(&self) -> bool {
33        !matches!(
34            self,
35            Self::Decision
36                | Self::Event
37                | Self::Case
38                | Self::Observation
39                | Self::Measurement
40                | Self::Outcome
41        )
42    }
43}
44
45impl fmt::Display for EntityType {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        let s = serde_json::to_value(self)
48            .ok()
49            .and_then(|v| v.as_str().map(String::from))
50            .unwrap_or_else(|| format!("{self:?}"));
51        write!(f, "{s}")
52    }
53}
54
55impl std::str::FromStr for EntityType {
56    type Err = String;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        serde_json::from_value(serde_json::Value::String(s.to_string()))
60            .map_err(|_| format!("unknown entity type: {s}"))
61    }
62}
63
64/// Input for creating a new entity.
65#[derive(Debug, Clone)]
66pub struct NewEntity {
67    pub name: String,
68    pub entity_type: EntityType,
69    pub abstract_text: String,
70    pub overview: Option<String>,
71    pub content: Option<String>,
72    pub attributes: Option<serde_json::Value>,
73    pub source: Option<String>,
74}
75
76/// A stored entity with all fields.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Entity {
79    pub id: serde_json::Value,
80    pub name: String,
81    pub entity_type: EntityType,
82    #[serde(rename = "abstract")]
83    pub abstract_text: String,
84    pub overview: String,
85    pub content: Option<String>,
86    pub attributes: Option<serde_json::Value>,
87    #[serde(default)]
88    pub embedding: Option<Vec<f32>>,
89    #[serde(default = "default_true")]
90    pub mutable: bool,
91    #[serde(default)]
92    pub access_count: i64,
93    /// How useful this entity has been across sessions (0.0-1.0, default 0.5 neutral).
94    #[serde(default = "default_utility_score")]
95    pub utility_score: f64,
96    /// Number of times utility_score has been updated via outcome feedback.
97    #[serde(default)]
98    pub utility_updates: i64,
99    pub created_at: serde_json::Value,
100    pub updated_at: serde_json::Value,
101    pub source: Option<String>,
102}
103
104impl Entity {
105    /// Get the record ID as a string (e.g. "entity:abc123").
106    #[must_use]
107    pub fn id_string(&self) -> String {
108        match &self.id {
109            serde_json::Value::String(s) => s.clone(),
110            other => other.to_string(),
111        }
112    }
113
114    /// Get the updated_at timestamp as a string.
115    #[must_use]
116    pub fn updated_at_string(&self) -> String {
117        match &self.updated_at {
118            serde_json::Value::String(s) => s.clone(),
119            other => other.to_string(),
120        }
121    }
122}
123
124fn default_true() -> bool {
125    true
126}
127
128fn default_utility_score() -> f64 {
129    0.5
130}
131
132/// Fields that can be updated on an entity.
133#[derive(Debug, Clone, Default, Serialize)]
134pub struct EntityUpdate {
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub abstract_text: Option<String>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub overview: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub content: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub attributes: Option<serde_json::Value>,
143}
144
145/// Input for creating a new relationship.
146#[derive(Debug, Clone)]
147pub struct NewRelationship {
148    pub from_entity: String,
149    pub to_entity: String,
150    pub rel_type: String,
151    pub description: Option<String>,
152    pub confidence: Option<f32>,
153    pub source: Option<String>,
154}
155
156/// A stored relationship.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Relationship {
159    pub id: serde_json::Value,
160    #[serde(rename = "in")]
161    pub from_id: serde_json::Value,
162    #[serde(rename = "out")]
163    pub to_id: serde_json::Value,
164    pub rel_type: String,
165    pub description: Option<String>,
166    pub valid_from: serde_json::Value,
167    pub valid_until: Option<serde_json::Value>,
168    pub confidence: f64,
169    /// When this relationship was last reinforced (Bayesian corroboration).
170    /// Used by temporal decay: effective_confidence = confidence × 0.5^(days_since / half_life).
171    #[serde(default)]
172    pub last_reinforced: Option<serde_json::Value>,
173    pub source: Option<String>,
174}
175
176impl Relationship {
177    /// Get the record ID as a string.
178    #[must_use]
179    pub fn id_string(&self) -> String {
180        match &self.id {
181            serde_json::Value::String(s) => s.clone(),
182            other => other.to_string(),
183        }
184    }
185}
186
187/// Direction for relationship queries.
188#[derive(Debug, Clone, Copy)]
189pub enum Direction {
190    Outgoing,
191    Incoming,
192    Both,
193}
194
195// ── Tiered entity projections ────────────────────────────────────────
196
197/// L0 — Minimal entity for traversal. No embedding, no content.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct EntitySummary {
200    pub id: serde_json::Value,
201    pub name: String,
202    pub entity_type: EntityType,
203    #[serde(rename = "abstract")]
204    pub abstract_text: String,
205}
206
207impl EntitySummary {
208    #[must_use]
209    pub fn id_string(&self) -> String {
210        match &self.id {
211            serde_json::Value::String(s) => s.clone(),
212            other => other.to_string(),
213        }
214    }
215}
216
217/// L1 — Search result detail. Everything except content and embedding.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct EntityDetail {
220    pub id: serde_json::Value,
221    pub name: String,
222    pub entity_type: EntityType,
223    #[serde(rename = "abstract")]
224    pub abstract_text: String,
225    pub overview: String,
226    pub attributes: Option<serde_json::Value>,
227    #[serde(default)]
228    pub access_count: i64,
229    #[serde(default = "default_utility_score")]
230    pub utility_score: f64,
231    pub updated_at: serde_json::Value,
232    pub source: Option<String>,
233}
234
235impl EntityDetail {
236    #[must_use]
237    pub fn id_string(&self) -> String {
238        match &self.id {
239            serde_json::Value::String(s) => s.clone(),
240            other => other.to_string(),
241        }
242    }
243
244    #[must_use]
245    pub fn updated_at_string(&self) -> String {
246        match &self.updated_at {
247            serde_json::Value::String(s) => s.clone(),
248            other => other.to_string(),
249        }
250    }
251}
252
253// ── Search types ────────────────────────────────────────────────────
254
255/// Options for entity search.
256#[derive(Debug, Clone, Default)]
257pub struct SearchOptions {
258    pub limit: usize,
259    pub entity_type: Option<String>,
260    pub keyword: Option<String>,
261}
262
263/// How an entity was found.
264#[derive(Debug, Clone)]
265pub enum MatchSource {
266    /// Found via semantic similarity.
267    Semantic,
268    /// Found via graph expansion from a parent entity.
269    Graph { parent: String, rel_type: String },
270    /// Found via keyword filter match.
271    Keyword,
272}
273
274/// A scored entity in search results.
275#[derive(Debug, Clone)]
276pub struct ScoredEntity {
277    pub entity: EntityDetail,
278    pub score: f64,
279    pub source: MatchSource,
280}
281
282/// An episode search result.
283#[derive(Debug, Clone)]
284pub struct EpisodeSearchResult {
285    pub episode: Episode,
286    pub score: f64,
287    pub distance: f64,
288}
289
290/// Options for hybrid query (semantic + graph expansion + episodes).
291#[derive(Debug, Clone)]
292pub struct QueryOptions {
293    pub limit: usize,
294    pub entity_type: Option<String>,
295    pub keyword: Option<String>,
296    pub graph_depth: u32,
297    pub include_episodes: bool,
298}
299
300impl Default for QueryOptions {
301    fn default() -> Self {
302        Self {
303            limit: 10,
304            entity_type: None,
305            keyword: None,
306            graph_depth: 1,
307            include_episodes: false,
308        }
309    }
310}
311
312/// Result of a hybrid query.
313#[derive(Debug, Clone)]
314pub struct QueryResult {
315    pub entities: Vec<ScoredEntity>,
316    pub episodes: Vec<EpisodeSearchResult>,
317}
318
319/// A search result with scoring (legacy — wraps full Entity).
320#[derive(Debug, Clone)]
321pub struct SearchResult {
322    pub entity: Entity,
323    pub score: f64,
324    pub distance: f64,
325}
326
327/// A node in a traversal tree.
328#[derive(Debug, Clone)]
329pub struct TraversalNode {
330    pub entity: EntitySummary,
331    pub edges: Vec<TraversalEdge>,
332}
333
334/// An edge in a traversal tree.
335#[derive(Debug, Clone)]
336pub struct TraversalEdge {
337    pub rel_type: String,
338    pub direction: String,
339    pub target: TraversalNode,
340    pub valid_from: serde_json::Value,
341    pub valid_until: Option<serde_json::Value>,
342    pub confidence: f64,
343}
344
345/// A row from a relationship query (shared by traverse and query).
346#[derive(Debug, Clone, serde::Deserialize)]
347pub struct EdgeRow {
348    pub rel_type: String,
349    pub valid_from: serde_json::Value,
350    pub valid_until: Option<serde_json::Value>,
351    pub target_id: serde_json::Value,
352    #[serde(default = "default_confidence")]
353    pub confidence: f64,
354    #[serde(default)]
355    pub last_reinforced: Option<serde_json::Value>,
356}
357
358fn default_confidence() -> f64 {
359    1.0
360}
361
362impl EdgeRow {
363    #[must_use]
364    pub fn target_id_string(&self) -> String {
365        match &self.target_id {
366            serde_json::Value::String(s) => s.clone(),
367            other => other.to_string(),
368        }
369    }
370}
371
372/// Graph-level statistics.
373#[derive(Debug, Clone)]
374pub struct GraphStats {
375    pub entity_count: u64,
376    pub relationship_count: u64,
377    pub episode_count: u64,
378    pub entity_type_counts: HashMap<String, u64>,
379}
380
381// ── Ingestion types (Phase 2) ────────────────────────────────────────
382
383/// Input for creating a new episode.
384#[derive(Debug, Clone)]
385pub struct NewEpisode {
386    pub session_id: String,
387    pub abstract_text: String,
388    pub overview: Option<String>,
389    pub content: Option<String>,
390    pub log_number: Option<u32>,
391}
392
393/// A stored episode.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct Episode {
396    pub id: serde_json::Value,
397    pub session_id: String,
398    pub timestamp: serde_json::Value,
399    #[serde(rename = "abstract")]
400    pub abstract_text: String,
401    pub overview: Option<String>,
402    pub content: Option<String>,
403    #[serde(default)]
404    pub embedding: Option<Vec<f32>>,
405    pub log_number: Option<i64>,
406}
407
408impl Episode {
409    #[must_use]
410    pub fn id_string(&self) -> String {
411        match &self.id {
412            serde_json::Value::String(s) => s.clone(),
413            other => other.to_string(),
414        }
415    }
416}
417
418/// A candidate entity extracted by the LLM from a conversation chunk.
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct ExtractedEntity {
421    pub name: String,
422    #[serde(rename = "type")]
423    pub entity_type: EntityType,
424    #[serde(rename = "abstract")]
425    pub abstract_text: String,
426    pub overview: Option<String>,
427    pub content: Option<String>,
428    pub attributes: Option<serde_json::Value>,
429}
430
431impl ExtractedEntity {
432    /// Convert this extraction result into a `NewEntity` ready for storage.
433    #[must_use]
434    pub fn to_new_entity(&self, session_id: &str) -> NewEntity {
435        NewEntity {
436            name: self.name.clone(),
437            entity_type: self.entity_type.clone(),
438            abstract_text: self.abstract_text.clone(),
439            overview: self.overview.clone(),
440            content: self.content.clone(),
441            attributes: self.attributes.clone(),
442            source: Some(session_id.to_string()),
443        }
444    }
445}
446
447/// A candidate relationship extracted by the LLM.
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct ExtractedRelationship {
450    pub source: String,
451    pub target: String,
452    pub rel_type: String,
453    pub description: Option<String>,
454    #[serde(default)]
455    pub confidence: Option<String>,
456}
457
458/// An extracted case (problem-solution pair).
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct ExtractedCase {
461    pub problem: String,
462    pub solution: String,
463    pub context: Option<String>,
464}
465
466/// An extracted pattern (reusable process).
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct ExtractedPattern {
469    pub name: String,
470    pub process: String,
471    pub conditions: Option<String>,
472}
473
474/// An extracted preference (one per facet).
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct ExtractedPreference {
477    pub facet: String,
478    pub value: String,
479    pub context: Option<String>,
480}
481
482/// Full extraction result from a single conversation chunk.
483#[derive(Debug, Clone, Serialize, Deserialize, Default)]
484pub struct ExtractionResult {
485    #[serde(default)]
486    pub entities: Vec<ExtractedEntity>,
487    #[serde(default)]
488    pub relationships: Vec<ExtractedRelationship>,
489    #[serde(default)]
490    pub cases: Vec<ExtractedCase>,
491    #[serde(default)]
492    pub patterns: Vec<ExtractedPattern>,
493    #[serde(default)]
494    pub preferences: Vec<ExtractedPreference>,
495}
496
497/// LLM deduplication decision for a candidate entity.
498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
499#[serde(rename_all = "snake_case")]
500pub enum DedupDecision {
501    Skip,
502    Create,
503    Merge { target: String },
504}
505
506// ── Pipeline types ───────────────────────────────────────────────────
507
508/// Canonical relationship types for the praxis pipeline.
509pub mod pipeline_rels {
510    pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
511    pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
512    pub const INFORMED_BY: &str = "INFORMED_BY";
513    pub const EXPLORES: &str = "EXPLORES";
514    pub const GRADUATED_TO: &str = "GRADUATED_TO";
515    pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
516    pub const CONNECTED_TO: &str = "CONNECTED_TO";
517    pub const PROMPTED_BY: &str = "PROMPTED_BY";
518    pub const ANSWERED_BY: &str = "ANSWERED_BY";
519}
520
521/// Canonical relationship types for vigil-pulse data.
522pub mod vigil_rels {
523    pub const MEASURED_DURING: &str = "MEASURED_DURING";
524    pub const RESULTED_IN: &str = "RESULTED_IN";
525    pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
526}
527
528/// Report from a vigil sync operation.
529#[derive(Debug, Clone, Default)]
530pub struct VigilSyncReport {
531    pub measurements_created: u32,
532    pub outcomes_created: u32,
533    pub events_created: u32,
534    pub relationships_created: u32,
535    pub skipped: u32,
536    pub errors: Vec<String>,
537}
538
539/// Contents of all 5 pipeline markdown files.
540#[derive(Debug, Clone, Default)]
541pub struct PipelineDocuments {
542    pub learning: String,
543    pub thoughts: String,
544    pub curiosity: String,
545    pub reflections: String,
546    pub praxis: String,
547}
548
549/// Report from a pipeline sync operation.
550#[derive(Debug, Clone, Default)]
551pub struct PipelineSyncReport {
552    pub entities_created: u32,
553    pub entities_updated: u32,
554    pub entities_archived: u32,
555    pub relationships_created: u32,
556    pub relationships_skipped: u32,
557    pub errors: Vec<String>,
558}
559
560/// Pipeline health stats from the graph.
561#[derive(Debug, Clone, Default)]
562pub struct PipelineGraphStats {
563    pub by_stage: HashMap<String, HashMap<String, u64>>,
564    pub stale_thoughts: Vec<EntityDetail>,
565    pub stale_questions: Vec<EntityDetail>,
566    pub orphan_count: u64,
567    pub total_entities: u64,
568    pub last_movement: Option<String>,
569}
570
571/// A parsed pipeline entry from a markdown document.
572#[derive(Debug, Clone)]
573pub struct PipelineEntry {
574    /// Title from ### heading (cleaned of dates and markers).
575    pub title: String,
576    /// Full content under the heading.
577    pub body: String,
578    /// Status: "active", "graduated", "dissolved", "explored", "retired".
579    pub status: String,
580    /// Stage: "learning", "thoughts", "curiosity", "reflections", "praxis".
581    pub stage: String,
582    /// Mapped entity type.
583    pub entity_type: EntityType,
584    /// Date from heading or metadata field.
585    pub date: Option<String>,
586    /// **Source:** field value.
587    pub source_ref: Option<String>,
588    /// **Destination:** field value.
589    pub destination: Option<String>,
590    /// Parsed "Connected to:" references.
591    pub connected_to: Vec<String>,
592    /// Sub-type for special sections: "theme", "pattern", "phronesis".
593    pub sub_type: Option<String>,
594}
595
596/// Result of a full ingestion run.
597#[derive(Debug, Clone, Default)]
598pub struct IngestionReport {
599    pub episodes_created: u32,
600    pub entities_created: u32,
601    pub entities_merged: u32,
602    pub entities_skipped: u32,
603    pub relationships_created: u32,
604    pub relationships_skipped: u32,
605    pub errors: Vec<String>,
606    pub estimated_tokens: u64,
607}