Skip to main content

velesdb_memory/
model.rs

1//! Domain data model: the request/response value types of the memory layer.
2//!
3//! These are pure data — the shapes a caller links, recalls, filters on, and
4//! gets back — with no dependency on [`MemoryService`](crate::service::MemoryService)
5//! itself. Keeping them here separates *what the memory layer exchanges* from
6//! *how the service computes it*, and gives every adapter (MCP, bindings) one
7//! canonical place to import the contract from.
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12
13/// A typed link from a freshly remembered fact to an existing memory.
14#[derive(Debug, Clone, Deserialize, JsonSchema)]
15#[schemars(transform = crate::schema::strip_int_formats)]
16pub struct Link {
17    /// Id of the memory being linked to.
18    pub target: u64,
19    /// Relationship label (e.g. `"decided_in"`, `"references"`, `"depends_on"`).
20    pub relation: String,
21}
22
23/// One semantically recalled memory.
24#[derive(Debug, Clone, Serialize, JsonSchema)]
25#[schemars(transform = crate::schema::strip_int_formats)]
26pub struct Recollection {
27    /// Stable id of the memory.
28    pub id: u64,
29    /// Similarity score (higher is closer).
30    pub score: f32,
31    /// Stored fact content.
32    pub content: String,
33    /// Caller-supplied structured metadata stored with the fact (the `ColumnStore`
34    /// facet), with reserved system keys (`content`, `_veles_*`) excluded. `None`
35    /// when the fact carries no caller metadata. This is what makes dated recall
36    /// work: store a date (e.g. `occurred_at`) and it round-trips here, so a
37    /// `recall_where` result can be ordered into a chronological timeline.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub metadata: Option<Map<String, Value>>,
40}
41
42/// Comparison operator for a [`ColumnFilter`] in
43/// [`MemoryService::recall_where`](crate::service::MemoryService::recall_where).
44#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
45#[serde(rename_all = "lowercase")]
46pub enum ColumnOp {
47    /// `=`
48    Eq,
49    /// `!=`
50    Ne,
51    /// `<`
52    Lt,
53    /// `<=`
54    Le,
55    /// `>`
56    Gt,
57    /// `>=`
58    Ge,
59}
60
61impl ColumnOp {
62    /// The `VelesQL` operator token. Only [`crate::storage::NativeStore`]
63    /// builds `VelesQL` text; a non-`persistence` backend (e.g.
64    /// `velesdb-wasm`'s in-memory one) filters `ColumnFilter`s directly, with
65    /// no query-string step.
66    #[cfg(feature = "persistence")]
67    #[must_use]
68    pub(crate) fn as_sql(self) -> &'static str {
69        match self {
70            Self::Eq => "=",
71            Self::Ne => "!=",
72            Self::Lt => "<",
73            Self::Le => "<=",
74            Self::Gt => ">",
75            Self::Ge => ">=",
76        }
77    }
78}
79
80/// A structured predicate over a memory's metadata column, for the fused
81/// vector+`ColumnStore` recall
82/// [`MemoryService::recall_where`](crate::service::MemoryService::recall_where).
83/// Unlike the exact-match filter on
84/// [`MemoryService::recall`](crate::service::MemoryService::recall), this supports
85/// ranges and comparisons (e.g. `timestamp >= …`), so temporal and numeric facets
86/// become queryable, not just equal-matchable.
87#[derive(Debug, Clone, Deserialize, JsonSchema)]
88pub struct ColumnFilter {
89    /// Metadata field name (alphanumeric/underscore).
90    pub field: String,
91    /// Comparison operator.
92    pub op: ColumnOp,
93    /// Value to compare against (numbers, strings, booleans).
94    pub value: Value,
95}
96
97/// Tuning knobs for
98/// [`MemoryService::recall_fused`](crate::service::MemoryService::recall_fused).
99///
100/// `Default` matches the values validated on the LoCoMo/HotpotQA/TimeQA
101/// benchmarks (`examples/locomo`, `examples/multihop`, `examples/timeqa`):
102/// `graph_boost = 0.15` was the optimum of a sweep (0.30/0.50/0.80 all
103/// degraded ranking quality), and `hops = 2` is the minimum depth at which a
104/// fact wired only through a shared topic (the `remember_extracted` hub
105/// scaffolding: fact → hub is hop 1, hub → sibling fact is hop 2) becomes
106/// reachable at all.
107#[derive(Debug, Clone, Copy)]
108pub struct FusionOptions {
109    /// Hops the graph traversal walks from the top vector seed.
110    pub hops: usize,
111    /// Weight added to a graph-reached fact's normalised vector score.
112    pub graph_boost: f64,
113    /// Depth of the oversampled vector pool fusion re-ranks. `None` uses the
114    /// proven default (`k` scaled up, floored at 64 — see
115    /// `crate::fusion::pool_size`). Widen this to give
116    /// [`MemoryService::recall_fused_reranked`](crate::service::MemoryService::recall_fused_reranked)'s
117    /// reranker more candidates to work with.
118    pub pool: Option<usize>,
119}
120
121impl Default for FusionOptions {
122    fn default() -> Self {
123        Self {
124            hops: 2,
125            graph_boost: 0.15,
126            pool: None,
127        }
128    }
129}
130
131/// A node in an [`Explanation`] subgraph.
132#[derive(Debug, Clone, Serialize, JsonSchema)]
133#[schemars(transform = crate::schema::strip_int_formats)]
134pub struct MemoryNode {
135    /// Stable id of the memory.
136    pub id: u64,
137    /// Stored fact content.
138    pub content: String,
139    /// Distance in hops from the seed memory (the seed is hop `0`).
140    pub hop: usize,
141}
142
143/// A typed edge in an [`Explanation`] subgraph.
144#[derive(Debug, Clone, Serialize, JsonSchema)]
145#[schemars(transform = crate::schema::strip_int_formats)]
146pub struct MemoryEdge {
147    /// Source memory id.
148    pub from: u64,
149    /// Target memory id.
150    pub to: u64,
151    /// Relationship label.
152    pub relation: String,
153}
154
155/// The connected answer to a `why` question: the best-matching seed memory plus
156/// everything reachable from it within a hop budget. This connected subgraph is
157/// the differentiator — it surfaces related memories a purely vector recall is
158/// blind to (no textual similarity required).
159#[derive(Debug, Clone, Default, Serialize, JsonSchema)]
160pub struct Explanation {
161    /// Memories in the subgraph, seed first.
162    pub nodes: Vec<MemoryNode>,
163    /// Typed edges connecting the nodes.
164    pub edges: Vec<MemoryEdge>,
165}