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
233pub const MEMORY_SCHEMA_VERSION: &str = "locus-sdk.memory.v4";
234
235#[derive(Debug, Clone, Default)]
236pub struct MemorySchemaResult {
237    pub schema_version: String,
238    pub sort_fields: Vec<String>,
239    pub filter_fields: Vec<String>,
240    pub group_by_fields: Vec<String>,
241    pub fallback_policies: Vec<String>,
242    pub strictness_modes: Vec<String>,
243    pub transform_operations: Vec<String>,
244    pub evict_operations: Vec<String>,
245    pub reflex_actions: Vec<String>,
246    pub decision_types: Vec<String>,
247}
248
249#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(rename_all = "snake_case")]
251pub enum MemoryGroupBy {
252    SessionId,
253    Tier,
254    EmbeddingModel,
255    DateDay,
256    SemanticTag,
257}
258
259impl Default for MemoryGroupBy {
260    fn default() -> Self {
261        Self::SessionId
262    }
263}
264
265#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
266#[serde(rename_all = "camelCase")]
267pub struct NumericStats {
268    pub min: f32,
269    pub max: f32,
270    pub average: f32,
271}
272
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct MemoryAggregateRequest {
276    pub scope: MemoryScope,
277    pub filter: MemoryFilter,
278    pub group_by: MemoryGroupBy,
279    pub max_groups: usize,
280    pub max_nodes: usize,
281}
282
283#[derive(Debug, Clone)]
284pub struct MemoryAggregateGroup {
285    pub key: String,
286    pub node_count: usize,
287    pub embedding_coverage: f32,
288    pub avg_user_avec: AvecState,
289    pub avg_model_avec: AvecState,
290    pub avg_compression_avec: Option<AvecState>,
291    pub psi_stats: NumericStats,
292    pub rho_stats: NumericStats,
293    pub kappa_stats: NumericStats,
294}
295
296#[derive(Debug, Clone, Default)]
297pub struct MemoryAggregateResult {
298    pub groups: Vec<MemoryAggregateGroup>,
299    pub total_groups: usize,
300    pub scanned_nodes: usize,
301}
302
303#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "snake_case")]
305pub enum MemoryTransformOperation {
306    EmbedBackfill,
307    ReindexEmbeddings,
308    EmbedTagBackfill,
309    ReindexTagEmbeddings,
310}
311
312impl Default for MemoryTransformOperation {
313    fn default() -> Self {
314        Self::EmbedBackfill
315    }
316}
317
318#[derive(Debug, Clone, Default, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct MemoryTransformRequest {
321    pub scope: MemoryScope,
322    pub filter: MemoryFilter,
323    pub operation: MemoryTransformOperation,
324    pub dry_run: bool,
325    pub batch_size: usize,
326    pub max_nodes: usize,
327    pub provider_id: Option<String>,
328    pub model: Option<String>,
329}
330
331#[derive(Debug, Clone, Default)]
332pub struct MemoryTransformResult {
333    pub scanned: usize,
334    pub selected: usize,
335    pub updated: usize,
336    pub skipped: usize,
337    pub failed: usize,
338    pub duplicate: usize,
339    pub started_at: DateTime<Utc>,
340    pub completed_at: DateTime<Utc>,
341    pub failures: Vec<String>,
342}
343
344pub fn clamp_limit(limit: usize) -> usize {
345    limit.clamp(1, 200)
346}
347
348pub fn clamp_groups(limit: usize) -> usize {
349    limit.clamp(1, 5000)
350}
351
352pub fn clamp_nodes(limit: usize) -> usize {
353    limit.clamp(1, 50000)
354}
355
356pub fn clamp_batch_size(limit: usize) -> usize {
357    limit.clamp(1, 500)
358}