1use std::collections::HashMap;
2use std::pin::Pin;
3use std::sync::{
4 Arc,
5 atomic::{self, AtomicBool},
6};
7use std::task::{Context, Poll};
8
9use bytes::Bytes;
10use chrono::Utc;
11use futures::stream::{BoxStream, Peekable};
12use futures::{Stream, StreamExt, TryStreamExt};
13use headless_lms_base::config::ApplicationConfiguration;
14use headless_lms_models::chatbot_configurations::{ReasoningEffortLevel, VerbosityLevel};
15use headless_lms_models::chatbot_conversation_message_messages::{
16 ChatbotConversationMessageMessage, MessageRole,
17};
18use headless_lms_models::chatbot_conversation_messages::{
19 self, ChatbotConversationMessage, Message,
20};
21use pin_project::pin_project;
22use serde::{Deserialize, Serialize};
23use sqlx::PgPool;
24use tokio::{io::AsyncBufReadExt, sync::Mutex};
25use tokio_stream::wrappers::LinesStream;
26use tokio_util::io::StreamReader;
27use tracing::trace;
28use url::Url;
29use utoipa::ToSchema;
30
31use crate::chatbot_error::ChatbotResult;
32use crate::chatbot_tools::provider_tools::azure_ai_search::get_azure_ai_search_tool_definition;
33use crate::chatbot_tools::{
34 AzureLLMToolDefinition, call_chatbot_tool, get_chatbot_tool_definitions,
35};
36use crate::citations::chatbot_cited_documents_to_citations;
37use crate::llm_utils::{
38 APIInputMessage, APIOutputMessage, MessageContent, estimate_tokens, get_params_for_model,
39 make_streaming_llm_request,
40};
41
42use crate::prelude::*;
43
44pub const CONTENT_FIELD_SEPARATOR: &str = ",|||,";
45
46const ALL_EXPECTED_EVENTS: &[&str] = &[
49 "response.in_progress",
50 "response.queued",
51 "response.created",
52 "response.output_item.added",
53 "response.output_item.done",
54 "response.content_part.added",
55 "response.content_part.done",
56 "response.reasoning_summary_part.added",
58 "response.reasoning_summary_part.done",
59 "response.reasoning_summary_text.delta",
60 "response.reasoning_summary_text.done",
61 "response.reasoning_text.delta",
62 "response.reasoning_text.done",
63 "response.function_call_arguments.delta",
64 "response.function_call_arguments.done",
65 "response.custom_tool_call_input.delta",
66 "response.custom_tool_call_input.done",
67 "response.output_text.done",
68 "response.refusal.delta",
69 "response.refusal.done",
70];
71
72const SEARCH_GROUNDING_INSTRUCTION: &str = "\n\nSearch the course material with the azure_ai_search tool before answering, and ground your answer in the results with citations. Put only what you want to find in the query; the search is already limited to this course, so don't include the course name. Searching more than once is fine when it helps — to cover distinct sub-questions or angles, to refine when the first results don't answer, or when a follow-up or new instruction needs material you don't already have. When one search already answers, stop there. If you need more information about a specific document or a topic covered in it, use the document_lookup tool to retrieve the full document. Skip searching only for messages that don't need course material, like greetings or thanks. If you need more information about the course, like what pages and chapters are in it, use the course_structure tool.";
75
76enum ParsedResponseLine {
77 Event(String),
78 Data(Box<ResponseOutput>),
79}
80
81impl ParsedResponseLine {
82 pub fn parse(input: &str) -> ChatbotResult<Option<Self>> {
83 if input.starts_with("event: ") {
84 let event_type = input.trim_start_matches("event: ").to_string();
85 Ok(Some(ParsedResponseLine::Event(event_type)))
86 } else if input.starts_with("data: ") {
87 let data = input.trim_start_matches("data: ").to_string();
88 let response_output = match serde_json::from_str::<ResponseOutput>(&data) {
89 Ok(response_output) => response_output,
90 Err(e) => {
91 tracing::error!(
94 raw_line = %data,
95 error = %e,
96 "Failed to deserialize streamed response line from Azure"
97 );
98 return Err(ChatbotError::from(e));
99 }
100 };
101 Ok(Some(ParsedResponseLine::Data(Box::new(response_output))))
102 } else {
103 Ok(None)
104 }
105 }
106}
107
108pub struct ChatbotUserContext {
111 pub user_id: Option<Uuid>,
112 pub course_id: Option<Uuid>,
113 pub course_name: Option<String>,
114}
115
116#[derive(Deserialize, Serialize, Debug)]
117pub struct ContentFilterResults {
118 pub hate: Option<ContentFilter>,
119 pub self_harm: Option<ContentFilter>,
120 pub sexual: Option<ContentFilter>,
121 pub violence: Option<ContentFilter>,
122 }
124
125#[derive(Deserialize, Serialize, Debug)]
126pub struct ContentFilter {
127 pub blocked: bool,
128 pub source_type: ContentFilterSource,
129 pub content_filter_results: Vec<ContentFilterResults>,
130}
131#[derive(Deserialize, Serialize, Debug)]
132pub struct ContentFilterResult {
133 pub filtered: bool,
134 pub severity: String,
135}
136
137#[derive(Deserialize, Serialize, Debug)]
138#[serde(rename_all = "snake_case")]
139pub enum ContentFilterSource {
140 Prompt,
141 Completion,
142}
143
144#[derive(Deserialize, Serialize, Debug)]
146pub struct Response {
147 pub id: Option<String>,
148 pub error: Option<ResponseError>,
149}
150
151#[derive(Deserialize, Serialize, Debug, Clone)]
154pub struct ResponseError {
155 pub code: Option<String>,
156 pub message: Option<String>,
157 #[serde(rename = "type")]
158 pub error_type: Option<String>,
159 pub param: Option<String>,
160}
161
162impl std::fmt::Display for ResponseError {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 write!(
165 f,
166 "{}: {} (code: {}, param: {})",
167 self.error_type.as_deref().unwrap_or("Error"),
168 self.message.as_deref().unwrap_or("unknown error"),
169 self.code.as_deref().unwrap_or("none"),
170 self.param.as_deref().unwrap_or("none")
171 )
172 }
173}
174
175#[derive(Deserialize, Serialize, Debug)]
177pub struct IncompleteResponse {
178 pub id: String,
179 pub incomplete_details: IncompleteReason,
180 pub content_filters: Vec<ContentFilter>,
181}
182
183#[derive(Deserialize, Serialize, Debug)]
185pub struct IncompleteReason {
186 pub reason: String,
187}
188
189#[derive(Deserialize, Serialize, Debug)]
191pub struct ResponseOutput {
192 #[serde(rename = "type")]
196 pub response_type: Option<String>, pub delta: Option<String>,
198 pub item: Option<OutputItem>,
199 pub response: Option<Response>,
200 pub incomplete_response: Option<IncompleteResponse>,
201 pub error: Option<ResponseError>,
202}
203
204#[derive(Deserialize, Serialize, Debug, Clone)]
205#[serde(tag = "type")]
206#[serde(rename_all = "snake_case")]
207pub enum OutputItem {
208 Message {
209 response_id: String,
210 role: MessageRole,
211 content: MessageContent,
212 #[serde(skip_serializing_if = "Option::is_none")]
214 phase: Option<MessagePhase>,
215 },
216 Reasoning {
217 response_id: String,
218 id: String,
219 summary: Vec<ReasoningOutput>,
220 },
221 AzureAiSearchCall {
222 response_id: String,
223 call_id: String,
224 arguments: String,
226 },
227 AzureAiSearchCallOutput {
228 response_id: String,
229 call_id: String,
230 output: String,
232 },
233 FunctionCall {
234 response_id: String,
235 call_id: String,
236 #[serde(rename = "name")]
237 tool_name: String,
238 arguments: String,
240 },
241 FunctionCallOutput {
242 response_id: String,
243 call_id: String,
244 output: String,
245 },
246}
247
248#[derive(Deserialize, Serialize, Debug, Clone)]
251#[serde(rename_all = "snake_case")]
252pub enum MessagePhase {
253 Commentary,
254 FinalAnswer,
255}
256
257impl From<StreamItem> for ChatbotChatStreamEvent {
258 fn from(value: StreamItem) -> Self {
259 match value {
260 StreamItem {
261 item: OutputItem::Reasoning { id, .. },
262 finished,
263 } => ChatbotChatStreamEvent::Reasoning {
264 finished,
265 reasoning_id: id,
266 },
267 StreamItem {
268 item:
269 OutputItem::AzureAiSearchCall {
270 arguments, call_id, ..
271 },
272 finished,
273 } => ChatbotChatStreamEvent::ToolCall {
274 tool_name: Some("azure_ai_search".to_string()),
275 arguments: Some(arguments),
276 tool_call_id: call_id,
277 finished,
278 },
279 StreamItem {
280 item:
281 OutputItem::FunctionCall {
282 tool_name,
283 arguments,
284 call_id,
285 ..
286 },
287 ..
288 } => ChatbotChatStreamEvent::ToolCall {
289 tool_name: Some(tool_name),
290 arguments: Some(arguments),
291 tool_call_id: call_id,
292 finished: false,
293 },
294 StreamItem {
295 item: OutputItem::AzureAiSearchCallOutput { call_id, .. },
296 ..
297 } => ChatbotChatStreamEvent::ToolCall {
298 tool_name: Some("azure_ai_search".to_string()),
299 arguments: None,
300 tool_call_id: call_id,
301 finished: true,
302 },
303 StreamItem {
304 item: OutputItem::FunctionCallOutput { call_id, .. },
305 ..
306 } => ChatbotChatStreamEvent::ToolCall {
307 tool_name: None,
310 arguments: None,
311 tool_call_id: call_id,
312 finished: true,
313 },
314 StreamItem {
315 item: OutputItem::Message { .. },
316 ..
317 } => ChatbotChatStreamEvent::Invalid,
318 }
319 }
320}
321
322#[derive(Deserialize, Serialize, Debug, Clone)]
323#[serde(tag = "type")]
324#[serde(rename_all = "snake_case")]
325pub enum InputItem {
326 Message {
327 role: MessageRole,
328 content: MessageContent,
329 },
330 FunctionCall {
331 call_id: String,
332 #[serde(rename = "name")]
333 tool_name: String,
334 arguments: String,
335 },
336 FunctionCallOutput {
337 call_id: String,
338 output: String,
339 },
340 Reasoning {
341 id: String,
342 summary: Vec<ReasoningOutput>,
343 },
344}
345
346#[derive(Deserialize, Serialize, Debug, Clone)]
347pub struct AISearchOutput {
348 pub get_urls: Vec<Url>,
349}
350
351#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
352#[serde(rename_all = "snake_case")]
353pub enum LLMToolChoice {
354 Auto,
355 None,
356}
357
358#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
359pub struct ThinkingParams {
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub reasoning: Option<Reasoning>,
362}
363
364#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
365pub struct RequestTextOptions {
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub verbosity: Option<VerbosityLevel>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub format: Option<LLMRequestResponseFormatParam>,
370}
371#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
372pub struct Reasoning {
373 pub effort: ReasoningEffortLevel,
374 pub summary: Option<SummaryType>,
376}
377
378#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
379#[serde(untagged)]
380pub enum SummaryType {
381 Concise,
382 Detailed,
383 Auto,
384}
385
386#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
387pub struct ReasoningOutput {
388 #[serde(rename = "type")]
389 pub output_type: String, pub text: String,
391}
392
393#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
394pub struct NonThinkingParams {
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub temperature: Option<f32>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub top_p: Option<f32>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 pub frequency_penalty: Option<f32>,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 pub presence_penalty: Option<f32>,
403}
404
405#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
406pub struct MistralParams {
407 pub test: bool,
409}
410
411#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
412#[serde(untagged)]
413pub enum LLMRequestParams {
414 GPTThinking(ThinkingParams),
415 GPTNonThinking(NonThinkingParams),
416 Mistral(MistralParams),
417}
418
419#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
420#[serde(rename_all = "snake_case")]
421pub enum JSONType {
422 JsonSchema,
423 Object,
424 Array,
425 String,
426}
427
428#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
430#[serde(rename_all = "camelCase")]
431pub struct Schema {
432 #[serde(rename = "type")]
433 pub type_field: JSONType,
435 pub properties: HashMap<String, SchemaPropertyType>,
436 pub required: Vec<String>,
438 pub additional_properties: bool,
440}
441
442#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
443#[serde(untagged)]
444pub enum SchemaPropertyType {
445 ArrayProperty(ArrayProperty),
446 Object(Schema),
447 Item(JsonItem),
448}
449
450#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
451pub struct ArrayProperty {
452 #[serde(rename = "type")]
453 pub type_field: JSONType,
454 pub items: ArrayItem,
455}
456
457#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
458#[serde(untagged)]
459pub enum ArrayItem {
460 Schema(Schema),
461 JsonItem(JsonItem),
462}
463
464#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
465pub struct JsonItem {
466 #[serde(rename = "type")]
467 pub type_field: JSONType,
468}
469
470#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
471pub struct LLMRequestResponseFormatParam {
472 #[serde(rename = "type")]
473 pub format_type: JSONType, pub name: String,
475 pub schema: Schema,
476 pub strict: bool, }
478
479#[derive(Serialize, Deserialize, Debug, Clone)]
480pub struct LLMRequest {
481 pub input: Vec<APIInputMessage>,
482 pub model: String,
483 #[serde(skip_serializing_if = "Vec::is_empty", default)]
484 pub tools: Vec<AzureLLMToolDefinition>,
485 #[serde(skip_serializing_if = "Option::is_none")]
486 pub tool_choice: Option<LLMToolChoice>,
487 #[serde(skip_serializing_if = "Option::is_none")]
488 pub parallel_tool_calls: Option<bool>,
489 #[serde(skip_serializing_if = "Option::is_none")]
490 pub max_output_tokens: Option<i32>,
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub text: Option<RequestTextOptions>,
493 #[serde(flatten)]
494 pub params: LLMRequestParams,
495}
496
497impl LLMRequest {
498 pub async fn build_and_insert_incoming_user_message_to_db(
499 conn: &mut PgConnection,
500 chatbot_configuration_id: Uuid,
501 conversation_id: Uuid,
502 message: &str,
503 app_config: &ApplicationConfiguration,
504 ) -> ChatbotResult<(Self, i32)> {
505 let configuration =
506 models::chatbot_configurations::get_by_id(conn, chatbot_configuration_id).await?;
507
508 let model = models::chatbot_configurations_models::get_by_chatbot_configuration_id(
509 conn,
510 chatbot_configuration_id,
511 )
512 .await?;
513
514 let conversation_messages =
515 models::chatbot_conversation_messages::get_by_conversation_id(conn, conversation_id)
516 .await?;
517
518 let new_order_number = conversation_messages
519 .iter()
520 .map(|m| m.order_number)
521 .max()
522 .unwrap_or(0)
523 + 1;
524
525 let new_message = models::chatbot_conversation_messages::insert(
526 conn,
527 ChatbotConversationMessage {
528 id: Uuid::new_v4(),
529 order_number: new_order_number,
530 created_at: Utc::now(),
531 updated_at: Utc::now(),
532 deleted_at: None,
533 conversation_id,
534 message: Message::Text(ChatbotConversationMessageMessage {
535 text: message.to_string(),
536 message_role: MessageRole::User,
537 message_is_complete: true,
538 used_tokens: estimate_tokens(message),
539 ..Default::default()
540 }),
541 },
542 )
543 .await?;
544
545 let mut api_chat_messages: Vec<APIInputMessage> = conversation_messages
546 .into_iter()
547 .filter_map(|m| match m.message {
548 Message::Reasoning(..) => None,
549 _ => Some(APIInputMessage::try_from(m)),
550 })
551 .collect::<ChatbotResult<Vec<_>>>()?;
552
553 api_chat_messages.push(new_message.clone().try_into()?);
555
556 let mut system_prompt = configuration.prompt.clone();
557 if configuration.use_azure_search {
558 system_prompt.push_str(SEARCH_GROUNDING_INSTRUCTION);
559 }
560
561 api_chat_messages.insert(
562 0,
563 APIInputMessage {
564 message_type: InputItem::Message {
565 role: MessageRole::System,
566 content: MessageContent::Text(system_prompt),
567 },
568 },
569 );
570
571 let mut tools = if configuration.use_tools {
572 get_chatbot_tool_definitions()
573 } else {
574 Vec::new()
575 };
576
577 if configuration.use_azure_search {
578 tools.extend(vec![AzureLLMToolDefinition::Search(
579 get_azure_ai_search_tool_definition(
580 app_config,
581 configuration.course_id.ok_or_else(|| {
582 chatbot_err!(Other, "Course id is missing from the chatbot configuration")
583 })?,
584 configuration.use_semantic_reranking,
585 )?,
586 )]);
587 };
588
589 let tool_choice = if configuration.use_azure_search || configuration.use_tools {
590 Some(LLMToolChoice::Auto)
591 } else {
592 None
593 };
594
595 let serialized_messages = serde_json::to_string(&api_chat_messages)?;
596 let request_estimated_tokens = estimate_tokens(&serialized_messages);
597
598 let params = get_params_for_model(&model.model, &model.model_type, Some(&configuration));
599
600 Ok((
601 Self {
602 input: api_chat_messages,
603 model: model.model,
604 max_output_tokens: Some(configuration.max_output_tokens),
605 tools,
606 tool_choice,
607 parallel_tool_calls: Some(true),
608 text: Some(RequestTextOptions {
609 verbosity: Some(configuration.verbosity),
610 format: None,
611 }),
612 params,
613 },
614 request_estimated_tokens,
615 ))
616 }
617}
618
619#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
620#[serde(tag = "type", content = "data")]
621pub enum ChatbotChatStreamEvent {
622 Delta {
623 text: String,
624 message_id: Uuid,
625 },
626 Reasoning {
627 finished: bool,
628 reasoning_id: String,
629 },
630 ToolCall {
631 tool_name: Option<String>,
632 arguments: Option<String>,
633 tool_call_id: String,
634 finished: bool,
635 },
636 Done,
637 Error(StreamEventError),
638 Invalid,
642}
643
644#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
645pub struct StreamEventError {
646 message: String,
647 details: Option<String>,
648}
649
650#[pin_project]
652struct GuardedStream<S> {
653 guard: RequestCancelledGuard,
654 #[pin]
655 stream: S,
656}
657
658impl<S> GuardedStream<S> {
659 fn new(guard: RequestCancelledGuard, stream: S) -> Self {
660 Self { guard, stream }
661 }
662}
663
664impl<S> Stream for GuardedStream<S>
665where
666 S: Stream<Item = ChatbotResult<Bytes>> + Send,
667{
668 type Item = S::Item;
669
670 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
671 let this = self.project();
672 let polled = this.stream.poll_next(cx);
673 if let Poll::Ready(Some(Err(error))) = &polled {
676 error!("Chatbot response stream error:\n{error:?}");
677 }
678 polled
679 }
680}
681
682type PeekableLinesStream<'a> = Pin<
685 Box<Peekable<LinesStream<StreamReader<BoxStream<'a, Result<Bytes, std::io::Error>>, Bytes>>>>,
686>;
687pub enum ResponseStreamType<'a> {
688 Toolcall(PeekableLinesStream<'a>),
689 TextResponse(PeekableLinesStream<'a>),
690}
691
692struct RequestCancelledGuard {
693 response_message_id: Arc<Mutex<Uuid>>,
694 received_string: Arc<Mutex<Vec<String>>>,
695 pool: PgPool,
696 done: Arc<AtomicBool>,
697 request_estimated_tokens: i32,
698}
699
700impl Drop for RequestCancelledGuard {
701 fn drop(&mut self) {
702 if self.done.load(atomic::Ordering::Relaxed) {
703 return;
704 }
705 warn!("Request was not cancelled. Cleaning up.");
706 let response_message_id = self.response_message_id.clone();
707 let received_string = self.received_string.clone();
708 let pool = self.pool.clone();
709 let request_estimated_tokens = self.request_estimated_tokens;
710 tokio::spawn(async move {
711 info!("Verifying the received message has been handled");
712 let mut conn = pool.acquire().await.expect("Could not acquire connection");
713 let full_response_text = received_string.lock().await;
714 let id = response_message_id.lock().await.to_owned();
715 if full_response_text.is_empty() {
716 info!("No response received. Deleting the response message");
717 models::chatbot_conversation_messages::delete(&mut conn, id)
718 .await
719 .expect("Could not delete response message");
720 return;
721 }
722 info!("Response received but not completed. Saving the text received so far.");
723 let full_response_as_string = full_response_text.join("");
724 let estimated_cost = estimate_tokens(&full_response_as_string);
725 info!(
726 "End of chatbot response stream. Estimated cost: {}. Response: {}",
727 estimated_cost, full_response_as_string
728 );
729
730 models::chatbot_conversation_message_messages::update(
732 &mut conn,
733 id,
734 &full_response_as_string,
735 true,
736 request_estimated_tokens + estimated_cost,
737 )
738 .await
739 .expect("Could not update response message");
740 });
741 }
742}
743
744pub async fn process_output_item(
748 conn: &mut PgConnection,
749 item: OutputItem,
750 conversation_id: Uuid,
751 app_config: &ApplicationConfiguration,
752) -> ChatbotResult<ChatbotConversationMessage> {
753 match item {
754 OutputItem::AzureAiSearchCall { .. } | OutputItem::Reasoning { .. } => {
755 let message = APIOutputMessage { message_type: item }
756 .to_chatbot_conversation_message(conversation_id)?;
757
758 ChatbotResult::Ok(chatbot_conversation_messages::insert(conn, message).await?)
759 }
760 OutputItem::AzureAiSearchCallOutput {
761 call_id,
762 output,
763 response_id,
764 } => {
765 let search_output: AISearchOutput = serde_json::from_str(&output)?;
766 let api_key = if let Some(azure_config) = &app_config.azure_configuration
767 && let Some(search_config) = &azure_config.search_config
768 {
769 &search_config.search_api_key
770 } else {
771 return ChatbotResult::Err(chatbot_err!(
772 Other,
773 "Azure search configuration not found, cannot process Azure AI search output item.".to_string()
774 ));
775 };
776 let get_urls = search_output.get_urls.to_owned();
777
778 let message = APIOutputMessage {
779 message_type: OutputItem::AzureAiSearchCallOutput {
780 call_id,
781 output,
782 response_id: response_id.to_owned(),
783 },
784 }
785 .to_chatbot_conversation_message(conversation_id)?;
786
787 let conversation_message = chatbot_conversation_messages::insert(conn, message).await?;
788
789 let res = chatbot_cited_documents_to_citations(
790 conn,
791 app_config.test_chatbot,
792 get_urls,
793 api_key,
794 conversation_message.id,
795 conversation_id,
796 )
797 .await;
798
799 if let Err(e) = res {
800 error!(
801 "Failed to save cited documents in the DB. Response id: {response_id} Error: {e}"
802 );
803 };
804
805 ChatbotResult::Ok(conversation_message)
806 }
807 OutputItem::Message { ref content, .. } => {
808 if let MessageContent::Refusal(..) = content {
809 let message = APIOutputMessage {
810 message_type: item.clone(),
811 }
812 .to_chatbot_conversation_message(conversation_id)?;
813
814 ChatbotResult::Ok(chatbot_conversation_messages::insert(conn, message).await?)
815 } else {
816 Err(chatbot_err!(
818 StreamingError,
819 "Unexpected message output item, it should have been streamed.".to_string()
820 ))
821 }
822 }
823 OutputItem::FunctionCall { .. } => {
824 Err(chatbot_err!(
826 StreamingError,
827 "Unexpected function call output item, it should have been processed.".to_string()
828 ))
829 }
830 OutputItem::FunctionCallOutput { .. } => {
831 Err(chatbot_err!(
835 StreamingError,
836 "Unexpected function call output item, this shouldn't happen.".to_string()
837 ))
838 }
839 }
840}
841
842async fn parse_tool<'a>(
847 conn: &'a mut PgConnection,
848 mut lines: PeekableLinesStream<'a>,
849 conversation_id: Uuid,
850 user_context: &'a ChatbotUserContext,
851) -> BoxStream<'a, ChatbotResult<StreamEvent<'a>>> {
852 let mut function_name_id_args: Vec<(String, String, String)> = vec![];
853 let mut messages = vec![];
854 let mut common_response_id: Option<String> = None;
855 let mut response_received = false;
856
857 trace!("Parsing tool calls...");
858
859 Box::pin(async_stream::try_stream! {
860 while let Some(val) = lines.next().await {
861 let line = val?;
862 let response_output: ResponseOutput = match ParsedResponseLine::parse(&line)? {
863 Some(ParsedResponseLine::Event(event_type)) => {
864 trace!("Event: {event_type}");
865 match event_type.as_str() {
866 "response.completed" | "response.incomplete" => {
867 response_received = true;
868 }
869 "response.output_text.delta" => {
870 Err(chatbot_err!(StreamingError,
871 "Error: Received response text while parsing tool calls. Either the tool call parsing failed or the LLM responded in an unexpected way."
872 ))?
873 }
874 "response.error" | "response.failed" | "error" => {
875 }
877 _ => {
878 if !ALL_EXPECTED_EVENTS.contains(&event_type.as_str()) {
879 warn!("Received unexpected event from Azure: Event: {}", event_type);
880 };
881 }
882 };
883 continue;
884 }
885 Some(ParsedResponseLine::Data(data)) => *data,
886 None => {
887 continue;
888 }
889 };
890
891 if let Some(response) = response_output.response
894 && let Some(err) = response.error
895 {
896 let mut error = chatbot_err!(
897 StreamingError,
898 format!("Error received from Azure API. Response id: {}", response.id.as_deref().unwrap_or("not received"))
899 );
900 error.add_azure_source(err);
901 Err(error)?
902 };
903 if let Some(err) = response_output.error {
905 let mut error = chatbot_err!(
906 StreamingError,
907 format!("Error received from Azure API. Response id: {}", common_response_id.as_deref().unwrap_or("not received"))
908 );
909 error.add_azure_source(err);
910 Err(error)?
911
912 };
913
914 if response_received {
915 if let Some(response) = &response_output.incomplete_response {
917 Err(chatbot_err!(StreamingError,
919 format!("The LLM response is incomplete. Reason: {}", response.incomplete_details.reason)
920 ))?
921 };
922 if function_name_id_args.is_empty() {
923 Err(chatbot_err!(StreamingError,
924 "The LLM response was supposed to contain function calls, but no function calls were found"
925 ))?
926 }
927 let Some(response_id) = &common_response_id else {
928 Err(chatbot_err!(StreamingError,
929 "Received tool response but response id not found, this shouldn't happen."
930 ))?
931 };
932
933 for (name, id, args) in function_name_id_args.into_iter() {
934 let mut tx = conn.begin().await.map_err(ChatbotError::from)?;
935 let tool_result = call_chatbot_tool(&mut tx, &name, args, user_context).await?;
936
937 let tool_call_message = APIOutputMessage {
938 message_type: OutputItem::FunctionCall {
939 response_id: response_id.to_owned(),
940 call_id: id.to_owned(),
941 tool_name: name.to_owned(),
942 arguments: tool_result.arguments,
943 },
944 };
945 chatbot_conversation_messages::insert(
946 &mut tx,
947 tool_call_message.to_chatbot_conversation_message(conversation_id)?,
948 )
949 .await?;
950
951 let function_call_output = OutputItem::FunctionCallOutput {
952 call_id: id.to_owned(),
953 output: tool_result.output,
954 response_id: response_id.to_owned(),
955 };
956 let output_message = APIOutputMessage {
957 message_type: function_call_output.to_owned(),
958 };
959 chatbot_conversation_messages::insert(
960 &mut tx,
961 output_message.to_chatbot_conversation_message(conversation_id)?,
962 )
963 .await?;
964 tx.commit().await.map_err(ChatbotError::from)?;
965
966 messages.extend([tool_call_message, output_message]);
967
968 yield StreamEvent::Item(StreamItem {
969 item: function_call_output,
970 finished: true,
971 });
972 }
973
974 let input_messages = messages.into_iter().map(APIInputMessage::from).collect::<Vec<APIInputMessage>>();
975 yield StreamEvent::Messages(input_messages);
976 break;
977 } else if let Some(item) = response_output.item {
978 match item.to_owned() {
979 OutputItem::FunctionCall {
980 call_id,
981 tool_name,
982 arguments,
983 response_id,
984 } => {
985 common_response_id = Some(response_id);
986 function_name_id_args.push((
987 tool_name,
988 call_id,
989 arguments,
990 ));
991 yield StreamEvent::Item(StreamItem { item, finished: false });
992 }
993 OutputItem::Message { content, .. } => {
994 if let MessageContent::Refusal(..) = content {
995 yield StreamEvent::Refusal(content.get_content_text());
996 messages.push(APIOutputMessage { message_type: item });
997
998 } else {
999 Err(chatbot_err!(
1000 StreamingError,
1001 "Error: unexpected message item !!!".to_string()
1002 ))?}
1003 },
1004 _ => {
1005 let finished = response_output.response_type.as_deref() == Some("response.output_item.done");
1006 yield StreamEvent::Item(StreamItem { item: item.to_owned(), finished});
1007
1008 messages.push(APIOutputMessage { message_type: item });
1011 }
1012 }
1013 }
1014 }})
1015}
1016
1017fn stream_and_detect_response_stream_type<'a>(
1022 mut lines: PeekableLinesStream<'a>,
1023) -> impl Stream<Item = ChatbotResult<StreamEvent<'a>>> {
1024 let mut response_id: Option<String> = None;
1025 let mut response_created_incoming = false;
1026 let mut error_incoming = false;
1027 let mut output_item_added = false;
1028 let mut output_item_done = false;
1029
1030 Box::pin(async_stream::try_stream! {
1031 loop {
1032 let line_res = lines.next().await;
1033 match line_res {
1034 None => {
1035 break;
1036 }
1037 Some(val) => {
1038 let line = val?;
1039 let response_output = match ParsedResponseLine::parse(&line)? {
1040 Some(ParsedResponseLine::Event(event_type)) => {
1041 trace!("Event: {event_type}");
1042 match event_type.as_str() {
1043 "response.created" => {
1044 response_created_incoming = true;
1045 }
1046 "response.output_item.added" => {
1047 output_item_added = true;
1048 }
1049 "response.output_item.done" => {
1050 output_item_done = true;
1051 }
1052 "response.function_call_arguments.delta" | "response.custom_tool_call_input.delta" => {
1053 if let Some(id) = &response_id {
1054 yield StreamEvent::ResponseIdStream((
1055 id.to_string(),
1056 ResponseStreamType::Toolcall(lines),
1057 ));
1058 break;
1059 } else {
1060 Err(chatbot_err!(StreamingError,
1061 "No response_id found! This should never happen!"
1062 ))?;
1063 };
1064 }
1065 "response.output_text.delta" | "response.refusal.delta" => {
1066 if let Some(id) = &response_id {
1067 yield StreamEvent::ResponseIdStream((
1068 id.to_string(),
1069 ResponseStreamType::TextResponse(lines),
1070 ));
1071 break;
1072 } else {
1073 Err(chatbot_err!(StreamingError,
1074 "No response_id found! This should never happen!"
1075 ))?;
1076 };
1077 }
1078 "response.incomplete" => {
1079 break Err(chatbot_err!(StreamingError, format!("Response incomplete. Response id: {}", response_id.as_deref().unwrap_or("not received"))))?
1081 },
1082 "response.error" | "error" | "response.failed" => { error_incoming = true; }
1083 _ => {
1084 if !ALL_EXPECTED_EVENTS.contains(&event_type.as_str()) {
1085 warn!("Received unexpected event from Azure: Event: {}", event_type);
1086 };
1087 }
1088 }
1089 continue;
1090 }
1091 Some(ParsedResponseLine::Data(response_output)) => response_output,
1092 None => {
1093 continue;
1094 }
1095 };
1096
1097 if error_incoming {
1098 let fallback_error = chatbot_err!(StreamingError, format!("Response failed without receiving an API error. Response output: {:?} Response id: {}", &response_output, response_id.as_deref().unwrap_or("not received")));
1099
1100 if let Some(response) = response_output.response
1101 && let Some(err) = response.error {
1102 let mut error = chatbot_err!(
1103 StreamingError,
1104 format!("Error received from Azure API. Response id: {}", response_id.as_deref().unwrap_or("not received"))
1105 );
1106 error.add_azure_source(err);
1107 break Err(error)?
1108 } else if let Some(err) = response_output.error {
1109 let mut error = chatbot_err!(
1110 StreamingError,
1111 format!("Error received from Azure API. Response id: {}", response_id.as_deref().unwrap_or("not received"))
1112 );
1113 error.add_azure_source(err);
1114 break Err(error)?
1115 } else {
1116 break Err(fallback_error)?
1117 };
1118 };
1119 if response_created_incoming {
1120 let res = response_output.response.ok_or(chatbot_err!(
1121 DeserializationError,
1122 "Expected response object"
1123 ))?;
1124 response_id = res.id;
1125 response_created_incoming = false;
1126 }
1127 if output_item_added {
1128 let item = response_output.item.ok_or(chatbot_err!(
1129 DeserializationError,
1130 "Expected response output item"
1131 ))?;
1132 yield StreamEvent::Item(StreamItem {item, finished: false});
1133 output_item_added = false;
1134 }
1135 else if output_item_done {
1136 let item = response_output.item.ok_or(chatbot_err!(
1137 DeserializationError,
1138 "Expected response output item"
1139 ))?;
1140 yield StreamEvent::Item(StreamItem {item, finished: true});
1141 output_item_done = false;
1142 }
1143 }
1144 }
1145 continue;
1146 }
1147 Err(chatbot_err!(StreamingError, format!(
1148 "The response received from Azure ended unexpectedly. Response id: {}", response_id.as_deref().unwrap_or("not received")
1149 )))?
1150 })
1151}
1152
1153async fn parse_text_response<'a>(
1158 conn: &'a mut PgConnection,
1159 mut lines: PeekableLinesStream<'a>,
1160 full_response_text: Arc<Mutex<Vec<String>>>,
1161 done: Arc<AtomicBool>,
1162 response_message: ChatbotConversationMessage,
1163 request_estimated_tokens: i32,
1164 response_id: String,
1165) -> BoxStream<'a, ChatbotResult<StreamEvent<'a>>> {
1166 trace!("Parsing stream to user...");
1167
1168 let mut response_received = false;
1169
1170 Box::pin(async_stream::try_stream! {
1171 while let Some(val) = lines.next().await {
1172 let line = val?;
1173 let response_output: ResponseOutput = match ParsedResponseLine::parse(&line)? {
1174 Some(ParsedResponseLine::Event(event_type)) => {
1175 trace!("Event: {event_type}");
1176 match event_type.as_str() {
1177 "response.completed" | "response.incomplete" => {response_received = true;},
1178 "response.output_text.delta" | "response.refusal.delta" => {
1179 },
1181 "response.function_call_arguments.delta" | "response.custom_tool_call_input.delta" => {
1182 error!("ERROR, function call received but can't be processed while streaming to user.");
1183 return Err(chatbot_err!(StreamingError, "Unexpected function call while streaming to user"))?
1184 },
1185 "response.error" | "error" | "response.failed" => {
1186 }
1188 _ => {
1189 if !ALL_EXPECTED_EVENTS.contains(&event_type.as_str()) {
1190 warn!("Received unexpected event from Azure: Event: {}", event_type);
1191 };
1192 }
1193 };
1194 continue;
1195 },
1196 Some(ParsedResponseLine::Data(data)) => *data,
1197 None => {continue;},
1198 };
1199
1200 if let Some(response) = response_output.response
1203 && let Some(err) = response.error {
1204 let mut error = chatbot_err!(
1205 StreamingError,
1206 format!("Error received from Azure API. Response id: {}", &response_id)
1207 );
1208 error.add_azure_source(err);
1209 Err(error)?
1210 } else if let Some(err) = response_output.error {
1212 let mut error = chatbot_err!(
1213 StreamingError,
1214 format!("Error received from Azure API. Response id: {}", &response_id)
1215 );
1216 error.add_azure_source(err);
1217 Err(error)?
1218 };
1219
1220 let mut full_response_text = full_response_text.lock().await;
1221
1222 if response_received {
1223 if let Some(response) = &response_output.incomplete_response {
1224 Err(chatbot_err!(StreamingError,
1226 format!("The LLM response is incomplete. Reason: {}", response.incomplete_details.reason)
1227 ))?
1228 };
1229 let full_response_as_string = full_response_text.join("");
1230 let estimated_cost = estimate_tokens(&full_response_as_string);
1232 trace!(
1233 "End of chatbot response stream. Estimated cost: {}. Response: {}",
1234 estimated_cost, full_response_as_string
1235 );
1236 models::chatbot_conversation_messages::update(
1237 conn,
1238 response_message.id,
1239 &full_response_as_string,
1240 true,
1241 request_estimated_tokens + estimated_cost,
1242 ).await?;
1243
1244 done.store(true, atomic::Ordering::Relaxed);
1245 yield StreamEvent::Done;
1246 break;
1247 }
1248
1249 if let Some(delta) = &response_output.delta {
1250 full_response_text.push(delta.to_owned());
1251 yield StreamEvent::Delta(delta.clone());
1252 }
1253
1254 if let Some(item) = &response_output.item {
1255 match item {
1256 OutputItem::Message { .. } => continue,
1257 OutputItem::FunctionCall { .. } => Err(chatbot_err!(StreamingError, "Error: unexpected function call after / during a text response.".to_string()))?,
1258 _ => {
1259 let finished = response_output.response_type.as_deref() == Some("response.output_item.done");
1260 yield StreamEvent::Item(StreamItem { item: item.to_owned(), finished });
1261 continue;
1262 },
1263 };
1264 }
1265 }
1266 if !done.load(atomic::Ordering::Relaxed) {
1267 Err(chatbot_err!(StreamingError,"Stream ended unexpectedly"))?;
1268 }
1269 })
1270}
1271
1272enum StreamEvent<'a> {
1274 Delta(String),
1275 Refusal(String),
1276 Item(StreamItem),
1277 Messages(Vec<APIInputMessage>),
1278 ResponseIdStream((String, ResponseStreamType<'a>)),
1279 Done,
1280}
1281
1282#[derive(Deserialize, Serialize, Debug, Clone)]
1283struct StreamItem {
1284 item: OutputItem,
1286 finished: bool,
1288}
1289
1290pub async fn make_request_and_create_stream<'a>(
1292 chat_request: LLMRequest,
1293 app_config: &ApplicationConfiguration,
1294) -> ChatbotResult<PeekableLinesStream<'a>> {
1295 let response = make_streaming_llm_request(chat_request, app_config).await?;
1296
1297 trace!("Receiving chat response with {:?}", response.version());
1298
1299 if !response.status().is_success() {
1300 let status = response.status();
1301 let error_message = response.text().await?;
1302 return Err(chatbot_err!(
1303 StreamingError,
1304 format!(
1305 "Failed to send chat request. Status: {}. Error: {}",
1306 status, error_message
1307 )
1308 ));
1309 }
1310
1311 let stream = response
1312 .bytes_stream()
1313 .map_err(std::io::Error::other)
1314 .boxed();
1315 let reader = StreamReader::new(stream);
1316 let lines = reader.lines();
1317 let lines_stream = LinesStream::new(lines);
1318 let peekable_lines_stream = lines_stream.peekable();
1319 let pinned_lines = Box::pin(peekable_lines_stream);
1320
1321 Ok(pinned_lines)
1322}
1323
1324fn error_event_string_from_message(
1330 message: Option<&str>,
1331 error: Option<&ChatbotError>,
1332) -> ChatbotResult<String> {
1333 let (message, details): (&str, Option<String>) = if let Some(e) = error {
1334 let e_msg = if let Some(s) = e.azure_source() {
1335 format!("{s}")
1336 } else {
1337 e.message().to_string()
1338 };
1339 (message.unwrap_or(e.message()), Some(e_msg))
1340 } else {
1341 (
1342 message.ok_or(chatbot_err!(
1343 Other,
1344 "Called error_event_string_from_message with incorrect arguments"
1345 ))?,
1346 None,
1347 )
1348 };
1349 let err = ChatbotChatStreamEvent::Error(StreamEventError {
1350 message: message.to_string(),
1351 details,
1352 });
1353 serde_json::to_string(&err).map_err(ChatbotError::from)
1354}
1355
1356fn check_error_should_terminate_stream(err: &ChatbotErrorType) -> bool {
1359 matches!(
1360 err,
1361 ChatbotErrorType::SerdeJson
1362 | ChatbotErrorType::DeserializationError
1363 | ChatbotErrorType::SqlxError
1364 | ChatbotErrorType::ReqwestError
1365 | ChatbotErrorType::UrlParse
1366 )
1367}
1368
1369async fn answer_unfinished_tool_calls(
1370 conn: &mut PgConnection,
1371 conversation_id: Uuid,
1372) -> ChatbotResult<()> {
1373 trace!(
1374 "Dealing with unfinished tool calls for conversation {}",
1375 conversation_id
1376 );
1377 let res = headless_lms_models::chatbot_conversation_messages::answer_hanging_tool_call_messages_for_conversation(
1378 conn,
1379 conversation_id,
1380 )
1381 .await
1382 .map_err(ChatbotError::from)?;
1383 trace!("Answered {} hanging tool calls", res.len());
1384 Ok(())
1385}
1386
1387pub async fn send_chat_request_and_parse_stream(
1390 pool: PgPool,
1391 app_configuration: &ApplicationConfiguration,
1392 chatbot_configuration_id: Uuid,
1393 conversation_id: Uuid,
1394 message: &str,
1395 user_context: ChatbotUserContext,
1396) -> ChatbotResult<Pin<Box<dyn Stream<Item = ChatbotResult<Bytes>> + Send>>> {
1397 let mut conn = pool.acquire().await?;
1398 let app_config = app_configuration.to_owned();
1399 let (mut chat_request, request_estimated_tokens) =
1400 LLMRequest::build_and_insert_incoming_user_message_to_db(
1401 &mut conn,
1402 chatbot_configuration_id,
1403 conversation_id,
1404 message,
1405 &app_config,
1406 )
1407 .await?;
1408
1409 let mut max_iterations_left = 15;
1410
1411 let response_id = Arc::new(Mutex::new(String::new()));
1414
1415 let done = Arc::new(AtomicBool::new(false));
1416 let mut should_clean_tool_calls = false;
1417 let full_response_text = Arc::new(Mutex::new(Vec::new()));
1418 let response_message_id = Arc::new(Mutex::new(Uuid::nil()));
1419
1420 let guard = RequestCancelledGuard {
1422 response_message_id: response_message_id.clone(),
1423 received_string: full_response_text.clone(),
1424 pool: pool.clone(),
1425 done: done.clone(),
1426 request_estimated_tokens,
1427 };
1428
1429 let response_stream = async_stream::try_stream! {
1430 'outer: loop {
1431 let mut conn = pool.acquire().await?;
1432
1433 max_iterations_left -= 1;
1434 if max_iterations_left == 0 {
1435 error!("Maximum tool call iterations exceeded");
1436 let event_string = error_event_string_from_message(Some("Maximum tool call iterations exceeded. The LLM may be stuck in a loop."), None)?;
1437 yield Bytes::from(event_string);
1438 yield Bytes::from("\n");
1439 done.store(true, atomic::Ordering::Relaxed);
1440 break 'outer;
1441 }
1442
1443 let lines = match make_request_and_create_stream(chat_request.clone(), &app_config).await {
1444 Ok(val) => val,
1445 Err(error) => {
1446 if check_error_should_terminate_stream(error.error_type()) {
1447 break Err(error)?;
1448 };
1449 let event_string = error_event_string_from_message(None, Some(&error))?;
1450 yield Bytes::from(event_string);
1451 yield Bytes::from("\n");
1452 done.store(true, atomic::Ordering::Relaxed);
1453 break 'outer;
1454 },
1455 };
1456 let mut response_stream = stream_and_detect_response_stream_type(lines);
1457 let (received_response_id, typed_response_stream);
1458 loop {
1459 if let Some(val) = response_stream.next().await {
1460 match val {
1461 Ok(StreamEvent::ResponseIdStream(stuff)) => {
1462 (received_response_id, typed_response_stream) = stuff;
1463 break;
1464 },
1465 Ok(StreamEvent::Item(item)) => {
1466 if item.finished {
1467 let message = process_output_item(&mut conn, item.item.to_owned(), conversation_id, &app_config).await?;
1471 let input_message = APIInputMessage::try_from(message)?;
1472 chat_request.input.push(input_message);
1473
1474 }
1475 let event = ChatbotChatStreamEvent::from(item.to_owned());
1476 if event != ChatbotChatStreamEvent::Invalid {
1477 let event_string = serde_json::to_string(&event)?;
1478 yield Bytes::from(event_string);
1479 yield Bytes::from("\n");
1480 };
1481 },
1482 Ok(StreamEvent::Refusal(text)) => {
1483 error!("Chatbot refusal event encountered before response id was received.");
1486 let message_id = *response_message_id.lock().await;
1487 let event = ChatbotChatStreamEvent::Delta { text, message_id };
1488 let event_string = serde_json::to_string(&event)?;
1489 yield Bytes::from(event_string);
1490 yield Bytes::from("\n");
1491
1492 },
1493 Ok(StreamEvent::Done) => {
1494 done.store(true, atomic::Ordering::Relaxed);
1495 Err(chatbot_err!(StreamingError, "Stream ended unxpectedly."))?
1496 },
1497 Ok(StreamEvent::Messages(_)) | Ok(StreamEvent::Delta(_)) => {
1498 done.store(true, atomic::Ordering::Relaxed);
1499 Err(chatbot_err!(StreamingError, "This shouldn't happen, messages or response delta not expected."))?
1500 },
1501 Err(e) => {
1502 error!("Stream ended unexpectedly. Response id: {} Error: {}", response_id.lock().await, e);
1503 should_clean_tool_calls = true;
1504 if check_error_should_terminate_stream(e.error_type()) {
1505 if let Err(e2) = answer_unfinished_tool_calls(&mut conn, conversation_id).await {
1506 error!("Error in chatbot streaming and couldn't answer unfinished tool calls: {e2}. Response id: {}", response_id.lock().await);
1507 };
1508 return Err(e)?;
1509 };
1510 let event_string = error_event_string_from_message(None, Some(&e))?;
1511 yield Bytes::from(event_string);
1512 yield Bytes::from("\n");
1513 done.store(true, atomic::Ordering::Relaxed);
1514 break 'outer;
1515 },
1516 }}
1517 }
1518
1519 {
1520 let mut response_id = response_id.lock().await;
1522 *response_id = received_response_id;
1523 }
1524
1525 let response_message: ChatbotConversationMessage;
1527
1528 let mut final_stream = match typed_response_stream {
1529 ResponseStreamType::Toolcall(stream) => {
1530 parse_tool(&mut conn, stream, conversation_id, &user_context).await
1531 }
1532 ResponseStreamType::TextResponse(stream) => {
1533 let response_id = response_id.lock().await;
1534 response_message = models::chatbot_conversation_messages::insert(
1536 &mut conn,
1537 ChatbotConversationMessage {
1538 conversation_id,
1539 message: Message::Text(ChatbotConversationMessageMessage {
1540 text: "".to_string(),
1541 message_role: MessageRole::Assistant,
1542 message_is_complete: false,
1543 used_tokens: request_estimated_tokens,
1544 response_id: Some(response_id.to_owned()),
1545 ..Default::default()
1546 }),
1547 ..Default::default()
1548 },
1549 ).await?;
1550
1551 let mut response_message_id = response_message_id.lock().await;
1553 *response_message_id = response_message.id;
1554
1555 models::chatbot_conversation_messages_citations::update_citation_message_ids(
1557 &mut conn,
1558 response_id.to_string(),
1559 response_message.id,
1560 ).await?;
1561
1562 parse_text_response(&mut conn, stream, full_response_text.clone(), done.clone(), response_message, request_estimated_tokens, response_id.to_string()).await
1563 }
1564 };
1565
1566 let message_id = *response_message_id.lock().await;
1567 let response_id = response_id.lock().await;
1568 while let Some(line) = final_stream.next().await {
1569 let val = match line {
1570 Ok(val) => val,
1571 Err(e) => {
1572 error!("Stream ended unexpectedly. Response id: {} Error: {}", response_id.to_string(), e);
1573 let full_response_as_string = full_response_text.lock().await.join("");
1574 let mut conn = pool.acquire().await?;
1575 if !full_response_as_string.is_empty() {
1576 let estimated_cost = estimate_tokens(&full_response_as_string);
1578 models::chatbot_conversation_messages::update(
1579 &mut conn,
1580 message_id,
1581 &full_response_as_string,
1582 true,
1583 request_estimated_tokens + estimated_cost,
1584 ).await?;
1585 };
1586 should_clean_tool_calls = true;
1587 if check_error_should_terminate_stream(e.error_type()) {
1588 if let Err(e2) = answer_unfinished_tool_calls(&mut conn, conversation_id).await {
1589 error!("Error in chatbot streaming and couldn't answer unfinished tool calls: {e2}. Response id: {}", response_id.to_string());
1590 };
1591 return Err(e)?;
1592 };
1593 let event_string = error_event_string_from_message(None, Some(&e))?;
1594 yield Bytes::from(event_string);
1595 yield Bytes::from("\n");
1596 done.store(true, atomic::Ordering::Relaxed);
1597 break 'outer;
1598 }
1599 };
1600 match val {
1601 StreamEvent::Delta(text) | StreamEvent::Refusal(text) => {
1602 let delta = ChatbotChatStreamEvent::Delta { text, message_id };
1603 let delta_as_string = serde_json::to_string(&delta)?;
1604 yield Bytes::from(delta_as_string);
1605 yield Bytes::from("\n");
1606 },
1607 StreamEvent::Item(stream_item) => {
1608 match stream_item.item {
1609 OutputItem::FunctionCall { .. } | OutputItem::FunctionCallOutput { .. } => {
1610 },
1612 _ => {
1613 if stream_item.finished {
1615 let mut conn = pool.acquire().await?;
1616 process_output_item(&mut conn, stream_item.item.to_owned(), conversation_id, &app_config).await?;
1617 }
1618 },
1619 };
1620
1621 let response = ChatbotChatStreamEvent::from(stream_item);
1622 if response != ChatbotChatStreamEvent::Invalid {
1623 let event_string = serde_json::to_string(&response)?;
1624 yield Bytes::from(event_string);
1625 yield Bytes::from("\n");
1626 };
1627 },
1628 StreamEvent::Messages(messages) => {
1629 chat_request.input.extend(messages);
1630 },
1631 StreamEvent::Done => {
1632 let event = ChatbotChatStreamEvent::Done;
1633 let event_string = serde_json::to_string(&event)?;
1634 yield Bytes::from(event_string);
1635 yield Bytes::from("\n");
1636 break 'outer;
1637 }
1638 StreamEvent::ResponseIdStream(..) => {
1639 done.store(true, atomic::Ordering::Relaxed);
1640 Err(chatbot_err!(StreamingError, "This shouldn't happen, response stream received while already streaming a response stream to user."))?
1641 },
1642 }
1643 }
1644 }
1645 if should_clean_tool_calls { answer_unfinished_tool_calls(&mut conn, conversation_id).await?;}
1646
1647 if !done.load(atomic::Ordering::Relaxed) {
1648 let id = response_id.lock().await;
1649 let event_string = error_event_string_from_message(Some(format!("Stream ended unexpectedly. Response id: {id}").as_str()), None)?;
1650 yield Bytes::from(event_string);
1651 yield Bytes::from("\n");
1652 }
1653 };
1654
1655 let guarded_stream = GuardedStream::new(guard, response_stream);
1658
1659 Ok(Box::pin(guarded_stream))
1661}
1662
1663#[cfg(test)]
1664mod tests {
1665 use super::*;
1666
1667 #[test]
1670 fn response_failed_with_error_object_deserializes() {
1671 let line = r#"{"type":"response.failed","response":{"id":"resp_abc","status":"failed","error":{"code":"tool_user_error","message":"Could not complete vectorization action."}},"sequence_number":8}"#;
1672
1673 let parsed: ResponseOutput = serde_json::from_str(line).unwrap();
1674 let error = parsed
1675 .response
1676 .expect("response object")
1677 .error
1678 .expect("error object");
1679
1680 assert_eq!(error.code.as_deref(), Some("tool_user_error"));
1681 assert!(error.message.unwrap().contains("vectorization"));
1682 }
1683
1684 #[test]
1686 fn azure_ai_search_output_items_deserialize() {
1687 let call = r#"{"type":"azure_ai_search_call","id":"fc_1","response_id":"resp_abc","call_id":"call_1","arguments":"{\"query\":\"trademarks\"}","status":"completed"}"#;
1688 match serde_json::from_str::<OutputItem>(call).unwrap() {
1689 OutputItem::AzureAiSearchCall { arguments, .. } => {
1690 assert!(arguments.contains("trademarks"))
1691 }
1692 other => panic!("expected AzureAiSearchCall, got {other:?}"),
1693 }
1694
1695 let output = r#"{"type":"azure_ai_search_call_output","id":"fco_1","response_id":"resp_abc","call_id":"call_1","output":"remote tool call failed","status":"in_progress"}"#;
1696 match serde_json::from_str::<OutputItem>(output).unwrap() {
1697 OutputItem::AzureAiSearchCallOutput { output, .. } => {
1698 assert_eq!(output, "remote tool call failed")
1699 }
1700 other => panic!("expected AzureAiSearchCallOutput, got {other:?}"),
1701 }
1702 }
1703}