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