agent_ui: Route agent thread feedback through Cloud (#49324)

Marshall Bowers created

This PR updates the `Agent Thread Rated` event to be routed through
Cloud instead of the normal telemetry pipeline.

Closes CLO-223.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/acp/thread_view/active_thread.rs | 25 ++++++++++---
crates/client/src/user.rs                            |  4 ++
crates/cloud_api_client/src/cloud_api_client.rs      | 25 ++++++++++++++
crates/cloud_api_types/Cargo.toml                    |  1 
crates/cloud_api_types/src/cloud_api_types.rs        |  9 +++++
5 files changed, 57 insertions(+), 7 deletions(-)

Detailed changes

crates/agent_ui/src/acp/thread_view/active_thread.rs 🔗

@@ -1,3 +1,4 @@
+use cloud_api_types::SubmitAgentThreadFeedbackBody;
 use gpui::{Corner, List};
 use language_model::LanguageModelEffortLevel;
 use settings::update_settings_file;
@@ -23,6 +24,11 @@ impl ThreadFeedbackState {
             return;
         };
 
+        let project = thread.read(cx).project().read(cx);
+        let client = project.client();
+        let user_store = project.user_store();
+        let organization = user_store.read(cx).current_organization();
+
         if self.feedback == Some(feedback) {
             return;
         }
@@ -45,13 +51,18 @@ impl ThreadFeedbackState {
         };
         cx.background_spawn(async move {
             let thread = task.await?;
-            telemetry::event!(
-                "Agent Thread Rated",
-                agent = agent_telemetry_id,
-                session_id = session_id,
-                rating = rating,
-                thread = thread
-            );
+
+            client
+                .cloud_client()
+                .submit_agent_feedback(SubmitAgentThreadFeedbackBody {
+                    organization_id: organization.map(|organization| organization.id.clone()),
+                    agent: agent_telemetry_id.to_string(),
+                    session_id: session_id.to_string(),
+                    rating: rating.to_string(),
+                    thread,
+                })
+                .await?;
+
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);

crates/client/src/user.rs 🔗

@@ -670,6 +670,10 @@ impl UserStore {
         self.current_user.borrow().clone()
     }
 
+    pub fn current_organization(&self) -> Option<Arc<Organization>> {
+        self.current_organization.clone()
+    }
+
     pub fn plan(&self) -> Option<Plan> {
         #[cfg(debug_assertions)]
         if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {

crates/cloud_api_client/src/cloud_api_client.rs 🔗

@@ -181,6 +181,31 @@ impl CloudApiClient {
             }
         }
     }
+
+    pub async fn submit_agent_feedback(&self, body: SubmitAgentThreadFeedbackBody) -> Result<()> {
+        let request = self.build_request(
+            Request::builder().method(Method::POST).uri(
+                self.http_client
+                    .build_zed_cloud_url("/client/feedback/agent_thread")?
+                    .as_ref(),
+            ),
+            AsyncBody::from(serde_json::to_string(&body)?),
+        )?;
+
+        let mut response = self.http_client.send(request).await?;
+
+        if !response.status().is_success() {
+            let mut body = String::new();
+            response.body_mut().read_to_string(&mut body).await?;
+
+            anyhow::bail!(
+                "Failed to submit agent feedback.\nStatus: {:?}\nBody: {body}",
+                response.status()
+            )
+        }
+
+        Ok(())
+    }
 }
 
 fn build_request(

crates/cloud_api_types/Cargo.toml 🔗

@@ -17,6 +17,7 @@ chrono.workspace = true
 ciborium.workspace = true
 cloud_llm_client.workspace = true
 serde.workspace = true
+serde_json.workspace = true
 strum.workspace = true
 
 [dev-dependencies]

crates/cloud_api_types/src/cloud_api_types.rs 🔗

@@ -56,3 +56,12 @@ pub struct LlmToken(pub String);
 pub struct CreateLlmTokenResponse {
     pub token: LlmToken,
 }
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct SubmitAgentThreadFeedbackBody {
+    pub organization_id: Option<OrganizationId>,
+    pub agent: String,
+    pub session_id: String,
+    pub rating: String,
+    pub thread: serde_json::Value,
+}