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(&section.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(&section.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}