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}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct MemoryPage {
118 pub limit: usize,
119 pub cursor: Option<String>,
120}
121
122impl Default for MemoryPage {
123 fn default() -> Self {
124 Self {
125 limit: 50,
126 cursor: None,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct MemorySort {
134 pub field: MemorySortField,
135 pub direction: SortDirection,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct MemoryScoring {
141 pub resonance_weight: f32,
142 pub semantic_weight: f32,
143 pub lexical_weight: f32,
144 pub alpha: f32,
145 pub beta: f32,
146 pub fallback_policy: FallbackPolicy,
147 pub strictness: StrictnessMode,
148}
149
150impl Default for MemoryScoring {
151 fn default() -> Self {
152 Self {
153 resonance_weight: 1.0,
154 semantic_weight: 0.0,
155 lexical_weight: 0.0,
156 alpha: 0.7,
157 beta: 0.3,
158 fallback_policy: FallbackPolicy::OnEmpty,
159 strictness: StrictnessMode::Balanced,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct MemoryFindRequest {
167 pub scope: MemoryScope,
168 pub filter: MemoryFilter,
169 pub page: MemoryPage,
170 pub sort: MemorySort,
171}
172
173#[derive(Debug, Clone)]
174pub struct MemoryFindResult {
175 pub nodes: Vec<SttpNode>,
176 pub retrieved: usize,
177 pub has_more: bool,
178 pub next_cursor: Option<String>,
179}
180
181#[derive(Debug, Clone, Default)]
182pub struct MemoryRecallRequest {
183 pub scope: MemoryScope,
184 pub filter: MemoryFilter,
185 pub page: MemoryPage,
186 pub scoring: MemoryScoring,
187 pub current_avec: Option<AvecState>,
188 pub query_text: Option<String>,
189 pub query_embedding: Option<Vec<f32>>,
190}
191
192#[derive(Debug, Clone)]
193pub struct MemoryRecallResult {
194 pub nodes: Vec<SttpNode>,
195 pub retrieved: usize,
196 pub psi_range: PsiRange,
197 pub retrieval_path: RetrievalPath,
198 pub has_more: bool,
199 pub next_cursor: Option<String>,
200}
201
202#[derive(Debug, Clone)]
203pub struct MemoryExplainRequest {
204 pub recall: MemoryRecallRequest,
205}
206
207#[derive(Debug, Clone)]
208pub struct MemoryExplainStage {
209 pub stage: String,
210 pub count: usize,
211}
212
213#[derive(Debug, Clone)]
214pub struct MemoryExplainResult {
215 pub retrieval_path: RetrievalPath,
216 pub fallback_triggered: bool,
217 pub fallback_reason: Option<String>,
218 pub stages: Vec<MemoryExplainStage>,
219 pub scoring: MemoryScoring,
220}
221
222#[derive(Debug, Clone, Default)]
223pub struct MemorySchemaResult {
224 pub schema_version: String,
225 pub sort_fields: Vec<String>,
226 pub filter_fields: Vec<String>,
227 pub group_by_fields: Vec<String>,
228 pub fallback_policies: Vec<String>,
229 pub strictness_modes: Vec<String>,
230 pub transform_operations: Vec<String>,
231}
232
233#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub enum MemoryGroupBy {
236 SessionId,
237 Tier,
238 EmbeddingModel,
239 DateDay,
240}
241
242impl Default for MemoryGroupBy {
243 fn default() -> Self {
244 Self::SessionId
245 }
246}
247
248#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
249#[serde(rename_all = "camelCase")]
250pub struct NumericStats {
251 pub min: f32,
252 pub max: f32,
253 pub average: f32,
254}
255
256#[derive(Debug, Clone, Default, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct MemoryAggregateRequest {
259 pub scope: MemoryScope,
260 pub filter: MemoryFilter,
261 pub group_by: MemoryGroupBy,
262 pub max_groups: usize,
263 pub max_nodes: usize,
264}
265
266#[derive(Debug, Clone)]
267pub struct MemoryAggregateGroup {
268 pub key: String,
269 pub node_count: usize,
270 pub embedding_coverage: f32,
271 pub avg_user_avec: AvecState,
272 pub avg_model_avec: AvecState,
273 pub avg_compression_avec: Option<AvecState>,
274 pub psi_stats: NumericStats,
275 pub rho_stats: NumericStats,
276 pub kappa_stats: NumericStats,
277}
278
279#[derive(Debug, Clone, Default)]
280pub struct MemoryAggregateResult {
281 pub groups: Vec<MemoryAggregateGroup>,
282 pub total_groups: usize,
283 pub scanned_nodes: usize,
284}
285
286#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
287#[serde(rename_all = "snake_case")]
288pub enum MemoryTransformOperation {
289 EmbedBackfill,
290 ReindexEmbeddings,
291}
292
293impl Default for MemoryTransformOperation {
294 fn default() -> Self {
295 Self::EmbedBackfill
296 }
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct MemoryTransformRequest {
302 pub scope: MemoryScope,
303 pub filter: MemoryFilter,
304 pub operation: MemoryTransformOperation,
305 pub dry_run: bool,
306 pub batch_size: usize,
307 pub max_nodes: usize,
308 pub provider_id: Option<String>,
309 pub model: Option<String>,
310}
311
312#[derive(Debug, Clone, Default)]
313pub struct MemoryTransformResult {
314 pub scanned: usize,
315 pub selected: usize,
316 pub updated: usize,
317 pub skipped: usize,
318 pub failed: usize,
319 pub duplicate: usize,
320 pub started_at: DateTime<Utc>,
321 pub completed_at: DateTime<Utc>,
322 pub failures: Vec<String>,
323}
324
325pub fn clamp_limit(limit: usize) -> usize {
326 limit.clamp(1, 200)
327}
328
329pub fn clamp_groups(limit: usize) -> usize {
330 limit.clamp(1, 5000)
331}
332
333pub fn clamp_nodes(limit: usize) -> usize {
334 limit.clamp(1, 50000)
335}
336
337pub fn clamp_batch_size(limit: usize) -> usize {
338 limit.clamp(1, 500)
339}