| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use pyo3::prelude::*; |
| use regex::Regex; |
| use once_cell::sync::Lazy; |
|
|
| |
|
|
| |
| const MARK_BUDGET_TABLE: &[(u32, u32, u32)] = &[ |
| (1, 2, 150), |
| (3, 4, 350), |
| (5, 6, 600), |
| (7, 8, 900), |
| (9, 10, 1200), |
| (11, 15, 1800), |
| (16, 99, 2500), |
| ]; |
|
|
| |
| const DEFAULT_BUDGET: u32 = 512; |
|
|
| |
|
|
| |
| 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") |
| }); |
|
|
| |
| 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") |
| }); |
|
|
| |
| 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") |
| }); |
|
|
| |
| 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") |
| }); |
|
|
| |
|
|
| fn compute_budget_internal(prompt: &str, explicit_marks: Option<u32>) -> u32 { |
| |
| if let Some(marks) = explicit_marks { |
| return marks_to_tokens(marks); |
| } |
|
|
| |
| if let Some(detected) = detect_marks_in_prompt(prompt) { |
| return marks_to_tokens(detected); |
| } |
|
|
| |
| 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(); |
|
|
| |
| let mut score: i32 = 0; |
|
|
| |
| score += (prompt_len / 50).min(20) as i32; |
|
|
| |
| score += (deep_signals as i32) * 15; |
|
|
| |
| score -= (brief_signals as i32) * 10; |
|
|
| |
| score += (expansion_signals as i32) * 20; |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| fn detect_marks_in_prompt(prompt: &str) -> Option<u32> { |
| |
| let cap = MARKS_PATTERN.captures(prompt)?; |
|
|
| |
| 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 |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[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)) |
| } |
|
|
| |
|
|
| #[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() { |
| |
| 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); |
| } |
| } |
|
|