feat: (tool router) adds extension name in vector db & search tool (#2855)

This commit is contained in:
Wendy Tang
2025-06-13 10:28:02 -07:00
committed by GitHub
parent 9368a242ce
commit de5fc9b450
5 changed files with 119 additions and 37 deletions
+23 -7
View File
@@ -246,13 +246,29 @@ impl Agent {
))) )))
} else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME { } else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME {
let selector = self.router_tool_selector.lock().await.clone(); let selector = self.router_tool_selector.lock().await.clone();
ToolCallResult::from(if let Some(selector) = selector { let selected_tools = match selector.as_ref() {
selector.select_tools(tool_call.arguments.clone()).await Some(selector) => match selector.select_tools(tool_call.arguments.clone()).await {
} else { Ok(tools) => tools,
Err(ToolError::ExecutionError( Err(e) => {
"Encountered vector search error.".to_string(), return (
)) request_id,
}) Err(ToolError::ExecutionError(format!(
"Failed to select tools: {}",
e
))),
)
}
},
None => {
return (
request_id,
Err(ToolError::ExecutionError(
"No tool selector available".to_string(),
)),
)
}
};
ToolCallResult::from(Ok(selected_tools))
} else { } else {
// Clone the result to ensure no references to extension_manager are returned // Clone the result to ensure no references to extension_manager are returned
let result = extension_manager let result = extension_manager
@@ -22,7 +22,7 @@ pub enum RouterToolSelectionStrategy {
#[async_trait] #[async_trait]
pub trait RouterToolSelector: Send + Sync { pub trait RouterToolSelector: Send + Sync {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ToolError>; async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ToolError>;
async fn index_tools(&self, tools: &[Tool]) -> Result<(), ToolError>; async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ToolError>;
async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolError>; async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolError>;
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ToolError>; async fn record_tool_call(&self, tool_name: &str) -> Result<(), ToolError>;
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ToolError>; async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ToolError>;
@@ -76,6 +76,9 @@ impl RouterToolSelector for VectorToolSelector {
let k = params.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize; let k = params.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
// Extract extension_name from params if present
let extension_name = params.get("extension_name").and_then(|v| v.as_str());
// Check if provider supports embeddings // Check if provider supports embeddings
if !self.embedding_provider.supports_embeddings() { if !self.embedding_provider.supports_embeddings() {
return Err(ToolError::ExecutionError( return Err(ToolError::ExecutionError(
@@ -98,7 +101,7 @@ impl RouterToolSelector for VectorToolSelector {
let vector_db = self.vector_db.read().await; let vector_db = self.vector_db.read().await;
let tools = vector_db let tools = vector_db
.search_tools(query_embedding, k) .search_tools(query_embedding, k, extension_name)
.await .await
.map_err(|e| ToolError::ExecutionError(format!("Failed to search tools: {}", e)))?; .map_err(|e| ToolError::ExecutionError(format!("Failed to search tools: {}", e)))?;
@@ -119,7 +122,7 @@ impl RouterToolSelector for VectorToolSelector {
Ok(selected_tools) Ok(selected_tools)
} }
async fn index_tools(&self, tools: &[Tool]) -> Result<(), ToolError> { async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ToolError> {
let texts_to_embed: Vec<String> = tools let texts_to_embed: Vec<String> = tools
.iter() .iter()
.map(|tool| { .map(|tool| {
@@ -155,6 +158,7 @@ impl RouterToolSelector for VectorToolSelector {
description: tool.description.clone(), description: tool.description.clone(),
schema: schema_str, schema: schema_str,
vector, vector,
extension_name: extension_name.to_string(),
} }
}) })
.collect(); .collect();
+8 -6
View File
@@ -12,18 +12,20 @@ pub fn vector_search_tool() -> Tool {
Format a query to search for the most relevant tools based on the user's messages. Format a query to search for the most relevant tools based on the user's messages.
Pay attention to the keywords in the user's messages, especially the last message and potential tools they are asking for. Pay attention to the keywords in the user's messages, especially the last message and potential tools they are asking for.
This tool should be invoked when the user's messages suggest they are asking for a tool to be run. This tool should be invoked when the user's messages suggest they are asking for a tool to be run.
Examples: You have the list of extension names available to you in your system prompt.
- {"User": "what is the weather in Tokyo?", "Query": "weather in Tokyo"} Use the extension_name parameter to filter tools by the appropriate extension.
- {"User": "read this pdf file for me", "Query": "read pdf file"} For example, if the user is asking to list the files in the current directory, you filter for the "developer" extension.
- {"User": "run this command ls -l in the terminal", "Query": "run command in terminal ls -l"} Example: {"User": "list the files in the current directory", "Query": "list files in current directory", "Extension Name": "developer", "k": 5}
Extension name is not optional, it is required.
"#} "#}
.to_string(), .to_string(),
json!({ json!({
"type": "object", "type": "object",
"required": ["query"], "required": ["query", "extension_name"],
"properties": { "properties": {
"query": {"type": "string", "description": "The query to search for the most relevant tools based on the user's messages"}, "query": {"type": "string", "description": "The query to search for the most relevant tools based on the user's messages"},
"k": {"type": "integer", "description": "The number of tools to retrieve (defaults to 5)", "default": 5} "k": {"type": "integer", "description": "The number of tools to retrieve (defaults to 5)", "default": 5},
"extension_name": {"type": "string", "description": "Name of the extension to filter tools by"}
} }
}), }),
Some(ToolAnnotations { Some(ToolAnnotations {
@@ -26,13 +26,16 @@ impl ToolRouterIndexManager {
if !tools.is_empty() { if !tools.is_empty() {
// Index all tools at once // Index all tools at once
selector.index_tools(&tools).await.map_err(|e| { selector
anyhow!( .index_tools(&tools, extension_name)
"Failed to index tools for extension {}: {}", .await
extension_name, .map_err(|e| {
e anyhow!(
) "Failed to index tools for extension {}: {}",
})?; extension_name,
e
)
})?;
tracing::info!( tracing::info!(
"Indexed {} tools for extension {}", "Indexed {} tools for extension {}",
@@ -42,16 +45,20 @@ impl ToolRouterIndexManager {
} }
} }
"remove" => { "remove" => {
// Get tool names for the extension to remove them // Remove all tools for this extension
let tools = extension_manager let tools = extension_manager
.get_prefixed_tools(Some(extension_name.to_string())) .get_prefixed_tools(Some(extension_name.to_string()))
.await?; .await?;
for tool in &tools { for tool in &tools {
selector selector.remove_tool(&tool.name).await.map_err(|e| {
.remove_tool(&tool.name) anyhow!(
.await "Failed to remove tool {} for extension {}: {}",
.map_err(|e| anyhow!("Failed to remove tool {}: {}", tool.name, e))?; tool.name,
extension_name,
e
)
})?;
} }
tracing::info!( tracing::info!(
@@ -61,7 +68,7 @@ impl ToolRouterIndexManager {
); );
} }
_ => { _ => {
anyhow::bail!("Invalid action '{}' for tool indexing", action); return Err(anyhow!("Invalid action: {}", action));
} }
} }
@@ -87,7 +94,7 @@ impl ToolRouterIndexManager {
// Index all platform tools at once // Index all platform tools at once
selector selector
.index_tools(&tools) .index_tools(&tools, "platform")
.await .await
.map_err(|e| anyhow!("Failed to index platform tools: {}", e))?; .map_err(|e| anyhow!("Failed to index platform tools: {}", e))?;
+60 -7
View File
@@ -18,6 +18,7 @@ pub struct ToolRecord {
pub description: String, pub description: String,
pub schema: String, pub schema: String,
pub vector: Vec<f32>, pub vector: Vec<f32>,
pub extension_name: String,
} }
pub struct ToolVectorDB { pub struct ToolVectorDB {
@@ -84,12 +85,14 @@ impl ToolVectorDB {
), ),
false, false,
), ),
Field::new("extension_name", DataType::Utf8, false),
])); ]));
// Create empty table // Create empty table
let tool_names = StringArray::from(vec![] as Vec<&str>); let tool_names = StringArray::from(vec![] as Vec<&str>);
let descriptions = StringArray::from(vec![] as Vec<&str>); let descriptions = StringArray::from(vec![] as Vec<&str>);
let schemas = StringArray::from(vec![] as Vec<&str>); let schemas = StringArray::from(vec![] as Vec<&str>);
let extension_names = StringArray::from(vec![] as Vec<&str>);
// Create empty fixed size list array for vectors // Create empty fixed size list array for vectors
let mut vectors_builder = let mut vectors_builder =
@@ -103,6 +106,7 @@ impl ToolVectorDB {
Arc::new(descriptions), Arc::new(descriptions),
Arc::new(schemas), Arc::new(schemas),
Arc::new(vectors), Arc::new(vectors),
Arc::new(extension_names),
], ],
) )
.context("Failed to create record batch")?; .context("Failed to create record batch")?;
@@ -163,6 +167,7 @@ impl ToolVectorDB {
let tool_names: Vec<&str> = tools.iter().map(|t| t.tool_name.as_str()).collect(); let tool_names: Vec<&str> = tools.iter().map(|t| t.tool_name.as_str()).collect();
let descriptions: Vec<&str> = tools.iter().map(|t| t.description.as_str()).collect(); let descriptions: Vec<&str> = tools.iter().map(|t| t.description.as_str()).collect();
let schemas: Vec<&str> = tools.iter().map(|t| t.schema.as_str()).collect(); let schemas: Vec<&str> = tools.iter().map(|t| t.schema.as_str()).collect();
let extension_names: Vec<&str> = tools.iter().map(|t| t.extension_name.as_str()).collect();
let vectors_data: Vec<Option<Vec<Option<f32>>>> = tools let vectors_data: Vec<Option<Vec<Option<f32>>>> = tools
.iter() .iter()
@@ -181,11 +186,13 @@ impl ToolVectorDB {
), ),
false, false,
), ),
Field::new("extension_name", DataType::Utf8, false),
])); ]));
let tool_names_array = StringArray::from(tool_names); let tool_names_array = StringArray::from(tool_names);
let descriptions_array = StringArray::from(descriptions); let descriptions_array = StringArray::from(descriptions);
let schemas_array = StringArray::from(schemas); let schemas_array = StringArray::from(schemas);
let extension_names_array = StringArray::from(extension_names);
// Build vectors array // Build vectors array
let mut vectors_builder = let mut vectors_builder =
FixedSizeListBuilder::new(arrow::array::Float32Builder::new(), 1536); FixedSizeListBuilder::new(arrow::array::Float32Builder::new(), 1536);
@@ -213,6 +220,7 @@ impl ToolVectorDB {
Arc::new(descriptions_array), Arc::new(descriptions_array),
Arc::new(schemas_array), Arc::new(schemas_array),
Arc::new(vectors_array), Arc::new(vectors_array),
Arc::new(extension_names_array),
], ],
) )
.context("Failed to create record batch")?; .context("Failed to create record batch")?;
@@ -239,7 +247,12 @@ impl ToolVectorDB {
Ok(()) Ok(())
} }
pub async fn search_tools(&self, query_vector: Vec<f32>, k: usize) -> Result<Vec<ToolRecord>> { pub async fn search_tools(
&self,
query_vector: Vec<f32>,
k: usize,
extension_name: Option<&str>,
) -> Result<Vec<ToolRecord>> {
let connection = self.connection.read().await; let connection = self.connection.read().await;
let table = connection let table = connection
@@ -248,9 +261,11 @@ impl ToolVectorDB {
.await .await
.context("Failed to open tools table")?; .context("Failed to open tools table")?;
let results = table let search = table
.vector_search(query_vector) .vector_search(query_vector)
.context("Failed to create vector search")? .context("Failed to create vector search")?;
let results = search
.limit(k) .limit(k)
.execute() .execute()
.await .await
@@ -281,6 +296,13 @@ impl ToolVectorDB {
.downcast_ref::<StringArray>() .downcast_ref::<StringArray>()
.context("Invalid schema column type")?; .context("Invalid schema column type")?;
let extension_names = batch
.column_by_name("extension_name")
.context("Missing extension_name column")?
.as_any()
.downcast_ref::<StringArray>()
.context("Invalid extension_name column type")?;
// Get the distance scores // Get the distance scores
let distances = batch let distances = batch
.column_by_name("_distance") .column_by_name("_distance")
@@ -292,12 +314,21 @@ impl ToolVectorDB {
for i in 0..batch.num_rows() { for i in 0..batch.num_rows() {
let tool_name = tool_names.value(i).to_string(); let tool_name = tool_names.value(i).to_string();
let _distance = distances.value(i); let _distance = distances.value(i);
let ext_name = extension_names.value(i).to_string();
// Filter by extension name if provided
if let Some(filter_ext) = extension_name {
if ext_name != filter_ext {
continue;
}
}
tools.push(ToolRecord { tools.push(ToolRecord {
tool_name, tool_name,
description: descriptions.value(i).to_string(), description: descriptions.value(i).to_string(),
schema: schemas.value(i).to_string(), schema: schemas.value(i).to_string(),
vector: vec![], // We don't need to return the vector vector: vec![], // We don't need to return the vector
extension_name: ext_name,
}); });
} }
} }
@@ -356,6 +387,7 @@ mod tests {
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"# schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#
.to_string(), .to_string(),
vector: vec![0.1; 1536], // Mock embedding vector vector: vec![0.1; 1536], // Mock embedding vector
extension_name: "test_extension".to_string(),
}, },
ToolRecord { ToolRecord {
tool_name: "test_tool_2".to_string(), tool_name: "test_tool_2".to_string(),
@@ -363,6 +395,7 @@ mod tests {
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"# schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#
.to_string(), .to_string(),
vector: vec![0.2; 1536], // Different mock embedding vector vector: vec![0.2; 1536], // Different mock embedding vector
extension_name: "test_extension".to_string(),
}, },
]; ];
@@ -371,7 +404,7 @@ mod tests {
// Search for tools using a query vector similar to test_tool_1 // Search for tools using a query vector similar to test_tool_1
let query_vector = vec![0.1; 1536]; let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector, 2).await?; let results = db.search_tools(query_vector.clone(), 2, None).await?;
// Verify results // Verify results
assert_eq!(results.len(), 2, "Should find both tools"); assert_eq!(results.len(), 2, "Should find both tools");
@@ -384,6 +417,25 @@ mod tests {
"Second result should be test_tool_2" "Second result should be test_tool_2"
); );
// Test filtering by extension name
let results = db
.search_tools(query_vector.clone(), 2, Some("test_extension"))
.await?;
assert_eq!(
results.len(),
2,
"Should find both tools with test_extension"
);
let results = db
.search_tools(query_vector.clone(), 2, Some("nonexistent_extension"))
.await?;
assert_eq!(
results.len(),
0,
"Should find no tools with nonexistent_extension"
);
Ok(()) Ok(())
} }
@@ -397,7 +449,7 @@ mod tests {
// Search in empty database // Search in empty database
let query_vector = vec![0.1; 1536]; let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector, 2).await?; let results = db.search_tools(query_vector, 2, None).await?;
// Verify no results returned // Verify no results returned
assert_eq!(results.len(), 0, "Empty database should return no results"); assert_eq!(results.len(), 0, "Empty database should return no results");
@@ -419,20 +471,21 @@ mod tests {
description: "A test tool that will be deleted".to_string(), description: "A test tool that will be deleted".to_string(),
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#.to_string(), schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#.to_string(),
vector: vec![0.1; 1536], vector: vec![0.1; 1536],
extension_name: "test_extension".to_string(),
}; };
db.index_tools(vec![test_tool]).await?; db.index_tools(vec![test_tool]).await?;
// Verify tool exists // Verify tool exists
let query_vector = vec![0.1; 1536]; let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector.clone(), 1).await?; let results = db.search_tools(query_vector.clone(), 1, None).await?;
assert_eq!(results.len(), 1, "Tool should exist before deletion"); assert_eq!(results.len(), 1, "Tool should exist before deletion");
// Delete the tool // Delete the tool
db.remove_tool("test_tool_to_delete").await?; db.remove_tool("test_tool_to_delete").await?;
// Verify tool is gone // Verify tool is gone
let results = db.search_tools(query_vector.clone(), 1).await?; let results = db.search_tools(query_vector.clone(), 1, None).await?;
assert_eq!(results.len(), 0, "Tool should be deleted"); assert_eq!(results.len(), 0, "Tool should be deleted");
Ok(()) Ok(())