headless_lms_chatbot/azure_chatbot/turn/
text_response.rs1use std::sync::Arc;
4
5use futures::StreamExt;
6use futures::stream::BoxStream;
7use tokio::sync::Mutex;
8use tracing::trace;
9
10use crate::azure_chatbot::azure::protocol::{
11 OutputItem, ReceivedOutputItem, ResponseOutput, check_response_complete, check_response_output,
12};
13use crate::azure_chatbot::azure::sse::{AzureStreamEvent, ParsedResponseLine};
14use crate::azure_chatbot::azure::transport::ResponseLinesStream;
15use crate::azure_chatbot::events::{StreamItem, TurnEvent};
16use crate::chatbot_error::ChatbotResult;
17use crate::llm_utils::estimate_tokens;
18use crate::prelude::*;
19
20pub(super) async fn parse_text_response<'a>(
28 mut lines: ResponseLinesStream<'a>,
29 full_response_text: Arc<Mutex<String>>,
30 response_id: String,
31) -> BoxStream<'a, ChatbotResult<TurnEvent>> {
32 trace!("Parsing stream to user...");
33
34 let mut response_received = false;
35 let mut response_incomplete = false;
36 let mut preceding_event: Option<AzureStreamEvent> = None;
37
38 Box::pin(async_stream::try_stream! {
39 while let Some(val) = lines.next().await {
40 let line = val?;
41 let response_output: ResponseOutput = match ParsedResponseLine::parse(&line)? {
42 Some(ParsedResponseLine::Event(event)) => {
43 trace!("Event: {event:?}");
44 match &event {
45 AzureStreamEvent::ResponseCompleted => {response_received = true;},
46 AzureStreamEvent::Incomplete => {response_received = true; response_incomplete = true;},
47 AzureStreamEvent::FunctionCallArgumentsDelta | AzureStreamEvent::CustomToolCallInputDelta => {
48 error!("ERROR, function call received but can't be processed while streaming to user.");
49 return Err(chatbot_err!(UnexpectedProtocolShape, "Unexpected function call while streaming to user"))?
50 },
51 AzureStreamEvent::ErrorReported => {
52 }
54 _ => {}
55 };
56 preceding_event = Some(event);
57 continue;
58 },
59 Some(ParsedResponseLine::Data(data)) => *data,
60 None => {continue;},
61 };
62
63 let event = AzureStreamEvent::of_data_line(preceding_event.take(), response_output.response_type.as_deref());
64
65 check_response_output(&response_output, Some(&response_id), "streaming_answer")?;
66
67 if response_received {
71 check_response_complete(&response_output, response_incomplete)?;
74 let full_response_as_string = full_response_text.lock().await.clone();
75 let estimated_cost = estimate_tokens(&full_response_as_string);
77 trace!(
78 "End of chatbot response stream. Estimated cost: {}. Response: {}",
79 estimated_cost, full_response_as_string
80 );
81 yield TurnEvent::Done { text: full_response_as_string, used_tokens: estimated_cost };
85 return;
86 }
87
88 if let Some(delta) = response_output.delta
91 && matches!(event, Some(AzureStreamEvent::OutputTextDelta | AzureStreamEvent::RefusalDelta))
92 {
93 full_response_text.lock().await.push_str(&delta);
94 yield TurnEvent::Delta(delta);
95 }
96
97 if let Some(item) = response_output.item.and_then(ReceivedOutputItem::known) {
98 match item {
99 OutputItem::Message { .. } => continue,
100 OutputItem::FunctionCall { .. } => Err(chatbot_err!(UnexpectedProtocolShape, "Error: unexpected function call after / during a text response.".to_string()))?,
101 item => {
102 let finished = matches!(event, Some(AzureStreamEvent::OutputItemDone));
103 yield TurnEvent::Item(StreamItem::Received { item, finished });
104 continue;
105 },
106 };
107 }
108 }
109 Err(chatbot_err!(StreamEndedEarly, "Stream ended unexpectedly"))?;
111 })
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::azure_chatbot::test_helpers::azure_response_stream;
118
119 #[tokio::test]
124 async fn done_reports_the_token_estimate_of_the_streamed_deltas() {
125 let answer = "A for loop.";
126
127 let mut events = parse_text_response(
128 azure_response_stream(&[
129 "event: response.output_text.delta",
130 &format!(r#"data: {{"type":"response.output_text.delta","delta":"{answer}"}}"#),
131 "event: response.completed",
132 r#"data: {"type":"response.completed","response":{"id":"resp_answer"}}"#,
133 ]),
134 Arc::new(Mutex::new(String::new())),
135 "resp_answer".to_string(),
136 )
137 .await;
138
139 let mut finished = None;
140 while let Some(event) = events.next().await {
141 if let TurnEvent::Done { text, used_tokens } =
142 event.expect("the response streams to the end")
143 {
144 finished = Some((text, used_tokens));
145 }
146 }
147
148 assert_eq!(
149 finished.expect("the answer finishes"),
150 (answer.to_string(), estimate_tokens(answer))
151 );
152 }
153
154 #[tokio::test]
156 async fn a_stream_that_ends_before_response_completed_errors() {
157 let mut events = parse_text_response(
158 azure_response_stream(&[
159 "event: response.output_text.delta",
160 r#"data: {"type":"response.output_text.delta","delta":"Cut off"}"#,
161 ]),
162 Arc::new(Mutex::new(String::new())),
163 "resp_answer".to_string(),
164 )
165 .await;
166
167 let error = loop {
168 match events.next().await.expect("the stream ends in an error") {
169 Ok(TurnEvent::Delta(_)) => continue,
170 Ok(other) => panic!("expected only a delta before the error, got {other:?}"),
171 Err(error) => break error,
172 }
173 };
174 assert_eq!(*error.error_type(), ChatbotErrorType::StreamEndedEarly);
175 }
176
177 #[tokio::test]
180 async fn a_response_incomplete_event_errors_instead_of_finishing() {
181 let mut events = parse_text_response(
182 azure_response_stream(&[
183 "event: response.output_text.delta",
184 r#"data: {"type":"response.output_text.delta","delta":"Cut off"}"#,
185 "event: response.incomplete",
186 r#"data: {"type":"response.incomplete","response":{"id":"resp_answer","incomplete_details":{"reason":"max_output_tokens"}}}"#,
187 ]),
188 Arc::new(Mutex::new(String::new())),
189 "resp_answer".to_string(),
190 )
191 .await;
192
193 let error = loop {
194 match events.next().await.expect("the stream ends in an error") {
195 Ok(TurnEvent::Delta(_)) => continue,
196 Ok(other) => panic!("expected only a delta before the error, got {other:?}"),
197 Err(error) => break error,
198 }
199 };
200 assert_eq!(*error.error_type(), ChatbotErrorType::ResponseIncomplete);
201 }
202}