feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit
This commit is contained in:
1 parent
5bd8b1587a
commit
e4a0fe72df
47 files changed
+6292
-3503
No files matched your search
+77
-25
@@ -27,6 +27,8 @@ pub struct MemoryIndex {
|
||||
pub body_field: Field,
|
||||
pub type_field: Field,
|
||||
pub namespace_field: Field,
|
||||
|
||||
pub query_parser: tantivy::query::QueryParser,
|
||||
}
|
||||
|
||||
impl MemoryIndex {
|
||||
@@ -45,14 +47,14 @@ impl MemoryIndex {
|
||||
let index = Index::open_in_dir(&index_dir)
|
||||
.or_else(|_| Index::create_in_dir(&index_dir, schema.clone()))?;
|
||||
|
||||
let mut writer = index.writer(50_000_000)?;
|
||||
writer.delete_all_documents()?;
|
||||
writer.commit()?;
|
||||
let writer = index.writer(50_000_000)?;
|
||||
let reader = index
|
||||
.reader_builder()
|
||||
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
||||
.try_into()?;
|
||||
|
||||
let query_parser = tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]);
|
||||
|
||||
Ok(Self {
|
||||
index,
|
||||
reader,
|
||||
@@ -63,6 +65,7 @@ impl MemoryIndex {
|
||||
body_field,
|
||||
type_field,
|
||||
namespace_field,
|
||||
query_parser,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,14 +79,14 @@ impl MemoryIndex {
|
||||
let schema = schema_builder.build();
|
||||
|
||||
let index = Index::create_in_ram(schema);
|
||||
let mut writer = index.writer(50_000_000)?;
|
||||
writer.delete_all_documents()?;
|
||||
writer.commit()?;
|
||||
let writer = index.writer(50_000_000)?;
|
||||
let reader = index
|
||||
.reader_builder()
|
||||
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
||||
.try_into()?;
|
||||
|
||||
let query_parser = tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]);
|
||||
|
||||
Ok(Self {
|
||||
index,
|
||||
reader,
|
||||
@@ -94,9 +97,20 @@ impl MemoryIndex {
|
||||
body_field,
|
||||
type_field,
|
||||
namespace_field,
|
||||
query_parser,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> tantivy::Result<()> {
|
||||
let mut writer = self
|
||||
.writer
|
||||
.lock()
|
||||
.map_err(|e| tantivy::TantivyError::SystemError(e.to_string()))?;
|
||||
writer.delete_all_documents()?;
|
||||
writer.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn index_entity(&self, e: &Entity) -> tokio::task::JoinHandle<tantivy::Result<()>> {
|
||||
self.index_entities_batch(std::slice::from_ref(e))
|
||||
}
|
||||
@@ -138,10 +152,18 @@ impl MemoryIndex {
|
||||
let id_val = t.id.clone();
|
||||
let needs_commit = Arc::clone(&self.needs_commit);
|
||||
|
||||
let criteria_body = t.acceptance_criteria.iter().fold(String::new(), |mut acc, c| {
|
||||
if !acc.is_empty() {
|
||||
acc.push('\n');
|
||||
}
|
||||
acc.push_str(&c.description);
|
||||
acc
|
||||
});
|
||||
|
||||
let doc = doc!(
|
||||
self.id_field => t.id.as_str(),
|
||||
self.title_field => t.title.as_str(),
|
||||
self.body_field => format!("{}\n{}", t.description, t.acceptance_criteria.iter().map(|c| c.description.as_str()).collect::<Vec<_>>().join("\n")),
|
||||
self.body_field => format!("{}\n{}", t.description, criteria_body),
|
||||
self.type_field => "task",
|
||||
self.namespace_field => "global"
|
||||
);
|
||||
@@ -172,6 +194,7 @@ impl MemoryIndex {
|
||||
pub async fn commit(&self) -> tantivy::Result<()> {
|
||||
let writer = Arc::clone(&self.writer);
|
||||
let needs_commit = Arc::clone(&self.needs_commit);
|
||||
let reader = self.reader.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if needs_commit.swap(false, std::sync::atomic::Ordering::SeqCst) {
|
||||
let mut writer = writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -181,6 +204,7 @@ impl MemoryIndex {
|
||||
tantivy::TantivyError::SystemError(format!("First commit error: {}; Second commit error: {}", first_err, second_err))
|
||||
})?;
|
||||
}
|
||||
let _ = reader.reload();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
@@ -208,11 +232,9 @@ impl MemoryIndex {
|
||||
namespace: Option<&str>,
|
||||
) -> tantivy::Result<Vec<SearchResultTuple>> {
|
||||
let searcher = self.reader.searcher();
|
||||
let query_parser = tantivy::query::QueryParser::for_index(
|
||||
&self.index,
|
||||
vec![self.title_field, self.body_field],
|
||||
);
|
||||
let q = query_parser.parse_query(query).or_else(|_| {
|
||||
let mut is_wildcard_fallback = false;
|
||||
|
||||
let q = self.query_parser.parse_query(query).or_else(|_| {
|
||||
let sanitized: String = query
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
@@ -223,17 +245,26 @@ impl MemoryIndex {
|
||||
.collect();
|
||||
let safe_query = sanitized.trim();
|
||||
if safe_query.is_empty() {
|
||||
query_parser.parse_query("*")
|
||||
is_wildcard_fallback = true;
|
||||
self.query_parser.parse_query("*")
|
||||
} else {
|
||||
query_parser.parse_query(safe_query)
|
||||
self.query_parser.parse_query(safe_query)
|
||||
}
|
||||
})?;
|
||||
|
||||
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
||||
let fetch_limit = if namespace.is_some() {
|
||||
// Fetch deeper candidate pool so other namespaces do not starve the target namespace
|
||||
1000
|
||||
} else {
|
||||
target_limit
|
||||
};
|
||||
let top_docs = searcher.search(
|
||||
&q,
|
||||
&tantivy::collector::TopDocs::with_limit(50).order_by_score(),
|
||||
&tantivy::collector::TopDocs::with_limit(fetch_limit).order_by_score(),
|
||||
)?;
|
||||
let mut results = Vec::with_capacity(top_docs.len());
|
||||
|
||||
let mut results = Vec::with_capacity(target_limit);
|
||||
for (score, doc_address) in top_docs {
|
||||
let retrieved_doc = searcher.doc::<tantivy::TantivyDocument>(doc_address)?;
|
||||
let id = retrieved_doc
|
||||
@@ -267,6 +298,9 @@ impl MemoryIndex {
|
||||
continue;
|
||||
}
|
||||
results.push((id, doc_type, title, body, score));
|
||||
if results.len() >= target_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
@@ -319,59 +353,73 @@ impl MemoryIndex {
|
||||
|
||||
pub fn add_entity_sync(&self, e: &Entity) {
|
||||
let writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = writer.add_document(doc!(
|
||||
writer.delete_term(tantivy::Term::from_field_text(self.id_field, &e.name));
|
||||
if let Err(err) = writer.add_document(doc!(
|
||||
self.id_field => e.name.as_str(),
|
||||
self.title_field => e.name.as_str(),
|
||||
self.body_field => e.observations.join(" "),
|
||||
self.type_field => "entity",
|
||||
self.namespace_field => e.namespace.as_str()
|
||||
));
|
||||
)) {
|
||||
tracing::warn!("Sync entity indexing failed for '{}': {}", e.name, err);
|
||||
}
|
||||
self.needs_commit
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn delete_all(&self) {
|
||||
let writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = writer.delete_all_documents();
|
||||
if let Err(err) = writer.delete_all_documents() {
|
||||
tracing::warn!("delete_all_documents failed: {}", err);
|
||||
}
|
||||
self.needs_commit
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn add_task_sync(&self, t: &Task) {
|
||||
let writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _res = writer.add_document(doc!(
|
||||
writer.delete_term(tantivy::Term::from_field_text(self.id_field, &t.id));
|
||||
if let Err(err) = writer.add_document(doc!(
|
||||
self.id_field => t.id.as_str(),
|
||||
self.title_field => t.title.as_str(),
|
||||
self.body_field => t.description.as_str(),
|
||||
self.type_field => "task",
|
||||
self.namespace_field => "global"
|
||||
));
|
||||
)) {
|
||||
tracing::warn!("Sync task indexing failed for '{}': {}", t.id, err);
|
||||
}
|
||||
self.needs_commit
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn add_snippet_sync(&self, s: &Snippet) {
|
||||
let writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = writer.add_document(doc!(
|
||||
writer.delete_term(tantivy::Term::from_field_text(self.id_field, &s.name));
|
||||
if let Err(err) = writer.add_document(doc!(
|
||||
self.id_field => s.name.as_str(),
|
||||
self.title_field => s.name.as_str(),
|
||||
self.body_field => format!("{} {}", s.language, s.description),
|
||||
self.type_field => "snippet",
|
||||
self.namespace_field => "global"
|
||||
));
|
||||
)) {
|
||||
tracing::warn!("Sync snippet indexing failed for '{}': {}", s.name, err);
|
||||
}
|
||||
self.needs_commit
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn add_adr_sync(&self, a: &Adr) {
|
||||
let writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _ = writer.add_document(doc!(
|
||||
writer.delete_term(tantivy::Term::from_field_text(self.id_field, &a.id));
|
||||
if let Err(err) = writer.add_document(doc!(
|
||||
self.id_field => a.id.as_str(),
|
||||
self.title_field => a.title.as_str(),
|
||||
self.body_field => format!("{} {} {}", a.context, a.decision, a.consequence),
|
||||
self.type_field => "adr",
|
||||
self.namespace_field => "global"
|
||||
));
|
||||
)) {
|
||||
tracing::warn!("Sync ADR indexing failed for '{}': {}", a.id, err);
|
||||
}
|
||||
self.needs_commit
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
@@ -393,6 +441,7 @@ mod tests {
|
||||
observations: vec!["This is a test observation".to_string()],
|
||||
namespace: "global".to_string(),
|
||||
git_branch: None,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = index.index_entity(&entity).await.unwrap();
|
||||
|
||||
@@ -408,6 +457,7 @@ mod tests {
|
||||
dependencies: vec![],
|
||||
parent_id: None,
|
||||
expires_at: None,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = index.index_task(&task).await.unwrap();
|
||||
|
||||
@@ -419,6 +469,7 @@ mod tests {
|
||||
updated_at: 0,
|
||||
tags: vec![],
|
||||
embedding: None,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = index.index_snippet(&snippet).await.unwrap();
|
||||
|
||||
@@ -431,6 +482,7 @@ mod tests {
|
||||
status: "accepted".to_string(),
|
||||
supersedes: None,
|
||||
timestamp: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = index.index_adr(&adr).await.unwrap();
|
||||
|
||||
|
||||
Reference in new issue
Block a user