headless_lms_chatbot/azure_chatbot/azure/
sse.rs1use futures::StreamExt;
5use tracing::trace;
6
7use super::protocol::{OutputItem, ResponseOutput, reported_azure_error};
8use super::transport::{ResponseLinesStream, ResponseStreamType};
9use crate::azure_chatbot::events::StreamItem;
10use crate::chatbot_error::ChatbotResult;
11use crate::prelude::*;
12
13pub(crate) const ALL_EXPECTED_EVENTS: &[&str] = &[
17 "response.in_progress",
18 "response.queued",
19 "response.content_part.added",
20 "response.content_part.done",
21 "response.reasoning_summary_part.added",
22 "response.reasoning_summary_part.done",
23 "response.reasoning_summary_text.delta",
24 "response.reasoning_summary_text.done",
25 "response.reasoning_text.delta",
26 "response.reasoning_text.done",
27 "response.function_call_arguments.done",
28 "response.custom_tool_call_input.done",
29 "response.output_text.done",
30 "response.output_text.annotation.added",
31 "response.refusal.done",
32];
33
34#[derive(Debug, Clone, PartialEq, Eq)]
43pub(crate) enum AzureStreamEvent {
44 ResponseCreated,
45 ResponseCompleted,
46 OutputItemAdded,
47 OutputItemDone,
48 FunctionCallArgumentsDelta,
49 CustomToolCallInputDelta,
50 OutputTextDelta,
51 RefusalDelta,
52 Incomplete,
53 ErrorReported,
54 Other,
57}
58
59impl AzureStreamEvent {
60 pub(crate) fn from_wire(name: &str) -> Self {
61 match name {
62 "response.created" => Self::ResponseCreated,
63 "response.completed" => Self::ResponseCompleted,
64 "response.output_item.added" => Self::OutputItemAdded,
65 "response.output_item.done" => Self::OutputItemDone,
66 "response.function_call_arguments.delta" => Self::FunctionCallArgumentsDelta,
67 "response.custom_tool_call_input.delta" => Self::CustomToolCallInputDelta,
68 "response.output_text.delta" => Self::OutputTextDelta,
69 "response.refusal.delta" => Self::RefusalDelta,
70 "response.incomplete" => Self::Incomplete,
71 "response.error" | "error" | "response.failed" => Self::ErrorReported,
72 _ => Self::Other,
73 }
74 }
75
76 pub(crate) fn of_data_line(
83 preceding_event: Option<Self>,
84 response_type: Option<&str>,
85 ) -> Option<Self> {
86 preceding_event.or_else(|| response_type.map(Self::from_wire))
87 }
88
89 fn for_event_line(name: &str) -> Self {
92 let event = Self::from_wire(name);
93 if matches!(event, Self::Other) && !ALL_EXPECTED_EVENTS.contains(&name) {
94 warn!("Received unexpected event from Azure: Event: {}", name);
95 }
96 event
97 }
98}
99
100pub(crate) enum ParsedResponseLine {
101 Event(AzureStreamEvent),
102 Data(Box<ResponseOutput>),
103}
104
105fn sse_field<'a>(line: &'a str, field: &str) -> Option<&'a str> {
108 let value = line.strip_prefix(field)?.strip_prefix(':')?;
109 Some(value.strip_prefix(' ').unwrap_or(value))
110}
111
112impl ParsedResponseLine {
113 pub(crate) fn parse(input: &str) -> ChatbotResult<Option<Self>> {
114 if let Some(event_type) = sse_field(input, "event") {
115 Ok(Some(ParsedResponseLine::Event(
116 AzureStreamEvent::for_event_line(event_type),
117 )))
118 } else if let Some(data) = sse_field(input, "data") {
119 if data.trim() == "[DONE]" {
121 return Ok(None);
122 }
123 let response_output = match serde_json::from_str::<ResponseOutput>(data) {
124 Ok(response_output) => response_output,
125 Err(e) => {
126 tracing::error!(error = %e, "Failed to deserialize streamed response line from Azure");
127 tracing::trace!(raw_line = %data, "Raw line for the deserialization failure above");
130 return Err(ChatbotError::from(e));
131 }
132 };
133 Ok(Some(ParsedResponseLine::Data(Box::new(response_output))))
134 } else {
135 Ok(None)
136 }
137 }
138}
139
140pub(crate) struct ClassifiedResponse<'a> {
142 pub(crate) response_id: String,
143 pub(crate) items: Vec<StreamItem>,
146 pub(crate) stream: ResponseStreamType<'a>,
148}
149
150pub(crate) async fn detect_response_kind<'a>(
155 mut lines: ResponseLinesStream<'a>,
156) -> ChatbotResult<ClassifiedResponse<'a>> {
157 let mut response_id: Option<String> = None;
158 let mut items: Vec<StreamItem> = Vec::new();
159 let mut preceding_event: Option<AzureStreamEvent> = None;
161
162 while let Some(line) = lines.next().await {
163 let line = line?;
164 let response_output = match ParsedResponseLine::parse(&line)? {
165 Some(ParsedResponseLine::Event(event)) => {
166 trace!("Event: {event:?}");
167 match &event {
168 AzureStreamEvent::FunctionCallArgumentsDelta
171 | AzureStreamEvent::CustomToolCallInputDelta => {
172 return classified(response_id, items, ResponseStreamType::ToolCall(lines));
173 }
174 AzureStreamEvent::OutputTextDelta | AzureStreamEvent::RefusalDelta => {
175 return classified(
176 response_id,
177 items,
178 ResponseStreamType::TextResponse(lines),
179 );
180 }
181 AzureStreamEvent::Incomplete => Err(chatbot_err!(
183 ResponseIncomplete,
184 format!(
185 "Response incomplete. Response id: {}",
186 response_id.as_deref().unwrap_or("not received")
187 )
188 ))?,
189 _ => {}
190 }
191 preceding_event = Some(event);
192 continue;
193 }
194 Some(ParsedResponseLine::Data(response_output)) => response_output,
195 None => continue,
196 };
197
198 let event = AzureStreamEvent::of_data_line(
199 preceding_event.take(),
200 response_output.response_type.as_deref(),
201 );
202 match event {
203 Some(AzureStreamEvent::ErrorReported) => {
204 if let Some(error) = reported_azure_error(&response_output, response_id.as_deref())
205 {
206 Err(error)?
207 } else {
208 Err(chatbot_err!(
209 UnexpectedProtocolShape,
210 format!(
211 "Response failed without receiving an API error. Response output: {:?} Response id: {}",
212 &response_output,
213 response_id.as_deref().unwrap_or("not received")
214 )
215 ))?
216 }
217 }
218 Some(AzureStreamEvent::ResponseCreated) => {
219 let response = response_output.response.ok_or(chatbot_err!(
220 DeserializationError,
221 "Expected response object"
222 ))?;
223 response_id = response.id;
224 }
225 Some(
226 item_event @ (AzureStreamEvent::OutputItemAdded | AzureStreamEvent::OutputItemDone),
227 ) => {
228 let received = response_output.item.ok_or(chatbot_err!(
229 DeserializationError,
230 "Expected response output item"
231 ))?;
232 let Some(item) = received.known() else {
233 continue;
234 };
235 let parser = parser_for_item(&item);
236 items.push(StreamItem::Received {
237 item,
238 finished: item_event == AzureStreamEvent::OutputItemDone,
239 });
240 if let Some(parser) = parser {
241 return classified(response_id, items, parser(lines));
242 }
243 }
244 _ => {}
245 }
246 }
247
248 Err(chatbot_err!(
251 StreamEndedEarly,
252 format!(
253 "The response received from Azure ended unexpectedly. Response id: {}",
254 response_id.as_deref().unwrap_or("not received")
255 )
256 ))
257}
258
259fn parser_for_item<'a>(
266 item: &OutputItem,
267) -> Option<fn(ResponseLinesStream<'a>) -> ResponseStreamType<'a>> {
268 match item {
269 OutputItem::FunctionCall { .. } => Some(ResponseStreamType::ToolCall),
270 OutputItem::Message { .. } => Some(ResponseStreamType::TextResponse),
271 OutputItem::Reasoning { .. }
272 | OutputItem::AzureAiSearchCall { .. }
273 | OutputItem::AzureAiSearchCallOutput { .. }
274 | OutputItem::FunctionCallOutput { .. } => None,
275 }
276}
277
278fn classified<'a>(
280 response_id: Option<String>,
281 items: Vec<StreamItem>,
282 stream: ResponseStreamType<'a>,
283) -> ChatbotResult<ClassifiedResponse<'a>> {
284 let response_id = response_id.ok_or(chatbot_err!(
285 StreamInvariantViolation,
286 "No response_id found! This should never happen!"
287 ))?;
288 Ok(ClassifiedResponse {
289 response_id,
290 items,
291 stream,
292 })
293}