Accept Views on LanguageModelTools (#10956)

Creates a `ToolView` trait to allow interactivity. This brings expanding
and collapsing to the excerpts from project index searches.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Kyle Kelley 2024-04-25 13:03:43 -07:00 committed by GitHub
parent 7005f0b424
commit f176e8f0e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 268 additions and 269 deletions

View file

@ -1,4 +1,5 @@
use anyhow::Context as _;
/// This example creates a basic Chat UI with a function for rolling a die.
use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
@ -83,9 +84,32 @@ struct DiceRoll {
rolls: Vec<DieRoll>,
}
pub struct DiceView {
result: Result<DiceRoll>,
}
impl Render for DiceView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let output = match &self.result {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
};
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
}
}
impl LanguageModelTool for RollDiceTool {
type Input = DiceParams;
type Output = DiceRoll;
type View = DiceView;
fn name(&self) -> String {
"roll_dice".to_string()
@ -110,23 +134,21 @@ impl LanguageModelTool for RollDiceTool {
return Task::ready(Ok(DiceRoll { rolls }));
}
fn render(
_tool_call_id: &str,
_input: &Self::Input,
output: &Self::Output,
_cx: &mut WindowContext,
) -> gpui::AnyElement {
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
fn new_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| DiceView { result })
}
fn format(_input: &Self::Input, output: &Self::Output) -> String {
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
let output = match output {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".to_string(),
};
let mut result = String::new();
for roll in &output.rolls {
let die = &roll.die;

View file

@ -322,9 +322,11 @@ impl AssistantChat {
};
call_count += 1;
let messages = this.completion_messages(cx);
CompletionProvider::get(cx).complete(
this.model.clone(),
this.completion_messages(cx),
messages,
Vec::new(),
1.0,
definitions,
@ -407,6 +409,10 @@ impl AssistantChat {
}
let tools = join_all(tool_tasks.into_iter()).await;
// If the WindowContext went away for any tool's view we don't include it
// especially since the below call would fail for the same reason.
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
this.messages.last_mut()
@ -561,10 +567,9 @@ impl AssistantChat {
let result = &tool_call.result;
let name = tool_call.name.clone();
match result {
Some(result) => div()
.p_2()
.child(result.render(&name, &tool_call.id, cx))
.into_any(),
Some(result) => {
div().p_2().child(result.into_any_element(&name)).into_any()
}
None => div()
.p_2()
.child(Label::new(name).color(Color::Modified))
@ -577,7 +582,7 @@ impl AssistantChat {
}
}
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
let mut completion_messages = Vec::new();
for message in &self.messages {

View file

@ -1,10 +1,10 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use gpui::{prelude::*, AnyElement, AppContext, Model, Task};
use gpui::{prelude::*, AppContext, Model, Task};
use project::Fs;
use schemars::JsonSchema;
use semantic_index::ProjectIndex;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use std::sync::Arc;
use ui::{
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
@ -14,11 +14,13 @@ use util::ResultExt as _;
const DEFAULT_SEARCH_LIMIT: usize = 20;
#[derive(Serialize, Clone)]
#[derive(Clone)]
pub struct CodebaseExcerpt {
path: SharedString,
text: SharedString,
score: f32,
element_id: ElementId,
expanded: bool,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
@ -32,6 +34,79 @@ pub struct CodebaseQuery {
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<Vec<CodebaseExcerpt>>,
}
impl ProjectIndexView {
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
if let Ok(excerpts) = &mut self.output {
if let Some(excerpt) = excerpts
.iter_mut()
.find(|excerpt| excerpt.element_id == element_id)
{
excerpt.expanded = !excerpt.expanded;
cx.notify();
}
}
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let excerpts = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(excerpts) => excerpts,
};
div()
.v_flex()
.gap_2()
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.child(Label::new("Query: ").color(Color::Modified))
.child(Label::new(query).color(Color::Muted)),
),
)
.children(excerpts.iter().map(|excerpt| {
let element_id = excerpt.element_id.clone();
let expanded = excerpt.expanded;
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_expanded(element_id.clone(), cx);
}))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
excerpt.text.clone(), // todo!(): Show as an editor block
),
)
}))
}
}
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
@ -47,6 +122,7 @@ impl ProjectIndexTool {
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = Vec<CodebaseExcerpt>;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
@ -90,6 +166,8 @@ impl LanguageModelTool for ProjectIndexTool {
}
anyhow::Ok(CodebaseExcerpt {
element_id: ElementId::Name(nanoid::nanoid!().into()),
expanded: false,
path: path.to_string_lossy().to_string().into(),
text: SharedString::from(text[start..end].to_string()),
score: result.score,
@ -106,71 +184,37 @@ impl LanguageModelTool for ProjectIndexTool {
})
}
fn render(
_tool_call_id: &str,
input: &Self::Input,
excerpts: &Self::Output,
fn new_view(
_tool_call_id: String,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> AnyElement {
let query = input.query.clone();
div()
.v_flex()
.gap_2()
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.child(Label::new("Query: ").color(Color::Modified))
.child(Label::new(query).color(Color::Muted)),
),
)
.children(excerpts.iter().map(|excerpt| {
// This render doesn't have state/model, so we can't use the listener
// let expanded = excerpt.expanded;
// let element_id = excerpt.element_id.clone();
let element_id = ElementId::Name(nanoid::nanoid!().into());
let expanded = false;
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
// .on_click(cx.listener(move |this, _, cx| {
// this.toggle_expanded(element_id.clone(), cx);
// }))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
excerpt.text.clone(), // todo!(): Show as an editor block
),
)
}))
.into_any_element()
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView { input, output })
}
fn format(_input: &Self::Input, excerpts: &Self::Output) -> String {
let mut body = "Semantic search results:\n".to_string();
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
match &output {
Ok(excerpts) => {
if excerpts.len() == 0 {
return "No results found".to_string();
}
for excerpt in excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
let mut body = "Semantic search results:\n".to_string();
for excerpt in excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
body
}
}