1use std::sync::Arc;
4
5use arrow_array::{
6 types::Float32Type, Array, FixedSizeListArray, Float32Array, RecordBatch, StringArray,
7 UInt64Array,
8};
9use arrow_schema::{DataType, Field, Schema};
10use futures::TryStreamExt;
11use lancedb::connect;
12use lancedb::database::CreateTableMode;
13use lancedb::query::{ExecutableQuery, QueryBase};
14use serde::{Deserialize, Serialize};
15
16use super::embeddings::Embedder;
17use crate::error::{AgentError, Result};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct KnowledgeEntry {
26 pub text: String,
27 pub source: String,
28 #[serde(default)]
29 pub metadata: serde_json::Value,
30 #[serde(default = "default_timestamp")]
32 pub timestamp: i64,
33}
34
35fn default_timestamp() -> i64 {
36 chrono::Utc::now().timestamp()
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct SearchResult {
42 pub text: String,
43 pub source: String,
44 pub score: f32,
45 pub metadata: serde_json::Value,
46 pub timestamp: i64,
47}
48
49pub struct VectorStore {
55 conn: lancedb::Connection,
56 embedder: Arc<Embedder>,
57 default_collection: String,
58 dim: usize,
59}
60
61impl std::fmt::Debug for VectorStore {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("VectorStore")
64 .field("default_collection", &self.default_collection)
65 .field("dim", &self.dim)
66 .field("embedder", &self.embedder)
67 .finish_non_exhaustive()
68 }
69}
70
71impl VectorStore {
72 pub async fn new(
74 db_path: &str,
75 embedder: Arc<Embedder>,
76 default_collection: &str,
77 ) -> Result<Self> {
78 let conn = connect(db_path).execute().await.map_err(|e| {
79 AgentError::KnowledgeError(format!(
80 "Failed to connect to LanceDB at '{}': {}",
81 db_path, e
82 ))
83 })?;
84 let dim = embedder.dimension();
85 Ok(Self {
86 conn,
87 embedder,
88 default_collection: default_collection.to_owned(),
89 dim,
90 })
91 }
92
93 fn schema(dim: i32) -> Arc<Schema> {
95 Arc::new(Schema::new(vec![
96 Field::new("id", DataType::Utf8, false),
97 Field::new(
98 "vector",
99 DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Float32, true)), dim),
100 true,
101 ),
102 Field::new("text", DataType::Utf8, false),
103 Field::new("source", DataType::Utf8, false),
104 Field::new("metadata_json", DataType::Utf8, true),
105 Field::new("timestamp", DataType::UInt64, false),
106 ]))
107 }
108
109 pub async fn create_collection(&self, name: &str) -> Result<()> {
111 let schema = Self::schema(self.dim as i32);
112 self.conn
113 .create_empty_table(name, schema)
114 .mode(CreateTableMode::exist_ok(|req| req))
115 .execute()
116 .await
117 .map_err(|e| {
118 AgentError::KnowledgeError(format!("Failed to create collection '{}': {}", name, e))
119 })?;
120 Ok(())
121 }
122
123 pub async fn insert(&self, collection: &str, entries: Vec<KnowledgeEntry>) -> Result<()> {
125 if entries.is_empty() {
126 return Ok(());
127 }
128
129 self.create_collection(collection).await?;
131
132 let texts: Vec<String> = entries.iter().map(|e| e.text.clone()).collect();
134 let embedder = self.embedder.clone();
135 let embeddings = tokio::task::spawn_blocking(move || embedder.embed_batch_passages(&texts))
136 .await
137 .map_err(|e| AgentError::KnowledgeError(format!("Embedding task panicked: {}", e)))?
138 .map_err(|e| AgentError::KnowledgeError(format!("Embedding failed: {}", e)))?;
139
140 let n = entries.len();
142 let dim = self.dim as i32;
143
144 let ids: Vec<String> = (0..n).map(|_| uuid::Uuid::new_v4().to_string()).collect();
145 let id_array = StringArray::from(ids);
146
147 let vector_array = FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
148 embeddings
149 .iter()
150 .map(|emb| Some(emb.iter().map(|&v| Some(v)))),
151 dim,
152 );
153
154 let text_array =
155 StringArray::from(entries.iter().map(|e| e.text.as_str()).collect::<Vec<_>>());
156 let source_array = StringArray::from(
157 entries
158 .iter()
159 .map(|e| e.source.as_str())
160 .collect::<Vec<_>>(),
161 );
162 let metadata_json_array = StringArray::from(
163 entries
164 .iter()
165 .map(|e| {
166 if e.metadata.is_null() {
167 None
168 } else {
169 Some(serde_json::to_string(&e.metadata).unwrap_or_default())
170 }
171 })
172 .collect::<Vec<_>>(),
173 );
174 let timestamp_array = UInt64Array::from(
175 entries
176 .iter()
177 .map(|e| e.timestamp as u64)
178 .collect::<Vec<_>>(),
179 );
180
181 let schema = Self::schema(dim);
182 let batch = RecordBatch::try_new(
183 schema.clone(),
184 vec![
185 Arc::new(id_array),
186 Arc::new(vector_array),
187 Arc::new(text_array),
188 Arc::new(source_array),
189 Arc::new(metadata_json_array),
190 Arc::new(timestamp_array),
191 ],
192 )
193 .map_err(|e| AgentError::KnowledgeError(format!("Failed to build RecordBatch: {}", e)))?;
194
195 let table = self
197 .conn
198 .open_table(collection)
199 .execute()
200 .await
201 .map_err(|e| {
202 AgentError::KnowledgeError(format!(
203 "Failed to open collection '{}': {}",
204 collection, e
205 ))
206 })?;
207
208 table.add(batch).execute().await.map_err(|e| {
209 AgentError::KnowledgeError(format!(
210 "Failed to insert into collection '{}': {}",
211 collection, e
212 ))
213 })?;
214
215 Ok(())
216 }
217
218 pub async fn search(
220 &self,
221 collection: &str,
222 query: &str,
223 limit: usize,
224 ) -> Result<Vec<SearchResult>> {
225 let embedder = self.embedder.clone();
227 let query_owned = query.to_owned();
228 let query_vec = tokio::task::spawn_blocking(move || embedder.embed_query(&query_owned))
229 .await
230 .map_err(|e| AgentError::KnowledgeError(format!("Query embed panicked: {}", e)))?
231 .map_err(|e| AgentError::KnowledgeError(format!("Query embed failed: {}", e)))?;
232
233 let table = match self.conn.open_table(collection).execute().await {
235 Ok(t) => t,
236 Err(_) => return Ok(Vec::new()),
237 };
238
239 let results: Vec<RecordBatch> = table
241 .query()
242 .nearest_to(query_vec.as_slice())
243 .map_err(|e| AgentError::KnowledgeError(format!("Failed to build query: {}", e)))?
244 .limit(limit)
245 .execute()
246 .await
247 .map_err(|e| AgentError::KnowledgeError(format!("Search failed: {}", e)))?
248 .try_collect()
249 .await
250 .map_err(|e| AgentError::KnowledgeError(format!("Failed to collect results: {}", e)))?;
251
252 let mut search_results = Vec::new();
254 for batch in &results {
255 let schema = batch.schema();
256 let text_idx = schema.index_of("text").unwrap_or(0);
257 let source_idx = schema.index_of("source").unwrap_or(1);
258 let metadata_idx = schema.index_of("metadata_json").ok();
259 let timestamp_idx = schema.index_of("timestamp").unwrap_or(5);
260
261 let distance_idx = schema.index_of("_distance").ok();
263
264 for row in 0..batch.num_rows() {
265 let text_col = batch
266 .column(text_idx)
267 .as_any()
268 .downcast_ref::<StringArray>()
269 .unwrap();
270 let source_col = batch
271 .column(source_idx)
272 .as_any()
273 .downcast_ref::<StringArray>()
274 .unwrap();
275 let ts_col = batch
276 .column(timestamp_idx)
277 .as_any()
278 .downcast_ref::<UInt64Array>()
279 .unwrap();
280
281 let text = text_col.value(row).to_owned();
282 let source = source_col.value(row).to_owned();
283 let timestamp = ts_col.value(row) as i64;
284
285 let metadata = if let Some(mi) = metadata_idx {
286 let meta_col = batch
287 .column(mi)
288 .as_any()
289 .downcast_ref::<StringArray>()
290 .unwrap();
291 if meta_col.is_null(row) {
292 serde_json::Value::Null
293 } else {
294 serde_json::from_str(meta_col.value(row)).unwrap_or(serde_json::Value::Null)
295 }
296 } else {
297 serde_json::Value::Null
298 };
299
300 let score = if let Some(di) = distance_idx {
301 let dist_col = batch
302 .column(di)
303 .as_any()
304 .downcast_ref::<Float32Array>()
305 .unwrap();
306 let dist = dist_col.value(row);
309 1.0 / (1.0 + dist)
310 } else {
311 1.0
312 };
313
314 search_results.push(SearchResult {
315 text,
316 source,
317 score,
318 metadata,
319 timestamp,
320 });
321 }
322 }
323
324 Ok(search_results)
325 }
326
327 pub async fn list_collections(&self) -> Result<Vec<String>> {
329 let names = self.conn.table_names().execute().await.map_err(|e| {
330 AgentError::KnowledgeError(format!("Failed to list collections: {}", e))
331 })?;
332 Ok(names)
333 }
334
335 pub fn default_collection(&self) -> &str {
337 &self.default_collection
338 }
339}