From c381a500f8e834a1039a0835e9e6d49da147095a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 15 Apr 2025 18:58:11 +0200 Subject: [PATCH] agent: Show a warning when some tools are incompatible with the selected model (#28755) WIP image image Release Notes: - agent: Improve compatibility with Gemini Tool Calling APIs. When a tool is incompatible with the Gemini APIs a warning indicator will be displayed. Incompatible tools will be automatically excluded from the conversation --------- Co-authored-by: Danilo Leal --- crates/agent/src/assistant.rs | 1 + crates/agent/src/message_editor.rs | 224 ++++++++++++-------- crates/agent/src/tool_compatibility.rs | 89 ++++++++ crates/language_model/src/language_model.rs | 2 +- 4 files changed, 229 insertions(+), 87 deletions(-) create mode 100644 crates/agent/src/tool_compatibility.rs diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs index 78c757f2981740637c6337b6cb94e04930ca221e..1f067af7343aab5fa79b96dd17ec2e74710abe75 100644 --- a/crates/agent/src/assistant.rs +++ b/crates/agent/src/assistant.rs @@ -18,6 +18,7 @@ mod terminal_inline_assistant; mod thread; mod thread_history; mod thread_store; +mod tool_compatibility; mod tool_use; mod ui; diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index d23002f02a18ed3d932d36c52372ffb6413ef464..59c01d16dd373d55caadd2373874959d3fafdca1 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::sync::Arc; use crate::assistant_model_selector::ModelType; +use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use buffer_diff::BufferDiff; use collections::HashSet; use editor::actions::MoveUp; @@ -41,6 +42,7 @@ use crate::{ pub struct MessageEditor { thread: Entity, + incompatible_tools_state: Entity, editor: Entity, #[allow(dead_code)] workspace: WeakEntity, @@ -124,6 +126,9 @@ impl MessageEditor { ) }); + let incompatible_tools = + cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx)); + let subscriptions = vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)]; @@ -131,6 +136,7 @@ impl MessageEditor { editor: editor.clone(), project: thread.read(cx).project().clone(), thread, + incompatible_tools_state: incompatible_tools.clone(), workspace, context_store, context_strip, @@ -368,6 +374,23 @@ impl MessageEditor { let is_model_selected = self.is_model_selected(cx); let is_editor_empty = self.is_editor_empty(cx); + let model = LanguageModelRegistry::read_global(cx) + .default_model() + .map(|default| default.model.clone()); + + let incompatible_tools = model + .as_ref() + .map(|model| { + self.incompatible_tools_state.update(cx, |state, cx| { + state + .incompatible_tools(model, cx) + .iter() + .cloned() + .collect::>() + }) + }) + .unwrap_or_default(); + let is_editor_expanded = self.editor_is_expanded; let expand_icon = if is_editor_expanded { IconName::Minimize @@ -472,54 +495,80 @@ impl MessageEditor { .flex_none() .justify_between() .child(h_flex().gap_2().child(self.profile_selector.clone())) - .child(h_flex().gap_1().child(self.model_selector.clone()).map({ - let focus_handle = focus_handle.clone(); - move |parent| { - if is_generating { - parent - .when(is_editor_empty, |parent| { - parent.child( - IconButton::new( - "stop-generation", - IconName::StopFilled, - ) - .icon_color(Color::Error) - .style(ButtonStyle::Tinted( - ui::TintColor::Error, - )) - .tooltip(move |window, cx| { - Tooltip::for_action( - "Stop Generation", - &editor::actions::Cancel, - window, - cx, - ) + .child( + h_flex() + .gap_1() + .when(!incompatible_tools.is_empty(), |this| { + this.child( + IconButton::new( + "tools-incompatible-warning", + IconName::Warning, + ) + .icon_color(Color::Warning) + .icon_size(IconSize::Small) + .tooltip({ + move |_, cx| { + cx.new(|_| IncompatibleToolsTooltip { + incompatible_tools: incompatible_tools + .clone(), }) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_event, window, cx| { - focus_handle.dispatch_action( - &editor::actions::Cancel, - window, - cx, - ); - } + .into() + } + }), + ) + }) + .child(self.model_selector.clone()) + .map({ + let focus_handle = focus_handle.clone(); + move |parent| { + if is_generating { + parent + .when(is_editor_empty, |parent| { + parent.child( + IconButton::new( + "stop-generation", + IconName::StopFilled, + ) + .icon_color(Color::Error) + .style(ButtonStyle::Tinted( + ui::TintColor::Error, + )) + .tooltip(move |window, cx| { + Tooltip::for_action( + "Stop Generation", + &editor::actions::Cancel, + window, + cx, + ) + }) + .on_click({ + let focus_handle = + focus_handle.clone(); + move |_event, window, cx| { + focus_handle.dispatch_action( + &editor::actions::Cancel, + window, + cx, + ); + } + }) + .with_animation( + "pulsating-label", + Animation::new( + Duration::from_secs(2), + ) + .repeat() + .with_easing(pulsating_between( + 0.4, 1.0, + )), + |icon_button, delta| { + icon_button.alpha(delta) + }, + ), + ) }) - .with_animation( - "pulsating-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between( - 0.4, 1.0, - )), - |icon_button, delta| { - icon_button.alpha(delta) - }, - ), - ) - }) - .when(!is_editor_empty, |parent| { - parent.child( + .when(!is_editor_empty, |parent| { + parent.child( IconButton::new("send-message", IconName::Send) .icon_color(Color::Accent) .style(ButtonStyle::Filled) @@ -545,48 +594,51 @@ impl MessageEditor { ) }), ) - }) - } else { - parent.child( - IconButton::new("send-message", IconName::Send) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled( - is_editor_empty - || !is_model_selected - || self.waiting_for_summaries_to_send, - ) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_event, window, cx| { - focus_handle - .dispatch_action(&Chat, window, cx); - } - }) - .when( - !is_editor_empty && is_model_selected, - |button| { - button.tooltip(move |window, cx| { - Tooltip::for_action( - "Send", &Chat, window, cx, - ) + }) + } else { + parent.child( + IconButton::new("send-message", IconName::Send) + .icon_color(Color::Accent) + .style(ButtonStyle::Filled) + .disabled( + is_editor_empty + || !is_model_selected + || self + .waiting_for_summaries_to_send, + ) + .on_click({ + let focus_handle = focus_handle.clone(); + move |_event, window, cx| { + focus_handle.dispatch_action( + &Chat, window, cx, + ); + } }) - }, + .when( + !is_editor_empty && is_model_selected, + |button| { + button.tooltip(move |window, cx| { + Tooltip::for_action( + "Send", &Chat, window, cx, + ) + }) + }, + ) + .when(is_editor_empty, |button| { + button.tooltip(Tooltip::text( + "Type a message to submit", + )) + }) + .when(!is_model_selected, |button| { + button.tooltip(Tooltip::text( + "Select a model to continue", + )) + }), ) - .when(is_editor_empty, |button| { - button.tooltip(Tooltip::text( - "Type a message to submit", - )) - }) - .when(!is_model_selected, |button| { - button.tooltip(Tooltip::text( - "Select a model to continue", - )) - }), - ) - } - } - })), + } + } + }), + ), ), ) } diff --git a/crates/agent/src/tool_compatibility.rs b/crates/agent/src/tool_compatibility.rs new file mode 100644 index 0000000000000000000000000000000000000000..363bc305df21f94bb88045e8f2f399d8907d915e --- /dev/null +++ b/crates/agent/src/tool_compatibility.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent}; +use collections::HashMap; +use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window}; +use language_model::{LanguageModel, LanguageModelToolSchemaFormat}; +use ui::prelude::*; + +pub struct IncompatibleToolsState { + cache: HashMap>>, + tool_working_set: Entity, + _tool_working_set_subscription: Subscription, +} + +impl IncompatibleToolsState { + pub fn new(tool_working_set: Entity, cx: &mut Context) -> Self { + let _tool_working_set_subscription = + cx.subscribe(&tool_working_set, |this, _, event, _| match event { + ToolWorkingSetEvent::EnabledToolsChanged => { + this.cache.clear(); + } + }); + + Self { + cache: HashMap::default(), + tool_working_set, + _tool_working_set_subscription, + } + } + + pub fn incompatible_tools( + &mut self, + model: &Arc, + cx: &App, + ) -> &[Arc] { + self.cache + .entry(model.tool_input_format()) + .or_insert_with(|| { + self.tool_working_set + .read(cx) + .enabled_tools(cx) + .iter() + .filter(|tool| tool.input_schema(model.tool_input_format()).is_err()) + .cloned() + .collect() + }) + } +} + +pub struct IncompatibleToolsTooltip { + pub incompatible_tools: Vec>, +} + +impl Render for IncompatibleToolsTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + ui::tooltip_container(window, cx, |container, _, cx| { + container + .w_72() + .child(Label::new("Incompatible Tools").size(LabelSize::Small)) + .child( + Label::new( + "This model is incompatible with the following tools from your MCPs:", + ) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + v_flex() + .my_1p5() + .py_0p5() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .children( + self.incompatible_tools + .iter() + .map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)), + ), + ) + .child(Label::new("What To Do Instead").size(LabelSize::Small)) + .child( + Label::new( + "Every other tool continues to work with this model, but to specifically use those, switch to another model.", + ) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }) + } +} diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index a0e38c629e4af0c2486f7ce17d84cb4c29336fbc..cf695023c8b2e32b826f2be917a8d4c2c841e7eb 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -67,7 +67,7 @@ pub enum LanguageModelCompletionEvent { } /// Indicates the format used to define the input schema for a language model tool. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum LanguageModelToolSchemaFormat { /// A JSON schema, see https://json-schema.org JsonSchema,