1use anda_core::{BoxError, BoxPinFut, ObjectMeta, Path, PutMode, PutResult, path_join};
20use futures::TryStreamExt;
21use object_store::PutOptions;
22use std::sync::Arc;
23
24pub use object_store::{ObjectStore, ObjectStoreExt, local::LocalFileSystem, memory::InMemory};
25
26pub const MAX_STORE_OBJECT_SIZE: usize = 1024 * 1024 * 2; pub trait VectorSearchFeaturesDyn: Send + Sync + 'static {
31 fn top_n(
33 &self,
34 namespace: Path,
35 query: String,
36 n: usize,
37 ) -> BoxPinFut<Result<Vec<String>, BoxError>>;
38
39 fn top_n_ids(
41 &self,
42 namespace: Path,
43 query: String,
44 n: usize,
45 ) -> BoxPinFut<Result<Vec<String>, BoxError>>;
46}
47
48#[derive(Clone)]
50pub struct VectorStore {
51 inner: Arc<dyn VectorSearchFeaturesDyn>,
52}
53
54impl VectorStore {
55 pub fn new(inner: Arc<dyn VectorSearchFeaturesDyn>) -> Self {
57 Self { inner }
58 }
59
60 pub fn not_implemented() -> Self {
62 Self {
63 inner: Arc::new(NotImplemented),
64 }
65 }
66}
67
68impl VectorSearchFeaturesDyn for VectorStore {
69 fn top_n(
70 &self,
71 namespace: Path,
72 query: String,
73 n: usize,
74 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
75 self.inner.top_n(namespace, query, n)
76 }
77
78 fn top_n_ids(
79 &self,
80 namespace: Path,
81 query: String,
82 n: usize,
83 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
84 self.inner.top_n_ids(namespace, query, n)
85 }
86}
87
88#[derive(Clone, Debug)]
90pub struct NotImplemented;
91
92impl VectorSearchFeaturesDyn for NotImplemented {
93 fn top_n(
94 &self,
95 _namespace: Path,
96 _query: String,
97 _n: usize,
98 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
99 Box::pin(futures::future::ready(Err("not implemented".into())))
100 }
101
102 fn top_n_ids(
103 &self,
104 _namespace: Path,
105 _query: String,
106 _n: usize,
107 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
108 Box::pin(futures::future::ready(Err("not implemented".into())))
109 }
110}
111
112#[derive(Clone, Debug)]
114pub struct MockImplemented;
115
116impl VectorSearchFeaturesDyn for MockImplemented {
117 fn top_n(
118 &self,
119 _namespace: Path,
120 _query: String,
121 _n: usize,
122 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
123 Box::pin(futures::future::ready(Ok(vec![])))
124 }
125
126 fn top_n_ids(
127 &self,
128 _namespace: Path,
129 _query: String,
130 _n: usize,
131 ) -> BoxPinFut<Result<Vec<String>, BoxError>> {
132 Box::pin(futures::future::ready(Ok(vec![])))
133 }
134}
135
136#[derive(Clone)]
150pub struct Store {
151 store: Arc<dyn ObjectStore>,
152}
153
154impl Store {
155 pub fn new(store: Arc<dyn ObjectStore>) -> Self {
157 Self { store }
158 }
159
160 pub async fn store_get(
162 &self,
163 namespace: &Path,
164 path: &Path,
165 ) -> Result<(bytes::Bytes, ObjectMeta), BoxError> {
166 let path = path_join(namespace, path);
167 let res = self.store.get_opts(&path, Default::default()).await?;
168 let meta = res.meta.clone();
169 let data = res.bytes().await?;
170 Ok((data, meta))
171 }
172
173 pub async fn store_list(
179 &self,
180 namespace: &Path,
181 prefix: Option<&Path>,
182 offset: &Path,
183 ) -> Result<Vec<ObjectMeta>, BoxError> {
184 let prefix = prefix.map(|p| path_join(namespace, p));
185 let offset = path_join(namespace, offset);
186
187 let mut res = if offset.is_root() {
188 self.store.list(prefix.as_ref())
189 } else {
190 self.store.list_with_offset(prefix.as_ref(), &offset)
191 };
192 let mut metas = Vec::new();
193 while let Some(meta) = res.try_next().await? {
194 metas.push(meta)
195 }
196
197 Ok(metas)
198 }
199
200 pub async fn store_put(
207 &self,
208 namespace: &Path,
209 path: &Path,
210 mode: PutMode,
211 val: bytes::Bytes,
212 ) -> Result<PutResult, BoxError> {
213 let full_path = path_join(namespace, path);
214 if val.len() > MAX_STORE_OBJECT_SIZE {
217 return Err(format!(
218 "object size {} bytes exceeds the {} byte limit (path: {})",
219 val.len(),
220 MAX_STORE_OBJECT_SIZE,
221 full_path
222 )
223 .into());
224 }
225 let res = self
226 .store
227 .put_opts(
228 &full_path,
229 val.into(),
230 PutOptions {
231 mode,
232 ..Default::default()
233 },
234 )
235 .await?;
236 Ok(res)
237 }
238
239 pub async fn store_rename_if_not_exists(
245 &self,
246 namespace: &Path,
247 from: &Path,
248 to: &Path,
249 ) -> Result<(), BoxError> {
250 let from = path_join(namespace, from);
251 let to = path_join(namespace, to);
252 self.store.rename_if_not_exists(&from, &to).await?;
253 Ok(())
254 }
255
256 pub async fn store_delete(&self, namespace: &Path, path: &Path) -> Result<(), BoxError> {
261 let path = path_join(namespace, path);
262 self.store.delete(&path).await?;
263 Ok(())
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use bytes::Bytes;
271
272 #[tokio::test(flavor = "current_thread")]
273 async fn vector_store_delegates_to_implemented_and_placeholder_backends() {
274 let implemented = VectorStore::new(Arc::new(MockImplemented));
275 assert!(
276 implemented
277 .top_n(Path::from("ns"), "query".to_string(), 3)
278 .await
279 .unwrap()
280 .is_empty()
281 );
282 assert!(
283 implemented
284 .top_n_ids(Path::from("ns"), "query".to_string(), 3)
285 .await
286 .unwrap()
287 .is_empty()
288 );
289
290 let missing = VectorStore::not_implemented();
291 assert!(
292 missing
293 .top_n(Path::from("ns"), "query".to_string(), 3)
294 .await
295 .unwrap_err()
296 .to_string()
297 .contains("not implemented")
298 );
299 assert!(
300 missing
301 .top_n_ids(Path::from("ns"), "query".to_string(), 3)
302 .await
303 .unwrap_err()
304 .to_string()
305 .contains("not implemented")
306 );
307 }
308
309 #[tokio::test(flavor = "current_thread")]
310 async fn store_applies_namespace_to_crud_list_and_rename_operations() {
311 let store = Store::new(Arc::new(InMemory::new()));
312 let namespace = Path::from("agent/root");
313 let docs = Path::from("docs");
314 let first = Path::from("docs/first.txt");
315 let second = Path::from("docs/second.txt");
316 let renamed = Path::from("docs/renamed.txt");
317
318 store
319 .store_put(
320 &namespace,
321 &first,
322 PutMode::Overwrite,
323 Bytes::from_static(b"first"),
324 )
325 .await
326 .unwrap();
327 store
328 .store_put(
329 &namespace,
330 &second,
331 PutMode::Overwrite,
332 Bytes::from_static(b"second"),
333 )
334 .await
335 .unwrap();
336
337 let (data, meta) = store.store_get(&namespace, &first).await.unwrap();
338 assert_eq!(data, Bytes::from_static(b"first"));
339 assert_eq!(meta.location, Path::from("agent/root/docs/first.txt"));
340
341 let listed = store
342 .store_list(&namespace, Some(&docs), &Path::default())
343 .await
344 .unwrap();
345 assert_eq!(listed.len(), 2);
346 assert!(
347 listed
348 .iter()
349 .all(|meta| meta.location.as_ref().starts_with("agent/root/docs/"))
350 );
351
352 let listed_after_offset = store
353 .store_list(&namespace, Some(&docs), &first)
354 .await
355 .unwrap();
356 assert_eq!(listed_after_offset.len(), 1);
357 assert_eq!(
358 listed_after_offset[0].location,
359 Path::from("agent/root/docs/second.txt")
360 );
361
362 store
363 .store_rename_if_not_exists(&namespace, &second, &renamed)
364 .await
365 .unwrap();
366 let (data, _) = store.store_get(&namespace, &renamed).await.unwrap();
367 assert_eq!(data, Bytes::from_static(b"second"));
368
369 store.store_delete(&namespace, &renamed).await.unwrap();
370 assert!(store.store_get(&namespace, &renamed).await.is_err());
371 }
372}