use futures_core::Stream;
use serde::{Deserialize, Serialize};
use crate::chat::stream::{CollectedResponse, StreamCollector};
use crate::{Error, Result, StreamEvent};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StreamTranscript {
pub events: Vec<StreamEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terminal_error: Option<Error>,
}
impl StreamTranscript {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, item: Result<StreamEvent>) {
match item {
Ok(event) => self.events.push(event),
Err(error) => self.terminal_error = Some(error),
}
}
pub fn to_collected(&self) -> Result<CollectedResponse> {
let mut collector = StreamCollector::new();
for event in &self.events {
collector.push_ref(event)?;
}
if let Some(error) = &self.terminal_error {
collector.set_terminal_error(error.clone());
}
collector.finish_partial()
}
pub fn replay(self) -> impl Stream<Item = Result<StreamEvent>> + Send + Unpin + 'static {
futures_util::stream::iter(
self.events
.into_iter()
.map(|mut event| {
mark_warning_emitted(&mut event);
Ok(event)
})
.chain(self.terminal_error.map(Err)),
)
}
}
pub(crate) fn mark_warning_emitted(event: &mut StreamEvent) {
if let StreamEvent::Warning { warning } = event {
warning.mark_emitted();
}
}
#[cfg(test)]
async fn collect_transcript<S>(stream: S) -> StreamTranscript
where
S: Stream<Item = Result<StreamEvent>> + Send,
{
use futures_util::StreamExt;
let mut transcript = StreamTranscript::new();
let mut stream = std::pin::pin!(stream);
while let Some(item) = stream.next().await {
let failed = item.is_err();
transcript.record(item);
if failed {
break;
}
}
transcript
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ChatResponse, ChatStreamExt, ContentBlock};
use futures_util::StreamExt;
fn response() -> ChatResponse {
ChatResponse::new(vec![ContentBlock::text("hello")]).id("r1")
}
#[tokio::test]
async fn complete_streams_round_trip() {
let transcript = collect_transcript(response().into_stream()).await;
assert!(transcript.terminal_error.is_none());
let collected = transcript.to_collected().unwrap();
assert!(collected.completeness.is_complete());
assert_eq!(collected.response, response());
let value = serde_json::to_value(&transcript).unwrap();
assert!(value.get("terminal_error").is_none());
let back: StreamTranscript = serde_json::from_value(value).unwrap();
assert_eq!(back.events, transcript.events);
assert_eq!(back.replay().collect_response().await.unwrap(), response());
}
#[tokio::test]
async fn partial_output_and_the_terminal_error_are_kept() {
let stream = futures_util::stream::iter(vec![
Ok(StreamEvent::ResponseStart {
id: None,
model: None,
}),
Ok(StreamEvent::TextDelta {
index: 0,
text: "par".into(),
}),
Err(Error::overloaded("busy")),
Ok(StreamEvent::ResponseStop),
]);
let transcript = collect_transcript(stream).await;
assert_eq!(transcript.events.len(), 2, "recording stops at the error");
let value = serde_json::to_value(&transcript).unwrap();
assert_eq!(value["terminal_error"]["type"], "overloaded");
let back: StreamTranscript = serde_json::from_value(value).unwrap();
let collected = back.to_collected().unwrap();
assert_eq!(collected.response.text().as_deref(), Some("par"));
assert!(!collected.completeness.is_complete());
assert!(matches!(
collected.terminal_error,
Some(Error::Overloaded { .. })
));
let replayed: Vec<_> = back.replay().collect().await;
assert_eq!(replayed.len(), 3);
assert!(replayed[2].is_err());
}
#[tokio::test]
async fn replayed_warnings_are_marked_emitted() {
let warning = crate::Warning::new(crate::WarningKind::Other, crate::Subject::Response, "w");
let transcript = StreamTranscript {
events: vec![StreamEvent::warning(warning)],
terminal_error: None,
};
let back: StreamTranscript =
serde_json::from_value(serde_json::to_value(&transcript).unwrap()).unwrap();
let StreamEvent::Warning { warning } = &back.events[0] else {
panic!("a warning event");
};
assert!(!crate::warning::was_emitted(warning));
let replayed: Vec<_> = back.replay().collect().await;
let Ok(StreamEvent::Warning { warning }) = &replayed[0] else {
panic!("a warning event");
};
assert!(crate::warning::was_emitted(warning));
}
}