1use std::pin::Pin;
4use std::sync::Arc;
5use std::time::Duration;
6
7use futures::stream::{Stream, StreamExt};
8use reqwest_eventsource::{Event, EventSource};
9use serde::Serialize;
10use tokio::sync::RwLock;
11use tracing::{debug, error, info, warn};
12
13use crate::auth::Target;
14use crate::client::{ApiKeyPosition, ClientRequest, OramaClient};
15use crate::error::{OramaError, Result};
16use crate::types::*;
17use crate::utils::{generate_uuid, parse_ai_response};
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum StreamChunk {
22 ConnectionOpened,
24 Content(String),
26 StatusUpdate(String),
28 RawData(String),
30 Done,
32 Retry { attempt: u32, delay_ms: u64 },
34}
35
36#[derive(Debug, Clone)]
38pub struct StreamConfig {
39 pub max_retries: u32,
41 pub initial_retry_delay: u64,
43 pub max_retry_delay: u64,
45 pub connection_timeout: u64,
47 pub stream_timeout: u64,
49}
50
51impl Default for StreamConfig {
52 fn default() -> Self {
53 Self {
54 max_retries: 3,
55 initial_retry_delay: 1000, max_retry_delay: 30000, connection_timeout: 30, stream_timeout: 300, }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct CreateAiSessionConfig {
66 pub llm_config: Option<LlmConfig>,
67 pub initial_messages: Option<Vec<Message>>,
68}
69
70#[derive(Debug, Clone, Serialize)]
72pub struct AnswerConfig {
73 pub query: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub interaction_id: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub visitor_id: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub session_id: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub messages: Option<Vec<Message>>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub related: Option<RelatedQuestionsConfig>,
84 #[serde(rename = "datasourceIDs", skip_serializing_if = "Option::is_none")]
85 pub datasource_ids: Option<Vec<String>>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub min_similarity: Option<f64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub max_documents: Option<u32>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub ragat_notation: Option<String>,
92 #[serde(rename = "LLMConfig", skip_serializing_if = "Option::is_none")]
93 pub llm_config: Option<LlmConfig>,
94}
95
96#[derive(Debug, Clone)]
98pub struct Interaction {
99 pub id: String,
100 pub query: String,
101 pub response: String,
102 pub sources: Option<AnyObject>,
103 pub loading: bool,
104 pub error: bool,
105 pub error_message: Option<String>,
106 pub aborted: bool,
107 pub related: Option<String>,
108 pub current_step: Option<String>,
109 pub current_step_verbose: Option<String>,
110 pub selected_llm: Option<LlmConfig>,
111 pub optimized_query: Option<SearchParams>,
112 pub advanced_autoquery: Option<serde_json::Value>,
113}
114
115impl Interaction {
116 pub fn new(id: String, query: String) -> Self {
118 Self {
119 id,
120 query,
121 response: String::new(),
122 sources: None,
123 loading: true,
124 error: false,
125 error_message: None,
126 aborted: false,
127 related: None,
128 current_step: Some("starting".to_string()),
129 current_step_verbose: None,
130 selected_llm: None,
131 optimized_query: None,
132 advanced_autoquery: None,
133 }
134 }
135}
136
137#[derive(Debug)]
139pub struct OramaCoreStream {
140 collection_id: String,
141 client: OramaClient,
142 session_id: String,
143 llm_config: Option<LlmConfig>,
144 messages: Arc<RwLock<Vec<Message>>>,
145 state: Arc<RwLock<Vec<Interaction>>>,
146 last_interaction_params: Arc<RwLock<Option<AnswerConfig>>>,
147 stream_config: StreamConfig,
148}
149
150impl OramaCoreStream {
151 pub async fn new(collection_id: String, client: OramaClient) -> Result<Self> {
153 Ok(Self {
154 collection_id,
155 client,
156 session_id: generate_uuid(),
157 llm_config: None,
158 messages: Arc::new(RwLock::new(Vec::new())),
159 state: Arc::new(RwLock::new(Vec::new())),
160 last_interaction_params: Arc::new(RwLock::new(None)),
161 stream_config: StreamConfig::default(),
162 })
163 }
164
165 pub async fn with_config(
167 collection_id: String,
168 client: OramaClient,
169 config: CreateAiSessionConfig,
170 ) -> Result<Self> {
171 let messages = config.initial_messages.unwrap_or_default();
172
173 Ok(Self {
174 collection_id,
175 client,
176 session_id: generate_uuid(),
177 llm_config: config.llm_config,
178 messages: Arc::new(RwLock::new(messages)),
179 state: Arc::new(RwLock::new(Vec::new())),
180 last_interaction_params: Arc::new(RwLock::new(None)),
181 stream_config: StreamConfig::default(),
182 })
183 }
184
185 pub async fn with_stream_config(
187 collection_id: String,
188 client: OramaClient,
189 config: CreateAiSessionConfig,
190 stream_config: StreamConfig,
191 ) -> Result<Self> {
192 let messages = config.initial_messages.unwrap_or_default();
193
194 Ok(Self {
195 collection_id,
196 client,
197 session_id: generate_uuid(),
198 llm_config: config.llm_config,
199 messages: Arc::new(RwLock::new(messages)),
200 state: Arc::new(RwLock::new(Vec::new())),
201 last_interaction_params: Arc::new(RwLock::new(None)),
202 stream_config,
203 })
204 }
205
206 pub async fn answer(&self, data: AnswerConfig) -> Result<String> {
208 info!("Starting AI answer request");
209 let enriched_config = self.enrich_config(data).await;
210 debug!("Enriched config: {:?}", enriched_config);
211
212 {
214 let mut last_params = self.last_interaction_params.write().await;
215 *last_params = Some(enriched_config.clone());
216 }
217
218 {
220 let mut messages = self.messages.write().await;
221 messages.push(Message {
222 role: Role::User,
223 content: enriched_config.query.clone(),
224 });
225 messages.push(Message {
226 role: Role::Assistant,
227 content: String::new(),
228 });
229 }
230
231 let interaction_id = enriched_config
233 .interaction_id
234 .clone()
235 .unwrap_or_else(generate_uuid);
236
237 let interaction = Interaction::new(interaction_id.clone(), enriched_config.query.clone());
238
239 {
240 let mut state = self.state.write().await;
241 state.push(interaction);
242 }
243
244 let request = ClientRequest::post(
246 format!("/v1/collections/{}/ai/answer", self.collection_id),
247 Target::Reader,
248 ApiKeyPosition::QueryParams,
249 enriched_config,
250 );
251
252 let response: serde_json::Value = self.client.request(request).await.map_err(|e| {
253 error!("API request failed: {}", e);
254 e
255 })?;
256
257 let answer = response["answer"].as_str().unwrap_or_default().to_string();
259
260 {
262 let mut state = self.state.write().await;
263 if let Some(last_interaction) = state.last_mut() {
264 last_interaction.response = answer.clone();
265 last_interaction.loading = false;
266 last_interaction.current_step = Some("completed".to_string());
267
268 if let Some(sources) = response.get("sources") {
270 last_interaction.sources = Some(sources.clone());
271 }
272 if let Some(_related) = response.get("related") {
273 last_interaction.related = response["related"].as_str().map(String::from);
274 }
275 }
276 }
277
278 {
279 let mut messages = self.messages.write().await;
280 if let Some(last_message) = messages.last_mut() {
281 last_message.content = answer.clone();
282 }
283 }
284
285 info!("AI answer completed successfully, length: {}", answer.len());
286 Ok(answer)
287 }
288
289 async fn create_resilient_stream(
291 &self,
292 client: OramaClient,
293 stream_url: String,
294 auth_ref: crate::auth::AuthRef,
295 enriched_config: AnswerConfig,
296 messages: Arc<RwLock<Vec<Message>>>,
297 state: Arc<RwLock<Vec<Interaction>>>,
298 ) -> Result<impl Stream<Item = Result<StreamChunk>> + Send> {
299 let stream_timeout = Duration::from_secs(self.stream_config.stream_timeout);
300 let start_time = std::time::Instant::now();
301
302 let request_builder = client
304 .inner()
305 .post(&stream_url)
306 .header("Accept", "text/event-stream")
307 .header("Cache-Control", "no-cache")
308 .header("Connection", "keep-alive")
309 .header("Authorization", format!("Bearer {}", auth_ref.bearer))
310 .timeout(Duration::from_secs(self.stream_config.connection_timeout))
311 .json(&enriched_config);
312
313 let event_source = EventSource::new(request_builder).map_err(|e| {
315 error!("Failed to create EventSource: {}", e);
316 OramaError::generic(format!("EventSource creation failed: {e}"))
317 })?;
318
319 info!("Successfully created EventSource for streaming");
320
321 let event_stream = event_source.map(move |event_result| {
323 if start_time.elapsed() >= stream_timeout {
325 let timeout_secs = stream_timeout.as_secs();
326 error!("Stream timeout after {} seconds", timeout_secs);
327 let state_clone = state.clone();
328 let timeout_msg = format!("Stream timeout after {timeout_secs} seconds");
329 tokio::spawn(async move {
330 Self::mark_interaction_error(state_clone, timeout_msg).await;
331 });
332 return Err(OramaError::generic(format!(
333 "Stream timeout after {timeout_secs} seconds"
334 )));
335 }
336
337 match event_result {
338 Ok(event) => match event {
339 Event::Open => {
340 debug!("Stream connection opened");
341 Ok(StreamChunk::ConnectionOpened)
342 }
343 Event::Message(message) => {
344 debug!("Received streaming message: {}", message.data);
345
346 match message.data.as_str() {
347 "[DONE]" => {
348 info!("Streaming completed successfully");
349 let state_clone = state.clone();
350 tokio::spawn(async move {
351 let mut state = state_clone.write().await;
352 if let Some(interaction) = state.last_mut() {
353 interaction.loading = false;
354 interaction.current_step = Some("completed".to_string());
355 }
356 });
357 Ok(StreamChunk::Done)
358 }
359 data => {
360 Self::process_stream_data(data, messages.clone(), state.clone())
361 }
362 }
363 }
364 },
365 Err(event_error) => {
366 error!("Stream event error: {}", event_error);
367 let state_clone = state.clone();
368 let error_msg = event_error.to_string();
369 tokio::spawn(async move {
370 Self::mark_interaction_error(state_clone, error_msg).await;
371 });
372 Err(OramaError::generic(format!(
373 "Stream event error: {event_error}"
374 )))
375 }
376 }
377 });
378
379 Ok(event_stream)
380 }
381
382 pub async fn answer_stream(
384 &self,
385 data: AnswerConfig,
386 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk>> + Send>>> {
387 info!("Starting streaming AI answer request");
388 let enriched_config = self.enrich_config(data).await;
389 debug!("Enriched streaming config: {:?}", enriched_config);
390
391 {
393 let mut last_params = self.last_interaction_params.write().await;
394 *last_params = Some(enriched_config.clone());
395 }
396
397 {
399 let mut messages = self.messages.write().await;
400 messages.push(Message {
401 role: Role::User,
402 content: enriched_config.query.clone(),
403 });
404 messages.push(Message {
405 role: Role::Assistant,
406 content: String::new(),
407 });
408 }
409
410 let interaction_id = enriched_config
412 .interaction_id
413 .clone()
414 .unwrap_or_else(generate_uuid);
415
416 let interaction = Interaction::new(interaction_id.clone(), enriched_config.query.clone());
417
418 {
419 let mut state = self.state.write().await;
420 state.push(interaction);
421 }
422
423 let client = self.client.clone();
424 let collection_id = self.collection_id.clone();
425 let messages = self.messages.clone();
426 let state = self.state.clone();
427
428 let auth_ref = client.get_auth_ref(Target::Reader).await.map_err(|e| {
430 error!("Failed to get auth reference: {}", e);
431 e
432 })?;
433
434 let base_url = &auth_ref.base_url;
435 let stream_url = format!("{base_url}/v1/collections/{collection_id}/ai/answer/stream");
436
437 debug!("Creating streaming request to: {}", stream_url);
438
439 let stream = self
441 .create_resilient_stream(
442 client.clone(),
443 stream_url,
444 auth_ref,
445 enriched_config,
446 messages.clone(),
447 state.clone(),
448 )
449 .await?;
450
451 Ok(Box::pin(stream))
452 }
453
454 pub async fn regenerate_last(&self, stream: bool) -> Result<String> {
456 info!("Starting regenerate_last, stream: {}", stream);
457
458 let state_len = {
459 let state = self.state.read().await;
460 state.len()
461 };
462
463 let messages_len = {
464 let messages = self.messages.read().await;
465 messages.len()
466 };
467
468 if state_len == 0 || messages_len == 0 {
469 warn!("No messages to regenerate");
470 return Err(OramaError::generic("No messages to regenerate"));
471 }
472
473 {
475 let messages = self.messages.read().await;
476 if let Some(last_message) = messages.last() {
477 if !matches!(last_message.role, Role::Assistant) {
478 warn!("Last message is not from assistant");
479 return Err(OramaError::generic(
480 "Last message is not an assistant message",
481 ));
482 }
483 }
484 }
485
486 let last_params = {
488 let params = self.last_interaction_params.read().await;
489 params.clone()
490 };
491
492 let last_params = last_params.ok_or_else(|| {
493 warn!("No last interaction parameters available");
494 OramaError::generic("No last interaction parameters available")
495 })?;
496
497 {
499 let mut messages = self.messages.write().await;
500 messages.pop();
501 }
502
503 {
504 let mut state = self.state.write().await;
505 state.pop();
506 }
507
508 if stream {
510 info!("Regenerating with streaming");
511 let mut stream_result = self.answer_stream(last_params).await?;
512 let mut complete_response = String::new();
513
514 while let Some(chunk_result) = stream_result.next().await {
516 match chunk_result? {
517 StreamChunk::Content(content) => {
518 complete_response.push_str(&content);
519 }
520 StreamChunk::Done => {
521 break;
522 }
523 StreamChunk::StatusUpdate(status) => {
524 debug!("Status update during regeneration: {}", status);
525 }
526 _ => {
527 }
529 }
530 }
531
532 Ok(complete_response)
533 } else {
534 info!("Regenerating without streaming");
535 self.answer(last_params).await
536 }
537 }
538
539 pub async fn clear_session(&self) {
541 {
542 let mut messages = self.messages.write().await;
543 messages.clear();
544 }
545
546 {
547 let mut state = self.state.write().await;
548 state.clear();
549 }
550 }
551
552 pub async fn get_messages(&self) -> Vec<Message> {
554 let messages = self.messages.read().await;
555 messages.clone()
556 }
557
558 pub async fn get_state(&self) -> Vec<Interaction> {
560 let state = self.state.read().await;
561 state.clone()
562 }
563
564 pub fn session_id(&self) -> &str {
566 &self.session_id
567 }
568
569 pub fn get_stream_config(&self) -> &StreamConfig {
571 &self.stream_config
572 }
573
574 pub fn set_stream_config(&mut self, config: StreamConfig) {
576 self.stream_config = config;
577 }
578
579 async fn enrich_config(&self, mut config: AnswerConfig) -> AnswerConfig {
581 if config.visitor_id.is_none() {
582 config.visitor_id = Some(DEFAULT_SERVER_USER_ID.to_string());
583 }
584
585 if config.interaction_id.is_none() {
586 config.interaction_id = Some(generate_uuid());
587 }
588
589 if config.session_id.is_none() {
590 config.session_id = Some(self.session_id.clone());
591 }
592
593 if config.llm_config.is_none() {
595 config.llm_config = self.llm_config.clone();
596 }
597
598 config
599 }
600
601 fn process_stream_data(
603 data: &str,
604 messages: Arc<RwLock<Vec<Message>>>,
605 state: Arc<RwLock<Vec<Interaction>>>,
606 ) -> Result<StreamChunk> {
607 match parse_ai_response::<serde_json::Value>(data) {
609 Ok(parsed) => {
610 if let Some(content) = parsed.get("content").and_then(|c| c.as_str()) {
611 let content = content.to_string();
613 let content_for_update = content.clone();
614 let parsed_clone = parsed.clone();
615
616 tokio::spawn(async move {
617 {
619 let mut messages = messages.write().await;
620 if let Some(last_message) = messages.last_mut() {
621 if matches!(last_message.role, Role::Assistant) {
622 last_message.content.push_str(&content_for_update);
623 }
624 }
625 }
626
627 {
629 let mut state = state.write().await;
630 if let Some(last_interaction) = state.last_mut() {
631 last_interaction.response.push_str(&content_for_update);
632
633 if let Some(step) =
635 parsed_clone.get("step").and_then(|s| s.as_str())
636 {
637 last_interaction.current_step = Some(step.to_string());
638 }
639
640 if let Some(verbose) =
642 parsed_clone.get("verbose_step").and_then(|s| s.as_str())
643 {
644 last_interaction.current_step_verbose =
645 Some(verbose.to_string());
646 }
647 }
648 }
649 });
650
651 Ok(StreamChunk::Content(content))
652 } else if let Some(step) = parsed.get("step").and_then(|s| s.as_str()) {
653 let step = step.to_string();
655 let step_for_update = step.clone();
656
657 tokio::spawn(async move {
658 let mut state = state.write().await;
659 if let Some(last_interaction) = state.last_mut() {
660 last_interaction.current_step = Some(step_for_update);
661 }
662 });
663
664 Ok(StreamChunk::StatusUpdate(step))
665 } else if let Some(error_msg) = parsed.get("error").and_then(|e| e.as_str()) {
666 warn!("Stream error received: {}", error_msg);
668
669 let state_clone = state.clone();
670 let error_message = error_msg.to_string();
671 tokio::spawn(async move {
672 Self::mark_interaction_error(state_clone, error_message).await;
673 });
674 Err(OramaError::generic(error_msg))
675 } else {
676 debug!("Unknown structured stream data: {}", data);
678 Ok(StreamChunk::RawData(data.to_string()))
679 }
680 }
681 Err(parse_err) => {
682 debug!(
684 "Failed to parse AI response as JSON ({}): {}",
685 parse_err, data
686 );
687 Ok(StreamChunk::RawData(data.to_string()))
688 }
689 }
690 }
691
692 async fn mark_interaction_error(state: Arc<RwLock<Vec<Interaction>>>, error_message: String) {
694 let mut state = state.write().await;
695 if let Some(interaction) = state.last_mut() {
696 interaction.error = true;
697 interaction.error_message = Some(error_message);
698 interaction.loading = false;
699 }
700 }
701}
702
703impl AnswerConfig {
705 pub fn new<S: Into<String>>(query: S) -> Self {
707 Self {
708 query: query.into(),
709 interaction_id: None,
710 visitor_id: None,
711 session_id: None,
712 messages: None,
713 related: None,
714 datasource_ids: None,
715 min_similarity: None,
716 max_documents: None,
717 ragat_notation: None,
718 llm_config: None,
719 }
720 }
721
722 pub fn with_interaction_id<S: Into<String>>(mut self, id: S) -> Self {
724 self.interaction_id = Some(id.into());
725 self
726 }
727
728 pub fn with_visitor_id<S: Into<String>>(mut self, id: S) -> Self {
730 self.visitor_id = Some(id.into());
731 self
732 }
733
734 pub fn with_session_id<S: Into<String>>(mut self, id: S) -> Self {
736 self.session_id = Some(id.into());
737 self
738 }
739
740 pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
742 self.messages = Some(messages);
743 self
744 }
745
746 pub fn with_related(mut self, related: RelatedQuestionsConfig) -> Self {
748 self.related = Some(related);
749 self
750 }
751
752 pub fn with_datasource_ids(mut self, ids: Vec<String>) -> Self {
754 self.datasource_ids = Some(ids);
755 self
756 }
757
758 pub fn with_min_similarity(mut self, similarity: f64) -> Self {
760 self.min_similarity = Some(similarity);
761 self
762 }
763
764 pub fn with_max_documents(mut self, max_docs: u32) -> Self {
766 self.max_documents = Some(max_docs);
767 self
768 }
769
770 pub fn with_ragat_notation<S: Into<String>>(mut self, notation: S) -> Self {
772 self.ragat_notation = Some(notation.into());
773 self
774 }
775
776 pub fn with_llm_config(mut self, config: LlmConfig) -> Self {
778 self.llm_config = Some(config);
779 self
780 }
781}
782
783impl CreateAiSessionConfig {
784 pub fn new() -> Self {
786 Self {
787 llm_config: None,
788 initial_messages: None,
789 }
790 }
791
792 pub fn with_llm_config(mut self, config: LlmConfig) -> Self {
794 self.llm_config = Some(config);
795 self
796 }
797
798 pub fn with_initial_messages(mut self, messages: Vec<Message>) -> Self {
800 self.initial_messages = Some(messages);
801 self
802 }
803}
804
805impl Default for CreateAiSessionConfig {
806 fn default() -> Self {
807 Self::new()
808 }
809}