File size: 7,919 Bytes
0f6a233 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | //! Dynamic Token Budget Calculator
//!
//! Computes `max_new_tokens` dynamically based on question complexity.
//! This is the kernel-level implementation of the "smart token budget" feature.
//!
//! Strategy:
//! 1. If explicit marks are embedded in the question (e.g., "2 marks", "8 marks")
//! β use a calibrated lookup table
//! 2. Otherwise, estimate complexity from:
//! - Prompt length (characters)
//! - Keyword signals ("explain", "list", "compare", "analyze", "define")
//! - Question type (open/closed, single/multi-part)
//!
//! All pattern matching uses bounded regexes to prevent ReDoS.
use pyo3::prelude::*;
use regex::Regex;
use once_cell::sync::Lazy;
// ββ Token budget lookup table βββββββββββββββββββββββββββββββββββββββββββββββββ
/// (min_marks, max_marks) β max_new_tokens
const MARK_BUDGET_TABLE: &[(u32, u32, u32)] = &[
(1, 2, 150), // Brief definition / fill-in-the-blank
(3, 4, 350), // Short paragraph answer
(5, 6, 600), // Medium explanation
(7, 8, 900), // Detailed section answer
(9, 10, 1200), // Extended analysis
(11, 15, 1800), // Essay / multi-part
(16, 99, 2500), // Comprehensive long-form
];
/// Default budget when no marks/complexity signals found
const DEFAULT_BUDGET: u32 = 512;
// ββ Patterns ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/// Detects "X marks", "X mark", "(X)", "[X marks]" in prompt text
static MARKS_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(\d{1,2})\s*(?:marks?|pts?|points?)\b|\[(\d{1,2})\s*marks?\]|\((\d{1,2})\s*marks?\)")
.expect("MARKS_PATTERN regex is valid")
});
/// "Explain", "Analyze", "Compare", "Evaluate" β longer answer
static DEEP_KEYWORDS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(explain|analyze|analyse|compare|contrast|evaluate|discuss|elaborate|describe in detail|critically|justify|argue|examine)\b")
.expect("DEEP_KEYWORDS regex valid")
});
/// "Define", "List", "Name", "State", "Give" β shorter answer
static BRIEF_KEYWORDS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(define|list|name|state|give|identify|what is|what are|who is|when did|where is)\b")
.expect("BRIEF_KEYWORDS regex valid")
});
/// "Step by step", "With examples", "Show working" β extra tokens needed
static EXPANSION_KEYWORDS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(step[\s-]by[\s-]step|with examples?|show (?:your )?work(?:ing)?|in detail|thoroughly|comprehensively)\b")
.expect("EXPANSION_KEYWORDS regex valid")
});
// ββ Core logic ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
fn compute_budget_internal(prompt: &str, explicit_marks: Option<u32>) -> u32 {
// Priority 1: Explicit marks provided by caller
if let Some(marks) = explicit_marks {
return marks_to_tokens(marks);
}
// Priority 2: Detect marks from prompt text
if let Some(detected) = detect_marks_in_prompt(prompt) {
return marks_to_tokens(detected);
}
// Priority 3: Keyword-based heuristic
let p = prompt;
let prompt_len = p.len();
let deep_signals = DEEP_KEYWORDS.find_iter(p).count();
let brief_signals = BRIEF_KEYWORDS.find_iter(p).count();
let expansion_signals = EXPANSION_KEYWORDS.find_iter(p).count();
// Question complexity score (0 = minimal, 100+ = essay)
let mut score: i32 = 0;
// Prompt length contributes to complexity
score += (prompt_len / 50).min(20) as i32; // Max +20 from length
// Deep keywords strongly suggest longer response
score += (deep_signals as i32) * 15;
// Brief keywords suggest concise response
score -= (brief_signals as i32) * 10;
// Expansion modifiers bump the budget
score += (expansion_signals as i32) * 20;
// Map score to token budget
let budget = match score {
s if s <= 0 => 150,
1..=10 => 250,
11..=20 => 400,
21..=35 => 600,
36..=50 => 900,
51..=70 => 1200,
_ => 1800,
};
budget
}
/// Look up max_new_tokens from a mark value using the budget table.
fn marks_to_tokens(marks: u32) -> u32 {
for &(min_m, max_m, tokens) in MARK_BUDGET_TABLE {
if marks >= min_m && marks <= max_m {
return tokens;
}
}
DEFAULT_BUDGET
}
/// Try to extract a mark value from the prompt text.
fn detect_marks_in_prompt(prompt: &str) -> Option<u32> {
// Use only the first match (first marks reference in prompt)
let cap = MARKS_PATTERN.captures(prompt)?;
// Try capture groups in order: group 1, 2, 3
for i in 1..=3 {
if let Some(m) = cap.get(i) {
if let Ok(n) = m.as_str().parse::<u32>() {
return Some(n);
}
}
}
None
}
// ββ Python-exposed function βββββββββββββββββββββββββββββββββββββββββββββββββββ
/// Compute the appropriate max_new_tokens for a given prompt.
///
/// Args:
/// prompt (str): The user question/prompt text
/// explicit_marks (int | None): Explicit mark value if known (overrides detection)
///
/// Returns:
/// int: Recommended max_new_tokens value
///
/// Examples:
/// >>> compute_budget("What is photosynthesis? [2 marks]")
/// 150
/// >>> compute_budget("Explain with examples how TCP/IP works.", None)
/// 600
/// >>> compute_budget("Any question", explicit_marks=10)
/// 1200
#[pyfunction]
#[pyo3(signature = (prompt, explicit_marks = None))]
pub fn compute_budget(
py: Python<'_>,
prompt: String,
explicit_marks: Option<u32>,
) -> u32 {
py.allow_threads(move || compute_budget_internal(&prompt, explicit_marks))
}
// ββ Unit Tests ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_explicit_marks_override() {
assert_eq!(compute_budget_internal("anything", Some(2)), 150);
assert_eq!(compute_budget_internal("anything", Some(8)), 900);
assert_eq!(compute_budget_internal("anything", Some(15)), 1800);
}
#[test]
fn test_marks_detection_in_prompt() {
assert_eq!(compute_budget_internal("Define photosynthesis. [2 marks]", None), 150);
assert_eq!(compute_budget_internal("Evaluate the French Revolution (8 marks)", None), 900);
assert_eq!(compute_budget_internal("Explain 5 marks worth of TCP/IP", None), 600);
}
#[test]
fn test_keyword_heuristics() {
// Deep keywords should increase budget
let long_q = compute_budget_internal("Analyze and compare the causes of WW1 and WW2 in detail step-by-step", None);
let short_q = compute_budget_internal("Define photosynthesis", None);
assert!(long_q > short_q, "Deep question should get more tokens");
}
#[test]
fn test_empty_prompt() {
let result = compute_budget_internal("", None);
assert!(result > 0, "Should return a valid budget even for empty prompts");
}
#[test]
fn test_marks_to_tokens_table() {
assert_eq!(marks_to_tokens(1), 150);
assert_eq!(marks_to_tokens(2), 150);
assert_eq!(marks_to_tokens(3), 350);
assert_eq!(marks_to_tokens(10), 1200);
assert_eq!(marks_to_tokens(20), 2500);
}
}
|