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;
Bennet Bo Fenner and Danilo Leal created
WIP
<img width="644" alt="image"
src="https://github.com/user-attachments/assets/b24e1a57-f82e-457c-b788-1b314ade7c84"
/>
<img width="644" alt="image"
src="https://github.com/user-attachments/assets/b158953c-2015-4cc8-b8ed-35c6fcbe162d"
/>
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 <daniloleal09@gmail.com>
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(-)
@@ -18,6 +18,7 @@ mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
+mod tool_compatibility;
mod tool_use;
mod ui;
@@ -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<Thread>,
+ incompatible_tools_state: Entity<IncompatibleToolsState>,
editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>,
@@ -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::<Vec<_>>()
+ })
+ })
+ .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",
- ))
- }),
- )
- }
- }
- })),
+ }
+ }
+ }),
+ ),
),
)
}
@@ -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<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
+ tool_working_set: Entity<ToolWorkingSet>,
+ _tool_working_set_subscription: Subscription,
+}
+
+impl IncompatibleToolsState {
+ pub fn new(tool_working_set: Entity<ToolWorkingSet>, cx: &mut Context<Self>) -> 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<dyn LanguageModel>,
+ cx: &App,
+ ) -> &[Arc<dyn Tool>] {
+ 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<Arc<dyn Tool>>,
+}
+
+impl Render for IncompatibleToolsTooltip {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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),
+ )
+ })
+ }
+}
@@ -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,