language_barrier_core/
chat.rs

1use crate::compactor::{ChatHistoryCompactor, DropOldestCompactor};
2use crate::message::{Content, Message};
3use crate::token::TokenCounter;
4use crate::tool::LlmToolInfo;
5use crate::{ModelInfo, Result, ToolDefinition};
6
7/// The main Chat client that users will interact with.
8/// All methods return a new instance rather than mutating the existing one,
9/// following the immutable builder pattern.
10pub struct Chat<M: ModelInfo> {
11    // Immutable after construction
12    pub model: M,
13
14    // Tunable knobs / state
15    pub system_prompt: String,
16    pub max_output_tokens: usize,
17
18    // History and token tracking
19    pub history: Vec<Message>,
20    token_counter: TokenCounter,
21    #[allow(dead_code)]
22    compactor: Box<dyn ChatHistoryCompactor>,
23
24    // Registry for type-safe tool definitions (optional)
25    pub tools: Option<Vec<LlmToolInfo>>,
26}
27
28impl<M> Chat<M>
29where
30    M: ModelInfo,
31{
32    /// Creates a new Chat instance with a model and provider
33    pub fn new(model: M) -> Self {
34        Self {
35            model,
36            system_prompt: String::new(),
37            max_output_tokens: 2048,
38            history: Vec::new(),
39            token_counter: TokenCounter::default(),
40            compactor: Box::<DropOldestCompactor>::default(),
41            tools: None,
42        }
43    }
44
45    /// Sets system prompt and returns a new instance
46    #[must_use]
47    pub fn with_system_prompt(self, prompt: impl Into<String>) -> Self {
48        let p = prompt.into();
49        let mut token_counter = self.token_counter.clone();
50        token_counter.observe(&p);
51        
52        let mut new_chat = Self {
53            system_prompt: p,
54            token_counter,
55            ..self
56        };
57        
58        new_chat = new_chat.trim_to_context_window();
59        new_chat
60    }
61
62    /// Sets max output tokens and returns a new instance
63    #[must_use]
64    pub fn with_max_output_tokens(self, n: usize) -> Self {
65        Self {
66            max_output_tokens: n,
67            ..self
68        }
69    }
70
71    /// Sets history and returns a new instance
72    #[must_use]
73    pub fn with_history(self, history: Vec<Message>) -> Self {
74        // Create a new token counter from scratch
75        let mut token_counter = TokenCounter::default();
76        
77        // Count tokens in system prompt
78        token_counter.observe(&self.system_prompt);
79        
80        // Count tokens in message history
81        for msg in &history {
82            match msg {
83                Message::User { content, .. } => {
84                    if let Content::Text(text) = content {
85                        token_counter.observe(text);
86                    }
87                }
88                Message::Assistant { content, .. } => {
89                    if let Some(Content::Text(text)) = content {
90                        token_counter.observe(text);
91                    }
92                }
93                Message::System { content, .. } | Message::Tool { content, .. } => {
94                    token_counter.observe(content);
95                }
96            }
97        }
98        
99        let mut new_chat = Self {
100            history,
101            token_counter,
102            ..self
103        };
104        
105        new_chat = new_chat.trim_to_context_window();
106        new_chat
107    }
108
109    /// Sets compactor and returns a new instance
110    #[must_use]
111    pub fn with_compactor<C: ChatHistoryCompactor + 'static>(self, comp: C) -> Self {
112        let mut new_chat = Self {
113            compactor: Box::new(comp),
114            ..self
115        };
116        
117        new_chat = new_chat.trim_to_context_window();
118        new_chat
119    }
120
121    /// Adds a message to the conversation history and returns a new instance
122    #[must_use]
123    pub fn add_message(self, msg: Message) -> Self {
124        let mut token_counter = self.token_counter.clone();
125        let mut history = self.history.clone();
126        
127        // Count tokens based on message type
128        match &msg {
129            Message::User { content, .. } => {
130                if let Content::Text(text) = content {
131                    token_counter.observe(text);
132                }
133            }
134            Message::Assistant { content, .. } => {
135                if let Some(Content::Text(text)) = content {
136                    token_counter.observe(text);
137                }
138            }
139            Message::System { content, .. } | Message::Tool { content, .. } => {
140                token_counter.observe(content);
141            }
142        }
143        
144        history.push(msg);
145        
146        let mut new_chat = Self {
147            history,
148            token_counter,
149            ..self
150        };
151        
152        new_chat = new_chat.trim_to_context_window();
153        new_chat
154    }
155    
156    /// Alias for `add_message` for backward compatibility
157    #[must_use]
158    pub fn push_message(self, msg: Message) -> Self {
159        self.add_message(msg)
160    }
161
162    /// Trims the conversation history to fit within token budget and returns a new instance
163    #[must_use]
164    fn trim_to_context_window(self) -> Self {
165        const MAX_TOKENS: usize = 32_768; // could be model-specific
166        
167        let mut history = self.history.clone();
168        let mut token_counter = self.token_counter.clone();
169        
170        // Create a fresh compactor of the same default type
171        // Note: In a real implementation, you would want a way to clone the compactor
172        // or to properly reconstruct the specific type that was being used.
173        let new_compactor = Box::<DropOldestCompactor>::default();
174        
175        // Use the compactor to trim history
176        new_compactor.compact(&mut history, &mut token_counter, MAX_TOKENS);
177        
178        Self {
179            history,
180            token_counter,
181            compactor: new_compactor as Box<dyn ChatHistoryCompactor>,
182            ..self
183        }
184    }
185
186    /// Gets the current token count
187    pub fn tokens_used(&self) -> usize {
188        self.token_counter.total()
189    }
190
191    /// Add a tool and returns a new instance with the tool added
192    #[must_use = "This returns a new Chat with the tool added"]
193    pub fn with_tool(self, tool: impl ToolDefinition) -> Result<Self> {
194        let info = LlmToolInfo {
195            name: tool.name(),
196            description: tool.description(),
197            parameters: tool.schema()?,
198        };
199        
200        let tools = match self.tools {
201            Some(mut tools) => {
202                tools.push(info);
203                Some(tools)
204            },
205            None => Some(vec![info]),
206        };
207        
208        let new_chat = Self {
209            tools,
210            ..self
211        };
212
213        Ok(new_chat)
214    }
215}