Skip to main content

velesdb_mobile/
agent.rs

1//! `AgentMemory` Mobile bindings (EPIC-016 US-003)
2//!
3//! Provides semantic memory for AI agents on iOS/Android.
4
5use super::{DistanceMetric, VelesCollection, VelesDatabase, VelesError, VelesPoint};
6
7/// Result from semantic memory query.
8#[derive(Debug, Clone, uniffi::Record)]
9pub struct SemanticResult {
10    /// Knowledge fact ID.
11    pub id: u64,
12    /// Similarity score.
13    pub score: f32,
14    /// Knowledge content text.
15    pub content: String,
16}
17
18/// Semantic Memory for AI agents on mobile.
19///
20/// Stores knowledge facts as vectors with similarity search.
21///
22/// Fact content text is persisted in the point payload (mirroring the core
23/// `SemanticMemory`), so content survives a database reload.
24///
25/// # Example (Swift)
26///
27/// ```swift
28/// let memory = try VelesSemanticMemory(db: db, dimension: 384)
29/// try memory.store(id: 1, content: "Paris is the capital of France", embedding: embedding)
30/// let results = try memory.query(embedding: queryEmbedding, topK: 5)
31/// ```
32#[derive(uniffi::Object)]
33pub struct VelesSemanticMemory {
34    collection: std::sync::Arc<VelesCollection>,
35}
36
37impl VelesSemanticMemory {
38    /// Extracts the `content` text from a stored point's JSON payload.
39    fn content_from_payload(payload: Option<&String>) -> String {
40        payload
41            .and_then(|p| serde_json::from_str::<serde_json::Value>(p).ok())
42            .as_ref()
43            .and_then(|v| v.get("content"))
44            .and_then(serde_json::Value::as_str)
45            .unwrap_or_default()
46            .to_string()
47    }
48}
49
50#[uniffi::export]
51impl VelesSemanticMemory {
52    /// Creates a new `VelesSemanticMemory` with the given embedding dimension.
53    #[uniffi::constructor]
54    pub fn new(db: &VelesDatabase, dimension: u32) -> Result<Self, VelesError> {
55        let collection_name = "_semantic_memory";
56
57        // Try to get existing or create new collection
58        let collection = match db.get_collection(collection_name.to_string())? {
59            Some(coll) => coll,
60            None => {
61                db.create_collection(
62                    collection_name.to_string(),
63                    dimension,
64                    DistanceMetric::Cosine,
65                )?;
66                db.get_collection(collection_name.to_string())?
67                    .ok_or(VelesError::database(
68                        "Failed to retrieve collection after creation".to_string(),
69                    ))?
70            }
71        };
72
73        Ok(Self { collection })
74    }
75
76    /// Stores a knowledge fact with its embedding vector.
77    ///
78    /// The content text is persisted in the point payload as `{"content": ...}`
79    /// so it survives a database reload.
80    pub fn store(&self, id: u64, content: String, embedding: Vec<f32>) -> Result<(), VelesError> {
81        let payload = serde_json::to_string(&serde_json::json!({ "content": content }))
82            .map_err(|e| VelesError::database(format!("Failed to encode content payload: {e}")))?;
83        let point = VelesPoint {
84            id,
85            vector: embedding,
86            payload: Some(payload),
87        };
88        self.collection.upsert(point)?;
89        Ok(())
90    }
91
92    /// Queries semantic memory by similarity search.
93    ///
94    /// Content text is read back from each matched point's payload.
95    pub fn query(
96        &self,
97        embedding: Vec<f32>,
98        top_k: u32,
99    ) -> Result<Vec<SemanticResult>, VelesError> {
100        let results = self.collection.search(embedding, top_k)?;
101
102        let ids: Vec<u64> = results.iter().map(|r| r.id).collect();
103        let contents: std::collections::HashMap<u64, String> = self
104            .collection
105            .get(ids)
106            .into_iter()
107            .map(|p| (p.id, Self::content_from_payload(p.payload.as_ref())))
108            .collect();
109
110        Ok(results
111            .into_iter()
112            .map(|r| SemanticResult {
113                id: r.id,
114                score: r.score,
115                content: contents.get(&r.id).cloned().unwrap_or_default(),
116            })
117            .collect())
118    }
119
120    /// Returns the number of stored knowledge facts.
121    pub fn len(&self) -> Result<u64, VelesError> {
122        Ok(self.collection.count())
123    }
124
125    /// Returns true if no knowledge facts are stored.
126    pub fn is_empty(&self) -> Result<bool, VelesError> {
127        Ok(self.len()? == 0)
128    }
129
130    /// Deletes a knowledge fact by ID.
131    pub fn delete(&self, id: u64) -> Result<(), VelesError> {
132        self.collection.delete(id)
133    }
134
135    /// Removes a knowledge fact by ID.
136    ///
137    /// Deprecated alias for [`Self::delete`], kept for backward compatibility
138    /// and naming parity with prior mobile releases.
139    pub fn remove(&self, id: u64) -> Result<(), VelesError> {
140        self.delete(id)
141    }
142
143    /// Removes all stored knowledge facts.
144    ///
145    /// Best-effort: individual delete failures are non-fatal so the operation
146    /// clears as much as possible.
147    pub fn clear(&self) -> Result<(), VelesError> {
148        for id in self.collection.all_ids() {
149            let _ = self.collection.delete(id);
150        }
151        Ok(())
152    }
153
154    /// Returns the embedding dimension.
155    pub fn dimension(&self) -> u32 {
156        self.collection.dimension()
157    }
158}
159
160#[cfg(test)]
161#[path = "agent_tests.rs"]
162mod tests;