headless_lms_chatbot/azure_chatbot/
events.rs1use std::pin::Pin;
5
6use bytes::Bytes;
7use futures::Stream;
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11use super::azure::protocol::OutputItem;
12use crate::azure_chatbot::azure::tools::AZURE_AI_SEARCH_TOOL_NAME;
13use crate::chatbot_error::ChatbotResult;
14use crate::llm_utils::APIInputMessage;
15use crate::prelude::*;
16
17pub(super) fn stream_event_for(value: StreamItem) -> Option<ChatbotChatStreamEvent> {
20 let (item, finished) = match value {
21 StreamItem::ServerToolOutput { call_id } => return Some(finished_tool_call(call_id)),
22 StreamItem::Received { item, finished } => (item, finished),
23 };
24 Some(match item {
25 OutputItem::Reasoning { id, .. } => ChatbotChatStreamEvent::Reasoning {
26 finished,
27 reasoning_id: id,
28 },
29 OutputItem::AzureAiSearchCall {
30 arguments, call_id, ..
31 } => ChatbotChatStreamEvent::ToolCall {
32 tool_name: Some(AZURE_AI_SEARCH_TOOL_NAME.to_string()),
33 arguments: Some(arguments),
34 tool_call_id: call_id,
35 finished,
36 },
37 OutputItem::FunctionCall {
40 tool_name,
41 arguments,
42 call_id,
43 ..
44 } => ChatbotChatStreamEvent::ToolCall {
45 tool_name: Some(tool_name),
46 arguments: Some(arguments),
47 tool_call_id: call_id,
48 finished: false,
49 },
50 OutputItem::AzureAiSearchCallOutput { call_id, .. } => ChatbotChatStreamEvent::ToolCall {
51 tool_name: Some(AZURE_AI_SEARCH_TOOL_NAME.to_string()),
52 arguments: None,
53 tool_call_id: call_id,
54 finished: true,
55 },
56 OutputItem::FunctionCallOutput { call_id, .. } => finished_tool_call(call_id),
57 OutputItem::Message { .. } => return None,
58 })
59}
60
61fn finished_tool_call(call_id: String) -> ChatbotChatStreamEvent {
64 ChatbotChatStreamEvent::ToolCall {
65 tool_name: None,
66 arguments: None,
67 tool_call_id: call_id,
68 finished: true,
69 }
70}
71
72#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
73#[serde(tag = "type", content = "data")]
74pub enum ChatbotChatStreamEvent {
75 Delta {
76 text: String,
77 message_id: Uuid,
78 },
79 Reasoning {
80 finished: bool,
81 reasoning_id: String,
82 },
83 ToolCall {
84 tool_name: Option<String>,
85 arguments: Option<String>,
86 tool_call_id: String,
87 finished: bool,
88 },
89 Done,
90 Suspended,
97 ActionExecuted {
101 tool_call_id: String,
102 payload: serde_json::Value,
103 },
104 Error(StreamEventError),
105}
106
107#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
108pub struct StreamEventError {
109 message: String,
110 details: Option<String>,
111}
112
113#[derive(Debug)]
118pub(super) enum TurnEvent {
119 Delta(String),
120 Refusal {
123 text: String,
124 message_id: Uuid,
125 },
126 Item(StreamItem),
127 ItemAnnounced(OutputItem),
131 Messages(Vec<APIInputMessage>),
132 Done {
134 text: String,
135 used_tokens: i32,
137 },
138 Suspended,
141}
142
143#[derive(Debug, Clone)]
145pub(super) enum StreamItem {
146 Received { item: OutputItem, finished: bool },
149 ServerToolOutput { call_id: String },
153}
154
155pub(super) fn ndjson_line(event: &ChatbotChatStreamEvent) -> ChatbotResult<Bytes> {
157 let mut line = serde_json::to_string(event)?;
158 line.push('\n');
159 Ok(Bytes::from(line))
160}
161
162pub(super) fn error_event_from_text(message: &str) -> ChatbotResult<Bytes> {
164 ndjson_line(&ChatbotChatStreamEvent::Error(StreamEventError {
165 message: message.to_string(),
166 details: None,
167 }))
168}
169
170pub(super) fn error_event_from_error(error: &ChatbotError) -> ChatbotResult<Bytes> {
173 let details = match error.azure_source() {
174 Some(source) => format!("{source}"),
175 None => error.message().to_string(),
176 };
177 ndjson_line(&ChatbotChatStreamEvent::Error(StreamEventError {
178 message: error.message().to_string(),
179 details: Some(details),
180 }))
181}
182
183pub(super) fn single_event_stream(
185 event: ChatbotChatStreamEvent,
186) -> ChatbotResult<Pin<Box<dyn Stream<Item = ChatbotResult<Bytes>> + Send>>> {
187 let line = ndjson_line(&event)?;
188 Ok(Box::pin(futures::stream::once(async move { Ok(line) })))
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
198 fn the_suspended_event_serialises_without_a_data_key() {
199 assert_eq!(
200 serde_json::to_string(&ChatbotChatStreamEvent::Suspended).unwrap(),
201 r#"{"type":"Suspended"}"#
202 );
203 }
204}