release/1.3.1
rs 117 lines 3.66 KB
Raw
1 //! Token streaming for plain chat.
2
3 use std::pin::Pin;
4 use std::task::{Context, Poll};
5
6 use futures_core::Stream;
7 use onde::inference::types::StreamChunk;
8 use onde::inference::InferenceError;
9 use tokio::sync::mpsc::Receiver;
10
11 /// The reply to one message, as a stream of text deltas.
12 ///
13 /// Yields `Ok(delta)` for each piece and ends after the last one. A failure
14 /// mid-stream is a final `Err`. Dropping the stream stops generation: `onde`
15 /// notices the closed channel and keeps only what was delivered.
16 ///
17 /// The finished turn is added to the engine's history when the stream ends,
18 /// as with [`Ed::send`](crate::Ed::send).
19 pub struct TextStream {
20 rx: Receiver<StreamChunk>,
21 finished: bool,
22 }
23
24 impl TextStream {
25 pub(crate) fn new(rx: Receiver<StreamChunk>) -> Self {
26 Self {
27 rx,
28 finished: false,
29 }
30 }
31 }
32
33 impl TextStream {
34 /// The next delta, `None` when the reply is complete. For callers without a stream
35 /// combinator library.
36 pub async fn next(&mut self) -> Option<Result<String, InferenceError>> {
37 std::future::poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
38 }
39 }
40
41 /// `onde` reports a failed stream as a final chunk whose `finish_reason`
42 /// starts with `error`.
43 pub(crate) fn chunk_error(chunk: &StreamChunk) -> Option<InferenceError> {
44 chunk
45 .finish_reason
46 .as_deref()
47 .filter(|reason| reason.starts_with("error"))
48 .map(|reason| InferenceError::Inference {
49 reason: reason.trim_start_matches("error:").trim().to_owned(),
50 })
51 }
52
53 impl Stream for TextStream {
54 type Item = Result<String, InferenceError>;
55
56 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
57 loop {
58 if self.finished {
59 return Poll::Ready(None);
60 }
61 match self.rx.poll_recv(cx) {
62 Poll::Pending => return Poll::Pending,
63 Poll::Ready(None) => {
64 self.finished = true;
65 return Poll::Ready(None);
66 }
67 Poll::Ready(Some(chunk)) => {
68 if chunk.done {
69 self.finished = true;
70 return Poll::Ready(chunk_error(&chunk).map(Err));
71 }
72 if chunk.delta.is_empty() {
73 continue;
74 }
75 return Poll::Ready(Some(Ok(chunk.delta)));
76 }
77 }
78 }
79 }
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85
86 fn chunk(delta: &str, done: bool, reason: Option<&str>) -> StreamChunk {
87 StreamChunk {
88 delta: delta.to_owned(),
89 done,
90 finish_reason: reason.map(str::to_owned),
91 }
92 }
93
94 #[tokio::test]
95 async fn yields_deltas_then_ends() {
96 let (tx, rx) = tokio::sync::mpsc::channel(8);
97 let mut stream = TextStream::new(rx);
98 tx.send(chunk("he", false, None)).await.unwrap();
99 tx.send(chunk("llo", false, None)).await.unwrap();
100 tx.send(chunk("", true, Some("stop"))).await.unwrap();
101
102 assert_eq!(stream.next().await.unwrap().unwrap(), "he");
103 assert_eq!(stream.next().await.unwrap().unwrap(), "llo");
104 assert!(stream.next().await.is_none());
105 }
106
107 #[tokio::test]
108 async fn a_failed_stream_ends_with_an_error() {
109 let (tx, rx) = tokio::sync::mpsc::channel(8);
110 let mut stream = TextStream::new(rx);
111 tx.send(chunk("", true, Some("error: boom"))).await.unwrap();
112
113 let err = stream.next().await.unwrap().unwrap_err();
114 assert!(err.to_string().contains("boom"));
115 assert!(stream.next().await.is_none());
116 }
117 }