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}