feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit

This commit is contained in:
Riz Ashraf committed 2026-10-07 06:36:09 +01:00
1 parent 5bd8b1587a
commit e4a0fe72df
47 files changed
+6292 -3503

No files matched your search

+89 -27
View File
@@ -38,21 +38,7 @@ impl<'a> GraphQueryBuilder<'a> {
}
pub fn find_shortest_path(&self, start: &str, end: &str) -> Option<Vec<String>> {
let mut adj: std::collections::HashMap<&str, Vec<(&str, &str, bool)>> =
std::collections::HashMap::with_capacity(self.graph.relations.len() * 2);
for rel in &self.graph.relations {
if let Some(rf) = self.relation_filter {
if rel.relation_type != rf {
continue;
}
}
adj.entry(rel.from.as_str())
.or_default()
.push((rel.to.as_str(), rel.relation_type.as_str(), false));
adj.entry(rel.to.as_str())
.or_default()
.push((rel.from.as_str(), rel.relation_type.as_str(), true));
}
let adj = self.graph.build_adjacency_map(self.relation_filter);
let mut queue = std::collections::VecDeque::new();
let mut visited = std::collections::HashSet::new();
@@ -253,6 +239,7 @@ impl McpTool for CreateRelationsHandler {
observations: vec!["Auto-created stub entity for relation endpoint".to_string()],
namespace: crate::models::default_namespace(),
git_branch: None,
..Default::default()
},
);
auto_created.push(node_name);
@@ -328,7 +315,10 @@ impl McpTool for DeleteEntitiesHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<DeleteEntitiesTool>("delete_entities", "Execute delete_entities")
crate::mcp::tool_def::<DeleteEntitiesTool>(
"delete_entities",
"Delete specified entities and all their associated relations and observations from the knowledge graph.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -378,7 +368,7 @@ impl McpTool for DeleteObservationsHandler {
fn schema(&self) -> Value {
crate::mcp::tool_def::<DeleteObservationsTool>(
"delete_observations",
"Execute delete_observations",
"Delete specific observations from existing entities in the knowledge graph without removing the entity itself.",
)
}
@@ -422,7 +412,10 @@ impl McpTool for DeleteRelationsHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<DeleteRelationsTool>("delete_relations", "Execute delete_relations")
crate::mcp::tool_def::<DeleteRelationsTool>(
"delete_relations",
"Delete specific relationships between entities in the knowledge graph by specifying source 'from', target 'to', and 'relation_type'.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -462,8 +455,45 @@ impl McpTool for ReadGraphHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let max_tokens = req.max_tokens;
let limit = req.limit;
let offset = req.offset.unwrap_or(0);
let data = state.read_graph(|full| -> crate::error::Result<String> {
let mut result_json = if let Some(ns) = req.namespace {
let mut result_json = if limit.is_some() || req.offset.is_some() {
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
.entities
.iter()
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
.collect();
let total_count = filtered_entities.len();
let effective_limit = limit.unwrap_or(50);
let paged_entities: std::collections::HashMap<&String, &crate::models::Entity> = filtered_entities
.into_iter()
.skip(offset)
.take(effective_limit)
.collect();
let has_more = offset + paged_entities.len() < total_count;
let relevant_relations: Vec<&crate::models::Relation> = full
.relations
.iter()
.filter(|r| {
let ns_match = req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns);
ns_match && (paged_entities.contains_key(&r.from) || paged_entities.contains_key(&r.to))
})
.collect();
serde_json::to_string(&serde_json::json!({
"entities": paged_entities,
"relations": relevant_relations,
"total_count": total_count,
"offset": offset,
"limit": effective_limit,
"has_more": has_more,
}))?
} else if let Some(ns) = req.namespace {
let mut filtered = BorrowedGraph::default();
for (k, v) in &full.entities {
if v.namespace == ns {
@@ -485,7 +515,7 @@ impl McpTool for ReadGraphHandler {
if result_json.len() > max_chars {
let valid_boundary = result_json.floor_char_boundary(max_chars);
result_json.truncate(valid_boundary);
result_json.push_str("\n... [TRUNCATED_TO_MAX_TOKENS. Use search_nodes or get_subgraph for targeted discovery]");
result_json.push_str("\n... [TRUNCATED_TO_MAX_TOKENS. Use search_nodes, pagination (limit, offset), or get_subgraph for targeted discovery]");
}
}
Ok(result_json)
@@ -503,7 +533,10 @@ impl McpTool for SearchNodesHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<SearchNodesTool>("search_nodes", "Execute search_nodes")
crate::mcp::tool_def::<SearchNodesTool>(
"search_nodes",
"Search knowledge graph entities and nodes by name or observation query. Returns matching entity names, types, and observations.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -550,7 +583,10 @@ impl McpTool for OpenNodesHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<OpenNodesTool>("open_nodes", "Execute open_nodes")
crate::mcp::tool_def::<OpenNodesTool>(
"open_nodes",
"Retrieve complete node details, observations, and connected relations for specific entities in the knowledge graph.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -588,7 +624,10 @@ impl McpTool for VisualizeGraphHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<VisualizeGraphTool>("visualize_graph", "Execute visualize_graph")
crate::mcp::tool_def::<VisualizeGraphTool>(
"visualize_graph",
"Generate a visual Mermaid representation of the knowledge graph topology or a filtered query subset.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -677,7 +716,10 @@ impl McpTool for CondenseEntityHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<CondenseEntityTool>("condense_entity", "Execute condense_entity")
crate::mcp::tool_def::<CondenseEntityTool>(
"condense_entity",
"Condense, summarize, and deduplicate redundant observations on an entity to optimize context size while preserving key facts.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -757,7 +799,10 @@ impl McpTool for MergeEntitiesHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<MergeEntitiesTool>("merge_entities", "Execute merge_entities")
crate::mcp::tool_def::<MergeEntitiesTool>(
"merge_entities",
"Merge two duplicate entities into a primary target entity, consolidating all observations and re-pointing relations.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -817,7 +862,10 @@ impl McpTool for FindOrphansHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<FindOrphansTool>("find_orphans", "Execute find_orphans")
crate::mcp::tool_def::<FindOrphansTool>(
"find_orphans",
"Find orphan nodes in the knowledge graph that have no incoming or outgoing relationships.",
)
}
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -847,7 +895,10 @@ impl McpTool for GetSubgraphHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<GetSubgraphTool>("get_subgraph", "Execute get_subgraph")
crate::mcp::tool_def::<GetSubgraphTool>(
"get_subgraph",
"Extract a connected neighborhood subgraph centered around specified seed nodes up to a given hop depth.",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
@@ -1188,6 +1239,7 @@ mod tests {
observations: vec![],
namespace: "global".to_string(),
git_branch: None,
..Default::default()
},
);
g.entities.insert(
@@ -1198,6 +1250,7 @@ mod tests {
observations: vec![],
namespace: "global".to_string(),
git_branch: None,
..Default::default()
},
);
});
@@ -1256,6 +1309,7 @@ mod tests {
observations: vec![],
namespace: "global".to_string(),
git_branch: None,
..Default::default()
},
);
});
@@ -1447,6 +1501,14 @@ mod tests {
let qsignal_res = qsignal_handler.execute(json!({"sender": "agent1"}), state.clone()).await.unwrap();
assert!(qsignal_res.contains("task_completed"));
let read_paged_handler = ReadGraphHandler;
let paged_res = read_paged_handler
.execute(json!({"limit": 1, "offset": 0}), state.clone())
.await
.unwrap();
let parsed_paged: serde_json::Value = serde_json::from_str(&paged_res).unwrap();
assert_eq!(parsed_paged["total_count"], 2);
assert_eq!(parsed_paged["has_more"], true);
}
}