Add assistant events (#2978)

Joseph T. Lyons created

Add assistant events

Release Notes:

- N/A

Change summary

Cargo.lock                     |  2 +
crates/ai/Cargo.toml           |  2 +
crates/ai/src/ai.rs            |  1 
crates/ai/src/assistant.rs     | 63 ++++++++++++++++++++++++++++++++++-
crates/client/src/telemetry.rs | 12 ++++++
crates/theme/src/components.rs |  1 
6 files changed, 77 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -101,6 +101,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
+ "client",
  "collections",
  "ctor",
  "editor",
@@ -127,6 +128,7 @@ dependencies = [
  "theme",
  "tiktoken-rs 0.4.5",
  "util",
+ "uuid 1.4.1",
  "workspace",
 ]
 

crates/ai/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/ai.rs"
 doctest = false
 
 [dependencies]
+client = { path = "../client" }
 collections = { path = "../collections"}
 editor = { path = "../editor" }
 fs = { path = "../fs" }
@@ -19,6 +20,7 @@ search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }
+uuid = { version = "1.1.2", features = ["v4"] }
 workspace = { path = "../workspace" }
 
 anyhow.workspace = true

crates/ai/src/ai.rs 🔗

@@ -61,6 +61,7 @@ struct SavedMessage {
 
 #[derive(Serialize, Deserialize)]
 struct SavedConversation {
+    id: Option<String>,
     zed: String,
     version: String,
     text: String,

crates/ai/src/assistant.rs 🔗

@@ -6,6 +6,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
+use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
 use collections::{hash_map, HashMap, HashSet, VecDeque};
 use editor::{
     display_map::{
@@ -48,6 +49,7 @@ use theme::{
     AssistantStyle,
 };
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
+use uuid::Uuid;
 use workspace::{
     dock::{DockPosition, Panel},
     searchable::Direction,
@@ -296,6 +298,7 @@ impl AssistantPanel {
                 self.include_conversation_in_next_inline_assist,
                 self.inline_prompt_history.clone(),
                 codegen.clone(),
+                self.workspace.clone(),
                 cx,
             );
             cx.focus_self();
@@ -724,6 +727,7 @@ impl AssistantPanel {
                 self.api_key.clone(),
                 self.languages.clone(),
                 self.fs.clone(),
+                self.workspace.clone(),
                 cx,
             )
         });
@@ -1059,6 +1063,7 @@ impl AssistantPanel {
         }
 
         let fs = self.fs.clone();
+        let workspace = self.workspace.clone();
         let api_key = self.api_key.clone();
         let languages = self.languages.clone();
         cx.spawn(|this, mut cx| async move {
@@ -1073,8 +1078,9 @@ impl AssistantPanel {
                 if let Some(ix) = this.editor_index_for_path(&path, cx) {
                     this.set_active_editor_index(Some(ix), cx);
                 } else {
-                    let editor = cx
-                        .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx));
+                    let editor = cx.add_view(|cx| {
+                        ConversationEditor::for_conversation(conversation, fs, workspace, cx)
+                    });
                     this.add_conversation(editor, cx);
                 }
             })?;
@@ -1348,6 +1354,7 @@ struct Summary {
 }
 
 struct Conversation {
+    id: Option<String>,
     buffer: ModelHandle<Buffer>,
     message_anchors: Vec<MessageAnchor>,
     messages_metadata: HashMap<MessageId, MessageMetadata>,
@@ -1398,6 +1405,7 @@ impl Conversation {
         let model = settings.default_open_ai_model.clone();
 
         let mut this = Self {
+            id: Some(Uuid::new_v4().to_string()),
             message_anchors: Default::default(),
             messages_metadata: Default::default(),
             next_message_id: Default::default(),
@@ -1435,6 +1443,7 @@ impl Conversation {
 
     fn serialize(&self, cx: &AppContext) -> SavedConversation {
         SavedConversation {
+            id: self.id.clone(),
             zed: "conversation".into(),
             version: SavedConversation::VERSION.into(),
             text: self.buffer.read(cx).text(),
@@ -1462,6 +1471,10 @@ impl Conversation {
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
+        let id = match saved_conversation.id {
+            Some(id) => Some(id),
+            None => Some(Uuid::new_v4().to_string()),
+        };
         let model = saved_conversation.model;
         let markdown = language_registry.language_for_name("Markdown");
         let mut message_anchors = Vec::new();
@@ -1491,6 +1504,7 @@ impl Conversation {
         });
 
         let mut this = Self {
+            id,
             message_anchors,
             messages_metadata: saved_conversation.message_metadata,
             next_message_id,
@@ -2108,6 +2122,7 @@ struct ScrollPosition {
 struct ConversationEditor {
     conversation: ModelHandle<Conversation>,
     fs: Arc<dyn Fs>,
+    workspace: WeakViewHandle<Workspace>,
     editor: ViewHandle<Editor>,
     blocks: HashSet<BlockId>,
     scroll_position: Option<ScrollPosition>,
@@ -2119,15 +2134,17 @@ impl ConversationEditor {
         api_key: Rc<RefCell<Option<String>>>,
         language_registry: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
-        Self::for_conversation(conversation, fs, cx)
+        Self::for_conversation(conversation, fs, workspace, cx)
     }
 
     fn for_conversation(
         conversation: ModelHandle<Conversation>,
         fs: Arc<dyn Fs>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let editor = cx.add_view(|cx| {
@@ -2150,6 +2167,7 @@ impl ConversationEditor {
             blocks: Default::default(),
             scroll_position: None,
             fs,
+            workspace,
             _subscriptions,
         };
         this.update_message_headers(cx);
@@ -2157,6 +2175,13 @@ impl ConversationEditor {
     }
 
     fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
+        report_assistant_event(
+            self.workspace.clone(),
+            self.conversation.read(cx).id.clone(),
+            AssistantKind::Panel,
+            cx,
+        );
+
         let cursors = self.cursors(cx);
 
         let user_messages = self.conversation.update(cx, |conversation, cx| {
@@ -2665,6 +2690,7 @@ enum InlineAssistantEvent {
 struct InlineAssistant {
     id: usize,
     prompt_editor: ViewHandle<Editor>,
+    workspace: WeakViewHandle<Workspace>,
     confirmed: bool,
     has_focus: bool,
     include_conversation: bool,
@@ -2780,6 +2806,7 @@ impl InlineAssistant {
         include_conversation: bool,
         prompt_history: VecDeque<String>,
         codegen: ModelHandle<Codegen>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let prompt_editor = cx.add_view(|cx| {
@@ -2801,6 +2828,7 @@ impl InlineAssistant {
         Self {
             id,
             prompt_editor,
+            workspace,
             confirmed: false,
             has_focus: false,
             include_conversation,
@@ -2859,6 +2887,8 @@ impl InlineAssistant {
         if self.confirmed {
             cx.emit(InlineAssistantEvent::Dismissed);
         } else {
+            report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
+
             let prompt = self.prompt_editor.read(cx).text(cx);
             self.prompt_editor.update(cx, |editor, cx| {
                 editor.set_read_only(true);
@@ -3347,3 +3377,30 @@ mod tests {
             .collect()
     }
 }
+
+fn report_assistant_event(
+    workspace: WeakViewHandle<Workspace>,
+    conversation_id: Option<String>,
+    assistant_kind: AssistantKind,
+    cx: &AppContext,
+) {
+    let Some(workspace) = workspace.upgrade(cx) else {
+        return;
+    };
+
+    let client = workspace.read(cx).project().read(cx).client();
+    let telemetry = client.telemetry();
+
+    let model = settings::get::<AssistantSettings>(cx)
+        .default_open_ai_model
+        .clone();
+
+    let event = ClickhouseEvent::Assistant {
+        conversation_id,
+        kind: assistant_kind,
+        model: model.full_name(),
+    };
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+    telemetry.report_clickhouse_event(event, telemetry_settings)
+}

crates/client/src/telemetry.rs 🔗

@@ -56,6 +56,13 @@ struct ClickhouseEventWrapper {
     event: ClickhouseEvent,
 }
 
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum AssistantKind {
+    Panel,
+    Inline,
+}
+
 #[derive(Serialize, Debug)]
 #[serde(tag = "type")]
 pub enum ClickhouseEvent {
@@ -76,6 +83,11 @@ pub enum ClickhouseEvent {
         room_id: Option<u64>,
         channel_id: Option<u64>,
     },
+    Assistant {
+        conversation_id: Option<String>,
+        kind: AssistantKind,
+        model: &'static str,
+    },
 }
 
 #[cfg(debug_assertions)]

crates/theme/src/components.rs 🔗

@@ -26,7 +26,6 @@ impl<C: SafeStylable> ComponentExt<C> for C {
 }
 
 pub mod disclosure {
-
     use gpui::{
         elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
         Action, Element,