From f68a475eca9a74311bd36d7e7650d4a428ac550a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 14 Mar 2025 15:41:50 +0100 Subject: [PATCH] Introduce rating for assistant threads (#26780) Release Notes: - N/A --------- Co-authored-by: Richard Feldman Co-authored-by: Agus Zubiaga --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/active_thread.rs | 56 ++--- crates/assistant2/src/history_store.rs | 4 +- crates/assistant2/src/message_editor.rs | 73 +++++- crates/assistant2/src/thread.rs | 232 ++++++++++++++++-- crates/assistant2/src/thread_history.rs | 6 +- crates/assistant2/src/thread_store.rs | 85 ++----- crates/assistant2/src/tool_use.rs | 8 +- crates/collab/src/api/events.rs | 4 + crates/gpui/src/app.rs | 2 +- .../telemetry_events/src/telemetry_events.rs | 21 ++ 12 files changed, 378 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b15dec770..85dce1d932 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,7 @@ dependencies = [ "fs", "futures 0.3.31", "fuzzy", + "git", "gpui", "heed", "html_to_markdown", @@ -496,6 +497,7 @@ dependencies = [ "settings", "smol", "streaming_diff", + "telemetry", "telemetry_events", "terminal", "terminal_view", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 1b4210b74f..df79bf77a4 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -38,6 +38,7 @@ file_icons.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true +git.workspace = true gpui.workspace = true heed.workspace = true html_to_markdown.workspace = true @@ -65,6 +66,7 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true streaming_diff.workspace = true +telemetry.workspace = true telemetry_events.workspace = true terminal.workspace = true terminal_view.workspace = true diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index 66b8f1f129..c5b72e5f44 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -1,6 +1,7 @@ -use std::sync::Arc; -use std::time::Duration; - +use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent}; +use crate::thread_store::ThreadStore; +use crate::tool_use::{ToolUse, ToolUseStatus}; +use crate::ui::ContextPill; use collections::HashMap; use editor::{Editor, MultiBuffer}; use gpui::{ @@ -14,15 +15,13 @@ use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role}; use markdown::{Markdown, MarkdownStyle}; use scripting_tool::{ScriptingTool, ScriptingToolInput}; use settings::Settings as _; +use std::sync::Arc; +use std::time::Duration; use theme::ThemeSettings; +use ui::Color; use ui::{prelude::*, Disclosure, KeyBinding}; use util::ResultExt as _; -use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent}; -use crate::thread_store::ThreadStore; -use crate::tool_use::{ToolUse, ToolUseStatus}; -use crate::ui::ContextPill; - pub struct ActiveThread { language_registry: Arc, thread_store: Entity, @@ -498,7 +497,7 @@ impl ActiveThread { }; let thread = self.thread.read(cx); - + // Get all the data we need from thread before we start using it in closures let context = thread.context_for_message(message_id); let tool_uses = thread.tool_uses_for_message(message_id); let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id); @@ -653,28 +652,27 @@ impl ActiveThread { ) .child(message_content), ), - Role::Assistant => v_flex() - .id(("message-container", ix)) - .child(message_content) - .map(|parent| { - if tool_uses.is_empty() && scripting_tool_uses.is_empty() { - return parent; - } - - parent.child( - v_flex() - .children( - tool_uses - .into_iter() - .map(|tool_use| self.render_tool_use(tool_use, cx)), + Role::Assistant => { + v_flex() + .id(("message-container", ix)) + .child(message_content) + .when( + !tool_uses.is_empty() || !scripting_tool_uses.is_empty(), + |parent| { + parent.child( + v_flex() + .children( + tool_uses + .into_iter() + .map(|tool_use| self.render_tool_use(tool_use, cx)), + ) + .children(scripting_tool_uses.into_iter().map(|tool_use| { + self.render_scripting_tool_use(tool_use, cx) + })), ) - .children( - scripting_tool_uses - .into_iter() - .map(|tool_use| self.render_scripting_tool_use(tool_use, cx)), - ), + }, ) - }), + } Role::System => div().id(("message-container", ix)).py_1().px_2().child( v_flex() .bg(colors.editor_background) diff --git a/crates/assistant2/src/history_store.rs b/crates/assistant2/src/history_store.rs index 44d50e00e6..169f7561a1 100644 --- a/crates/assistant2/src/history_store.rs +++ b/crates/assistant2/src/history_store.rs @@ -2,10 +2,10 @@ use assistant_context_editor::SavedContextMetadata; use chrono::{DateTime, Utc}; use gpui::{prelude::*, Entity}; -use crate::thread_store::{SavedThreadMetadata, ThreadStore}; +use crate::thread_store::{SerializedThreadMetadata, ThreadStore}; pub enum HistoryEntry { - Thread(SavedThreadMetadata), + Thread(SerializedThreadMetadata), Context(SavedContextMetadata), } diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 6708b7f40d..26b509fd3e 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -20,7 +20,8 @@ use ui::{ Tooltip, }; use vim_mode_setting::VimModeSetting; -use workspace::Workspace; +use workspace::notifications::{NotificationId, NotifyTaskExt}; +use workspace::{Toast, Workspace}; use crate::assistant_model_selector::AssistantModelSelector; use crate::context_picker::{ConfirmBehavior, ContextPicker}; @@ -34,6 +35,7 @@ use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker}; pub struct MessageEditor { thread: Entity, editor: Entity, + workspace: WeakEntity, context_store: Entity, context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, @@ -106,6 +108,7 @@ impl MessageEditor { Self { thread, editor: editor.clone(), + workspace, context_store, context_strip, context_picker_menu_handle, @@ -280,6 +283,34 @@ impl MessageEditor { self.context_strip.focus_handle(cx).focus(window); } } + + fn handle_feedback_click( + &mut self, + is_positive: bool, + window: &mut Window, + cx: &mut Context, + ) { + let workspace = self.workspace.clone(); + let report = self + .thread + .update(cx, |thread, cx| thread.report_feedback(is_positive, cx)); + + cx.spawn(|_, mut cx| async move { + report.await?; + workspace.update(&mut cx, |workspace, cx| { + let message = if is_positive { + "Positive feedback recorded. Thank you!" + } else { + "Negative feedback recorded. Thank you for helping us improve!" + }; + + struct ThreadFeedback; + let id = NotificationId::unique::(); + workspace.show_toast(Toast::new(id, message).autohide(), cx) + }) + }) + .detach_and_notify_err(window, cx); + } } impl Focusable for MessageEditor { @@ -497,7 +528,45 @@ impl Render for MessageEditor { .bg(bg_color) .border_t_1() .border_color(cx.theme().colors().border) - .child(self.context_strip.clone()) + .child( + h_flex() + .justify_between() + .child(self.context_strip.clone()) + .when(!self.thread.read(cx).is_empty(), |this| { + this.child( + h_flex() + .gap_2() + .child( + IconButton::new( + "feedback-thumbs-up", + IconName::ThumbsUp, + ) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Helpful")) + .on_click( + cx.listener(|this, _, window, cx| { + this.handle_feedback_click(true, window, cx); + }), + ), + ) + .child( + IconButton::new( + "feedback-thumbs-down", + IconName::ThumbsDown, + ) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Not Helpful")) + .on_click( + cx.listener(|this, _, window, cx| { + this.handle_feedback_click(false, window, cx); + }), + ), + ), + ) + }), + ) .child( v_flex() .gap_5() diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 0898cd9606..871795a6d5 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -5,7 +5,9 @@ use anyhow::{Context as _, Result}; use assistant_tool::ToolWorkingSet; use chrono::{DateTime, Utc}; use collections::{BTreeMap, HashMap, HashSet}; -use futures::StreamExt as _; +use futures::future::Shared; +use futures::{FutureExt, StreamExt as _}; +use git; use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task}; use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, @@ -21,7 +23,9 @@ use util::{post_inc, ResultExt, TryFutureExt as _}; use uuid::Uuid; use crate::context::{attach_context_to_message, ContextId, ContextSnapshot}; -use crate::thread_store::SavedThread; +use crate::thread_store::{ + SerializedMessage, SerializedThread, SerializedToolResult, SerializedToolUse, +}; use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState}; #[derive(Debug, Clone, Copy)] @@ -63,6 +67,27 @@ pub struct Message { pub text: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectSnapshot { + pub worktree_snapshots: Vec, + pub unsaved_buffer_paths: Vec, + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorktreeSnapshot { + pub worktree_path: String, + pub git_state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitState { + pub remote_url: Option, + pub head_sha: Option, + pub current_branch: Option, + pub diff: Option, +} + /// A thread of conversation with the LLM. pub struct Thread { id: ThreadId, @@ -81,6 +106,7 @@ pub struct Thread { tool_use: ToolUseState, scripting_session: Entity, scripting_tool_use: ToolUseState, + initial_project_snapshot: Shared>>>, cumulative_token_usage: TokenUsage, } @@ -91,8 +117,6 @@ impl Thread { prompt_builder: Arc, cx: &mut Context, ) -> Self { - let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx)); - Self { id: ThreadId::new(), updated_at: Utc::now(), @@ -104,43 +128,52 @@ impl Thread { context_by_message: HashMap::default(), completion_count: 0, pending_completions: Vec::new(), - project, + project: project.clone(), prompt_builder, tools, tool_use: ToolUseState::new(), - scripting_session, + scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)), scripting_tool_use: ToolUseState::new(), + initial_project_snapshot: { + let project_snapshot = Self::project_snapshot(project, cx); + cx.foreground_executor() + .spawn(async move { Some(project_snapshot.await) }) + .shared() + }, cumulative_token_usage: TokenUsage::default(), } } - pub fn from_saved( + pub fn deserialize( id: ThreadId, - saved: SavedThread, + serialized: SerializedThread, project: Entity, tools: Arc, prompt_builder: Arc, cx: &mut Context, ) -> Self { let next_message_id = MessageId( - saved + serialized .messages .last() .map(|message| message.id.0 + 1) .unwrap_or(0), ); - let tool_use = - ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME); + let tool_use = ToolUseState::from_serialized_messages(&serialized.messages, |name| { + name != ScriptingTool::NAME + }); let scripting_tool_use = - ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME); + ToolUseState::from_serialized_messages(&serialized.messages, |name| { + name == ScriptingTool::NAME + }); let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx)); Self { id, - updated_at: saved.updated_at, - summary: Some(saved.summary), + updated_at: serialized.updated_at, + summary: Some(serialized.summary), pending_summary: Task::ready(None), - messages: saved + messages: serialized .messages .into_iter() .map(|message| Message { @@ -160,6 +193,7 @@ impl Thread { tool_use, scripting_session, scripting_tool_use, + initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(), // TODO: persist token usage? cumulative_token_usage: TokenUsage::default(), } @@ -349,6 +383,47 @@ impl Thread { text } + /// Serializes this thread into a format for storage or telemetry. + pub fn serialize(&self, cx: &mut Context) -> Task> { + let initial_project_snapshot = self.initial_project_snapshot.clone(); + cx.spawn(|this, cx| async move { + let initial_project_snapshot = initial_project_snapshot.await; + this.read_with(&cx, |this, _| SerializedThread { + summary: this.summary_or_default(), + updated_at: this.updated_at(), + messages: this + .messages() + .map(|message| SerializedMessage { + id: message.id, + role: message.role, + text: message.text.clone(), + tool_uses: this + .tool_uses_for_message(message.id) + .into_iter() + .chain(this.scripting_tool_uses_for_message(message.id)) + .map(|tool_use| SerializedToolUse { + id: tool_use.id, + name: tool_use.name, + input: tool_use.input, + }) + .collect(), + tool_results: this + .tool_results_for_message(message.id) + .into_iter() + .chain(this.scripting_tool_results_for_message(message.id)) + .map(|tool_result| SerializedToolResult { + tool_use_id: tool_result.tool_use_id.clone(), + is_error: tool_result.is_error, + content: tool_result.content.clone(), + }) + .collect(), + }) + .collect(), + initial_project_snapshot, + }) + }) + } + pub fn send_to_model( &mut self, model: Arc, @@ -807,6 +882,133 @@ impl Thread { } } + /// Reports feedback about the thread and stores it in our telemetry backend. + pub fn report_feedback(&self, is_positive: bool, cx: &mut Context) -> Task> { + let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx); + let serialized_thread = self.serialize(cx); + let thread_id = self.id().clone(); + let client = self.project.read(cx).client(); + + cx.background_spawn(async move { + let final_project_snapshot = final_project_snapshot.await; + let serialized_thread = serialized_thread.await?; + let thread_data = + serde_json::to_value(serialized_thread).unwrap_or_else(|_| serde_json::Value::Null); + + let rating = if is_positive { "positive" } else { "negative" }; + telemetry::event!( + "Assistant Thread Rated", + rating, + thread_id, + thread_data, + final_project_snapshot + ); + client.telemetry().flush_events(); + + Ok(()) + }) + } + + /// Create a snapshot of the current project state including git information and unsaved buffers. + fn project_snapshot( + project: Entity, + cx: &mut Context, + ) -> Task> { + let worktree_snapshots: Vec<_> = project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| Self::worktree_snapshot(worktree, cx)) + .collect(); + + cx.spawn(move |_, cx| async move { + let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + + let mut unsaved_buffers = Vec::new(); + cx.update(|app_cx| { + let buffer_store = project.read(app_cx).buffer_store(); + for buffer_handle in buffer_store.read(app_cx).buffers() { + let buffer = buffer_handle.read(app_cx); + if buffer.is_dirty() { + if let Some(file) = buffer.file() { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); + } + } + } + }) + .ok(); + + Arc::new(ProjectSnapshot { + worktree_snapshots, + unsaved_buffer_paths: unsaved_buffers, + timestamp: Utc::now(), + }) + }) + } + + fn worktree_snapshot(worktree: Entity, cx: &App) -> Task { + cx.spawn(move |cx| async move { + // Get worktree path and snapshot + let worktree_info = cx.update(|app_cx| { + let worktree = worktree.read(app_cx); + let path = worktree.abs_path().to_string_lossy().to_string(); + let snapshot = worktree.snapshot(); + (path, snapshot) + }); + + let Ok((worktree_path, snapshot)) = worktree_info else { + return WorktreeSnapshot { + worktree_path: String::new(), + git_state: None, + }; + }; + + // Extract git information + let git_state = match snapshot.repositories().first() { + None => None, + Some(repo_entry) => { + // Get branch information + let current_branch = repo_entry.branch().map(|branch| branch.name.to_string()); + + // Get repository info + let repo_result = worktree.read_with(&cx, |worktree, _cx| { + if let project::Worktree::Local(local_worktree) = &worktree { + local_worktree.get_local_repo(repo_entry).map(|local_repo| { + let repo = local_repo.repo(); + (repo.remote_url("origin"), repo.head_sha(), repo.clone()) + }) + } else { + None + } + }); + + match repo_result { + Ok(Some((remote_url, head_sha, repository))) => { + // Get diff asynchronously + let diff = repository + .diff(git::repository::DiffType::HeadToWorktree, cx) + .await + .ok(); + + Some(GitState { + remote_url, + head_sha, + current_branch, + diff, + }) + } + Err(_) | Ok(None) => None, + } + } + }; + + WorktreeSnapshot { + worktree_path, + git_state, + } + }) + } + pub fn to_markdown(&self) -> Result { let mut markdown = Vec::new(); diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 2fdc8588f9..ebbc4ec96b 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -7,7 +7,7 @@ use time::{OffsetDateTime, UtcOffset}; use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; use crate::history_store::{HistoryEntry, HistoryStore}; -use crate::thread_store::SavedThreadMetadata; +use crate::thread_store::SerializedThreadMetadata; use crate::{AssistantPanel, RemoveSelectedThread}; pub struct ThreadHistory { @@ -221,14 +221,14 @@ impl Render for ThreadHistory { #[derive(IntoElement)] pub struct PastThread { - thread: SavedThreadMetadata, + thread: SerializedThreadMetadata, assistant_panel: WeakEntity, selected: bool, } impl PastThread { pub fn new( - thread: SavedThreadMetadata, + thread: SerializedThreadMetadata, assistant_panel: WeakEntity, selected: bool, ) -> Self { diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 7657ef9624..66657a500d 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -20,7 +20,7 @@ use prompt_store::PromptBuilder; use serde::{Deserialize, Serialize}; use util::ResultExt as _; -use crate::thread::{MessageId, Thread, ThreadId}; +use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId}; pub fn init(cx: &mut App) { ThreadsDatabase::init(cx); @@ -32,7 +32,7 @@ pub struct ThreadStore { prompt_builder: Arc, context_server_manager: Entity, context_server_tool_ids: HashMap, Vec>, - threads: Vec, + threads: Vec, } impl ThreadStore { @@ -70,13 +70,13 @@ impl ThreadStore { self.threads.len() } - pub fn threads(&self) -> Vec { + pub fn threads(&self) -> Vec { let mut threads = self.threads.iter().cloned().collect::>(); threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at)); threads } - pub fn recent_threads(&self, limit: usize) -> Vec { + pub fn recent_threads(&self, limit: usize) -> Vec { self.threads().into_iter().take(limit).collect() } @@ -107,7 +107,7 @@ impl ThreadStore { this.update(&mut cx, |this, cx| { cx.new(|cx| { - Thread::from_saved( + Thread::deserialize( id.clone(), thread, this.project.clone(), @@ -121,53 +121,14 @@ impl ThreadStore { } pub fn save_thread(&self, thread: &Entity, cx: &mut Context) -> Task> { - let (metadata, thread) = thread.update(cx, |thread, _cx| { - let id = thread.id().clone(); - let thread = SavedThread { - summary: thread.summary_or_default(), - updated_at: thread.updated_at(), - messages: thread - .messages() - .map(|message| { - let all_tool_uses = thread - .tool_uses_for_message(message.id) - .into_iter() - .chain(thread.scripting_tool_uses_for_message(message.id)) - .map(|tool_use| SavedToolUse { - id: tool_use.id, - name: tool_use.name, - input: tool_use.input, - }) - .collect(); - let all_tool_results = thread - .tool_results_for_message(message.id) - .into_iter() - .chain(thread.scripting_tool_results_for_message(message.id)) - .map(|tool_result| SavedToolResult { - tool_use_id: tool_result.tool_use_id.clone(), - is_error: tool_result.is_error, - content: tool_result.content.clone(), - }) - .collect(); - - SavedMessage { - id: message.id, - role: message.role, - text: message.text.clone(), - tool_uses: all_tool_uses, - tool_results: all_tool_results, - } - }) - .collect(), - }; - - (id, thread) - }); + let (metadata, serialized_thread) = + thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx))); let database_future = ThreadsDatabase::global_future(cx); cx.spawn(|this, mut cx| async move { + let serialized_thread = serialized_thread.await?; let database = database_future.await.map_err(|err| anyhow!(err))?; - database.save_thread(metadata, thread).await?; + database.save_thread(metadata, serialized_thread).await?; this.update(&mut cx, |this, cx| this.reload(cx))?.await }) @@ -270,39 +231,41 @@ impl ThreadStore { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SavedThreadMetadata { +pub struct SerializedThreadMetadata { pub id: ThreadId, pub summary: SharedString, pub updated_at: DateTime, } #[derive(Serialize, Deserialize)] -pub struct SavedThread { +pub struct SerializedThread { pub summary: SharedString, pub updated_at: DateTime, - pub messages: Vec, + pub messages: Vec, + #[serde(default)] + pub initial_project_snapshot: Option>, } #[derive(Debug, Serialize, Deserialize)] -pub struct SavedMessage { +pub struct SerializedMessage { pub id: MessageId, pub role: Role, pub text: String, #[serde(default)] - pub tool_uses: Vec, + pub tool_uses: Vec, #[serde(default)] - pub tool_results: Vec, + pub tool_results: Vec, } #[derive(Debug, Serialize, Deserialize)] -pub struct SavedToolUse { +pub struct SerializedToolUse { pub id: LanguageModelToolUseId, pub name: SharedString, pub input: serde_json::Value, } #[derive(Debug, Serialize, Deserialize)] -pub struct SavedToolResult { +pub struct SerializedToolResult { pub tool_use_id: LanguageModelToolUseId, pub is_error: bool, pub content: Arc, @@ -317,7 +280,7 @@ impl Global for GlobalThreadsDatabase {} pub(crate) struct ThreadsDatabase { executor: BackgroundExecutor, env: heed::Env, - threads: Database, SerdeJson>, + threads: Database, SerdeJson>, } impl ThreadsDatabase { @@ -364,7 +327,7 @@ impl ThreadsDatabase { }) } - pub fn list_threads(&self) -> Task>> { + pub fn list_threads(&self) -> Task>> { let env = self.env.clone(); let threads = self.threads; @@ -373,7 +336,7 @@ impl ThreadsDatabase { let mut iter = threads.iter(&txn)?; let mut threads = Vec::new(); while let Some((key, value)) = iter.next().transpose()? { - threads.push(SavedThreadMetadata { + threads.push(SerializedThreadMetadata { id: key, summary: value.summary, updated_at: value.updated_at, @@ -384,7 +347,7 @@ impl ThreadsDatabase { }) } - pub fn try_find_thread(&self, id: ThreadId) -> Task>> { + pub fn try_find_thread(&self, id: ThreadId) -> Task>> { let env = self.env.clone(); let threads = self.threads; @@ -395,7 +358,7 @@ impl ThreadsDatabase { }) } - pub fn save_thread(&self, id: ThreadId, thread: SavedThread) -> Task> { + pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task> { let env = self.env.clone(); let threads = self.threads; diff --git a/crates/assistant2/src/tool_use.rs b/crates/assistant2/src/tool_use.rs index d5eb50f99c..01b0379071 100644 --- a/crates/assistant2/src/tool_use.rs +++ b/crates/assistant2/src/tool_use.rs @@ -11,7 +11,7 @@ use language_model::{ }; use crate::thread::MessageId; -use crate::thread_store::SavedMessage; +use crate::thread_store::SerializedMessage; #[derive(Debug)] pub struct ToolUse { @@ -46,11 +46,11 @@ impl ToolUseState { } } - /// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s. + /// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s. /// /// Accepts a function to filter the tools that should be used to populate the state. - pub fn from_saved_messages( - messages: &[SavedMessage], + pub fn from_serialized_messages( + messages: &[SerializedMessage], mut filter_by_tool_name: impl FnMut(&str) -> bool, ) -> Self { let mut this = Self::new(); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index c7792690f8..3d9faab529 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -660,6 +660,10 @@ fn for_snowflake( e.event_type.clone(), serde_json::to_value(&e.event_properties).unwrap(), ), + Event::AssistantThreadFeedback(e) => ( + "Assistant Feedback".to_string(), + serde_json::to_value(&e).unwrap(), + ), }; if let serde_json::Value::Object(ref mut map) = event_properties { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 71df22e95d..83316dcbe1 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1046,7 +1046,7 @@ impl App { &self.foreground_executor } - /// Spawns the future returned by the given function on the thread pool. The closure will be invoked + /// Spawns the future returned by the given function on the main thread. The closure will be invoked /// with [AsyncApp], which allows the application state to be accessed across await points. #[track_caller] pub fn spawn(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index 81106b89da..29ad412874 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -97,6 +97,7 @@ pub enum Event { InlineCompletionRating(InlineCompletionRatingEvent), Call(CallEvent), Assistant(AssistantEvent), + AssistantThreadFeedback(AssistantThreadFeedbackEvent), Cpu(CpuEvent), Memory(MemoryEvent), App(AppEvent), @@ -230,6 +231,26 @@ pub struct ReplEvent { pub repl_session_id: String, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ThreadFeedbackRating { + Positive, + Negative, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssistantThreadFeedbackEvent { + /// Unique identifier for the thread + pub thread_id: String, + /// The feedback rating (thumbs up or thumbs down) + pub rating: ThreadFeedbackRating, + /// The serialized thread data containing messages, tool calls, etc. + pub thread_data: serde_json::Value, + /// The initial project snapshot taken when the thread was created + pub initial_project_snapshot: serde_json::Value, + /// The final project snapshot taken when the thread was first saved + pub final_project_snapshot: serde_json::Value, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BacktraceFrame { pub ip: usize,