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