Skip to main content

locus_sdk/domain/
memory.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use locus_core_rs::domain::models::{AvecState, PsiRange, SttpNode};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "snake_case")]
7pub enum FallbackPolicy {
8    Never,
9    OnEmpty,
10    Always,
11}
12
13impl Default for FallbackPolicy {
14    fn default() -> Self {
15        Self::OnEmpty
16    }
17}
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum StrictnessMode {
22    Precision,
23    Balanced,
24    Recall,
25}
26
27impl Default for StrictnessMode {
28    fn default() -> Self {
29        Self::Balanced
30    }
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35pub enum MemorySortField {
36    Timestamp,
37    UpdatedAt,
38    Psi,
39    Rho,
40    Kappa,
41}
42
43impl Default for MemorySortField {
44    fn default() -> Self {
45        Self::Timestamp
46    }
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "snake_case")]
51pub enum SortDirection {
52    Asc,
53    Desc,
54}
55
56impl Default for SortDirection {
57    fn default() -> Self {
58        Self::Desc
59    }
60}
61
62#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum RetrievalPath {
65    ResonanceOnly,
66    SemanticOnly,
67    Hybrid,
68    LexicalFallback,
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct MemoryScope {
74    pub tenant_id: Option<String>,
75    pub session_ids: Option<Vec<String>>,
76    pub tiers: Option<Vec<String>>,
77    pub from_utc: Option<DateTime<Utc>>,
78    pub to_utc: Option<DateTime<Utc>>,
79}
80
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct MetricRange {
84    pub min: Option<f32>,
85    pub max: Option<f32>,
86}
87
88impl MetricRange {
89    pub fn contains(&self, value: f32) -> bool {
90        if let Some(min) = self.min {
91            if value < min {
92                return false;
93            }
94        }
95        if let Some(max) = self.max {
96            if value > max {
97                return false;
98            }
99        }
100        true
101    }
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct MemoryFilter {
107    pub has_embedding: Option<bool>,
108    pub embedding_model: Option<String>,
109    pub psi: Option<MetricRange>,
110    pub rho: Option<MetricRange>,
111    pub kappa: Option<MetricRange>,
112    pub text_contains: Option<String>,
113    pub tags_contains: Option<Vec<String>>,
114    pub has_tag: Option<String>,
115    pub indexed_tags: Option<Vec<String>>,
116    pub tag_prefix: Option<String>,
117    pub has_semantic_links: Option<bool>,
118    pub link_rel: Option<String>,
119    pub link_target: Option<String>,
120    pub links_to_ref: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct MemoryPage {
126    pub limit: usize,
127    pub cursor: Option<String>,
128}
129
130impl Default for MemoryPage {
131    fn default() -> Self {
132        Self {
133            limit: 50,
134            cursor: None,
135        }
136    }
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct MemorySort {
142    pub field: MemorySortField,
143    pub direction: SortDirection,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct MemoryScoring {
149    pub resonance_weight: f32,
150    pub semantic_weight: f32,
151    pub lexical_weight: f32,
152    pub alpha: f32,
153    pub beta: f32,
154    pub gamma: f32,
155    pub fallback_policy: FallbackPolicy,
156    pub strictness: StrictnessMode,
157}
158
159impl Default for MemoryScoring {
160    fn default() -> Self {
161        Self {
162            resonance_weight: 1.0,
163            semantic_weight: 0.0,
164            lexical_weight: 0.0,
165            alpha: 0.7,
166            beta: 0.3,
167            gamma: 0.0,
168            fallback_policy: FallbackPolicy::OnEmpty,
169            strictness: StrictnessMode::Balanced,
170        }
171    }
172}
173
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct MemoryFindRequest {
177    pub scope: MemoryScope,
178    pub filter: MemoryFilter,
179    pub page: MemoryPage,
180    pub sort: MemorySort,
181}
182
183#[derive(Debug, Clone)]
184pub struct MemoryFindResult {
185    pub nodes: Vec<SttpNode>,
186    pub retrieved: usize,
187    pub has_more: bool,
188    pub next_cursor: Option<String>,
189}
190
191#[derive(Debug, Clone, Default)]
192pub struct MemoryRecallRequest {
193    pub scope: MemoryScope,
194    pub filter: MemoryFilter,
195    pub page: MemoryPage,
196    pub scoring: MemoryScoring,
197    pub current_avec: Option<AvecState>,
198    pub query_text: Option<String>,
199    pub query_embedding: Option<Vec<f32>>,
200    pub query_tag_embedding: Option<Vec<f32>>,
201}
202
203#[derive(Debug, Clone)]
204pub struct MemoryRecallResult {
205    pub nodes: Vec<SttpNode>,
206    pub retrieved: usize,
207    pub psi_range: PsiRange,
208    pub retrieval_path: RetrievalPath,
209    pub has_more: bool,
210    pub next_cursor: Option<String>,
211}
212
213#[derive(Debug, Clone)]
214pub struct MemoryExplainRequest {
215    pub recall: MemoryRecallRequest,
216}
217
218#[derive(Debug, Clone)]
219pub struct MemoryExplainStage {
220    pub stage: String,
221    pub count: usize,
222}
223
224#[derive(Debug, Clone)]
225pub struct MemoryExplainResult {
226    pub retrieval_path: RetrievalPath,
227    pub fallback_triggered: bool,
228    pub fallback_reason: Option<String>,
229    pub stages: Vec<MemoryExplainStage>,
230    pub scoring: MemoryScoring,
231}
232
233#[derive(Debug, Clone, Default)]
234pub struct MemorySchemaResult {
235    pub schema_version: String,
236    pub sort_fields: Vec<String>,
237    pub filter_fields: Vec<String>,
238    pub group_by_fields: Vec<String>,
239    pub fallback_policies: Vec<String>,
240    pub strictness_modes: Vec<String>,
241    pub transform_operations: Vec<String>,
242    pub evict_operations: Vec<String>,
243}
244
245#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "snake_case")]
247pub enum MemoryGroupBy {
248    SessionId,
249    Tier,
250    EmbeddingModel,
251    DateDay,
252    SemanticTag,
253}
254
255impl Default for MemoryGroupBy {
256    fn default() -> Self {
257        Self::SessionId
258    }
259}
260
261#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
262#[serde(rename_all = "camelCase")]
263pub struct NumericStats {
264    pub min: f32,
265    pub max: f32,
266    pub average: f32,
267}
268
269#[derive(Debug, Clone, Default, Serialize, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub struct MemoryAggregateRequest {
272    pub scope: MemoryScope,
273    pub filter: MemoryFilter,
274    pub group_by: MemoryGroupBy,
275    pub max_groups: usize,
276    pub max_nodes: usize,
277}
278
279#[derive(Debug, Clone)]
280pub struct MemoryAggregateGroup {
281    pub key: String,
282    pub node_count: usize,
283    pub embedding_coverage: f32,
284    pub avg_user_avec: AvecState,
285    pub avg_model_avec: AvecState,
286    pub avg_compression_avec: Option<AvecState>,
287    pub psi_stats: NumericStats,
288    pub rho_stats: NumericStats,
289    pub kappa_stats: NumericStats,
290}
291
292#[derive(Debug, Clone, Default)]
293pub struct MemoryAggregateResult {
294    pub groups: Vec<MemoryAggregateGroup>,
295    pub total_groups: usize,
296    pub scanned_nodes: usize,
297}
298
299#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
300#[serde(rename_all = "snake_case")]
301pub enum MemoryTransformOperation {
302    EmbedBackfill,
303    ReindexEmbeddings,
304    EmbedTagBackfill,
305    ReindexTagEmbeddings,
306}
307
308impl Default for MemoryTransformOperation {
309    fn default() -> Self {
310        Self::EmbedBackfill
311    }
312}
313
314#[derive(Debug, Clone, Default, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct MemoryTransformRequest {
317    pub scope: MemoryScope,
318    pub filter: MemoryFilter,
319    pub operation: MemoryTransformOperation,
320    pub dry_run: bool,
321    pub batch_size: usize,
322    pub max_nodes: usize,
323    pub provider_id: Option<String>,
324    pub model: Option<String>,
325}
326
327#[derive(Debug, Clone, Default)]
328pub struct MemoryTransformResult {
329    pub scanned: usize,
330    pub selected: usize,
331    pub updated: usize,
332    pub skipped: usize,
333    pub failed: usize,
334    pub duplicate: usize,
335    pub started_at: DateTime<Utc>,
336    pub completed_at: DateTime<Utc>,
337    pub failures: Vec<String>,
338}
339
340pub fn clamp_limit(limit: usize) -> usize {
341    limit.clamp(1, 200)
342}
343
344pub fn clamp_groups(limit: usize) -> usize {
345    limit.clamp(1, 5000)
346}
347
348pub fn clamp_nodes(limit: usize) -> usize {
349    limit.clamp(1, 50000)
350}
351
352pub fn clamp_batch_size(limit: usize) -> usize {
353    limit.clamp(1, 500)
354}