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}