mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
chore: remove vector search tool selection strategy (#3933)
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Generated
+18
-1880
File diff suppressed because it is too large
Load Diff
@@ -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!(),
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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}'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
+19
-24
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user