use alloc::collections::BTreeMap;
use serde_json::Value;
use super::StreamEvent;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Partial {
MessageStart,
BlockStart { index: u64, kind: String },
TextDelta { index: u64, text: String },
BlockStop { index: u64 },
MessageDelta { stop_reason: Option<String> },
MessageStop,
}
impl Partial {
pub fn of_event(e: &Value) -> Option<Self> {
let index = || e.get("index").and_then(Value::as_u64).unwrap_or(0);
let s = |v: Option<&Value>| v.and_then(Value::as_str).map(str::to_owned);
Some(match e.get("type").and_then(Value::as_str)? {
"message_start" => Self::MessageStart,
"content_block_start" => Self::BlockStart {
index: index(),
kind: s(e.pointer("/content_block/type")).unwrap_or_default(),
},
"content_block_delta" => {
if e.pointer("/delta/type").and_then(Value::as_str) != Some("text_delta") {
return None;
}
Self::TextDelta {
index: index(),
text: s(e.pointer("/delta/text"))?,
}
}
"content_block_stop" => Self::BlockStop { index: index() },
"message_delta" => Self::MessageDelta {
stop_reason: s(e.pointer("/delta/stop_reason")),
},
"message_stop" => Self::MessageStop,
_ => return None,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PartialMessage {
pub api_message_id: Option<String>,
pub texts: BTreeMap<u64, String>,
pub stopped: Vec<u64>,
pub stop_reason: Option<String>,
pub done: bool,
}
impl PartialMessage {
pub fn push(&mut self, ev: &StreamEvent) -> Option<Partial> {
let id = ev.api_message_id.get();
if id.is_some() && id != self.api_message_id.as_ref() {
*self = Self {
api_message_id: id.cloned(),
..Self::default()
};
}
let p = ev.partial()?;
self.apply(&p);
Some(p)
}
pub fn apply(&mut self, p: &Partial) {
match p {
Partial::MessageStart => {
let id = self.api_message_id.take();
*self = Self {
api_message_id: id,
..Self::default()
};
}
Partial::BlockStart { index, kind } if kind == "text" => {
self.texts.entry(*index).or_default();
}
Partial::BlockStart { .. } => {}
Partial::TextDelta { index, text } => {
self.texts.entry(*index).or_default().push_str(text);
}
Partial::BlockStop { index } => self.stopped.push(*index),
Partial::MessageDelta { stop_reason } => {
if stop_reason.is_some() {
self.stop_reason.clone_from(stop_reason);
}
}
Partial::MessageStop => self.done = true,
}
}
pub fn text(&self) -> String {
let mut out = String::new();
for t in self.texts.values().filter(|t| !t.is_empty()) {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str(t);
}
out
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn se(event: Value, msg: &str) -> StreamEvent {
StreamEvent {
event,
api_message_id: msg.to_owned().into(),
..StreamEvent::default()
}
}
#[test]
fn reads_the_events_a_streamer_needs() {
let of = |v: Value| Partial::of_event(&v);
assert_eq!(
of(json!({"type": "message_start", "message": {}})),
Some(Partial::MessageStart)
);
assert_eq!(
of(json!({"type": "content_block_start", "index": 1,
"content_block": {"type": "tool_use"}})),
Some(Partial::BlockStart {
index: 1,
kind: "tool_use".into()
})
);
assert_eq!(
of(json!({"type": "content_block_delta", "index": 0,
"delta": {"type": "text_delta", "text": "Hi"}})),
Some(Partial::TextDelta {
index: 0,
text: "Hi".into()
})
);
assert_eq!(
of(json!({"type": "content_block_delta", "index": 0,
"delta": {"type": "input_json_delta", "partial_json": "{"}})),
None
);
assert_eq!(
of(json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}})),
Some(Partial::MessageDelta {
stop_reason: Some("end_turn".into())
})
);
assert_eq!(of(json!({"type": "ping"})), None);
assert_eq!(of(json!({"no": "type"})), None);
}
#[test]
fn assembles_a_message_and_starts_over_on_the_next() {
let mut m = PartialMessage::default();
for e in [
json!({"type": "message_start", "message": {}}),
json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hi"}}),
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": " there"}}),
json!({"type": "content_block_stop", "index": 0}),
json!({"type": "content_block_start", "index": 1, "content_block": {"type": "tool_use"}}),
json!({"type": "content_block_start", "index": 2, "content_block": {"type": "text", "text": ""}}),
json!({"type": "content_block_delta", "index": 2, "delta": {"type": "text_delta", "text": "Done."}}),
json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}}),
json!({"type": "message_stop"}),
] {
m.push(&se(e, "msg_1"));
}
assert_eq!(m.text(), "Hi there\n\nDone.");
assert_eq!(m.stopped, [0]);
assert_eq!(m.stop_reason.as_deref(), Some("end_turn"));
assert!(m.done);
assert_eq!(m.api_message_id.as_deref(), Some("msg_1"));
m.push(&se(
json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Next"}}),
"msg_2",
));
assert_eq!((m.text().as_str(), m.done), ("Next", false));
}
}