chore: remove vector search tool selection strategy (#3933)

Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Wendy Tang
2025-08-17 20:56:27 -07:00
committed by GitHub
parent 28228d8b0b
commit b23559640a
15 changed files with 161 additions and 3035 deletions
Generated
+18 -1880
View File
File diff suppressed because it is too large Load Diff
+14 -27
View File
@@ -1179,7 +1179,7 @@ pub async fn configure_settings_dialog() -> Result<(), Box<dyn Error>> {
.item(
"goose_router_strategy",
"Router Tool Selection Strategy",
"Configure the strategy for selecting tools to use",
"Experimental: configure a strategy for auto selecting tools to use",
)
.item(
"tool_permission",
@@ -1300,40 +1300,27 @@ pub fn configure_goose_mode_dialog() -> Result<(), Box<dyn Error>> {
pub fn configure_goose_router_strategy_dialog() -> Result<(), Box<dyn Error>> {
let config = Config::global();
// Check if GOOSE_ROUTER_STRATEGY is set as an environment variable
if std::env::var("GOOSE_ROUTER_TOOL_SELECTION_STRATEGY").is_ok() {
let _ = cliclack::log::info("Notice: GOOSE_ROUTER_TOOL_SELECTION_STRATEGY environment variable is set. Configuration will override this.");
}
let strategy = cliclack::select("Which router strategy would you like to use?")
let enable_router = cliclack::select("Would you like to enable smart tool routing?")
.item(
"vector",
"Vector Strategy",
"Use vector-based similarity to select tools",
"true",
"Enable Router",
"Use LLM-based intelligence to select tools",
)
.item(
"default",
"Default Strategy",
"false",
"Disable Router",
"Use the default tool selection strategy",
)
.interact()?;
match strategy {
"vector" => {
config.set_param(
"GOOSE_ROUTER_TOOL_SELECTION_STRATEGY",
Value::String("vector".to_string()),
)?;
cliclack::outro(
"Set to Vector Strategy - using vector-based similarity for tool selection",
)?;
match enable_router {
"true" => {
config.set_param("GOOSE_ENABLE_ROUTER", Value::String("true".to_string()))?;
cliclack::outro("Router enabled - using LLM-based intelligence for tool selection")?;
}
"default" => {
config.set_param(
"GOOSE_ROUTER_TOOL_SELECTION_STRATEGY",
Value::String("default".to_string()),
)?;
cliclack::outro("Set to Default Strategy - using default tool selection")?;
"false" => {
config.set_param("GOOSE_ENABLE_ROUTER", Value::String("false".to_string()))?;
cliclack::outro("Router disabled - using default tool selection")?;
}
_ => unreachable!(),
};
-2
View File
@@ -97,8 +97,6 @@ ahash = "0.8"
tokio-util = "0.7.15"
unicode-normalization = "0.1"
# Vector database for tool selection
lancedb = "0.13"
arrow = "52.2"
oauth2 = "5.0.0"
+37 -45
View File
@@ -21,8 +21,7 @@ use crate::agents::recipe_tools::dynamic_task_tools::{
create_dynamic_task, create_dynamic_task_tool, DYNAMIC_TASK_TOOL_NAME_PREFIX,
};
use crate::agents::retry::{RetryManager, RetryResult};
use crate::agents::router_tool_selector::RouterToolSelectionStrategy;
use crate::agents::router_tools::{ROUTER_LLM_SEARCH_TOOL_NAME, ROUTER_VECTOR_SEARCH_TOOL_NAME};
use crate::agents::router_tools::ROUTER_LLM_SEARCH_TOOL_NAME;
use crate::agents::sub_recipe_manager::SubRecipeManager;
use crate::agents::subagent_execution_tool::subagent_execute_task_tool::{
self, SUBAGENT_EXECUTE_TASK_TOOL_NAME,
@@ -530,9 +529,7 @@ impl Agent {
"Updated ({} chars)",
char_count
))]))
} else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME
|| tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME
{
} else if tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME {
match self
.tool_route_manager
.dispatch_route_search_tool(tool_call.arguments)
@@ -575,8 +572,8 @@ impl Agent {
extension_name: String,
request_id: String,
) -> (String, Result<Vec<Content>, ErrorData>) {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if ToolRouterIndexManager::is_tool_router_enabled(&selector) {
if self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let selector_action = if action == "disable" { "remove" } else { "add" };
let extension_manager = self.extension_manager.read().await;
@@ -593,7 +590,7 @@ impl Agent {
request_id,
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update vector index: {}", e),
format!("Failed to update LLM index: {}", e),
None,
)),
);
@@ -653,31 +650,29 @@ impl Agent {
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
drop(extension_manager);
// Update vector index if operation was successful and vector routing is enabled
if result.is_ok() {
// Update LLM index if operation was successful and LLM routing is functional
if result.is_ok() && self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if ToolRouterIndexManager::is_tool_router_enabled(&selector) {
if let Some(selector) = selector {
let vector_action = if action == "disable" { "remove" } else { "add" };
let extension_manager = self.extension_manager.read().await;
let selector = Arc::new(selector);
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&extension_manager,
&extension_name,
vector_action,
)
.await
{
return (
request_id,
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update vector index: {}", e),
None,
)),
);
}
if let Some(selector) = selector {
let llm_action = if action == "disable" { "remove" } else { "add" };
let extension_manager = self.extension_manager.read().await;
let selector = Arc::new(selector);
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&extension_manager,
&extension_name,
llm_action,
)
.await
{
return (
request_id,
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update LLM index: {}", e),
None,
)),
);
}
}
}
@@ -718,9 +713,9 @@ impl Agent {
}
}
// If vector tool selection is enabled, index the tools
let selector = self.tool_route_manager.get_router_tool_selector().await;
if ToolRouterIndexManager::is_tool_router_enabled(&selector) {
// If LLM tool selection is functional, index the tools
if self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let extension_manager = self.extension_manager.read().await;
let selector = Arc::new(selector);
@@ -787,12 +782,9 @@ impl Agent {
prefixed_tools
}
pub async fn list_tools_for_router(
&self,
strategy: Option<RouterToolSelectionStrategy>,
) -> Vec<Tool> {
pub async fn list_tools_for_router(&self) -> Vec<Tool> {
self.tool_route_manager
.list_tools_for_router(strategy, &self.extension_manager)
.list_tools_for_router(&self.extension_manager)
.await
}
@@ -801,9 +793,9 @@ impl Agent {
extension_manager.remove_extension(name).await?;
drop(extension_manager);
// If vector tool selection is enabled, remove tools from the index
let selector = self.tool_route_manager.get_router_tool_selector().await;
if ToolRouterIndexManager::is_tool_router_enabled(&selector) {
// If LLM tool selection is functional, remove tools from the index
if self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let extension_manager = self.extension_manager.read().await;
ToolRouterIndexManager::update_extension_tools(
@@ -1350,7 +1342,7 @@ impl Agent {
self.frontend_instructions.lock().await.clone(),
extension_manager.suggest_disable_extensions_prompt().await,
Some(model_name),
None,
false,
);
let recipe_prompt = prompt_manager.get_recipe_prompt().await;
@@ -1509,7 +1501,7 @@ mod tests {
let prompt_manager = agent.prompt_manager.lock().await;
let system_prompt =
prompt_manager.build_system_prompt(vec![], None, Value::Null, None, None);
prompt_manager.build_system_prompt(vec![], None, Value::Null, None, false);
let final_output_tool_ref = agent.final_output_tool.lock().await;
let final_output_tool_system_prompt =
-1
View File
@@ -21,7 +21,6 @@ pub mod todo_tools;
mod tool_execution;
mod tool_route_manager;
mod tool_router_index_manager;
pub(crate) mod tool_vectordb;
pub mod types;
pub use agent::{Agent, AgentEvent};
+12 -22
View File
@@ -3,8 +3,7 @@ use serde_json::Value;
use std::collections::HashMap;
use crate::agents::extension::ExtensionInfo;
use crate::agents::router_tool_selector::RouterToolSelectionStrategy;
use crate::agents::router_tools::{llm_search_tool_prompt, vector_search_tool_prompt};
use crate::agents::router_tools::llm_search_tool_prompt;
use crate::providers::base::get_current_model;
use crate::{config::Config, prompt_template, utils::sanitize_unicode_tags};
@@ -69,7 +68,7 @@ impl PromptManager {
frontend_instructions: Option<String>,
suggest_disable_extensions_prompt: Value,
model_name: Option<&str>,
tool_selection_strategy: Option<RouterToolSelectionStrategy>,
router_enabled: bool,
) -> String {
let mut context: HashMap<&str, Value> = HashMap::new();
let mut extensions_info = extensions_info.clone();
@@ -96,20 +95,11 @@ impl PromptManager {
serde_json::to_value(sanitized_extensions_info).unwrap(),
);
match tool_selection_strategy {
Some(RouterToolSelectionStrategy::Vector) => {
context.insert(
"tool_selection_strategy",
Value::String(vector_search_tool_prompt()),
);
}
Some(RouterToolSelectionStrategy::Llm) => {
context.insert(
"tool_selection_strategy",
Value::String(llm_search_tool_prompt()),
);
}
None => {}
if router_enabled {
context.insert(
"tool_selection_strategy",
Value::String(llm_search_tool_prompt()),
);
}
context.insert(
@@ -246,7 +236,7 @@ mod tests {
manager.set_system_prompt_override(malicious_override.to_string());
let result =
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, None);
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, false);
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@@ -262,7 +252,7 @@ mod tests {
manager.add_system_prompt_extra(malicious_extra.to_string());
let result =
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, None);
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, false);
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@@ -279,7 +269,7 @@ mod tests {
manager.add_system_prompt_extra("Third\u{E0043}instruction".to_string());
let result =
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, None);
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, false);
assert!(!result.contains('\u{E0041}'));
assert!(!result.contains('\u{E0042}'));
@@ -296,7 +286,7 @@ mod tests {
manager.add_system_prompt_extra(legitimate_unicode.to_string());
let result =
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, None);
manager.build_system_prompt(vec![], None, Value::String("".to_string()), None, false);
assert!(result.contains("世界"));
assert!(result.contains("🌍"));
@@ -318,7 +308,7 @@ mod tests {
None,
Value::String("".to_string()),
None,
None,
false,
);
assert!(!result.contains('\u{E0041}'));
+10 -18
View File
@@ -6,7 +6,6 @@ use async_stream::try_stream;
use futures::stream::StreamExt;
use super::super::agents::Agent;
use crate::agents::router_tool_selector::RouterToolSelectionStrategy;
use crate::conversation::message::{Message, MessageContent, ToolRequest};
use crate::conversation::Conversation;
use crate::providers::base::{stream_from_single_message, MessageStream, Provider, ProviderUsage};
@@ -35,24 +34,17 @@ async fn toolshim_postprocess(
impl Agent {
/// Prepares tools and system prompt for a provider request
pub async fn prepare_tools_and_prompt(&self) -> anyhow::Result<(Vec<Tool>, Vec<Tool>, String)> {
// Get tool selection strategy from config
let tool_selection_strategy = self
.tool_route_manager
.get_router_tool_selection_strategy()
.await;
// Get router enabled status
let router_enabled = self.tool_route_manager.is_router_enabled().await;
// Get tools from extension manager
let mut tools = match tool_selection_strategy {
Some(RouterToolSelectionStrategy::Vector) => {
self.list_tools_for_router(Some(RouterToolSelectionStrategy::Vector))
.await
}
Some(RouterToolSelectionStrategy::Llm) => {
self.list_tools_for_router(Some(RouterToolSelectionStrategy::Llm))
.await
}
_ => self.list_tools(None).await,
};
let mut tools = self.list_tools_for_router().await;
// If router is disabled and no tools were returned, fall back to regular tools
if !router_enabled && tools.is_empty() {
tools = self.list_tools(None).await;
}
// Add frontend tools
let frontend_tools = self.frontend_tools.lock().await;
for frontend_tool in frontend_tools.values() {
@@ -74,7 +66,7 @@ impl Agent {
self.frontend_instructions.lock().await.clone(),
extension_manager.suggest_disable_extensions_prompt().await,
Some(model_name),
tool_selection_strategy,
router_enabled,
);
// Handle toolshim if enabled
+5 -266
View File
@@ -1,22 +1,19 @@
use rmcp::model::Tool;
use rmcp::model::{Content, ErrorCode, ErrorData};
use anyhow::{Context, Result};
use anyhow::Result;
use async_trait::async_trait;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::env;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::agents::tool_vectordb::ToolVectorDB;
use crate::conversation::message::Message;
use crate::model::ModelConfig;
use crate::prompt_template::render_global_file;
use crate::providers::{self, base::Provider};
use crate::providers::base::Provider;
#[derive(Serialize)]
struct ToolSelectorContext {
@@ -24,12 +21,6 @@ struct ToolSelectorContext {
query: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RouterToolSelectionStrategy {
Vector,
Llm,
}
#[async_trait]
pub trait RouterToolSelector: Send + Sync {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData>;
@@ -37,240 +28,6 @@ pub trait RouterToolSelector: Send + Sync {
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData>;
fn selector_type(&self) -> RouterToolSelectionStrategy;
}
pub struct VectorToolSelector {
vector_db: Arc<RwLock<ToolVectorDB>>,
embedding_provider: Arc<dyn Provider>,
recent_tool_calls: Arc<RwLock<VecDeque<String>>>,
}
impl VectorToolSelector {
pub async fn new(provider: Arc<dyn Provider>, table_name: String) -> Result<Self> {
let vector_db = ToolVectorDB::new(Some(table_name)).await?;
let embedding_provider = if env::var("GOOSE_EMBEDDING_MODEL_PROVIDER").is_ok() {
// If env var is set, create a new provider for embeddings
// Get embedding model and provider from environment variables
let embedding_model = env::var("GOOSE_EMBEDDING_MODEL")
.unwrap_or_else(|_| "text-embedding-3-small".to_string());
let embedding_provider_name =
env::var("GOOSE_EMBEDDING_MODEL_PROVIDER").unwrap_or_else(|_| "openai".to_string());
// Create the provider using the factory
let model_config = ModelConfig::new(embedding_model.as_str())
.context("Failed to create model config for embedding provider")?;
providers::create(&embedding_provider_name, model_config).context(format!(
"Failed to create {} provider for embeddings. If using OpenAI, make sure OPENAI_API_KEY env var is set or that you have configured the OpenAI provider via Goose before.",
embedding_provider_name
))?
} else {
// Otherwise fall back to using the same provider instance as used for base goose model
provider.clone()
};
Ok(Self {
vector_db: Arc::new(RwLock::new(vector_db)),
embedding_provider,
recent_tool_calls: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
})
}
}
#[async_trait]
impl RouterToolSelector for VectorToolSelector {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let query = params
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'query' parameter"),
data: None,
})?;
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
if !self.embedding_provider.supports_embeddings() {
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Embedding provider does not support embeddings"),
data: None,
});
}
let embeddings = self
.embedding_provider
.create_embeddings(vec![query.to_string()])
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to generate query embedding: {}", e)),
data: None,
})?;
let query_embedding = embeddings.into_iter().next().ok_or_else(|| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("No embedding returned"),
data: None,
})?;
let vector_db = self.vector_db.read().await;
let tools = vector_db
.search_tools(query_embedding, k, extension_name)
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search tools: {}", e)),
data: None,
})?;
let selected_tools: Vec<Content> = tools
.into_iter()
.map(|tool| {
let text = format!(
"Tool: {}\nDescription: {}\nSchema: {}",
tool.tool_name, tool.description, tool.schema
);
Content::text(text)
})
.collect();
Ok(selected_tools)
}
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData> {
let texts_to_embed: Vec<String> = tools
.iter()
.map(|tool| {
let schema_str = serde_json::to_string_pretty(&tool.input_schema)
.unwrap_or_else(|_| "{}".to_string());
format!(
"{} {} {}",
tool.name,
tool.description
.as_ref()
.map(|d| d.as_ref())
.unwrap_or_default(),
schema_str
)
})
.collect();
if !self.embedding_provider.supports_embeddings() {
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Embedding provider does not support embeddings"),
data: None,
});
}
let embeddings = self
.embedding_provider
.create_embeddings(texts_to_embed)
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to generate tool embeddings: {}", e)),
data: None,
})?;
// Create tool records
let tool_records: Vec<crate::agents::tool_vectordb::ToolRecord> = tools
.iter()
.zip(embeddings.into_iter())
.map(|(tool, vector)| {
let schema_str = serde_json::to_string_pretty(&tool.input_schema)
.unwrap_or_else(|_| "{}".to_string());
crate::agents::tool_vectordb::ToolRecord {
tool_name: tool.name.to_string(),
description: tool
.description
.as_ref()
.map(|d| d.to_string())
.unwrap_or_default(),
schema: schema_str,
vector,
extension_name: extension_name.to_string(),
}
})
.collect();
// Get vector_db lock
let vector_db = self.vector_db.read().await;
// Filter out tools that already exist in the database
let mut new_tool_records = Vec::new();
for record in tool_records {
// Check if tool exists by searching for it
let existing_tools = vector_db
.search_tools(record.vector.clone(), 1, Some(&record.extension_name))
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search for existing tools: {}", e)),
data: None,
})?;
// Only add if no exact match found
if !existing_tools
.iter()
.any(|t| t.tool_name == record.tool_name)
{
new_tool_records.push(record);
}
}
// Only index if there are new tools to add
if !new_tool_records.is_empty() {
vector_db
.index_tools(new_tool_records)
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to index tools: {}", e)),
data: None,
})?;
}
Ok(())
}
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData> {
let vector_db = self.vector_db.read().await;
vector_db
.remove_tool(tool_name)
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to remove tool {}: {}", tool_name, e)),
data: None,
})?;
Ok(())
}
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut recent_calls = self.recent_tool_calls.write().await;
if recent_calls.len() >= 100 {
recent_calls.pop_front();
}
recent_calls.push_back(tool_name.to_string());
Ok(())
}
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData> {
let recent_calls = self.recent_tool_calls.read().await;
Ok(recent_calls.iter().rev().take(limit).cloned().collect())
}
fn selector_type(&self) -> RouterToolSelectionStrategy {
RouterToolSelectionStrategy::Vector
}
}
pub struct LLMToolSelector {
@@ -338,7 +95,7 @@ impl RouterToolSelector for LLMToolSelector {
let user_message = Message::user().with_text(&user_prompt);
let response = self
.llm_provider
.complete("", &[user_message], &[])
.complete("system", &[user_message], &[])
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
@@ -413,30 +170,12 @@ impl RouterToolSelector for LLMToolSelector {
let recent_calls = self.recent_tool_calls.read().await;
Ok(recent_calls.iter().rev().take(limit).cloned().collect())
}
fn selector_type(&self) -> RouterToolSelectionStrategy {
RouterToolSelectionStrategy::Llm
}
}
// Helper function to create a boxed tool selector
pub async fn create_tool_selector(
strategy: Option<RouterToolSelectionStrategy>,
provider: Arc<dyn Provider>,
table_name: Option<String>,
) -> Result<Box<dyn RouterToolSelector>> {
match strategy {
Some(RouterToolSelectionStrategy::Vector) => {
let selector = VectorToolSelector::new(provider, table_name.unwrap()).await?;
Ok(Box::new(selector))
}
Some(RouterToolSelectionStrategy::Llm) => {
let selector = LLMToolSelector::new(provider).await?;
Ok(Box::new(selector))
}
None => {
let selector = LLMToolSelector::new(provider).await?;
Ok(Box::new(selector))
}
}
let selector = LLMToolSelector::new(provider).await?;
Ok(Box::new(selector))
}
-57
View File
@@ -6,65 +6,8 @@ use indoc::indoc;
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;
pub const ROUTER_VECTOR_SEARCH_TOOL_NAME: &str = "router__vector_search";
pub const ROUTER_LLM_SEARCH_TOOL_NAME: &str = "router__llm_search";
pub fn vector_search_tool() -> Tool {
Tool::new(
ROUTER_VECTOR_SEARCH_TOOL_NAME.to_string(),
indoc! {r#"
Searches for 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.
This tool should be invoked when the user's messages suggest they are asking for a tool to be run.
You have the list of extension names available to you in your system prompt.
Use the extension_name parameter to filter tools by the appropriate extension.
For example, if the user is asking to list the files in the current directory, you filter for the "developer" extension.
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(),
object!({
"type": "object",
"required": ["query", "extension_name"],
"properties": {
"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},
"extension_name": {"type": "string", "description": "Name of the extension to filter tools by"}
}
})
).annotate(ToolAnnotations {
title: Some("Vector search for relevant tools".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
idempotent_hint: Some(false),
open_world_hint: Some(false),
})
}
pub fn vector_search_tool_prompt() -> String {
format!(
r#"# Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the vector search tool to actually retrieve the most relevant tools to use according to the user's messages.
For example, if the user has 3 extensions enabled, but they are asking for a tool to read a pdf file, \
you would invoke the vector_search tool to find the most relevant read pdf tool.
By dynamically enabling tools, you (Goose) as the agent save context window space and allow the user to dynamically retrieve the most relevant tools.
Be sure to format the query to search rather than pass in the user's messages directly.
In addition to the extension names available to you, you also have platform extension tools available to you.
The platform extension contains the following tools:
- {}
- {}
- {}
- {}
"#,
PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME,
PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME,
PLATFORM_READ_RESOURCE_TOOL_NAME,
PLATFORM_LIST_RESOURCES_TOOL_NAME
)
}
pub fn llm_search_tool() -> Tool {
Tool::new(
ROUTER_LLM_SEARCH_TOOL_NAME.to_string(),
+40 -45
View File
@@ -1,11 +1,8 @@
use crate::agents::extension_manager::ExtensionManager;
use crate::agents::router_tool_selector::{
create_tool_selector, RouterToolSelectionStrategy, RouterToolSelector,
};
use crate::agents::router_tool_selector::{create_tool_selector, RouterToolSelector};
use crate::agents::router_tools::{self};
use crate::agents::tool_execution::ToolCallResult;
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::agents::tool_vectordb::generate_table_id;
use crate::config::Config;
use crate::conversation::message::ToolRequest;
use crate::providers::base::Provider;
@@ -70,21 +67,18 @@ impl ToolRouteManager {
}
}
pub async fn get_router_tool_selection_strategy(&self) -> Option<RouterToolSelectionStrategy> {
pub async fn is_router_enabled(&self) -> bool {
if *self.router_disabled_override.lock().await {
return None;
return false;
}
let config = Config::global();
let router_tool_selection_strategy = config
.get_param("GOOSE_ROUTER_TOOL_SELECTION_STRATEGY")
.unwrap_or_else(|_| "default".to_string());
match router_tool_selection_strategy.to_lowercase().as_str() {
"vector" => Some(RouterToolSelectionStrategy::Vector),
"llm" => Some(RouterToolSelectionStrategy::Llm),
_ => None,
if let Ok(config_value) = config.get_param::<String>("GOOSE_ENABLE_ROUTER") {
return config_value.to_lowercase() == "true";
}
// Default to false if neither is set
false
}
pub async fn update_router_tool_selector(
@@ -93,33 +87,27 @@ impl ToolRouteManager {
reindex_all: Option<bool>,
extension_manager: &Arc<RwLock<ExtensionManager>>,
) -> Result<()> {
let strategy = self.get_router_tool_selection_strategy().await;
let selector = match strategy {
Some(RouterToolSelectionStrategy::Vector) => {
let table_name = generate_table_id();
let selector = create_tool_selector(strategy, provider.clone(), Some(table_name))
.await
.map_err(|e| anyhow!("Failed to create tool selector: {}", e))?;
Arc::new(selector)
}
Some(RouterToolSelectionStrategy::Llm) => {
let selector = create_tool_selector(strategy, provider.clone(), None)
.await
.map_err(|e| anyhow!("Failed to create tool selector: {}", e))?;
Arc::new(selector)
}
None => return Ok(()),
};
let enabled = self.is_router_enabled().await;
if !enabled {
return Ok(());
}
let selector = create_tool_selector(provider.clone())
.await
.map_err(|e| anyhow!("Failed to create tool selector: {}", e))?;
// Wrap selector in Arc for the index manager methods
let selector_arc = Arc::new(selector);
// First index platform tools
let extension_manager = extension_manager.read().await;
ToolRouterIndexManager::index_platform_tools(&selector, &extension_manager).await?;
ToolRouterIndexManager::index_platform_tools(&selector_arc, &extension_manager).await?;
if reindex_all.unwrap_or(false) {
let enabled_extensions = extension_manager.list_extensions().await?;
for extension_name in enabled_extensions {
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&selector_arc,
&extension_manager,
&extension_name,
"add",
@@ -135,7 +123,7 @@ impl ToolRouteManager {
}
// Update the selector
*self.router_tool_selector.lock().await = Some(selector.clone());
*self.router_tool_selector.lock().await = Some(selector_arc);
Ok(())
}
@@ -144,27 +132,34 @@ impl ToolRouteManager {
self.router_tool_selector.lock().await.clone()
}
/// Check if the router is actually functional (enabled in config AND initialized)
pub async fn is_router_functional(&self) -> bool {
if !self.is_router_enabled().await {
return false;
}
// Check if the selector actually exists (meaning it was successfully initialized)
self.router_tool_selector.lock().await.is_some()
}
pub async fn list_tools_for_router(
&self,
strategy: Option<RouterToolSelectionStrategy>,
extension_manager: &Arc<RwLock<ExtensionManager>>,
) -> Vec<Tool> {
// If router is disabled or overridden, return empty
if *self.router_disabled_override.lock().await {
return vec![];
}
let mut prefixed_tools = vec![];
match strategy {
Some(RouterToolSelectionStrategy::Vector) => {
prefixed_tools.push(router_tools::vector_search_tool());
}
Some(RouterToolSelectionStrategy::Llm) => {
prefixed_tools.push(router_tools::llm_search_tool());
}
None => {}
}
// Get recent tool calls from router tool selector if available
// If router is enabled but not functional (no provider), just return the search tool
if !self.is_router_functional().await {
return prefixed_tools;
}
prefixed_tools.push(router_tools::llm_search_tool());
// Get recent tool calls from router tool selector
let selector = self.router_tool_selector.lock().await.clone();
if let Some(selector) = selector {
if let Ok(recent_calls) = selector.get_recent_tool_calls(20).await {
@@ -4,13 +4,13 @@ use tracing;
use crate::agents::extension_manager::ExtensionManager;
use crate::agents::platform_tools;
use crate::agents::router_tool_selector::{RouterToolSelectionStrategy, RouterToolSelector};
use crate::agents::router_tool_selector::RouterToolSelector;
/// Manages tool indexing operations for the router when vector routing is enabled
/// Manages tool indexing operations for the router when LLM routing is enabled
pub struct ToolRouterIndexManager;
impl ToolRouterIndexManager {
/// Updates the vector index for tools when extensions are added or removed
/// Updates the LLM index for tools when extensions are added or removed
pub async fn update_extension_tools(
selector: &Arc<Box<dyn RouterToolSelector>>,
extension_manager: &ExtensionManager,
@@ -98,14 +98,7 @@ impl ToolRouterIndexManager {
.await
.map_err(|e| anyhow!("Failed to index platform tools: {}", e))?;
tracing::info!("Indexed platform tools for vector search");
tracing::info!("Indexed platform tools for LLM search");
Ok(())
}
/// Helper to check if vector or llm tool router is enabled
pub fn is_tool_router_enabled(selector: &Option<Arc<Box<dyn RouterToolSelector>>>) -> bool {
selector.is_some()
&& (selector.as_ref().unwrap().selector_type() == RouterToolSelectionStrategy::Vector
|| selector.as_ref().unwrap().selector_type() == RouterToolSelectionStrategy::Llm)
}
}
-605
View File
@@ -1,605 +0,0 @@
use anyhow::{Context, Result};
use arrow::array::{FixedSizeListBuilder, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use chrono::Local;
use etcetera::base_strategy::{BaseStrategy, Xdg};
use futures::TryStreamExt;
use lancedb::connect;
use lancedb::connection::Connection;
use lancedb::query::{ExecutableQuery, QueryBase};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::config::Config;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRecord {
pub tool_name: String,
pub description: String,
pub schema: String,
pub vector: Vec<f32>,
pub extension_name: String,
}
pub struct ToolVectorDB {
connection: Arc<RwLock<Connection>>,
table_name: String,
}
impl ToolVectorDB {
pub async fn new(table_name: Option<String>) -> Result<Self> {
let db_path = Self::get_db_path()?;
// Ensure the directory exists
if let Some(parent) = db_path.parent() {
tokio::fs::create_dir_all(parent)
.await
.context("Failed to create database directory")?;
}
let connection = connect(db_path.to_str().unwrap())
.execute()
.await
.context("Failed to connect to LanceDB")?;
let tool_db = Self {
connection: Arc::new(RwLock::new(connection)),
table_name: table_name.unwrap_or_else(|| "tools".to_string()),
};
// Initialize the table if it doesn't exist
tool_db.init_table().await?;
Ok(tool_db)
}
pub fn get_db_path() -> Result<PathBuf> {
let config = Config::global();
// Check for custom database path override
if let Ok(custom_path) = config.get_param::<String>("GOOSE_VECTOR_DB_PATH") {
let path = PathBuf::from(custom_path);
// Validate the path is absolute
if !path.is_absolute() {
return Err(anyhow::anyhow!(
"GOOSE_VECTOR_DB_PATH must be an absolute path, got: {}",
path.display()
));
}
return Ok(path);
}
// Fall back to default XDG-based path
let data_dir = Xdg::new()
.context("Failed to determine base strategy")?
.data_dir();
Ok(data_dir.join("goose").join("tool_db"))
}
async fn init_table(&self) -> Result<()> {
let connection = self.connection.read().await;
// Check if table exists
let table_names = connection
.table_names()
.execute()
.await
.context("Failed to list tables")?;
if !table_names.contains(&self.table_name) {
// Create the table schema
let schema = Arc::new(Schema::new(vec![
Field::new("tool_name", DataType::Utf8, false),
Field::new("description", DataType::Utf8, false),
Field::new("schema", DataType::Utf8, false),
Field::new(
"vector",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, true)),
1536, // OpenAI embedding dimension
),
false,
),
Field::new("extension_name", DataType::Utf8, false),
]));
// Create empty table
let tool_names = StringArray::from(vec![] as Vec<&str>);
let descriptions = 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
let mut vectors_builder =
FixedSizeListBuilder::new(arrow::array::Float32Builder::new(), 1536);
let vectors = vectors_builder.finish();
let batch = arrow::record_batch::RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(tool_names),
Arc::new(descriptions),
Arc::new(schemas),
Arc::new(vectors),
Arc::new(extension_names),
],
)
.context("Failed to create record batch")?;
// Create an empty table with the schema
// LanceDB will create the table from the RecordBatch
drop(connection);
let connection = self.connection.write().await;
// Use the RecordBatch directly
let reader = arrow::record_batch::RecordBatchIterator::new(
vec![Ok(batch)].into_iter(),
schema.clone(),
);
connection
.create_table(&self.table_name, Box::new(reader))
.execute()
.await
.map_err(|e| {
anyhow::anyhow!("Failed to create tools table '{}': {}", self.table_name, e)
})?;
}
Ok(())
}
#[cfg(test)]
pub async fn clear_tools(&self) -> Result<()> {
let connection = self.connection.write().await;
// Try to open the table first
match connection.open_table(&self.table_name).execute().await {
Ok(table) => {
// Delete all records instead of dropping the table
table
.delete("1=1") // This will match all records
.await
.context("Failed to delete all records")?;
}
Err(_) => {
// If table doesn't exist, that's fine - we'll create it
}
}
drop(connection);
// Ensure table exists with correct schema
self.init_table().await?;
Ok(())
}
pub async fn index_tools(&self, tools: Vec<ToolRecord>) -> Result<()> {
if tools.is_empty() {
return Ok(());
}
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 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
.iter()
.map(|t| Some(t.vector.iter().map(|&v| Some(v)).collect()))
.collect();
let schema = Arc::new(Schema::new(vec![
Field::new("tool_name", DataType::Utf8, false),
Field::new("description", DataType::Utf8, false),
Field::new("schema", DataType::Utf8, false),
Field::new(
"vector",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, true)),
1536,
),
false,
),
Field::new("extension_name", DataType::Utf8, false),
]));
let tool_names_array = StringArray::from(tool_names);
let descriptions_array = StringArray::from(descriptions);
let schemas_array = StringArray::from(schemas);
let extension_names_array = StringArray::from(extension_names);
// Build vectors array
let mut vectors_builder =
FixedSizeListBuilder::new(arrow::array::Float32Builder::new(), 1536);
for vector_opt in vectors_data {
if let Some(vector) = vector_opt {
let values = vectors_builder.values();
for val_opt in vector {
if let Some(val) = val_opt {
values.append_value(val);
} else {
values.append_null();
}
}
vectors_builder.append(true);
} else {
vectors_builder.append(false);
}
}
let vectors_array = vectors_builder.finish();
let batch = arrow::record_batch::RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(tool_names_array),
Arc::new(descriptions_array),
Arc::new(schemas_array),
Arc::new(vectors_array),
Arc::new(extension_names_array),
],
)
.context("Failed to create record batch")?;
let connection = self.connection.read().await;
let table = connection
.open_table(&self.table_name)
.execute()
.await
.context("Failed to open tools table")?;
// Add batch to table using RecordBatchIterator
let reader = arrow::record_batch::RecordBatchIterator::new(
vec![Ok(batch)].into_iter(),
schema.clone(),
);
table
.add(Box::new(reader))
.execute()
.await
.context("Failed to add tools to table")?;
Ok(())
}
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 table = connection
.open_table(&self.table_name)
.execute()
.await
.context("Failed to open tools table")?;
let search = table
.vector_search(query_vector)
.context("Failed to create vector search")?;
let results = search
.limit(k)
.execute()
.await
.context("Failed to execute vector search")?;
let batches: Vec<_> = results.try_collect().await?;
let mut tools = Vec::new();
for batch in batches {
let tool_names = batch
.column_by_name("tool_name")
.context("Missing tool_name column")?
.as_any()
.downcast_ref::<StringArray>()
.context("Invalid tool_name column type")?;
let descriptions = batch
.column_by_name("description")
.context("Missing description column")?
.as_any()
.downcast_ref::<StringArray>()
.context("Invalid description column type")?;
let schemas = batch
.column_by_name("schema")
.context("Missing schema column")?
.as_any()
.downcast_ref::<StringArray>()
.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
let distances = batch
.column_by_name("_distance")
.context("Missing _distance column")?
.as_any()
.downcast_ref::<arrow::array::Float32Array>()
.context("Invalid _distance column type")?;
for i in 0..batch.num_rows() {
let tool_name = tool_names.value(i).to_string();
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 {
tool_name,
description: descriptions.value(i).to_string(),
schema: schemas.value(i).to_string(),
vector: vec![], // We don't need to return the vector
extension_name: ext_name,
});
}
}
Ok(tools)
}
pub async fn remove_tool(&self, tool_name: &str) -> Result<()> {
let connection = self.connection.read().await;
let table = connection
.open_table(&self.table_name)
.execute()
.await
.context("Failed to open tools table")?;
// Delete records matching the tool name
table
.delete(&format!("tool_name = '{}'", tool_name))
.await
.context("Failed to delete tool")?;
Ok(())
}
}
pub fn generate_table_id() -> String {
Local::now().format("%Y%m%d_%H%M%S").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
impl ToolVectorDB {
async fn new_test_db(
base_name: &str,
) -> Result<(Self, impl std::future::Future<Output = ()>)> {
let unique_name = format!("{}_{}", base_name, uuid::Uuid::new_v4().simple());
let db = Self::new(Some(unique_name)).await?;
let table_name = db.table_name.clone();
let connection = db.connection.clone();
let cleanup = async move {
let _ = async move {
let _ = connection.read().await.drop_table(&table_name).await;
};
};
Ok((db, cleanup))
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_tool_vectordb_creation() -> Result<()> {
let (db, cleanup) = ToolVectorDB::new_test_db("test_tools_vectordb_creation").await?;
let result = async {
db.clear_tools().await?;
assert!(db.table_name.contains("test_tools_vectordb_creation"));
Ok(())
}
.await;
cleanup.await;
result
}
#[tokio::test]
#[serial_test::serial]
async fn test_tool_vectordb_operations() -> Result<()> {
let (db, cleanup) = ToolVectorDB::new_test_db("test_tool_vectordb_operations").await?;
let result = async {
db.clear_tools().await?;
let test_tools = vec![
ToolRecord {
tool_name: "test_tool_1".to_string(),
description: "A test tool for reading files".to_string(),
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#
.to_string(),
vector: vec![0.1; 1536],
extension_name: "test_extension".to_string(),
},
ToolRecord {
tool_name: "test_tool_2".to_string(),
description: "A test tool for writing files".to_string(),
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#
.to_string(),
vector: vec![0.2; 1536],
extension_name: "test_extension".to_string(),
},
];
db.index_tools(test_tools).await?;
let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector.clone(), 2, None).await?;
assert_eq!(results.len(), 2, "Should find both tools");
assert_eq!(
results[0].tool_name, "test_tool_1",
"First result should be test_tool_1"
);
assert_eq!(
results[1].tool_name, "test_tool_2",
"Second result should be test_tool_2"
);
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(())
}
.await;
cleanup.await;
result
}
#[tokio::test]
#[serial_test::serial]
async fn test_empty_db() -> Result<()> {
let (db, cleanup) = ToolVectorDB::new_test_db("test_empty_db").await?;
let result = async {
db.clear_tools().await?;
let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector, 2, None).await?;
assert_eq!(results.len(), 0, "Empty database should return no results");
Ok(())
}
.await;
cleanup.await;
result
}
#[tokio::test]
#[serial_test::serial]
async fn test_tool_deletion() -> Result<()> {
let (db, cleanup) = ToolVectorDB::new_test_db("test_tool_deletion").await?;
let result = async {
db.clear_tools().await?;
let test_tool = ToolRecord {
tool_name: "test_tool_to_delete".to_string(),
description: "A test tool that will be deleted".to_string(),
schema: r#"{"type": "object", "properties": {"path": {"type": "string"}}}"#
.to_string(),
vector: vec![0.1; 1536],
extension_name: "test_extension".to_string(),
};
db.index_tools(vec![test_tool]).await?;
let query_vector = vec![0.1; 1536];
let results = db.search_tools(query_vector.clone(), 1, None).await?;
assert_eq!(results.len(), 1, "Tool should exist before deletion");
db.remove_tool("test_tool_to_delete").await?;
let results = db.search_tools(query_vector.clone(), 1, None).await?;
assert_eq!(results.len(), 0, "Tool should be deleted");
Ok(())
}
.await;
cleanup.await;
result
}
#[test]
#[serial_test::serial]
fn test_custom_db_path_override() -> Result<()> {
use std::env;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let custom_path = temp_dir.path().join("custom_vector_db");
env::set_var("GOOSE_VECTOR_DB_PATH", custom_path.to_str().unwrap());
let db_path = ToolVectorDB::get_db_path()?;
assert_eq!(db_path, custom_path);
env::remove_var("GOOSE_VECTOR_DB_PATH");
Ok(())
}
#[test]
#[serial_test::serial]
fn test_custom_db_path_validation() {
use std::env;
env::set_var("GOOSE_VECTOR_DB_PATH", "relative/path");
let result = ToolVectorDB::get_db_path();
assert!(
result.is_err(),
"Expected error for relative path, got: {:?}",
result
);
assert!(result
.unwrap_err()
.to_string()
.contains("must be an absolute path"));
env::remove_var("GOOSE_VECTOR_DB_PATH");
}
#[test]
#[serial_test::serial]
fn test_fallback_to_default_path() -> Result<()> {
use std::env;
env::remove_var("GOOSE_VECTOR_DB_PATH");
let db_path = ToolVectorDB::get_db_path()?;
assert!(
db_path.to_string_lossy().contains("goose"),
"Path should contain 'goose', got: {}",
db_path.display()
);
assert!(
db_path.to_string_lossy().contains("tool_db"),
"Path should contain 'tool_db', got: {}",
db_path.display()
);
Ok(())
}
}
@@ -232,36 +232,6 @@ export GOOSE_EDITOR_HOST="http://localhost:8000/v1"
export GOOSE_EDITOR_MODEL="your-model"
```
## Tool Selection Strategy
These variables configure the [tool selection strategy](/docs/guides/managing-tools/tool-router).
| Variable | Purpose | Values | Default |
|----------|---------|---------|--------|
| `GOOSE_ROUTER_TOOL_SELECTION_STRATEGY` | The tool selection strategy to use | "default", "vector", "llm" | "default" |
| `GOOSE_EMBEDDING_MODEL_PROVIDER` | The provider to use for generating embeddings for the "vector" strategy | [See available providers](/docs/getting-started/providers#available-providers) (must support embeddings) | "openai" |
| `GOOSE_EMBEDDING_MODEL` | The model to use for generating embeddings for the "vector" strategy | Model name (provider-specific) | "text-embedding-3-small" |
**Examples**
```bash
# Use vector-based tool selection with custom settings
export GOOSE_ROUTER_TOOL_SELECTION_STRATEGY=vector
export GOOSE_EMBEDDING_MODEL_PROVIDER=ollama
export GOOSE_EMBEDDING_MODEL=nomic-embed-text
# Or use LLM-based selection
export GOOSE_ROUTER_TOOL_SELECTION_STRATEGY=llm
```
**Embedding Provider Support**
The default embedding provider is OpenAI. If using a different provider:
- Ensure the provider supports embeddings
- Specify an appropriate embedding model for that provider
- Ensure the provider is properly configured with necessary credentials
## Security Configuration
These variables control security related features.
@@ -54,8 +54,8 @@ export default function ChatSettingsSection() {
<CardHeader className="pb-0">
<CardTitle className="">Tool Selection Strategy (preview)</CardTitle>
<CardDescription>
Configure how Goose selects tools for your requests. Recommended when many extensions
are enabled. Available only with Claude models served on Databricks for now.
Experimental: configure how Goose selects tools for your requests, useful when there are
many tools. Only tested with Claude models currently.
</CardDescription>
</CardHeader>
<CardContent className="px-2">
@@ -3,37 +3,32 @@ import { useConfig } from '../../ConfigContext';
import { getApiUrl } from '../../../config';
interface ToolSelectionStrategy {
key: string;
key: boolean;
label: string;
description: string;
}
export const all_tool_selection_strategies: ToolSelectionStrategy[] = [
{
key: 'default',
label: 'Default',
description: 'Loads all tools from enabled extensions',
key: false,
label: 'Disabled',
description: 'Use the default tool selection strategy',
},
{
key: 'vector',
label: 'Vector',
description: 'Filter tools based on vector similarity.',
},
{
key: 'llm',
label: 'LLM-based',
key: true,
label: 'Enabled',
description:
'Uses LLM to intelligently select the most relevant tools based on the user query context.',
'Use LLM-based intelligence to select the most relevant tools based on the user query context.',
},
];
export const ToolSelectionStrategySection = () => {
const [currentStrategy, setCurrentStrategy] = useState('default');
const [routerEnabled, setRouterEnabled] = useState(false);
const [_error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { read, upsert } = useConfig();
const handleStrategyChange = async (newStrategy: string) => {
const handleStrategyChange = async (enableRouter: boolean) => {
if (isLoading) return; // Prevent multiple simultaneous requests
setError(null); // Clear any previous errors
@@ -42,7 +37,7 @@ export const ToolSelectionStrategySection = () => {
try {
// First update the configuration
try {
await upsert('GOOSE_ROUTER_TOOL_SELECTION_STRATEGY', newStrategy, false);
await upsert('GOOSE_ENABLE_ROUTER', enableRouter.toString(), false);
} catch (error) {
console.error('Error updating configuration:', error);
setError(`Failed to update configuration: ${error}`);
@@ -82,7 +77,7 @@ export const ToolSelectionStrategySection = () => {
}
// If both succeeded, update the UI
setCurrentStrategy(newStrategy);
setRouterEnabled(enableRouter);
} catch (error) {
console.error('Error updating tool selection strategy:', error);
setError(`Failed to update tool selection strategy: ${error}`);
@@ -93,13 +88,13 @@ export const ToolSelectionStrategySection = () => {
const fetchCurrentStrategy = useCallback(async () => {
try {
const strategy = (await read('GOOSE_ROUTER_TOOL_SELECTION_STRATEGY', false)) as string;
const strategy = (await read('GOOSE_ENABLE_ROUTER', false)) as string;
if (strategy) {
setCurrentStrategy(strategy);
setRouterEnabled(strategy === 'true');
}
} catch (error) {
console.error('Error fetching current tool selection strategy:', error);
setError(`Failed to fetch current strategy: ${error}`);
console.error('Error fetching current router setting:', error);
setError(`Failed to fetch current router setting: ${error}`);
}
}, [read]);
@@ -110,9 +105,9 @@ export const ToolSelectionStrategySection = () => {
return (
<div className="space-y-1">
{all_tool_selection_strategies.map((strategy) => (
<div className="group hover:cursor-pointer" key={strategy.key}>
<div className="group hover:cursor-pointer" key={strategy.key.toString()}>
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${currentStrategy === strategy.key ? 'bg-background-muted' : 'bg-background-default hover:bg-background-muted'} rounded-lg transition-all`}
className={`flex items-center justify-between text-text-default py-2 px-2 ${routerEnabled === strategy.key ? 'bg-background-muted' : 'bg-background-default hover:bg-background-muted'} rounded-lg transition-all`}
onClick={() => handleStrategyChange(strategy.key)}
>
<div className="flex">
@@ -126,8 +121,8 @@ export const ToolSelectionStrategySection = () => {
<input
type="radio"
name="tool-selection-strategy"
value={strategy.key}
checked={currentStrategy === strategy.key}
value={strategy.key.toString()}
checked={routerEnabled === strategy.key}
onChange={() => handleStrategyChange(strategy.key)}
disabled={isLoading}
className="peer sr-only"