use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, pin::Pin, task::Poll};
#[allow(unused_imports)] use crate::{
client::AnthropicError,
prompt::{
self,
message::{Block, Content},
},
response::{self, StopReason, Usage},
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Event<'a> {
Ping,
MessageStart {
message: response::Message<'a>,
},
ContentBlockStart {
index: usize,
content_block: Block<'a>,
},
ContentBlockDelta {
index: usize,
delta: Delta<'a>,
},
ContentBlockStop {
index: usize,
},
MessageDelta {
delta: MessageDelta,
},
MessageStop,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum ApiResult<'a> {
Event {
#[serde(flatten)]
event: Event<'a>,
},
Error { error: AnthropicError },
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum Delta<'a> {
#[serde(alias = "text_delta")]
Text {
text: Cow<'a, str>,
},
#[serde(rename = "input_json_delta")]
Json {
partial_json: Cow<'a, str>,
},
}
#[derive(Serialize, thiserror::Error, Debug)]
#[error("`Delta::{from:?}` canot be applied to `{to}`.")]
pub struct ContentMismatch<'a> {
pub from: Delta<'a>,
pub to: &'static str,
}
#[derive(Serialize, thiserror::Error, Debug)]
#[error("Index {index} out of bounds. Max index is {max}.")]
pub struct OutOfBounds {
pub index: usize,
pub max: usize,
}
#[derive(Serialize, thiserror::Error, Debug, derive_more::From)]
#[allow(missing_docs)]
pub enum DeltaError<'a> {
#[error("Cannot apply delta because: {error}")]
ContentMismatch { error: ContentMismatch<'a> },
#[error("Cannot apply delta because: {error}")]
OutOfBounds { error: OutOfBounds },
#[error(
"Cannot apply delta because deserialization failed because: {error}"
)]
Parse { error: String },
}
impl Delta<'_> {
pub fn merge(mut self, delta: Delta) -> Result<Self, ContentMismatch> {
match (&mut self, delta) {
(Delta::Text { text }, Delta::Text { text: delta }) => {
text.to_mut().push_str(&delta);
}
(
Delta::Json { partial_json },
Delta::Json {
partial_json: delta,
},
) => {
partial_json.to_mut().push_str(&delta);
}
(to, from) => {
return Err(ContentMismatch {
from,
to: match to {
Delta::Text { .. } => stringify!(Delta::Text),
Delta::Json { .. } => stringify!(Delta::Json),
},
});
}
}
Ok(self)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HTTP error: {error}")]
Stream {
#[from]
error: eventsource_stream::EventStreamError<reqwest::Error>,
},
#[error("JSON error: {error}")]
Parse {
error: serde_json::Error,
event: eventsource_stream::Event,
},
#[error("API error: {error}")]
Anthropic {
error: AnthropicError,
event: eventsource_stream::Event,
},
}
pub struct Stream<'a> {
inner: Pin<
Box<
dyn futures::Stream<Item = Result<Event<'a>, Error>>
+ Send
+ 'static,
>,
>,
}
static_assertions::assert_impl_all!(Stream<'_>: futures::Stream, Send);
impl Stream<'_> {
pub fn new<S>(stream: S) -> Self
where
S: futures::Stream<
Item = Result<
eventsource_stream::Event,
eventsource_stream::EventStreamError<reqwest::Error>,
>,
> + Send
+ 'static,
{
Self {
inner: Box::pin(stream.map(|event| match event {
Ok(event) => {
#[cfg(feature = "log")]
log::trace!("Event: {:?}", event);
match serde_json::from_str::<ApiResult>(&event.data) {
Ok(ApiResult::Event { event }) => Ok(event),
Ok(ApiResult::Error { error }) => {
Err(Error::Anthropic { error, event })
}
Err(error) => Err(Error::Parse { error, event }),
}
}
Err(error) => {
#[cfg(feature = "log")]
log::error!("Stream error: {:?}", error);
Err(Error::Stream { error })
}
})),
}
}
}
impl<'a> futures::Stream for Stream<'a> {
type Item = Result<Event<'a>, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context,
) -> Poll<Option<Self::Item>> {
self.inner.as_mut().poll_next(cx)
}
}
pub trait FilterExt<'a>:
futures::stream::Stream<Item = Result<Event<'a>, Error>> + Sized + Send
{
fn filter_rate_limit(
self,
) -> impl futures::Stream<Item = Result<Event<'a>, Error>> + Send {
self.filter_map(|result| async move {
match result {
Ok(event) => Some(Ok(event)),
Err(Error::Anthropic {
error:
AnthropicError::Overloaded { .. }
| AnthropicError::RateLimit { .. },
..
}) => None,
Err(error) => Some(Err(error)),
}
})
}
fn deltas(
self,
) -> impl futures::Stream<Item = Result<Delta<'a>, Error>> + Send {
self.filter_map(|result| async move {
match result {
Ok(Event::ContentBlockDelta { delta, .. }) => Some(Ok(delta)),
_ => None,
}
})
}
fn text(
self,
) -> impl futures::Stream<Item = Result<Cow<'a, str>, Error>> + Send {
self.deltas().filter_map(|result| async move {
match result {
Ok(Delta::Text { text }) => Some(Ok(text)),
_ => None,
}
})
}
}
impl<'a, S> FilterExt<'a> for S where
S: futures::Stream<Item = Result<Event<'a>, Error>> + Send
{
}
#[cfg(test)]
pub(crate) mod tests {
use futures::TryStreamExt;
use super::*;
pub const CONTENT_BLOCK_START: &str = "{\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }";
pub const CONTENT_BLOCK_DELTA: &str = "{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Certainly! I\"} }";
pub fn mock_stream(text: &'static str) -> Stream<'static> {
use itertools::Itertools;
let inner = futures::stream::iter(
text.lines().tuples().map(|(event, data, _empty)| {
assert!(_empty.is_empty());
Ok(eventsource_stream::Event {
event: event.strip_prefix("event: ").unwrap().into(),
data: data.strip_prefix("data: ").unwrap().into(),
id: "".into(),
retry: None,
})
}),
);
Stream::new(inner)
}
#[test]
fn test_content_block_start() {
let event: Event = serde_json::from_str(CONTENT_BLOCK_START).unwrap();
match event {
Event::ContentBlockStart {
index,
content_block,
} => {
assert_eq!(index, 0);
#[cfg(feature = "prompt-caching")]
if let Block::Text {
text,
cache_control,
} = content_block
{
assert_eq!(text.as_ref(), "");
assert!(cache_control.is_none());
} else {
panic!("Unexpected content block: {:?}", content_block);
}
#[cfg(not(feature = "prompt-caching"))]
if let Block::Text { text } = content_block {
assert_eq!(text.as_ref(), "");
} else {
panic!("Unexpected content block: {:?}", content_block);
}
}
_ => panic!("Unexpected event: {:?}", event),
}
}
#[test]
fn test_content_block_delta() {
let event: Event = serde_json::from_str(CONTENT_BLOCK_DELTA).unwrap();
match event {
Event::ContentBlockDelta { index, delta } => {
assert_eq!(index, 0);
assert_eq!(
delta,
Delta::Text {
text: "Certainly! I".into()
}
);
}
_ => panic!("Unexpected event: {:?}", event),
}
}
#[test]
fn test_content_block_delta_merge() {
let text_delta = Delta::Text {
text: "Certainly! I".into(),
}
.merge(Delta::Text {
text: " can".into(),
})
.unwrap()
.merge(Delta::Text { text: " do".into() })
.unwrap();
assert_eq!(
text_delta,
Delta::Text {
text: "Certainly! I can do".into()
}
);
let json_delta = Delta::Json {
partial_json: r#"{"key":"#.into(),
}
.merge(Delta::Json {
partial_json: r#""value"}"#.into(),
})
.unwrap();
assert_eq!(
json_delta,
Delta::Json {
partial_json: r#"{"key":"value"}"#.into()
}
);
let mismatch = json_delta.merge(text_delta).unwrap_err();
assert_eq!(
mismatch.to_string(),
ContentMismatch {
from: Delta::Text {
text: "Certainly! I can do".into()
},
to: "Delta::Json"
}
.to_string()
);
let text_delta = Delta::Text {
text: "Certainly!".into(),
};
let json_delta = Delta::Json {
partial_json: r#"{"key":"value"}"#.into(),
};
let mismatch = text_delta.merge(json_delta).unwrap_err();
assert_eq!(
mismatch.to_string(),
ContentMismatch {
from: Delta::Json {
partial_json: r#"{"key":"value"}"#.into()
},
to: "Delta::Text"
}
.to_string()
);
}
#[tokio::test]
async fn test_stream() {
let stream = mock_stream(include_str!("../test/data/sse.stream.txt"));
let text: String = stream
.filter_rate_limit()
.text()
.try_collect()
.await
.unwrap();
assert_eq!(
text,
"Okay, let's check the weather for San Francisco, CA:"
);
}
}