From f4932acefb8dc1375e58090550fb6f2963a1bc72 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Fri, 10 Jul 2026 16:23:41 -0700 Subject: [PATCH 1/2] Add conversation failure breakdown command --- src/agent_discovery.rs | 1 + src/client/mod.rs | 9 ++++ src/client/models/conversation.rs | 89 ++++++++++++++++++++++++++++++- src/commands/conversations.rs | 48 ++++++++++++++++- tests/cli_tests.rs | 87 +++++++++++++++++++++++++++++- 5 files changed, 231 insertions(+), 3 deletions(-) diff --git a/src/agent_discovery.rs b/src/agent_discovery.rs index 64c8167..8862e54 100644 --- a/src/agent_discovery.rs +++ b/src/agent_discovery.rs @@ -287,6 +287,7 @@ const RESOURCE_SPECS: &[ResourceSpec] = &[ "submit", "metrics", "metric-detail", + "failure-breakdown", ], description: "Conversations are submitted or monitored customer interactions that can be evaluated with metrics.", id_name: "conversation_id", diff --git a/src/client/mod.rs b/src/client/mod.rs index c193198..fdf5c73 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -345,6 +345,15 @@ impl ConversationsClient<'_> { Ok(resp.conversation) } + pub async fn failure_breakdown( + &self, + params: models::FailureBreakdownParams, + ) -> Result { + let mut url = self.0.url("/v1/conversations"); + params.apply_to(&mut url); + self.0.get(url).await + } + pub async fn delete(&self, id: &str) -> Result<(), ApiError> { let url = self.0.url(&format!("/v1/conversations/{id}")); self.0.delete(url).await diff --git a/src/client/models/conversation.rs b/src/client/models/conversation.rs index b41e98e..160269f 100644 --- a/src/client/models/conversation.rs +++ b/src/client/models/conversation.rs @@ -1,5 +1,6 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; +use url::Url; use crate::output::Tabular; @@ -117,6 +118,92 @@ pub struct GetConversationResponse { pub conversation: Conversation, } +#[derive(Debug)] +pub struct FailureBreakdownParams { + pub metric_id: String, + pub group_by_metadata: Option, + pub failure_query: Option, + pub start_date: Option>, + pub end_date: Option>, + pub max_examples_per_group: u8, +} + +impl FailureBreakdownParams { + pub fn apply_to(&self, url: &mut Url) { + let mut pairs = url.query_pairs_mut(); + pairs.append_pair("view", "failure_breakdown"); + pairs.append_pair("metric_id", &self.metric_id); + if let Some(ref group_by_metadata) = self.group_by_metadata { + pairs.append_pair("group_by_metadata", group_by_metadata); + } + if let Some(ref failure_query) = self.failure_query { + pairs.append_pair("failure_query", failure_query); + } + if let Some(start_date) = self.start_date { + pairs.append_pair( + "start_date", + &start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true), + ); + } + if let Some(end_date) = self.end_date { + pairs.append_pair( + "end_date", + &end_date.to_rfc3339_opts(SecondsFormat::AutoSi, true), + ); + } + pairs.append_pair( + "max_examples_per_group", + &self.max_examples_per_group.to_string(), + ); + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FailureBreakdownResponse { + pub view: String, + pub metric_id: String, + #[serde(default)] + pub tree_definition: Vec, + pub group_by_metadata: Option, + pub failure_query: Option, + pub scope: FailureBreakdownScope, + #[serde(default)] + pub breakdown: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FailureBreakdownScope { + pub requested_start_date: Option, + pub requested_end_date: Option, + pub observed_start_date: Option>, + pub observed_end_date: Option>, + pub total_scored_conversations: u64, + pub structured_result_conversations: u64, + pub critical_failure_conversations: u64, + pub non_critical_failure_conversations: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FailureBreakdownRow { + pub failure_type: String, + pub node_id: String, + pub node_label: String, + pub metadata_value: Option, + pub conversation_count: u64, + pub occurrence_count: u64, + #[serde(default)] + pub examples: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FailureBreakdownExample { + pub conversation_id: String, + pub failure: String, + pub expected_bot_response: Option, + pub message_index: Option, + pub occurred_at: DateTime, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ConversationAudioUrlResponse { pub audio_url: String, diff --git a/src/commands/conversations.rs b/src/commands/conversations.rs index 3e78223..2a75fb4 100644 --- a/src/commands/conversations.rs +++ b/src/commands/conversations.rs @@ -9,7 +9,9 @@ use clap::{Args, Subcommand}; use indicatif::{ProgressBar, ProgressStyle}; use crate::client::error::ApiError; -use crate::client::models::{ListParams, MetricDetailResponse, SubmitConversationRequest}; +use crate::client::models::{ + FailureBreakdownParams, ListParams, MetricDetailResponse, SubmitConversationRequest, +}; use crate::client::CovalClient; use crate::input_json::{self, InputJsonArg}; use crate::next_actions; @@ -29,6 +31,8 @@ pub enum ConversationCommands { Metrics(MetricsArgs), #[command(name = "metric-detail")] MetricDetail(MetricDetailArgs), + #[command(name = "failure-breakdown")] + FailureBreakdown(FailureBreakdownArgs), Patch(PatchArgs), } @@ -43,6 +47,7 @@ impl ConversationCommands { Self::Submit(_) => "submit", Self::Metrics(_) => "metrics", Self::MetricDetail(_) => "metric-detail", + Self::FailureBreakdown(_) => "failure-breakdown", Self::Patch(_) => "patch", } } @@ -129,6 +134,27 @@ pub struct MetricDetailArgs { metric_id: String, } +#[derive(Args)] +pub struct FailureBreakdownArgs { + /// Metric ID whose structured failures should be counted + metric_id: String, + /// Conversation metadata key used to segment counts, such as nlp_provider + #[arg(long)] + group_by_metadata: Option, + /// Case-insensitive substring filter for failure text or node ID + #[arg(long)] + failure_query: Option, + /// Inclusive lower bound as a timezone-aware ISO 8601 timestamp + #[arg(long)] + start_date: Option>, + /// Inclusive upper bound as a timezone-aware ISO 8601 timestamp + #[arg(long)] + end_date: Option>, + /// Representative examples returned per failure group + #[arg(long, default_value = "3", value_parser = clap::value_parser!(u8).range(1..=5))] + max_examples_per_group: u8, +} + #[derive(Args)] pub struct PatchArgs { conversation_id: String, @@ -254,6 +280,26 @@ pub async fn execute( ), } } + ConversationCommands::FailureBreakdown(args) => { + let response = client + .conversations() + .failure_breakdown(FailureBreakdownParams { + metric_id: args.metric_id, + group_by_metadata: args.group_by_metadata, + failure_query: args.failure_query, + start_date: args.start_date, + end_date: args.end_date, + max_examples_per_group: args.max_examples_per_group, + }) + .await?; + emit_one_with_actions( + ctx, + "conversations", + operation, + &response, + vec![next_actions::context("conversations")], + ); + } ConversationCommands::Submit(args) => { let req = build_submit_request(args)?; let conversation = client.conversations().submit(req).await?; diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index a1c476b..539389d 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,91 @@ async fn test_conversations_audio_json_output() { assert_eq!(value["conversation_id"], "conv123"); } +#[tokio::test] +async fn test_conversations_failure_breakdown_json_output() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/v1/conversations")) + .and(header("X-API-Key", "test_key")) + .and(query_param("view", "failure_breakdown")) + .and(query_param("metric_id", "metric123")) + .and(query_param("group_by_metadata", "nlp_provider")) + .and(query_param("failure_query", "status")) + .and(query_param("start_date", "2026-04-09T00:00:00Z")) + .and(query_param("end_date", "2026-04-09T23:59:59Z")) + .and(query_param("max_examples_per_group", "2")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "view": "failure_breakdown", + "metric_id": "metric123", + "tree_definition": [ + {"id": "dispute_status_flow", "label": "Dispute Status Flow"} + ], + "group_by_metadata": "nlp_provider", + "failure_query": "status", + "scope": { + "requested_start_date": "2026-04-09T00:00:00Z", + "requested_end_date": "2026-04-09T23:59:59Z", + "observed_start_date": "2026-04-09T00:04:00Z", + "observed_end_date": "2026-04-09T23:51:00Z", + "total_scored_conversations": 29, + "structured_result_conversations": 29, + "critical_failure_conversations": 11, + "non_critical_failure_conversations": 26 + }, + "breakdown": [{ + "failure_type": "critical", + "node_id": "dispute_status_flow", + "node_label": "Dispute Status Flow", + "metadata_value": "sierra", + "conversation_count": 7, + "occurrence_count": 7, + "examples": [{ + "conversation_id": "conversation123", + "failure": "Wrong status guidance.", + "expected_bot_response": "Give approved status guidance or escalate.", + "message_index": 9, + "occurred_at": "2026-04-09T12:00:00Z" + }] + }] + }))) + .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("failure-breakdown") + .arg("metric123") + .arg("--group-by-metadata") + .arg("nlp_provider") + .arg("--failure-query") + .arg("status") + .arg("--start-date") + .arg("2026-04-09T00:00:00Z") + .arg("--end-date") + .arg("2026-04-09T23:59:59Z") + .arg("--max-examples-per-group") + .arg("2") + .assert() + .success() + .stderr(predicate::str::is_empty()), + ); + + assert_eq!(value["scope"]["total_scored_conversations"], 29); + assert_eq!(value["tree_definition"][0]["id"], "dispute_status_flow"); + assert_eq!( + value["breakdown"][0]["examples"][0]["expected_bot_response"], + "Give approved status guidance or escalate." + ); +} + #[tokio::test] async fn test_test_cases_stdin_json_summary() { let mock_server = MockServer::start().await; From 8df5582686ad224ea4bff1a773920b129f18aa3c Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Fri, 10 Jul 2026 17:05:27 -0700 Subject: [PATCH 2/2] Expose conversation metric outputs in CLI --- src/agent_discovery.rs | 1 - src/client/mod.rs | 22 +++++--- src/client/models/conversation.rs | 93 ++----------------------------- src/commands/conversations.rs | 61 +++++--------------- tests/cli_tests.rs | 88 ++++++++++++----------------- 5 files changed, 65 insertions(+), 200 deletions(-) diff --git a/src/agent_discovery.rs b/src/agent_discovery.rs index 8862e54..64c8167 100644 --- a/src/agent_discovery.rs +++ b/src/agent_discovery.rs @@ -287,7 +287,6 @@ const RESOURCE_SPECS: &[ResourceSpec] = &[ "submit", "metrics", "metric-detail", - "failure-breakdown", ], description: "Conversations are submitted or monitored customer interactions that can be evaluated with metrics.", id_name: "conversation_id", diff --git a/src/client/mod.rs b/src/client/mod.rs index fdf5c73..00815fc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -339,21 +339,25 @@ impl ConversationsClient<'_> { 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?; - Ok(resp.conversation) - } - - pub async fn failure_breakdown( + pub async fn list_with_metric_outputs( &self, - params: models::FailureBreakdownParams, - ) -> Result { + 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?; + Ok(resp.conversation) + } + pub async fn delete(&self, id: &str) -> Result<(), ApiError> { let url = self.0.url(&format!("/v1/conversations/{id}")); self.0.delete(url).await diff --git a/src/client/models/conversation.rs b/src/client/models/conversation.rs index 160269f..e36e176 100644 --- a/src/client/models/conversation.rs +++ b/src/client/models/conversation.rs @@ -1,10 +1,9 @@ -use chrono::{DateTime, SecondsFormat, Utc}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use url::Url; use crate::output::Tabular; -use super::TranscriptMessage; +use super::{SimpleMetricOutput, TranscriptMessage}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Conversation { @@ -29,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>, @@ -118,92 +119,6 @@ pub struct GetConversationResponse { pub conversation: Conversation, } -#[derive(Debug)] -pub struct FailureBreakdownParams { - pub metric_id: String, - pub group_by_metadata: Option, - pub failure_query: Option, - pub start_date: Option>, - pub end_date: Option>, - pub max_examples_per_group: u8, -} - -impl FailureBreakdownParams { - pub fn apply_to(&self, url: &mut Url) { - let mut pairs = url.query_pairs_mut(); - pairs.append_pair("view", "failure_breakdown"); - pairs.append_pair("metric_id", &self.metric_id); - if let Some(ref group_by_metadata) = self.group_by_metadata { - pairs.append_pair("group_by_metadata", group_by_metadata); - } - if let Some(ref failure_query) = self.failure_query { - pairs.append_pair("failure_query", failure_query); - } - if let Some(start_date) = self.start_date { - pairs.append_pair( - "start_date", - &start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true), - ); - } - if let Some(end_date) = self.end_date { - pairs.append_pair( - "end_date", - &end_date.to_rfc3339_opts(SecondsFormat::AutoSi, true), - ); - } - pairs.append_pair( - "max_examples_per_group", - &self.max_examples_per_group.to_string(), - ); - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FailureBreakdownResponse { - pub view: String, - pub metric_id: String, - #[serde(default)] - pub tree_definition: Vec, - pub group_by_metadata: Option, - pub failure_query: Option, - pub scope: FailureBreakdownScope, - #[serde(default)] - pub breakdown: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FailureBreakdownScope { - pub requested_start_date: Option, - pub requested_end_date: Option, - pub observed_start_date: Option>, - pub observed_end_date: Option>, - pub total_scored_conversations: u64, - pub structured_result_conversations: u64, - pub critical_failure_conversations: u64, - pub non_critical_failure_conversations: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FailureBreakdownRow { - pub failure_type: String, - pub node_id: String, - pub node_label: String, - pub metadata_value: Option, - pub conversation_count: u64, - pub occurrence_count: u64, - #[serde(default)] - pub examples: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FailureBreakdownExample { - pub conversation_id: String, - pub failure: String, - pub expected_bot_response: Option, - pub message_index: Option, - pub occurred_at: DateTime, -} - #[derive(Debug, Serialize, Deserialize)] pub struct ConversationAudioUrlResponse { pub audio_url: String, diff --git a/src/commands/conversations.rs b/src/commands/conversations.rs index 2a75fb4..0fbc881 100644 --- a/src/commands/conversations.rs +++ b/src/commands/conversations.rs @@ -9,9 +9,7 @@ use clap::{Args, Subcommand}; use indicatif::{ProgressBar, ProgressStyle}; use crate::client::error::ApiError; -use crate::client::models::{ - FailureBreakdownParams, ListParams, MetricDetailResponse, SubmitConversationRequest, -}; +use crate::client::models::{ListParams, MetricDetailResponse, SubmitConversationRequest}; use crate::client::CovalClient; use crate::input_json::{self, InputJsonArg}; use crate::next_actions; @@ -31,8 +29,6 @@ pub enum ConversationCommands { Metrics(MetricsArgs), #[command(name = "metric-detail")] MetricDetail(MetricDetailArgs), - #[command(name = "failure-breakdown")] - FailureBreakdown(FailureBreakdownArgs), Patch(PatchArgs), } @@ -47,7 +43,6 @@ impl ConversationCommands { Self::Submit(_) => "submit", Self::Metrics(_) => "metrics", Self::MetricDetail(_) => "metric-detail", - Self::FailureBreakdown(_) => "failure-breakdown", Self::Patch(_) => "patch", } } @@ -61,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)] @@ -134,27 +132,6 @@ pub struct MetricDetailArgs { metric_id: String, } -#[derive(Args)] -pub struct FailureBreakdownArgs { - /// Metric ID whose structured failures should be counted - metric_id: String, - /// Conversation metadata key used to segment counts, such as nlp_provider - #[arg(long)] - group_by_metadata: Option, - /// Case-insensitive substring filter for failure text or node ID - #[arg(long)] - failure_query: Option, - /// Inclusive lower bound as a timezone-aware ISO 8601 timestamp - #[arg(long)] - start_date: Option>, - /// Inclusive upper bound as a timezone-aware ISO 8601 timestamp - #[arg(long)] - end_date: Option>, - /// Representative examples returned per failure group - #[arg(long, default_value = "3", value_parser = clap::value_parser!(u8).range(1..=5))] - max_examples_per_group: u8, -} - #[derive(Args)] pub struct PatchArgs { conversation_id: String, @@ -181,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", @@ -280,26 +265,6 @@ pub async fn execute( ), } } - ConversationCommands::FailureBreakdown(args) => { - let response = client - .conversations() - .failure_breakdown(FailureBreakdownParams { - metric_id: args.metric_id, - group_by_metadata: args.group_by_metadata, - failure_query: args.failure_query, - start_date: args.start_date, - end_date: args.end_date, - max_examples_per_group: args.max_examples_per_group, - }) - .await?; - emit_one_with_actions( - ctx, - "conversations", - operation, - &response, - vec![next_actions::context("conversations")], - ); - } ConversationCommands::Submit(args) => { let req = build_submit_request(args)?; let conversation = client.conversations().submit(req).await?; diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 539389d..c9da411 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -1598,52 +1598,43 @@ async fn test_conversations_audio_json_output() { } #[tokio::test] -async fn test_conversations_failure_breakdown_json_output() { +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("view", "failure_breakdown")) + .and(query_param("include", "metric_outputs")) .and(query_param("metric_id", "metric123")) - .and(query_param("group_by_metadata", "nlp_provider")) - .and(query_param("failure_query", "status")) - .and(query_param("start_date", "2026-04-09T00:00:00Z")) - .and(query_param("end_date", "2026-04-09T23:59:59Z")) - .and(query_param("max_examples_per_group", "2")) + .and(query_param("page_size", "50")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "view": "failure_breakdown", - "metric_id": "metric123", - "tree_definition": [ - {"id": "dispute_status_flow", "label": "Dispute Status Flow"} - ], - "group_by_metadata": "nlp_provider", - "failure_query": "status", - "scope": { - "requested_start_date": "2026-04-09T00:00:00Z", - "requested_end_date": "2026-04-09T23:59:59Z", - "observed_start_date": "2026-04-09T00:04:00Z", - "observed_end_date": "2026-04-09T23:51:00Z", - "total_scored_conversations": 29, - "structured_result_conversations": 29, - "critical_failure_conversations": 11, - "non_critical_failure_conversations": 26 - }, - "breakdown": [{ - "failure_type": "critical", - "node_id": "dispute_status_flow", - "node_label": "Dispute Status Flow", - "metadata_value": "sierra", - "conversation_count": 7, - "occurrence_count": 7, - "examples": [{ - "conversation_id": "conversation123", - "failure": "Wrong status guidance.", - "expected_bot_response": "Give approved status guidance or escalate.", - "message_index": 9, - "occurred_at": "2026-04-09T12:00:00Z" + "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; @@ -1657,28 +1648,19 @@ async fn test_conversations_failure_breakdown_json_output() { .arg("--format") .arg("json") .arg("conversations") - .arg("failure-breakdown") + .arg("list") + .arg("--include-metric-outputs") .arg("metric123") - .arg("--group-by-metadata") - .arg("nlp_provider") - .arg("--failure-query") - .arg("status") - .arg("--start-date") - .arg("2026-04-09T00:00:00Z") - .arg("--end-date") - .arg("2026-04-09T23:59:59Z") - .arg("--max-examples-per-group") - .arg("2") .assert() .success() .stderr(predicate::str::is_empty()), ); - assert_eq!(value["scope"]["total_scored_conversations"], 29); - assert_eq!(value["tree_definition"][0]["id"], "dispute_status_flow"); + assert_eq!(value[0]["conversation_id"], "conversation123"); + assert_eq!(value[0]["metric_outputs"][0]["metric_id"], "metric123"); assert_eq!( - value["breakdown"][0]["examples"][0]["expected_bot_response"], - "Give approved status guidance or escalate." + value[0]["metric_outputs"][0]["result"]["raw_values"]["critical_failures"][0]["node_id"], + "dispute_status_flow" ); }