Skip to main content

dakera_client/
memory.rs

1//! Memory-oriented client methods for Dakera AI Agent Memory Platform
2//!
3//! Provides high-level methods for storing, recalling, and managing
4//! agent memories and sessions through the Dakera API.
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::Result;
9use crate::DakeraClient;
10
11// ============================================================================
12// Memory Types (client-side)
13// ============================================================================
14
15/// Memory type classification
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub enum MemoryType {
18    #[default]
19    Episodic,
20    Semantic,
21    Procedural,
22    Working,
23}
24
25/// Store a memory request
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct StoreMemoryRequest {
28    pub agent_id: String,
29    pub content: String,
30    #[serde(default)]
31    pub memory_type: MemoryType,
32    #[serde(default = "default_importance")]
33    pub importance: f32,
34    #[serde(default)]
35    pub tags: Vec<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub session_id: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub metadata: Option<serde_json::Value>,
40}
41
42fn default_importance() -> f32 {
43    0.5
44}
45
46impl StoreMemoryRequest {
47    /// Create a new store memory request
48    pub fn new(agent_id: impl Into<String>, content: impl Into<String>) -> Self {
49        Self {
50            agent_id: agent_id.into(),
51            content: content.into(),
52            memory_type: MemoryType::default(),
53            importance: 0.5,
54            tags: Vec::new(),
55            session_id: None,
56            metadata: None,
57        }
58    }
59
60    /// Set memory type
61    pub fn with_type(mut self, memory_type: MemoryType) -> Self {
62        self.memory_type = memory_type;
63        self
64    }
65
66    /// Set importance score
67    pub fn with_importance(mut self, importance: f32) -> Self {
68        self.importance = importance.clamp(0.0, 1.0);
69        self
70    }
71
72    /// Set tags
73    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
74        self.tags = tags;
75        self
76    }
77
78    /// Set session ID
79    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
80        self.session_id = Some(session_id.into());
81        self
82    }
83
84    /// Set metadata
85    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
86        self.metadata = Some(metadata);
87        self
88    }
89}
90
91/// Stored memory response
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct StoreMemoryResponse {
94    pub memory_id: String,
95    pub agent_id: String,
96    pub namespace: String,
97}
98
99/// Recall memories request
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct RecallRequest {
102    pub agent_id: String,
103    pub query: String,
104    #[serde(default = "default_top_k")]
105    pub top_k: usize,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub memory_type: Option<MemoryType>,
108    #[serde(default)]
109    pub min_importance: f32,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub session_id: Option<String>,
112    #[serde(default)]
113    pub tags: Vec<String>,
114}
115
116fn default_top_k() -> usize {
117    5
118}
119
120impl RecallRequest {
121    /// Create a new recall request
122    pub fn new(agent_id: impl Into<String>, query: impl Into<String>) -> Self {
123        Self {
124            agent_id: agent_id.into(),
125            query: query.into(),
126            top_k: 5,
127            memory_type: None,
128            min_importance: 0.0,
129            session_id: None,
130            tags: Vec::new(),
131        }
132    }
133
134    /// Set number of results
135    pub fn with_top_k(mut self, top_k: usize) -> Self {
136        self.top_k = top_k;
137        self
138    }
139
140    /// Filter by memory type
141    pub fn with_type(mut self, memory_type: MemoryType) -> Self {
142        self.memory_type = Some(memory_type);
143        self
144    }
145
146    /// Set minimum importance threshold
147    pub fn with_min_importance(mut self, min: f32) -> Self {
148        self.min_importance = min;
149        self
150    }
151
152    /// Filter by session
153    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
154        self.session_id = Some(session_id.into());
155        self
156    }
157
158    /// Filter by tags
159    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
160        self.tags = tags;
161        self
162    }
163}
164
165/// A recalled memory
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct RecalledMemory {
168    pub id: String,
169    pub content: String,
170    pub memory_type: MemoryType,
171    pub importance: f32,
172    pub score: f32,
173    #[serde(default)]
174    pub tags: Vec<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub session_id: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub metadata: Option<serde_json::Value>,
179    pub created_at: u64,
180    pub last_accessed_at: u64,
181    pub access_count: u32,
182}
183
184/// Recall response
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct RecallResponse {
187    pub memories: Vec<RecalledMemory>,
188    pub total_found: usize,
189}
190
191/// Forget (delete) memories request
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ForgetRequest {
194    pub agent_id: String,
195    #[serde(default)]
196    pub memory_ids: Vec<String>,
197    #[serde(default)]
198    pub tags: Vec<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub session_id: Option<String>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub before_timestamp: Option<u64>,
203}
204
205impl ForgetRequest {
206    /// Forget specific memories by ID
207    pub fn by_ids(agent_id: impl Into<String>, ids: Vec<String>) -> Self {
208        Self {
209            agent_id: agent_id.into(),
210            memory_ids: ids,
211            tags: Vec::new(),
212            session_id: None,
213            before_timestamp: None,
214        }
215    }
216
217    /// Forget memories with specific tags
218    pub fn by_tags(agent_id: impl Into<String>, tags: Vec<String>) -> Self {
219        Self {
220            agent_id: agent_id.into(),
221            memory_ids: Vec::new(),
222            tags,
223            session_id: None,
224            before_timestamp: None,
225        }
226    }
227
228    /// Forget all memories in a session
229    pub fn by_session(agent_id: impl Into<String>, session_id: impl Into<String>) -> Self {
230        Self {
231            agent_id: agent_id.into(),
232            memory_ids: Vec::new(),
233            tags: Vec::new(),
234            session_id: Some(session_id.into()),
235            before_timestamp: None,
236        }
237    }
238}
239
240/// Forget response
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct ForgetResponse {
243    pub deleted_count: u64,
244}
245
246/// Session start request
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct SessionStartRequest {
249    pub agent_id: String,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub metadata: Option<serde_json::Value>,
252}
253
254/// Session information
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Session {
257    pub id: String,
258    pub agent_id: String,
259    pub started_at: u64,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub ended_at: Option<u64>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub summary: Option<String>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub metadata: Option<serde_json::Value>,
266}
267
268/// Session end request
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct SessionEndRequest {
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub summary: Option<String>,
273}
274
275/// Request to update a memory
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct UpdateMemoryRequest {
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub content: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub metadata: Option<serde_json::Value>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub memory_type: Option<MemoryType>,
284}
285
286/// Request to update memory importance
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct UpdateImportanceRequest {
289    pub memory_ids: Vec<String>,
290    pub importance: f32,
291}
292
293/// Request to consolidate memories
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ConsolidateRequest {
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub memory_type: Option<String>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub threshold: Option<f32>,
300    #[serde(default)]
301    pub dry_run: bool,
302}
303
304/// Response from consolidation
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ConsolidateResponse {
307    pub consolidated_count: usize,
308    pub removed_count: usize,
309    pub new_memories: Vec<String>,
310}
311
312/// Request for memory feedback
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct FeedbackRequest {
315    pub memory_id: String,
316    pub feedback: String,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub relevance_score: Option<f32>,
319}
320
321/// Response from feedback
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct FeedbackResponse {
324    pub status: String,
325    pub updated_importance: Option<f32>,
326}
327
328// ============================================================================
329// CE-2: Batch Recall / Forget Types
330// ============================================================================
331
332/// Filter predicates for batch memory operations (CE-2).
333///
334/// All fields are optional.  For [`BatchForgetRequest`] at least one must be
335/// set (server-side safety guard).
336#[derive(Debug, Clone, Serialize, Deserialize, Default)]
337pub struct BatchMemoryFilter {
338    /// Restrict to memories that carry **all** listed tags.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub tags: Option<Vec<String>>,
341    /// Minimum importance (inclusive).
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub min_importance: Option<f32>,
344    /// Maximum importance (inclusive).
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub max_importance: Option<f32>,
347    /// Only memories created at or after this Unix timestamp (seconds).
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub created_after: Option<u64>,
350    /// Only memories created before or at this Unix timestamp (seconds).
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub created_before: Option<u64>,
353    /// Restrict to a specific memory type.
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub memory_type: Option<MemoryType>,
356    /// Restrict to memories from a specific session.
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub session_id: Option<String>,
359}
360
361impl BatchMemoryFilter {
362    /// Convenience: filter by tags.
363    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
364        self.tags = Some(tags);
365        self
366    }
367
368    /// Convenience: filter by minimum importance.
369    pub fn with_min_importance(mut self, min: f32) -> Self {
370        self.min_importance = Some(min);
371        self
372    }
373
374    /// Convenience: filter by maximum importance.
375    pub fn with_max_importance(mut self, max: f32) -> Self {
376        self.max_importance = Some(max);
377        self
378    }
379
380    /// Convenience: filter by session.
381    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
382        self.session_id = Some(session_id.into());
383        self
384    }
385}
386
387/// Request body for `POST /v1/memories/recall/batch`.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct BatchRecallRequest {
390    /// Agent whose memory namespace to search.
391    pub agent_id: String,
392    /// Filter predicates to apply.
393    #[serde(default)]
394    pub filter: BatchMemoryFilter,
395    /// Maximum number of results to return (default: 100).
396    #[serde(default = "default_batch_limit")]
397    pub limit: usize,
398}
399
400fn default_batch_limit() -> usize {
401    100
402}
403
404impl BatchRecallRequest {
405    /// Create a new batch recall request for an agent.
406    pub fn new(agent_id: impl Into<String>) -> Self {
407        Self {
408            agent_id: agent_id.into(),
409            filter: BatchMemoryFilter::default(),
410            limit: 100,
411        }
412    }
413
414    /// Set filter predicates.
415    pub fn with_filter(mut self, filter: BatchMemoryFilter) -> Self {
416        self.filter = filter;
417        self
418    }
419
420    /// Set result limit.
421    pub fn with_limit(mut self, limit: usize) -> Self {
422        self.limit = limit;
423        self
424    }
425}
426
427/// Response from `POST /v1/memories/recall/batch`.
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct BatchRecallResponse {
430    pub memories: Vec<RecalledMemory>,
431    /// Total memories in the agent namespace.
432    pub total: usize,
433    /// Number of memories that passed the filter.
434    pub filtered: usize,
435}
436
437/// Request body for `DELETE /v1/memories/forget/batch`.
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct BatchForgetRequest {
440    /// Agent whose memory namespace to purge from.
441    pub agent_id: String,
442    /// Filter predicates — **at least one must be set** (server safety guard).
443    pub filter: BatchMemoryFilter,
444}
445
446impl BatchForgetRequest {
447    /// Create a new batch forget request with the given filter.
448    pub fn new(agent_id: impl Into<String>, filter: BatchMemoryFilter) -> Self {
449        Self {
450            agent_id: agent_id.into(),
451            filter,
452        }
453    }
454}
455
456/// Response from `DELETE /v1/memories/forget/batch`.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct BatchForgetResponse {
459    pub deleted_count: usize,
460}
461
462// ============================================================================
463// Memory Client Methods
464// ============================================================================
465
466impl DakeraClient {
467    // ========================================================================
468    // Memory Operations
469    // ========================================================================
470
471    /// Store a memory for an agent
472    ///
473    /// # Example
474    ///
475    /// ```rust,no_run
476    /// use dakera_client::{DakeraClient, memory::StoreMemoryRequest};
477    ///
478    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
479    /// let client = DakeraClient::new("http://localhost:3000")?;
480    ///
481    /// let request = StoreMemoryRequest::new("agent-1", "The user prefers dark mode")
482    ///     .with_importance(0.8)
483    ///     .with_tags(vec!["preferences".to_string()]);
484    ///
485    /// let response = client.store_memory(request).await?;
486    /// println!("Stored memory: {}", response.memory_id);
487    /// # Ok(())
488    /// # }
489    /// ```
490    pub async fn store_memory(&self, request: StoreMemoryRequest) -> Result<StoreMemoryResponse> {
491        let url = format!("{}/v1/memory/store", self.base_url);
492        let response = self.client.post(&url).json(&request).send().await?;
493        self.handle_response(response).await
494    }
495
496    /// Recall memories by semantic query
497    ///
498    /// # Example
499    ///
500    /// ```rust,no_run
501    /// use dakera_client::{DakeraClient, memory::RecallRequest};
502    ///
503    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
504    /// let client = DakeraClient::new("http://localhost:3000")?;
505    ///
506    /// let request = RecallRequest::new("agent-1", "user preferences")
507    ///     .with_top_k(10);
508    ///
509    /// let response = client.recall(request).await?;
510    /// for memory in response.memories {
511    ///     println!("{}: {} (score: {})", memory.id, memory.content, memory.score);
512    /// }
513    /// # Ok(())
514    /// # }
515    /// ```
516    pub async fn recall(&self, request: RecallRequest) -> Result<RecallResponse> {
517        let url = format!("{}/v1/memory/recall", self.base_url);
518        let response = self.client.post(&url).json(&request).send().await?;
519        self.handle_response(response).await
520    }
521
522    /// Simple recall with just agent_id and query (convenience method)
523    pub async fn recall_simple(
524        &self,
525        agent_id: &str,
526        query: &str,
527        top_k: usize,
528    ) -> Result<RecallResponse> {
529        self.recall(RecallRequest::new(agent_id, query).with_top_k(top_k))
530            .await
531    }
532
533    /// Get a specific memory by ID
534    pub async fn get_memory(&self, memory_id: &str) -> Result<RecalledMemory> {
535        let url = format!("{}/v1/memory/get/{}", self.base_url, memory_id);
536        let response = self.client.get(&url).send().await?;
537        self.handle_response(response).await
538    }
539
540    /// Forget (delete) memories
541    pub async fn forget(&self, request: ForgetRequest) -> Result<ForgetResponse> {
542        let url = format!("{}/v1/memory/forget", self.base_url);
543        let response = self.client.post(&url).json(&request).send().await?;
544        self.handle_response(response).await
545    }
546
547    /// Search memories with advanced filters
548    pub async fn search_memories(&self, request: RecallRequest) -> Result<RecallResponse> {
549        let url = format!("{}/v1/memory/search", self.base_url);
550        let response = self.client.post(&url).json(&request).send().await?;
551        self.handle_response(response).await
552    }
553
554    /// Update an existing memory
555    pub async fn update_memory(
556        &self,
557        agent_id: &str,
558        memory_id: &str,
559        request: UpdateMemoryRequest,
560    ) -> Result<StoreMemoryResponse> {
561        let url = format!(
562            "{}/v1/agents/{}/memories/{}",
563            self.base_url, agent_id, memory_id
564        );
565        let response = self.client.put(&url).json(&request).send().await?;
566        self.handle_response(response).await
567    }
568
569    /// Update importance of memories
570    pub async fn update_importance(
571        &self,
572        agent_id: &str,
573        request: UpdateImportanceRequest,
574    ) -> Result<serde_json::Value> {
575        let url = format!(
576            "{}/v1/agents/{}/memories/importance",
577            self.base_url, agent_id
578        );
579        let response = self.client.put(&url).json(&request).send().await?;
580        self.handle_response(response).await
581    }
582
583    /// Consolidate memories for an agent
584    pub async fn consolidate(
585        &self,
586        agent_id: &str,
587        request: ConsolidateRequest,
588    ) -> Result<ConsolidateResponse> {
589        let url = format!(
590            "{}/v1/agents/{}/memories/consolidate",
591            self.base_url, agent_id
592        );
593        let response = self.client.post(&url).json(&request).send().await?;
594        self.handle_response(response).await
595    }
596
597    /// Submit feedback on a memory recall
598    pub async fn memory_feedback(
599        &self,
600        agent_id: &str,
601        request: FeedbackRequest,
602    ) -> Result<FeedbackResponse> {
603        let url = format!("{}/v1/agents/{}/memories/feedback", self.base_url, agent_id);
604        let response = self.client.post(&url).json(&request).send().await?;
605        self.handle_response(response).await
606    }
607
608    // ========================================================================
609    // Session Operations
610    // ========================================================================
611
612    /// Start a new session for an agent
613    pub async fn start_session(&self, agent_id: &str) -> Result<Session> {
614        let url = format!("{}/v1/sessions/start", self.base_url);
615        let request = SessionStartRequest {
616            agent_id: agent_id.to_string(),
617            metadata: None,
618        };
619        let response = self.client.post(&url).json(&request).send().await?;
620        self.handle_response(response).await
621    }
622
623    /// Start a session with metadata
624    pub async fn start_session_with_metadata(
625        &self,
626        agent_id: &str,
627        metadata: serde_json::Value,
628    ) -> Result<Session> {
629        let url = format!("{}/v1/sessions/start", self.base_url);
630        let request = SessionStartRequest {
631            agent_id: agent_id.to_string(),
632            metadata: Some(metadata),
633        };
634        let response = self.client.post(&url).json(&request).send().await?;
635        self.handle_response(response).await
636    }
637
638    /// End a session, optionally with a summary
639    pub async fn end_session(&self, session_id: &str, summary: Option<String>) -> Result<Session> {
640        let url = format!("{}/v1/sessions/{}/end", self.base_url, session_id);
641        let request = SessionEndRequest { summary };
642        let response = self.client.post(&url).json(&request).send().await?;
643        self.handle_response(response).await
644    }
645
646    /// Get a session by ID
647    pub async fn get_session(&self, session_id: &str) -> Result<Session> {
648        let url = format!("{}/v1/sessions/{}", self.base_url, session_id);
649        let response = self.client.get(&url).send().await?;
650        self.handle_response(response).await
651    }
652
653    /// List sessions for an agent
654    pub async fn list_sessions(&self, agent_id: &str) -> Result<Vec<Session>> {
655        let url = format!("{}/v1/sessions?agent_id={}", self.base_url, agent_id);
656        let response = self.client.get(&url).send().await?;
657        self.handle_response(response).await
658    }
659
660    /// Get memories in a session
661    pub async fn session_memories(&self, session_id: &str) -> Result<RecallResponse> {
662        let url = format!("{}/v1/sessions/{}/memories", self.base_url, session_id);
663        let response = self.client.get(&url).send().await?;
664        self.handle_response(response).await
665    }
666
667    // ========================================================================
668    // CE-2: Batch Recall / Forget
669    // ========================================================================
670
671    /// Bulk-recall memories using filter predicates (CE-2).
672    ///
673    /// Uses `POST /v1/memories/recall/batch` — no embedding required.
674    ///
675    /// # Example
676    ///
677    /// ```rust,no_run
678    /// use dakera_client::{DakeraClient, memory::{BatchRecallRequest, BatchMemoryFilter}};
679    ///
680    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
681    /// let client = DakeraClient::new("http://localhost:3000")?;
682    ///
683    /// let filter = BatchMemoryFilter::default().with_min_importance(0.7);
684    /// let req = BatchRecallRequest::new("agent-1").with_filter(filter).with_limit(50);
685    /// let resp = client.batch_recall(req).await?;
686    /// println!("Found {} memories", resp.filtered);
687    /// # Ok(())
688    /// # }
689    /// ```
690    pub async fn batch_recall(&self, request: BatchRecallRequest) -> Result<BatchRecallResponse> {
691        let url = format!("{}/v1/memories/recall/batch", self.base_url);
692        let response = self.client.post(&url).json(&request).send().await?;
693        self.handle_response(response).await
694    }
695
696    /// Bulk-delete memories using filter predicates (CE-2).
697    ///
698    /// Uses `DELETE /v1/memories/forget/batch`.  The server requires at least
699    /// one filter predicate to be set as a safety guard.
700    ///
701    /// # Example
702    ///
703    /// ```rust,no_run
704    /// use dakera_client::{DakeraClient, memory::{BatchForgetRequest, BatchMemoryFilter}};
705    ///
706    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
707    /// let client = DakeraClient::new("http://localhost:3000")?;
708    ///
709    /// let filter = BatchMemoryFilter::default().with_min_importance(0.0).with_max_importance(0.2);
710    /// let resp = client.batch_forget(BatchForgetRequest::new("agent-1", filter)).await?;
711    /// println!("Deleted {} memories", resp.deleted_count);
712    /// # Ok(())
713    /// # }
714    /// ```
715    pub async fn batch_forget(&self, request: BatchForgetRequest) -> Result<BatchForgetResponse> {
716        let url = format!("{}/v1/memories/forget/batch", self.base_url);
717        let response = self.client.delete(&url).json(&request).send().await?;
718        self.handle_response(response).await
719    }
720}