Skip to content

Commit 27b381c

Browse files
committed
feat: add user turn metrics, and more info in request metadata
1 parent f9448b5 commit 27b381c

File tree

9 files changed

+505
-134
lines changed

9 files changed

+505
-134
lines changed

crates/chat-cli/src/cli/chat/cli/compact.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ impl CompactArgs {
5757
Some(self.prompt.join(" "))
5858
};
5959

60+
// Compact interrupts the current conversation so this will always result in a new user
61+
// turn.
62+
session.reset_user_turn();
63+
6064
session
6165
.compact_history(os, prompt, self.show_summary, CompactStrategy {
6266
messages_to_exclude: self.messages_to_exclude.unwrap_or(default.messages_to_exclude),

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 174 additions & 95 deletions
Large diffs are not rendered by default.

crates/chat-cli/src/cli/chat/parser.rs

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::sync::Arc;
22
use std::time::{
33
Duration,
44
Instant,
5+
SystemTime,
6+
UNIX_EPOCH,
57
};
68

79
use eyre::Result;
@@ -37,8 +39,42 @@ use crate::api_client::{
3739
ApiClientError,
3840
};
3941
use crate::telemetry::ReasonCode;
40-
use crate::telemetry::core::ChatConversationType;
42+
use crate::telemetry::core::{
43+
ChatConversationType,
44+
MessageMetaTag,
45+
};
46+
47+
#[derive(Debug, Error)]
48+
pub struct SendMessageError {
49+
#[source]
50+
pub source: ApiClientError,
51+
pub request_metadata: RequestMetadata,
52+
}
53+
54+
impl SendMessageError {
55+
pub fn status_code(&self) -> Option<u16> {
56+
self.source.status_code()
57+
}
58+
}
59+
60+
impl ReasonCode for SendMessageError {
61+
fn reason_code(&self) -> String {
62+
self.source.reason_code()
63+
}
64+
}
65+
66+
impl std::fmt::Display for SendMessageError {
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+
write!(f, "Failed to send the request: ")?;
69+
if let Some(request_id) = self.request_metadata.request_id.as_ref() {
70+
write!(f, "request_id: {}, error: ", request_id)?;
71+
}
72+
write!(f, "{}", self.source)?;
73+
Ok(())
74+
}
75+
}
4176

77+
/// Errors associated with consuming the response stream.
4278
#[derive(Debug, Error)]
4379
pub struct RecvError {
4480
#[source]
@@ -163,16 +199,36 @@ impl SendMessageStream {
163199
client: &ApiClient,
164200
conversation_state: ConversationState,
165201
request_metadata_lock: Arc<Mutex<Option<RequestMetadata>>>,
166-
) -> Result<Self, ApiClientError> {
202+
message_meta_tags: Option<Vec<MessageMetaTag>>,
203+
) -> Result<Self, SendMessageError> {
167204
let message_id = uuid::Uuid::new_v4().to_string();
168205
info!(?message_id, "Generated new message id");
206+
let user_prompt_length = conversation_state.user_input_message.content.len();
207+
let model_id = conversation_state.user_input_message.model_id.clone();
208+
let message_meta_tags = message_meta_tags.unwrap_or_default();
169209

170210
let cancel_token = CancellationToken::new();
171211
let cancel_token_clone = cancel_token.clone();
172212

173213
let start_time = Instant::now();
214+
let start_time_sys = SystemTime::now();
174215
debug!(?start_time, "sending send_message request");
175-
let response = client.send_message(conversation_state).await?;
216+
let response = client
217+
.send_message(conversation_state)
218+
.await
219+
.map_err(|err| SendMessageError {
220+
source: err,
221+
request_metadata: RequestMetadata {
222+
message_id: message_id.clone(),
223+
request_start_timestamp_ms: system_time_to_unix_ms(start_time_sys),
224+
stream_end_timestamp_ms: system_time_to_unix_ms(SystemTime::now()),
225+
model_id: model_id.clone(),
226+
user_prompt_length,
227+
message_meta_tags: message_meta_tags.clone(),
228+
// Other fields are irrelevant if we can't get a successful response
229+
..Default::default()
230+
},
231+
})?;
176232
let elapsed = start_time.elapsed();
177233
debug!(?elapsed, "send_message succeeded");
178234

@@ -182,8 +238,12 @@ impl SendMessageStream {
182238
ResponseParser::new(
183239
response,
184240
message_id,
241+
model_id,
242+
user_prompt_length,
243+
message_meta_tags,
185244
ev_tx,
186245
start_time,
246+
start_time_sys,
187247
cancel_token_clone,
188248
request_metadata_lock,
189249
)
@@ -221,9 +281,8 @@ struct ResponseParser {
221281

222282
/// Message identifier for the assistant's response. Randomly generated on creation.
223283
message_id: String,
224-
284+
/// Whether or not the stream has completed.
225285
ended: bool,
226-
227286
/// Buffer to hold the next event in [SendMessageOutput].
228287
peek: Option<ChatResponseStream>,
229288
/// Buffer for holding the accumulated assistant response.
@@ -238,33 +297,50 @@ struct ResponseParser {
238297
cancel_token: CancellationToken,
239298

240299
// metadata fields
241-
/// Time immediately after sending the request.
242-
start_time: Instant,
300+
/// Id of the model used with this request.
301+
model_id: Option<String>,
302+
/// Length of the user prompt for the initial request.
303+
user_prompt_length: usize,
304+
/// Meta tags for the initial request.
305+
message_meta_tags: Vec<MessageMetaTag>,
306+
/// Time immediately before sending the request.
307+
request_start_time: Instant,
308+
/// Time immediately before sending the request, as a [SystemTime].
309+
request_start_time_sys: SystemTime,
243310
/// Total size (in bytes) of the response received so far.
244311
received_response_size: usize,
245312
time_to_first_chunk: Option<Duration>,
246313
time_between_chunks: Vec<Duration>,
247314
}
248315

249316
impl ResponseParser {
317+
#[allow(clippy::too_many_arguments)]
250318
fn new(
251319
response: SendMessageOutput,
252320
message_id: String,
321+
model_id: Option<String>,
322+
user_prompt_length: usize,
323+
message_meta_tags: Vec<MessageMetaTag>,
253324
event_tx: mpsc::Sender<Result<ResponseEvent, RecvError>>,
254-
start_time: Instant,
325+
request_start_time: Instant,
326+
request_start_time_sys: SystemTime,
255327
cancel_token: CancellationToken,
256328
request_metadata: Arc<Mutex<Option<RequestMetadata>>>,
257329
) -> Self {
258330
Self {
259331
response,
260332
message_id,
333+
model_id,
334+
user_prompt_length,
335+
message_meta_tags,
261336
ended: false,
262337
event_tx,
263338
peek: None,
264339
assistant_text: String::new(),
265340
tool_uses: Vec::new(),
266341
parsing_tool_use: None,
267-
start_time,
342+
request_start_time,
343+
request_start_time_sys,
268344
received_response_size: 0,
269345
time_to_first_chunk: None,
270346
time_between_chunks: Vec::new(),
@@ -481,7 +557,7 @@ impl ResponseParser {
481557

482558
// Track metadata about the chunk.
483559
self.time_to_first_chunk
484-
.get_or_insert_with(|| self.start_time.elapsed());
560+
.get_or_insert_with(|| self.request_start_time.elapsed());
485561
self.time_between_chunks.push(duration);
486562
if let Some(r) = ev.as_ref() {
487563
match r {
@@ -510,10 +586,6 @@ impl ResponseParser {
510586
}
511587
}
512588

513-
fn request_id(&self) -> Option<&str> {
514-
self.response.request_id()
515-
}
516-
517589
/// Helper to create a new [RecvError] populated with the associated request id for the stream.
518590
fn error(&self, source: impl Into<RecvErrorKind>) -> RecvError {
519591
RecvError {
@@ -524,11 +596,24 @@ impl ResponseParser {
524596

525597
fn make_metadata(&self, chat_conversation_type: Option<ChatConversationType>) -> RequestMetadata {
526598
RequestMetadata {
527-
request_id: self.request_id().map(String::from),
599+
request_id: self.response.request_id().map(String::from),
600+
message_id: self.message_id.clone(),
528601
time_to_first_chunk: self.time_to_first_chunk,
529602
time_between_chunks: self.time_between_chunks.clone(),
530603
response_size: self.received_response_size,
531604
chat_conversation_type,
605+
request_start_timestamp_ms: system_time_to_unix_ms(self.request_start_time_sys),
606+
// We always end the stream when this method is called, so just set the end timestamp
607+
// here.
608+
stream_end_timestamp_ms: system_time_to_unix_ms(SystemTime::now()),
609+
user_prompt_length: self.user_prompt_length,
610+
message_meta_tags: self.message_meta_tags.clone(),
611+
tool_use_ids_and_names: self
612+
.tool_uses
613+
.iter()
614+
.map(|t| (t.id.clone(), t.name.clone()))
615+
.collect::<_>(),
616+
model_id: self.model_id.clone(),
532617
}
533618
}
534619
}
@@ -554,18 +639,40 @@ pub enum ResponseEvent {
554639
}
555640

556641
/// Metadata about the sent request and associated response stream.
557-
#[derive(Debug, Clone, Serialize, Deserialize)]
642+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
558643
pub struct RequestMetadata {
559644
/// The request id associated with the [SendMessageOutput] stream.
560645
pub request_id: Option<String>,
646+
/// The randomly-generated id associated with the request. Equivalent to utterance id.
647+
pub message_id: String,
648+
/// Unix timestamp (milliseconds) immediately before sending the request.
649+
pub request_start_timestamp_ms: u64,
650+
/// Unix timestamp (milliseconds) once the stream has either completed or ended in an error.
651+
pub stream_end_timestamp_ms: u64,
561652
/// Time until the first chunk was received.
562653
pub time_to_first_chunk: Option<Duration>,
563654
/// Time between each received chunk in the stream.
564655
pub time_between_chunks: Vec<Duration>,
656+
/// Total size (in bytes) of the user prompt associated with the request.
657+
pub user_prompt_length: usize,
565658
/// Total size (in bytes) of the response.
566659
pub response_size: usize,
567660
/// [ChatConversationType] for the returned assistant message.
568661
pub chat_conversation_type: Option<ChatConversationType>,
662+
/// Tool uses returned by the assistant for this request.
663+
pub tool_use_ids_and_names: Vec<(String, String)>,
664+
/// Model id.
665+
pub model_id: Option<String>,
666+
/// Meta tags for the request.
667+
pub message_meta_tags: Vec<MessageMetaTag>,
668+
}
669+
670+
fn system_time_to_unix_ms(time: SystemTime) -> u64 {
671+
(time
672+
.duration_since(UNIX_EPOCH)
673+
.expect("time should never be before unix epoch")
674+
.as_secs_f64()
675+
* 1000.0) as u64
569676
}
570677

571678
#[cfg(test)]
@@ -623,8 +730,12 @@ mod tests {
623730
let mut parser = ResponseParser::new(
624731
mock,
625732
"".to_string(),
733+
None,
734+
1,
735+
vec![],
626736
mpsc::channel(32).0,
627737
Instant::now(),
738+
SystemTime::now(),
628739
CancellationToken::new(),
629740
Arc::new(Mutex::new(None)),
630741
);

crates/chat-cli/src/cli/chat/tool_manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ impl ToolManagerBuilder {
611611
Err(e) => {
612612
error!("Error initializing mcp client for server {}: {:?}", name, &e);
613613
os.telemetry
614-
.send_mcp_server_init(conversation_id.clone(), Some(e.to_string()), 0)
614+
.send_mcp_server_init(conversation_id.clone(), name, Some(e.to_string()), 0)
615615
.ok();
616616
let _ = messenger.send_tools_list_result(Err(e)).await;
617617
},
@@ -1401,7 +1401,7 @@ fn process_tool_specs(
14011401
specs.retain(|spec| !matches!(spec.tool_origin, ToolOrigin::Native));
14021402
// Send server load success metric datum
14031403
let conversation_id = conversation_id.to_string();
1404-
let _ = telemetry.send_mcp_server_init(conversation_id, None, number_of_tools);
1404+
let _ = telemetry.send_mcp_server_init(conversation_id, server_name.to_string(), None, number_of_tools);
14051405
// Tool name translation. This is beyond of the scope of what is
14061406
// considered a "server load". Reasoning being:
14071407
// - Failures here are not related to server load

crates/chat-cli/src/cli/chat/tools/mod.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ pub mod knowledge;
77
pub mod thinking;
88
pub mod use_aws;
99

10-
use std::borrow::Borrow;
10+
use std::borrow::{
11+
Borrow,
12+
Cow,
13+
};
1114
use std::io::Write;
1215
use std::path::{
1316
Path,
@@ -31,6 +34,7 @@ use serde::{
3134
Serialize,
3235
};
3336
use thinking::Thinking;
37+
use tracing::error;
3438
use use_aws::UseAws;
3539

3640
use super::consts::MAX_TOOL_RESPONSE_SIZE;
@@ -239,12 +243,15 @@ pub struct InvokeOutput {
239243
}
240244

241245
impl InvokeOutput {
242-
pub fn as_str(&self) -> &str {
246+
pub fn as_str(&self) -> Cow<'_, str> {
243247
match &self.output {
244-
OutputKind::Text(s) => s.as_str(),
245-
OutputKind::Json(j) => j.as_str().unwrap_or_default(),
246-
OutputKind::Images(_) => "",
247-
OutputKind::Mixed { text, .. } => text.as_str(), // Return the text part
248+
OutputKind::Text(s) => s.as_str().into(),
249+
OutputKind::Json(j) => serde_json::to_string(j)
250+
.map_err(|err| error!(?err, "failed to serialize tool to json"))
251+
.unwrap_or_default()
252+
.into(),
253+
OutputKind::Images(_) => "".into(),
254+
OutputKind::Mixed { text, .. } => text.as_str().into(), // Return the text part
248255
}
249256
}
250257
}

0 commit comments

Comments
 (0)