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    pub confidence: f64,
320}
321
322/// A row from a relationship query (shared by traverse and query).
323#[derive(Debug, Clone, serde::Deserialize)]
324pub struct EdgeRow {
325    pub rel_type: String,
326    pub valid_from: serde_json::Value,
327    pub valid_until: Option<serde_json::Value>,
328    pub target_id: serde_json::Value,
329    #[serde(default = "default_confidence")]
330    pub confidence: f64,
331}
332
333fn default_confidence() -> f64 {
334    1.0
335}
336
337impl EdgeRow {
338    pub fn target_id_string(&self) -> String {
339        match &self.target_id {
340            serde_json::Value::String(s) => s.clone(),
341            other => other.to_string(),
342        }
343    }
344}
345
346/// Graph-level statistics.
347#[derive(Debug, Clone)]
348pub struct GraphStats {
349    pub entity_count: u64,
350    pub relationship_count: u64,
351    pub episode_count: u64,
352    pub entity_type_counts: HashMap<String, u64>,
353}
354
355// ── Ingestion types (Phase 2) ────────────────────────────────────────
356
357/// Input for creating a new episode.
358#[derive(Debug, Clone)]
359pub struct NewEpisode {
360    pub session_id: String,
361    pub abstract_text: String,
362    pub overview: Option<String>,
363    pub content: Option<String>,
364    pub log_number: Option<u32>,
365}
366
367/// A stored episode.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct Episode {
370    pub id: serde_json::Value,
371    pub session_id: String,
372    pub timestamp: serde_json::Value,
373    #[serde(rename = "abstract")]
374    pub abstract_text: String,
375    pub overview: Option<String>,
376    pub content: Option<String>,
377    #[serde(default)]
378    pub embedding: Option<Vec<f32>>,
379    pub log_number: Option<i64>,
380}
381
382impl Episode {
383    pub fn id_string(&self) -> String {
384        match &self.id {
385            serde_json::Value::String(s) => s.clone(),
386            other => other.to_string(),
387        }
388    }
389}
390
391/// A candidate entity extracted by the LLM from a conversation chunk.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ExtractedEntity {
394    pub name: String,
395    #[serde(rename = "type")]
396    pub entity_type: EntityType,
397    #[serde(rename = "abstract")]
398    pub abstract_text: String,
399    pub overview: Option<String>,
400    pub content: Option<String>,
401    pub attributes: Option<serde_json::Value>,
402}
403
404/// A candidate relationship extracted by the LLM.
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ExtractedRelationship {
407    pub source: String,
408    pub target: String,
409    pub rel_type: String,
410    pub description: Option<String>,
411    #[serde(default)]
412    pub confidence: Option<String>,
413}
414
415/// An extracted case (problem-solution pair).
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct ExtractedCase {
418    pub problem: String,
419    pub solution: String,
420    pub context: Option<String>,
421}
422
423/// An extracted pattern (reusable process).
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct ExtractedPattern {
426    pub name: String,
427    pub process: String,
428    pub conditions: Option<String>,
429}
430
431/// An extracted preference (one per facet).
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct ExtractedPreference {
434    pub facet: String,
435    pub value: String,
436    pub context: Option<String>,
437}
438
439/// Full extraction result from a single conversation chunk.
440#[derive(Debug, Clone, Serialize, Deserialize, Default)]
441pub struct ExtractionResult {
442    #[serde(default)]
443    pub entities: Vec<ExtractedEntity>,
444    #[serde(default)]
445    pub relationships: Vec<ExtractedRelationship>,
446    #[serde(default)]
447    pub cases: Vec<ExtractedCase>,
448    #[serde(default)]
449    pub patterns: Vec<ExtractedPattern>,
450    #[serde(default)]
451    pub preferences: Vec<ExtractedPreference>,
452}
453
454/// LLM deduplication decision for a candidate entity.
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum DedupDecision {
458    Skip,
459    Create,
460    Merge { target: String },
461}
462
463// ── Pipeline types ───────────────────────────────────────────────────
464
465/// Canonical relationship types for the praxis pipeline.
466pub mod pipeline_rels {
467    pub const EVOLVED_FROM: &str = "EVOLVED_FROM";
468    pub const CRYSTALLIZED_FROM: &str = "CRYSTALLIZED_FROM";
469    pub const INFORMED_BY: &str = "INFORMED_BY";
470    pub const EXPLORES: &str = "EXPLORES";
471    pub const GRADUATED_TO: &str = "GRADUATED_TO";
472    pub const ARCHIVED_FROM: &str = "ARCHIVED_FROM";
473    pub const CONNECTED_TO: &str = "CONNECTED_TO";
474}
475
476/// Canonical relationship types for vigil-pulse data.
477pub mod vigil_rels {
478    pub const MEASURED_DURING: &str = "MEASURED_DURING";
479    pub const RESULTED_IN: &str = "RESULTED_IN";
480    pub const TRIGGERED_BY: &str = "TRIGGERED_BY";
481}
482
483/// Report from a vigil sync operation.
484#[derive(Debug, Clone, Default)]
485pub struct VigilSyncReport {
486    pub measurements_created: u32,
487    pub outcomes_created: u32,
488    pub events_created: u32,
489    pub relationships_created: u32,
490    pub skipped: u32,
491    pub errors: Vec<String>,
492}
493
494/// Contents of all 5 pipeline markdown files.
495#[derive(Debug, Clone, Default)]
496pub struct PipelineDocuments {
497    pub learning: String,
498    pub thoughts: String,
499    pub curiosity: String,
500    pub reflections: String,
501    pub praxis: String,
502}
503
504/// Report from a pipeline sync operation.
505#[derive(Debug, Clone, Default)]
506pub struct PipelineSyncReport {
507    pub entities_created: u32,
508    pub entities_updated: u32,
509    pub entities_archived: u32,
510    pub relationships_created: u32,
511    pub relationships_skipped: u32,
512    pub errors: Vec<String>,
513}
514
515/// Pipeline health stats from the graph.
516#[derive(Debug, Clone, Default)]
517pub struct PipelineGraphStats {
518    pub by_stage: HashMap<String, HashMap<String, u64>>,
519    pub stale_thoughts: Vec<EntityDetail>,
520    pub stale_questions: Vec<EntityDetail>,
521    pub total_entities: u64,
522    pub last_movement: Option<String>,
523}
524
525/// A parsed pipeline entry from a markdown document.
526#[derive(Debug, Clone)]
527pub struct PipelineEntry {
528    /// Title from ### heading (cleaned of dates and markers).
529    pub title: String,
530    /// Full content under the heading.
531    pub body: String,
532    /// Status: "active", "graduated", "dissolved", "explored", "retired".
533    pub status: String,
534    /// Stage: "learning", "thoughts", "curiosity", "reflections", "praxis".
535    pub stage: String,
536    /// Mapped entity type.
537    pub entity_type: EntityType,
538    /// Date from heading or metadata field.
539    pub date: Option<String>,
540    /// **Source:** field value.
541    pub source_ref: Option<String>,
542    /// **Destination:** field value.
543    pub destination: Option<String>,
544    /// Parsed "Connected to:" references.
545    pub connected_to: Vec<String>,
546    /// Sub-type for special sections: "theme", "pattern", "phronesis".
547    pub sub_type: Option<String>,
548}
549
550/// Result of a full ingestion run.
551#[derive(Debug, Clone, Default)]
552pub struct IngestionReport {
553    pub episodes_created: u32,
554    pub entities_created: u32,
555    pub entities_merged: u32,
556    pub entities_skipped: u32,
557    pub relationships_created: u32,
558    pub relationships_skipped: u32,
559    pub errors: Vec<String>,
560}