1use std::sync::Arc;
2
3use anyhow::Result;
4use serde_json::{Map, Value};
5use locus_core_rs::domain::contracts::NodeStore;
6use locus_core_rs::domain::models::AvecState;
7
8use crate::application::memory_aggregate::MemoryAggregateService;
9use crate::application::memory_explain::MemoryExplainService;
10use crate::application::manual_compression::ManualCompressionService;
11use crate::application::memory_recall::MemoryRecallService;
12use crate::application::memory_schema::MemorySchemaService;
13use crate::application::memory_transform::MemoryTransformService;
14use crate::domain::ai::AiProviderRegistry;
15use crate::domain::compression::ManualCompressionRequest;
16use crate::domain::memory::{
17 MemoryAggregateRequest, MemoryAggregateResult, MemoryExplainRequest, MemoryExplainResult,
18 MemoryFilter, MemoryGroupBy, MemoryRecallRequest, MemoryRecallResult, MemorySchemaResult,
19 MemoryScope, MemoryTransformRequest, MemoryTransformResult, clamp_nodes,
20};
21
22#[derive(Debug, Clone)]
23pub struct MemoryRecallWithExplainResult {
24 pub recall: MemoryRecallResult,
25 pub explain: MemoryExplainResult,
26}
27
28#[derive(Debug, Clone, Default)]
29pub struct MemoryDailyRollupRequest {
30 pub scope: MemoryScope,
31 pub filter: MemoryFilter,
32 pub max_days: usize,
33 pub max_nodes: usize,
34}
35
36#[derive(Debug, Clone)]
37pub struct MemoryTransformThenRecallRequest {
38 pub transform: MemoryTransformRequest,
39 pub recall: MemoryRecallRequest,
40}
41
42#[derive(Debug, Clone)]
43pub struct MemoryTransformThenRecallResult {
44 pub transform: MemoryTransformResult,
45 pub recall: MemoryRecallResult,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CompositeRole {
50 User,
51 Model,
52 Document,
53 Conversation,
54}
55
56impl CompositeRole {
57 fn as_str(self) -> &'static str {
58 match self {
59 Self::User => "user",
60 Self::Model => "model",
61 Self::Document => "document",
62 Self::Conversation => "conversation",
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
68pub struct CompositeInputItem {
69 pub role: CompositeRole,
70 pub text: String,
71 pub avec_override: Option<AvecState>,
72 pub context: Vec<CompositeInputItem>,
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct CompositeRoleAvecOverrides {
77 pub user: Option<AvecState>,
78 pub model: Option<AvecState>,
79 pub document: Option<AvecState>,
80 pub conversation: Option<AvecState>,
81}
82
83impl CompositeRoleAvecOverrides {
84 fn resolve(&self, role: CompositeRole) -> Option<AvecState> {
85 match role {
86 CompositeRole::User => self.user,
87 CompositeRole::Model => self.model,
88 CompositeRole::Document => self.document,
89 CompositeRole::Conversation => self.conversation,
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
95pub struct CompositeNodeFromTextOptions {
96 pub role_avec: CompositeRoleAvecOverrides,
97 pub global_avec: Option<AvecState>,
98 pub allow_llm_avec_fallback: bool,
99 pub max_recursion_depth: usize,
100}
101
102impl Default for CompositeNodeFromTextOptions {
103 fn default() -> Self {
104 Self {
105 role_avec: CompositeRoleAvecOverrides::default(),
106 global_avec: None,
107 allow_llm_avec_fallback: false,
108 max_recursion_depth: 5,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct CompositeNodeFromTextRequest {
115 pub items: Vec<CompositeInputItem>,
116 pub options: CompositeNodeFromTextOptions,
117}
118
119#[derive(Debug, Clone, Default)]
120pub struct CompositeNodeFromTextResult {
121 pub content: Value,
122 pub resolved_avec_count: usize,
123 pub unresolved_avec_count: usize,
124 pub requires_llm_avec: bool,
125}
126
127#[derive(Debug, Clone, Default)]
128struct CompositeBuildStats {
129 resolved_avec_count: usize,
130 unresolved_avec_count: usize,
131}
132
133pub struct MemoryCompositionService {
134 store: Arc<dyn NodeStore>,
135 recall: MemoryRecallService,
136 explain: MemoryExplainService,
137 aggregate: MemoryAggregateService,
138 schema: MemorySchemaService,
139}
140
141impl MemoryCompositionService {
142 pub fn new(store: Arc<dyn NodeStore>) -> Self {
143 Self {
144 store: store.clone(),
145 recall: MemoryRecallService::new(store.clone()),
146 explain: MemoryExplainService::new(store.clone()),
147 aggregate: MemoryAggregateService::new(store),
148 schema: MemorySchemaService::new(),
149 }
150 }
151
152 pub async fn recall_with_explain(
153 &self,
154 request: &MemoryRecallRequest,
155 ) -> Result<MemoryRecallWithExplainResult> {
156 let recall = self.recall.execute(request).await?;
157 let explain = self
158 .explain
159 .execute(&MemoryExplainRequest {
160 recall: request.clone(),
161 })
162 .await?;
163
164 Ok(MemoryRecallWithExplainResult { recall, explain })
165 }
166
167 pub async fn daily_rollup(
168 &self,
169 request: &MemoryDailyRollupRequest,
170 ) -> Result<MemoryAggregateResult> {
171 let max_days = if request.max_days == 0 {
172 30
173 } else {
174 request.max_days
175 };
176 let max_nodes = clamp_nodes(if request.max_nodes == 0 {
177 5000
178 } else {
179 request.max_nodes
180 });
181
182 self.aggregate
183 .execute(&MemoryAggregateRequest {
184 scope: request.scope.clone(),
185 filter: request.filter.clone(),
186 group_by: MemoryGroupBy::DateDay,
187 max_groups: max_days,
188 max_nodes,
189 })
190 .await
191 }
192
193 pub fn capability_bundle(&self) -> MemorySchemaResult {
194 self.schema.execute()
195 }
196
197 pub async fn transform_then_recall_verify(
198 &self,
199 providers: Arc<dyn AiProviderRegistry>,
200 request: &MemoryTransformThenRecallRequest,
201 ) -> Result<MemoryTransformThenRecallResult> {
202 let transform_service = MemoryTransformService::new(self.store.clone(), providers);
203 let transform = transform_service.execute(&request.transform).await?;
204 let recall = self.recall.execute(&request.recall).await?;
205
206 Ok(MemoryTransformThenRecallResult { transform, recall })
207 }
208
209 pub fn build_content_from_text(
210 &self,
211 request: &CompositeNodeFromTextRequest,
212 ) -> Result<CompositeNodeFromTextResult> {
213 let max_depth = request.options.max_recursion_depth.clamp(1, 5);
214 let compressor = ManualCompressionService::new();
215 let mut stats = CompositeBuildStats::default();
216
217 let mut root = Map::new();
218 for (idx, item) in request.items.iter().enumerate() {
219 let key = format!("entry_{idx}(.95)");
220 let value = build_composite_entry(
221 item,
222 1,
223 max_depth,
224 &request.options,
225 &compressor,
226 &mut stats,
227 )?;
228 root.insert(key, value);
229 }
230
231 let requires_llm = stats.unresolved_avec_count > 0;
232 if requires_llm && !request.options.allow_llm_avec_fallback {
233 anyhow::bail!(
234 "unable to resolve AVEC for {} item(s); provide overrides or enable llm fallback",
235 stats.unresolved_avec_count
236 );
237 }
238
239 Ok(CompositeNodeFromTextResult {
240 content: Value::Object(root),
241 resolved_avec_count: stats.resolved_avec_count,
242 unresolved_avec_count: stats.unresolved_avec_count,
243 requires_llm_avec: requires_llm,
244 })
245 }
246}
247
248fn build_composite_entry(
249 item: &CompositeInputItem,
250 depth: usize,
251 max_depth: usize,
252 options: &CompositeNodeFromTextOptions,
253 compressor: &ManualCompressionService,
254 stats: &mut CompositeBuildStats,
255) -> Result<Value> {
256 if depth > max_depth {
257 anyhow::bail!("composite context depth exceeded max depth of {max_depth}");
258 }
259
260 let resolved_avec = item
261 .avec_override
262 .or_else(|| options.role_avec.resolve(item.role))
263 .or(options.global_avec);
264
265 if resolved_avec.is_some() {
266 stats.resolved_avec_count += 1;
267 } else {
268 stats.unresolved_avec_count += 1;
269 }
270
271 let compressed = compressor.execute(&ManualCompressionRequest {
272 text: item.text.clone(),
273 ..Default::default()
274 });
275
276 let mut entry = Map::new();
277 entry.insert(
278 "role(.99)".to_string(),
279 Value::String(item.role.as_str().to_string()),
280 );
281 entry.insert("text(.70)".to_string(), Value::String(item.text.clone()));
282 entry.insert(
283 "anchor_topic(.86)".to_string(),
284 Value::String(compressed.anchor_topic),
285 );
286 entry.insert(
287 "key_points(.82)".to_string(),
288 Value::Array(
289 compressed
290 .key_points
291 .into_iter()
292 .map(Value::String)
293 .collect(),
294 ),
295 );
296
297 if let Some(avec) = resolved_avec {
298 let mut avec_obj = Map::new();
299 avec_obj.insert("stability(.99)".to_string(), Value::from(avec.stability as f64));
300 avec_obj.insert("friction(.99)".to_string(), Value::from(avec.friction as f64));
301 avec_obj.insert("logic(.99)".to_string(), Value::from(avec.logic as f64));
302 avec_obj.insert("autonomy(.99)".to_string(), Value::from(avec.autonomy as f64));
303 avec_obj.insert("psi(.99)".to_string(), Value::from(avec.psi() as f64));
304 entry.insert("resolved_avec(.95)".to_string(), Value::Object(avec_obj));
305 }
306
307 if !item.context.is_empty() {
308 let mut children = Map::new();
309 for (idx, child) in item.context.iter().enumerate() {
310 let child_key = format!("context_{idx}(.90)");
311 children.insert(
312 child_key,
313 build_composite_entry(child, depth + 1, max_depth, options, compressor, stats)?,
314 );
315 }
316 entry.insert("context(.88)".to_string(), Value::Object(children));
317 }
318
319 Ok(Value::Object(entry))
320}
321
322#[cfg(test)]
323mod tests {
324 use std::sync::Arc;
325
326 use anyhow::Result;
327 use async_trait::async_trait;
328 use chrono::{Duration, Utc};
329 use serde_json::{Map, Value};
330 use locus_core_rs::application::validation::TreeSitterValidator;
331 use locus_core_rs::domain::contracts::NodeValidator;
332 use locus_core_rs::domain::models::{AvecState, SttpNode};
333 use locus_core_rs::parsing::SttpNodeParser;
334 use locus_core_rs::{InMemoryNodeStore, NodeStore};
335
336 use super::{
337 CompositeInputItem, CompositeNodeFromTextOptions, CompositeNodeFromTextRequest,
338 CompositeRole, MemoryCompositionService, MemoryDailyRollupRequest,
339 MemoryTransformThenRecallRequest,
340 };
341 use crate::domain::ai::{AiCapability, AiProvider, EmbedRequest, ScoreAvecRequest};
342 use crate::domain::memory::{
343 FallbackPolicy, MemoryFilter, MemoryRecallRequest, MemoryScoring, MemoryTransformOperation,
344 MemoryTransformRequest, RetrievalPath,
345 };
346 use crate::infrastructure::registry::InMemoryAiProviderRegistry;
347
348 struct MockEmbeddingProvider;
349
350 #[async_trait]
351 impl AiProvider for MockEmbeddingProvider {
352 fn provider_id(&self) -> &str {
353 "mock"
354 }
355
356 fn capabilities(&self) -> &'static [AiCapability] {
357 &[AiCapability::SemanticEmbedding]
358 }
359
360 async fn embed_semantic(&self, _request: &EmbedRequest) -> Result<Vec<f32>> {
361 Ok(vec![0.2, 0.3, 0.4])
362 }
363
364 async fn embed_avec(&self, _request: &EmbedRequest) -> Result<Vec<f32>> {
365 Ok(vec![0.2, 0.3, 0.4])
366 }
367
368 async fn score_avec(&self, _request: &ScoreAvecRequest) -> Result<AvecState> {
369 Ok(AvecState::zero())
370 }
371 }
372
373 #[tokio::test]
374 async fn recall_with_explain_returns_both_results() {
375 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
376 store
377 .upsert_node_async(test_node("s-recipe", Utc::now(), "keyword in payload"))
378 .await
379 .expect("upsert should succeed");
380
381 let service = MemoryCompositionService::new(store);
382 let result = service
383 .recall_with_explain(&MemoryRecallRequest {
384 query_text: Some("keyword".to_string()),
385 ..Default::default()
386 })
387 .await
388 .expect("composed recall should succeed");
389
390 assert!(!result.explain.stages.is_empty());
391 assert!(result.recall.retrieved <= result.recall.nodes.len());
392 }
393
394 #[tokio::test]
395 async fn recall_with_explain_marks_lexical_fallback_on_empty_policy() {
396 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
397 store
398 .upsert_node_async(test_node("s-fallback", Utc::now(), "payload without match"))
399 .await
400 .expect("upsert should succeed");
401
402 let service = MemoryCompositionService::new(store);
403 let result = service
404 .recall_with_explain(&MemoryRecallRequest {
405 query_text: Some("needle".to_string()),
406 filter: MemoryFilter {
407 has_embedding: Some(true),
408 ..Default::default()
409 },
410 scoring: MemoryScoring {
411 fallback_policy: FallbackPolicy::OnEmpty,
412 ..Default::default()
413 },
414 ..Default::default()
415 })
416 .await
417 .expect("composed recall should succeed");
418
419 assert_eq!(result.recall.retrieval_path, RetrievalPath::LexicalFallback);
420 assert!(result.explain.fallback_triggered);
421 }
422
423 #[tokio::test]
424 async fn daily_rollup_groups_by_day() {
425 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
426 let now = Utc::now();
427 store
428 .upsert_node_async(test_node("s-rollup", now - Duration::days(1), "a"))
429 .await
430 .expect("upsert should succeed");
431 store
432 .upsert_node_async(test_node("s-rollup", now, "b"))
433 .await
434 .expect("upsert should succeed");
435
436 let service = MemoryCompositionService::new(store);
437 let result = service
438 .daily_rollup(&MemoryDailyRollupRequest {
439 max_days: 10,
440 max_nodes: 100,
441 ..Default::default()
442 })
443 .await
444 .expect("daily rollup should succeed");
445
446 assert!(result.total_groups >= 2);
447 }
448
449 #[test]
450 fn capability_bundle_exposes_schema() {
451 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
452 let service = MemoryCompositionService::new(store);
453 let schema = service.capability_bundle();
454
455 assert_eq!(schema.schema_version, "locus-sdk.memory.v1");
456 assert!(schema
457 .transform_operations
458 .contains(&"embed_backfill".to_string()));
459 }
460
461 #[tokio::test]
462 async fn transform_then_recall_verify_returns_both_sides() {
463 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
464 store
465 .upsert_node_async(test_node("s-verify", Utc::now(), "verification payload"))
466 .await
467 .expect("upsert should succeed");
468
469 let service = MemoryCompositionService::new(store.clone());
470
471 let mut providers = InMemoryAiProviderRegistry::new();
472 providers.register(MockEmbeddingProvider);
473
474 let result = service
475 .transform_then_recall_verify(
476 Arc::new(providers),
477 &MemoryTransformThenRecallRequest {
478 transform: MemoryTransformRequest {
479 operation: MemoryTransformOperation::EmbedBackfill,
480 dry_run: false,
481 max_nodes: 100,
482 batch_size: 10,
483 ..Default::default()
484 },
485 recall: MemoryRecallRequest {
486 query_text: Some("verification".to_string()),
487 ..Default::default()
488 },
489 },
490 )
491 .await
492 .expect("transform then recall should succeed");
493
494 assert_eq!(result.transform.failed, 0);
495 assert_eq!(result.transform.updated, 1);
496 assert!(result.recall.retrieved <= result.recall.nodes.len());
497 }
498
499 #[test]
500 fn build_content_from_text_resolves_avec_from_role_then_global() {
501 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
502 let service = MemoryCompositionService::new(store);
503
504 let role_state = AvecState {
505 stability: 0.6,
506 friction: 0.2,
507 logic: 0.8,
508 autonomy: 0.7,
509 };
510 let global_state = AvecState {
511 stability: 0.4,
512 friction: 0.3,
513 logic: 0.6,
514 autonomy: 0.5,
515 };
516
517 let result = service
518 .build_content_from_text(&CompositeNodeFromTextRequest {
519 items: vec![
520 CompositeInputItem {
521 role: CompositeRole::User,
522 text: "policy retrieval stability".to_string(),
523 avec_override: None,
524 context: Vec::new(),
525 },
526 CompositeInputItem {
527 role: CompositeRole::Document,
528 text: "technical writeup migration".to_string(),
529 avec_override: None,
530 context: Vec::new(),
531 },
532 ],
533 options: CompositeNodeFromTextOptions {
534 role_avec: super::CompositeRoleAvecOverrides {
535 user: Some(role_state),
536 ..Default::default()
537 },
538 global_avec: Some(global_state),
539 ..Default::default()
540 },
541 })
542 .expect("composite build should succeed");
543
544 assert_eq!(result.resolved_avec_count, 2);
545 assert_eq!(result.unresolved_avec_count, 0);
546 assert!(!result.requires_llm_avec);
547 }
548
549 #[test]
550 fn build_content_from_text_fails_without_resolved_avec_when_llm_disabled() {
551 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
552 let service = MemoryCompositionService::new(store);
553
554 let err = service
555 .build_content_from_text(&CompositeNodeFromTextRequest {
556 items: vec![CompositeInputItem {
557 role: CompositeRole::Conversation,
558 text: "user asked then model replied".to_string(),
559 avec_override: None,
560 context: Vec::new(),
561 }],
562 options: CompositeNodeFromTextOptions {
563 allow_llm_avec_fallback: false,
564 ..Default::default()
565 },
566 })
567 .expect_err("missing avec should fail when llm fallback is disabled");
568
569 assert!(err.to_string().contains("unable to resolve AVEC"));
570 }
571
572 #[test]
573 fn build_content_from_text_enforces_depth_limit() {
574 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
575 let service = MemoryCompositionService::new(store);
576
577 let leaf = CompositeInputItem {
578 role: CompositeRole::User,
579 text: "leaf".to_string(),
580 avec_override: Some(AvecState::zero()),
581 context: Vec::new(),
582 };
583
584 let depth2 = CompositeInputItem {
585 role: CompositeRole::User,
586 text: "depth2".to_string(),
587 avec_override: Some(AvecState::zero()),
588 context: vec![leaf],
589 };
590
591 let depth1 = CompositeInputItem {
592 role: CompositeRole::User,
593 text: "depth1".to_string(),
594 avec_override: Some(AvecState::zero()),
595 context: vec![depth2],
596 };
597
598 let err = service
599 .build_content_from_text(&CompositeNodeFromTextRequest {
600 items: vec![depth1],
601 options: CompositeNodeFromTextOptions {
602 max_recursion_depth: 2,
603 ..Default::default()
604 },
605 })
606 .expect_err("depth overflow should fail");
607
608 assert!(err.to_string().contains("depth exceeded"));
609 }
610
611 #[test]
612 fn composite_content_parses_and_validates_under_strict_profile() {
613 let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
614 let service = MemoryCompositionService::new(store);
615
616 let request = CompositeNodeFromTextRequest {
617 items: vec![CompositeInputItem {
618 role: CompositeRole::Conversation,
619 text: "user asked for deterministic recall and model proposed fallback policy".to_string(),
620 avec_override: Some(AvecState {
621 stability: 0.8,
622 friction: 0.2,
623 logic: 0.85,
624 autonomy: 0.75,
625 }),
626 context: vec![CompositeInputItem {
627 role: CompositeRole::Document,
628 text: "system notes include lexical fallback and ranked retrieval".to_string(),
629 avec_override: Some(AvecState {
630 stability: 0.7,
631 friction: 0.3,
632 logic: 0.8,
633 autonomy: 0.7,
634 }),
635 context: Vec::new(),
636 }],
637 }],
638 options: CompositeNodeFromTextOptions {
639 allow_llm_avec_fallback: false,
640 ..Default::default()
641 },
642 };
643
644 let result = service
645 .build_content_from_text(&request)
646 .expect("composite content build should succeed");
647
648 let raw_node = render_sttp_node("sdk-composite-session", &result.content);
649 let validator = TreeSitterValidator::new();
650 let validation = validator.validate(&raw_node);
651 assert!(validation.is_valid, "validation failed: {:?}", validation.error);
652
653 let parser = SttpNodeParser::new();
654 let parse = parser.try_parse_strict(&raw_node, "sdk-composite-session");
655 assert!(parse.success, "strict parse failed: {:?}", parse.error);
656 }
657
658 fn render_sttp_node(session_id: &str, content: &Value) -> String {
659 let content_text = render_sttp_value(content);
660 format!(
661 "⊕⟨ {{ trigger: manual, response_format: temporal_node, origin_session: \"{session_id}\", compression_depth: 1, parent_node: null, prime: {{ attractor_config: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75 }}, context_summary: \"sdk composite conformance\", relevant_tier: raw, retrieval_budget: 5 }} }} ⟩\n\
662⦿⟨ {{ timestamp: \"2026-05-03T00:00:00Z\", tier: raw, session_id: \"{session_id}\", user_avec: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75, psi: 2.60 }}, model_avec: {{ stability: 0.82, friction: 0.18, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }} ⟩\n\
663◈⟨ {content_text} ⟩\n\
664⍉⟨ {{ rho: 0.96, kappa: 0.94, psi: 2.60, compression_avec: {{ stability: 0.81, friction: 0.19, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }} ⟩"
665 )
666 }
667
668 fn render_sttp_value(value: &Value) -> String {
669 match value {
670 Value::Null => "null".to_string(),
671 Value::Bool(v) => v.to_string(),
672 Value::Number(v) => v.to_string(),
673 Value::String(v) => format!("\"{}\"", v.replace('"', "\\\"")),
674 Value::Array(values) => {
675 let rendered = values
676 .iter()
677 .map(render_sttp_value)
678 .collect::<Vec<_>>()
679 .join(", ");
680 format!("[{rendered}]")
681 }
682 Value::Object(obj) => render_sttp_object(obj),
683 }
684 }
685
686 fn render_sttp_object(obj: &Map<String, Value>) -> String {
687 let rendered = obj
688 .iter()
689 .map(|(key, value)| format!("{key}: {}", render_sttp_value(value)))
690 .collect::<Vec<_>>()
691 .join(", ");
692 format!("{{ {rendered} }}")
693 }
694
695 fn test_node(session_id: &str, timestamp: chrono::DateTime<Utc>, raw: &str) -> SttpNode {
696 let state = AvecState {
697 stability: 0.6,
698 friction: 0.4,
699 logic: 0.8,
700 autonomy: 0.7,
701 };
702
703 SttpNode {
704 raw: raw.to_string(),
705 session_id: session_id.to_string(),
706 tier: "raw".to_string(),
707 timestamp,
708 compression_depth: 1,
709 parent_node_id: None,
710 sync_key: format!("{}:{}", session_id, timestamp.timestamp_nanos_opt().unwrap_or_default()),
711 updated_at: timestamp,
712 source_metadata: None,
713 context_summary: Some(raw.to_string()),
714 embedding_dimensions: None,
715 embedding_model: None,
716 embedding: None,
717 embedded_at: None,
718 user_avec: state,
719 model_avec: state,
720 compression_avec: Some(state),
721 rho: 0.9,
722 kappa: 0.8,
723 psi: 2.5,
724 }
725 }
726}