Skip to main content

ds_api/conversation/
deepseek.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use futures::stream::BoxStream;
5
6use crate::api::{ApiClient, ApiRequest};
7use crate::error::{ApiError, Result};
8use crate::raw::request::message::{Message, Role};
9
10use crate::conversation::{Conversation, Summarizer, TokenBasedSummarizer};
11
12/// DeepseekConversation: a concrete Conversation implementation.
13/// - owns an `ApiClient` (used to send requests)
14/// - stores `history: Vec<Message>`
15/// - holds a `Summarizer` implementation (boxed)
16/// - supports auto-summary toggle
17pub struct DeepseekConversation {
18    client: ApiClient,
19    history: Vec<Message>,
20    summarizer: Box<dyn Summarizer + Send + Sync>,
21    auto_summary: bool,
22}
23
24impl DeepseekConversation {
25    /// Create a conversation with an ApiClient and default summarizer.
26    pub fn new(client: ApiClient) -> Self {
27        Self {
28            client,
29            history: vec![],
30            summarizer: Box::new(TokenBasedSummarizer::default()),
31            auto_summary: true,
32        }
33    }
34
35    /// Builder: set a custom summarizer
36    pub fn with_summarizer(mut self, s: impl Summarizer + 'static) -> Self {
37        self.summarizer = Box::new(s);
38        self
39    }
40
41    /// Builder: enable or disable auto-summary behavior
42    pub fn enable_auto_summary(mut self, v: bool) -> Self {
43        self.auto_summary = v;
44        self
45    }
46
47    /// Builder: seed conversation history with initial messages
48    pub fn with_history(mut self, history: Vec<Message>) -> Self {
49        self.history = history;
50        self
51    }
52
53    /// Inspect mutable history (advanced use)
54    pub fn history_mut(&mut self) -> &mut Vec<Message> {
55        &mut self.history
56    }
57
58    /// Internal helper that checks and runs summarization if needed.
59    fn maybe_do_summary(&mut self) {
60        if self.auto_summary && self.summarizer.should_summarize(&self.history) {
61            let _ = self.summarizer.summarize(&mut self.history);
62        }
63    }
64
65    /// Stream text fragments (delta.content) as a boxed stream of `Result<String, ApiError>`.
66    ///
67    /// This is an inherent async method (not part of the `Conversation` trait) to avoid
68    /// trait object lifetime complexity. It simply delegates to the underlying ApiClient.
69    pub async fn stream_text(
70        &mut self,
71    ) -> Result<BoxStream<'_, std::result::Result<String, ApiError>>> {
72        let req = ApiRequest::builder()
73            .messages(self.history.clone())
74            .stream(true);
75        let stream = self.client.stream_text(req).await?;
76        Ok(stream)
77    }
78}
79
80impl Conversation for DeepseekConversation {
81    fn history(&self) -> &Vec<Message> {
82        &self.history
83    }
84
85    fn add_message(&mut self, message: Message) {
86        self.history.push(message);
87        // After adding an arbitrary message, optionally summarize
88        self.maybe_do_summary();
89    }
90
91    fn push_user_input(&mut self, text: String) {
92        // push owned string directly into history as a User message
93        self.history.push(Message::new(Role::User, text.as_str()));
94        // Optionally perform summary check eagerly after user input
95        self.maybe_do_summary();
96    }
97
98    fn maybe_summarize(&mut self) {
99        self.maybe_do_summary();
100    }
101
102    fn send_once<'a>(
103        &'a mut self,
104    ) -> Pin<Box<dyn Future<Output = Result<Option<String>>> + Send + 'a>> {
105        Box::pin(async move {
106            // build ApiRequest from current history (use deepseek_chat by default)
107            // We choose the deepseek_chat constructor to be the default model.
108            let req = ApiRequest::builder().messages(self.history.clone());
109            let resp = self.client.send(req).await?;
110            // extract first choice
111            let choice = resp
112                .choices
113                .into_iter()
114                .next()
115                .ok_or_else(|| ApiError::Other("empty choices from API".to_string()))?;
116            let assistant_msg = choice.message;
117            let content = assistant_msg.content.clone();
118
119            // append assistant message to history
120            self.history.push(assistant_msg);
121
122            // maybe summarize after adding assistant
123            self.maybe_do_summary();
124
125            Ok(content)
126        })
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::api::ApiClient;
134
135    #[test]
136    fn conversation_builder_and_push() {
137        let client = ApiClient::new("fake-token");
138        let conv = DeepseekConversation::new(client).with_history(vec![]);
139        assert!(conv.history().is_empty());
140    }
141}