diff --git a/src/client/mod.rs b/src/client/mod.rs index c193198..00815fc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -339,6 +339,19 @@ impl ConversationsClient<'_> { self.0.get(url).await } + pub async fn list_with_metric_outputs( + &self, + params: models::ListParams, + metric_id: &str, + ) -> Result { + let mut url = self.0.url("/v1/conversations"); + params.apply_to(&mut url); + url.query_pairs_mut() + .append_pair("include", "metric_outputs") + .append_pair("metric_id", metric_id); + self.0.get(url).await + } + pub async fn get(&self, id: &str) -> Result { let url = self.0.url(&format!("/v1/conversations/{id}")); let resp: models::GetConversationResponse = self.0.get(url).await?; diff --git a/src/client/models/conversation.rs b/src/client/models/conversation.rs index b41e98e..e36e176 100644 --- a/src/client/models/conversation.rs +++ b/src/client/models/conversation.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::output::Tabular; -use super::TranscriptMessage; +use super::{SimpleMetricOutput, TranscriptMessage}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conversation { @@ -28,6 +28,8 @@ pub struct Conversation { #[serde(skip_serializing_if = "Option::is_none")] pub metric_ids: Option>, #[serde(skip_serializing_if = "Option::is_none")] + pub metric_outputs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, #[serde(skip_serializing_if = "Option::is_none")] pub transcript: Option>, diff --git a/src/commands/conversations.rs b/src/commands/conversations.rs index 3e78223..0fbc881 100644 --- a/src/commands/conversations.rs +++ b/src/commands/conversations.rs @@ -56,6 +56,9 @@ pub struct ListArgs { page_size: u32, #[arg(long)] order_by: Option, + /// Include full outputs for this metric on every conversation in the page + #[arg(long, value_name = "METRIC_ID")] + include_metric_outputs: Option, } #[derive(Args)] @@ -155,7 +158,15 @@ pub async fn execute( order_by: args.order_by, ..Default::default() }; - let response = client.conversations().list(params).await?; + let response = match args.include_metric_outputs { + Some(metric_id) => { + client + .conversations() + .list_with_metric_outputs(params, &metric_id) + .await? + } + None => client.conversations().list(params).await?, + }; emit_list_with_actions( ctx, "conversations", diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index a1c476b..c9da411 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -1,7 +1,7 @@ use assert_cmd::Command; use predicates::prelude::*; use serde_json::{json, Value}; -use wiremock::matchers::{body_partial_json, header, method, path}; +use wiremock::matchers::{body_partial_json, header, method, path, query_param}; use wiremock::{Match, Mock, MockServer, Request, ResponseTemplate}; fn coval() -> Command { @@ -1597,6 +1597,73 @@ async fn test_conversations_audio_json_output() { assert_eq!(value["conversation_id"], "conv123"); } +#[tokio::test] +async fn test_conversations_list_can_include_full_metric_outputs() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/v1/conversations")) + .and(header("X-API-Key", "test_key")) + .and(query_param("include", "metric_outputs")) + .and(query_param("metric_id", "metric123")) + .and(query_param("page_size", "50")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "conversations": [{ + "name": "conversations/conversation123", + "conversation_id": "conversation123", + "status": "COMPLETED", + "create_time": "2026-04-09T12:00:00Z", + "occurred_at": "2026-04-09T11:55:00Z", + "has_audio": true, + "metadata": {"nlp_provider": "sierra"}, + "metric_outputs": [{ + "metric_output_id": "01JMETRICOUTPUT00000000000", + "metric_id": "metric123", + "metric_version_ulid": "01JMETRICVERSION0000000000", + "status": "COMPLETED", + "value": 0.5, + "result": { + "raw_values": { + "critical_failures": [{ + "node_id": "dispute_status_flow", + "failure": "Wrong status guidance.", + "message_index": 9 + }], + "non_critical_failures": [] + } + } + }] + }], + "next_page_token": "page-2" + }))) + .mount(&mock_server) + .await; + + let value = stdout_json( + coval() + .arg("--api-key") + .arg("test_key") + .arg("--api-url") + .arg(mock_server.uri()) + .arg("--format") + .arg("json") + .arg("conversations") + .arg("list") + .arg("--include-metric-outputs") + .arg("metric123") + .assert() + .success() + .stderr(predicate::str::is_empty()), + ); + + assert_eq!(value[0]["conversation_id"], "conversation123"); + assert_eq!(value[0]["metric_outputs"][0]["metric_id"], "metric123"); + assert_eq!( + value[0]["metric_outputs"][0]["result"]["raw_values"]["critical_failures"][0]["node_id"], + "dispute_status_flow" + ); +} + #[tokio::test] async fn test_test_cases_stdin_json_summary() { let mock_server = MockServer::start().await;