ai_context_gen/token_counter.rs
1//! Token counting and content prioritization module.
2//!
3//! This module provides functionality for accurate token counting using the GPT-4
4//! tokenizer and intelligent content prioritization to fit within token limits.
5
6use anyhow::Result;
7use tiktoken_rs::{get_bpe_from_model, CoreBPE};
8
9/// Token counter using the GPT-4 tokenizer for accurate token counting.
10///
11/// Provides methods for counting tokens in text and truncating content to fit
12/// within specified token limits while maintaining text coherence.
13///
14/// # Examples
15///
16/// ```rust
17/// use ai_context_gen::token_counter::TokenCounter;
18///
19/// let counter = TokenCounter::new().unwrap();
20/// let text = "Hello, world!";
21/// let token_count = counter.count_tokens(text);
22/// println!("Text has {} tokens", token_count);
23/// ```
24pub struct TokenCounter {
25 bpe: CoreBPE,
26}
27
28impl TokenCounter {
29 /// Creates a new token counter using the GPT-4 tokenizer.
30 ///
31 /// # Returns
32 ///
33 /// A new `TokenCounter` instance configured with the GPT-4 BPE tokenizer.
34 ///
35 /// # Errors
36 ///
37 /// Returns an error if the GPT-4 tokenizer model cannot be loaded.
38 ///
39 /// # Examples
40 ///
41 /// ```rust
42 /// use ai_context_gen::token_counter::TokenCounter;
43 ///
44 /// let counter = TokenCounter::new().unwrap();
45 /// ```
46 pub fn new() -> Result<Self> {
47 let bpe = get_bpe_from_model("gpt-4")?;
48 Ok(Self { bpe })
49 }
50
51 /// Counts the number of tokens in the given text.
52 ///
53 /// Uses the GPT-4 tokenizer to provide accurate token counts that match
54 /// what would be used by OpenAI's models and similar systems.
55 ///
56 /// # Arguments
57 ///
58 /// * `text` - The text to count tokens for
59 ///
60 /// # Returns
61 ///
62 /// The number of tokens in the text.
63 ///
64 /// # Examples
65 ///
66 /// ```rust
67 /// use ai_context_gen::token_counter::TokenCounter;
68 ///
69 /// let counter = TokenCounter::new().unwrap();
70 /// let count = counter.count_tokens("Hello, world!");
71 /// assert!(count > 0);
72 /// ```
73 pub fn count_tokens(&self, text: &str) -> usize {
74 self.bpe.encode_with_special_tokens(text).len()
75 }
76
77 /// Truncates text to fit within a specified token limit.
78 ///
79 /// Attempts to preserve text coherence by truncating at token boundaries
80 /// rather than character boundaries. Falls back to character truncation
81 /// if token decoding fails.
82 ///
83 /// # Arguments
84 ///
85 /// * `text` - The text to truncate
86 /// * `max_tokens` - Maximum number of tokens to include
87 ///
88 /// # Returns
89 ///
90 /// The truncated text that fits within the token limit.
91 ///
92 /// # Examples
93 ///
94 /// ```rust
95 /// use ai_context_gen::token_counter::TokenCounter;
96 ///
97 /// let counter = TokenCounter::new().unwrap();
98 /// let long_text = "This is a very long text that exceeds the token limit...";
99 /// let truncated = counter.truncate_to_token_limit(long_text, 10);
100 /// assert!(counter.count_tokens(&truncated) <= 10);
101 /// ```
102 pub fn truncate_to_token_limit(&self, text: &str, max_tokens: usize) -> String {
103 let tokens = self.bpe.encode_with_special_tokens(text);
104
105 if tokens.len() <= max_tokens {
106 return text.to_string();
107 }
108
109 let truncated_tokens = &tokens[..max_tokens];
110 match self.bpe.decode(truncated_tokens.to_vec()) {
111 Ok(truncated_text) => truncated_text,
112 Err(_) => {
113 // Fallback: truncate by characters
114 let char_limit = (text.len() * max_tokens) / tokens.len();
115 text.chars().take(char_limit).collect()
116 }
117 }
118 }
119}
120
121/// Content prioritizer that manages sections based on priority and token limits.
122///
123/// The prioritizer sorts content sections by priority and ensures the total
124/// content fits within specified token limits by truncating lower priority
125/// sections when necessary.
126///
127/// # Examples
128///
129/// ```rust
130/// use ai_context_gen::token_counter::{ContentPrioritizer, ContentSection};
131///
132/// let prioritizer = ContentPrioritizer::new().unwrap();
133/// let sections = vec![
134/// ContentSection::new("High Priority".to_string(), "Content...".to_string(), 10),
135/// ContentSection::new("Low Priority".to_string(), "More content...".to_string(), 1),
136/// ];
137/// let prioritized = prioritizer.prioritize_content(sections, 1000);
138/// ```
139pub struct ContentPrioritizer {
140 token_counter: TokenCounter,
141}
142
143impl ContentPrioritizer {
144 /// Creates a new content prioritizer.
145 ///
146 /// # Returns
147 ///
148 /// A new `ContentPrioritizer` instance with an initialized token counter.
149 ///
150 /// # Errors
151 ///
152 /// Returns an error if the underlying token counter cannot be initialized.
153 pub fn new() -> Result<Self> {
154 Ok(Self {
155 token_counter: TokenCounter::new()?,
156 })
157 }
158
159 /// Prioritizes and truncates content sections to fit within token limits.
160 ///
161 /// Sorts sections by priority (highest first) and includes as many complete
162 /// sections as possible. When a section would exceed the token limit, it
163 /// attempts to truncate it if there are sufficient remaining tokens.
164 ///
165 /// # Arguments
166 ///
167 /// * `sections` - List of content sections to prioritize
168 /// * `max_tokens` - Maximum total tokens allowed
169 ///
170 /// # Returns
171 ///
172 /// A vector of sections that fit within the token limit, sorted by priority.
173 ///
174 /// # Examples
175 ///
176 /// ```rust
177 /// use ai_context_gen::token_counter::{ContentPrioritizer, ContentSection};
178 ///
179 /// let prioritizer = ContentPrioritizer::new().unwrap();
180 /// let sections = vec![
181 /// ContentSection::new("Important".to_string(), "Critical info".to_string(), 10),
182 /// ContentSection::new("Less Important".to_string(), "Extra details".to_string(), 5),
183 /// ];
184 /// let result = prioritizer.prioritize_content(sections, 100);
185 /// // Higher priority sections appear first
186 /// ```
187 pub fn prioritize_content(
188 &self,
189 sections: Vec<ContentSection>,
190 max_tokens: usize,
191 ) -> Vec<ContentSection> {
192 let mut prioritized = sections;
193
194 // Sort by priority (highest priority first)
195 prioritized.sort_by(|a, b| b.priority.cmp(&a.priority));
196
197 let mut total_tokens = 0;
198 let mut result = Vec::new();
199
200 for mut section in prioritized {
201 let section_tokens = self.token_counter.count_tokens(§ion.content);
202
203 if total_tokens + section_tokens <= max_tokens {
204 total_tokens += section_tokens;
205 result.push(section);
206 } else {
207 // Try to truncate content to fit within the limit
208 let remaining_tokens = max_tokens - total_tokens;
209 if remaining_tokens > 100 {
210 // Only include if at least 100 tokens remain
211 section.content = self
212 .token_counter
213 .truncate_to_token_limit(§ion.content, remaining_tokens);
214 section.truncated = true;
215 result.push(section);
216 break;
217 }
218 }
219 }
220
221 result
222 }
223}
224
225/// A content section with associated metadata for prioritization.
226///
227/// Represents a section of content (like project metadata, source code, or
228/// documentation) with a title, content, priority level, and truncation status.
229///
230/// # Priority Levels
231///
232/// - `9-10`: High priority (metadata, documentation)
233/// - `5-6`: Medium priority (AST analysis, structure)
234/// - `1-2`: Low priority (source code)
235///
236/// # Examples
237///
238/// ```rust
239/// use ai_context_gen::token_counter::ContentSection;
240///
241/// // Create a high-priority section
242/// let section = ContentSection::high_priority(
243/// "Project Metadata".to_string(),
244/// "Important project information...".to_string()
245/// );
246///
247/// // Create a custom priority section
248/// let custom = ContentSection::new(
249/// "Custom Section".to_string(),
250/// "Content here...".to_string(),
251/// 7
252/// );
253/// ```
254#[derive(Debug, Clone)]
255pub struct ContentSection {
256 /// Title of the content section.
257 pub title: String,
258
259 /// The actual content of the section.
260 pub content: String,
261
262 /// Priority level (higher numbers = higher priority).
263 pub priority: u8,
264
265 /// Whether this section was truncated to fit token limits.
266 pub truncated: bool,
267}
268
269impl ContentSection {
270 /// Creates a new content section with the specified priority.
271 ///
272 /// # Arguments
273 ///
274 /// * `title` - Display title for the section
275 /// * `content` - The content text
276 /// * `priority` - Priority level (0-255, higher is more important)
277 ///
278 /// # Examples
279 ///
280 /// ```rust
281 /// use ai_context_gen::token_counter::ContentSection;
282 ///
283 /// let section = ContentSection::new(
284 /// "My Section".to_string(),
285 /// "Section content...".to_string(),
286 /// 8
287 /// );
288 /// ```
289 pub fn new(title: String, content: String, priority: u8) -> Self {
290 Self {
291 title,
292 content,
293 priority,
294 truncated: false,
295 }
296 }
297
298 /// Creates a high-priority content section (priority 9).
299 ///
300 /// Use for critical content like project metadata and documentation
301 /// that should always be included.
302 ///
303 /// # Arguments
304 ///
305 /// * `title` - Display title for the section
306 /// * `content` - The content text
307 pub fn high_priority(title: String, content: String) -> Self {
308 Self::new(title, content, 9)
309 }
310
311 /// Creates a medium-priority content section (priority 5).
312 ///
313 /// Use for structural information like AST analysis and project organization.
314 ///
315 /// # Arguments
316 ///
317 /// * `title` - Display title for the section
318 /// * `content` - The content text
319 pub fn medium_priority(title: String, content: String) -> Self {
320 Self::new(title, content, 5)
321 }
322
323 /// Creates a low-priority content section (priority 1).
324 ///
325 /// Use for detailed content like complete source code that can be
326 /// truncated if necessary.
327 ///
328 /// # Arguments
329 ///
330 /// * `title` - Display title for the section
331 /// * `content` - The content text
332 pub fn low_priority(title: String, content: String) -> Self {
333 Self::new(title, content, 1)
334 }
335}