use regex::Regex;
use serde::Deserialize;
use std::sync::LazyLock;
#[derive(Debug, Clone, Deserialize)]
pub struct AnalyzerLang {
#[serde(default)]
pub plan: Vec<String>,
#[serde(default)]
pub read_file: Vec<String>,
#[serde(default)]
pub search: Vec<String>,
#[serde(default)]
pub write_file: Vec<String>,
#[serde(default)]
pub edit_file: Vec<String>,
#[serde(default)]
pub bash: Vec<String>,
#[serde(default)]
pub web_search: Vec<String>,
}
const EN_TOML: &str = include_str!("prompt_analyzer_lang/en.toml");
const RU_TOML: &str = include_str!("prompt_analyzer_lang/ru.toml");
const ES_TOML: &str = include_str!("prompt_analyzer_lang/es.toml");
const PT_TOML: &str = include_str!("prompt_analyzer_lang/pt.toml");
const FR_TOML: &str = include_str!("prompt_analyzer_lang/fr.toml");
const ID_TOML: &str = include_str!("prompt_analyzer_lang/id.toml");
pub(crate) static LANG_EN: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(EN_TOML).expect("BUG: en.toml failed to parse at runtime"));
pub(crate) static LANG_RU: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(RU_TOML).expect("BUG: ru.toml failed to parse at runtime"));
pub(crate) static LANG_ES: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(ES_TOML).expect("BUG: es.toml failed to parse at runtime"));
pub(crate) static LANG_PT: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(PT_TOML).expect("BUG: pt.toml failed to parse at runtime"));
pub(crate) static LANG_FR: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(FR_TOML).expect("BUG: fr.toml failed to parse at runtime"));
pub(crate) static LANG_ID: LazyLock<AnalyzerLang> =
LazyLock::new(|| toml::from_str(ID_TOML).expect("BUG: id.toml failed to parse at runtime"));
pub fn detect_language(text: &str) -> &'static AnalyzerLang {
let mut cyrillic = 0u32;
let mut latin_accent = 0u32;
let mut total_alpha = 0u32;
for ch in text.chars().take(500) {
if ch.is_alphabetic() {
total_alpha += 1;
if ('\u{0400}'..='\u{04FF}').contains(&ch) {
cyrillic += 1;
} else if ('\u{00C0}'..='\u{024F}').contains(&ch) {
latin_accent += 1;
}
}
}
if total_alpha == 0 {
return &LANG_EN;
}
if cyrillic * 5 > total_alpha {
return &LANG_RU;
}
if latin_accent > 0 {
if text.contains('ã')
|| text.contains('õ')
|| text.contains('ç')
|| text.contains('Ã')
|| text.contains('Õ')
|| text.contains('Ç')
{
return &LANG_PT;
}
if text.contains('ñ') || text.contains('Ñ') || text.contains('¿') || text.contains('¡')
{
return &LANG_ES;
}
if text.contains('à')
|| text.contains('â')
|| text.contains('é')
|| text.contains('è')
|| text.contains('ê')
|| text.contains('ë')
|| text.contains('î')
|| text.contains('ï')
|| text.contains('ô')
|| text.contains('û')
|| text.contains('ù')
|| text.contains('ü')
|| text.contains('ÿ')
{
return &LANG_FR;
}
}
&LANG_EN
}
pub fn all_langs() -> [&'static AnalyzerLang; 6] {
[&LANG_EN, &LANG_RU, &LANG_ES, &LANG_PT, &LANG_FR, &LANG_ID]
}
static SHARED: LazyLock<PromptAnalyzer> = LazyLock::new(PromptAnalyzer::new);
pub fn is_natural_chat(text: &str) -> bool {
let t = text.trim_start();
!t.starts_with('/') && !t.to_ascii_lowercase().starts_with("[system")
}
pub struct PromptAnalyzer {
plan_phrase: Regex,
read_file_phrase: Regex,
search_phrase: Regex,
write_file_phrase: Regex,
edit_file_phrase: Regex,
bash_phrase: Regex,
web_search_phrase: Regex,
}
impl PromptAnalyzer {
pub fn new() -> Self {
let plan_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.plan
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let read_file_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.read_file
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let search_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.search
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let write_file_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.write_file
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let edit_file_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.edit_file
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let bash_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.bash
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
let web_search_phrases: Vec<&str> = all_langs()
.iter()
.flat_map(|lang| {
lang.web_search
.iter()
.filter(|k| k.contains(' '))
.map(|s| s.as_str())
})
.collect();
Self {
plan_phrase: Self::build_keyword_regex(&plan_phrases),
read_file_phrase: Self::build_keyword_regex(&read_file_phrases),
search_phrase: Self::build_keyword_regex(&search_phrases),
write_file_phrase: Self::build_keyword_regex(&write_file_phrases),
edit_file_phrase: Self::build_keyword_regex(&edit_file_phrases),
bash_phrase: Self::build_keyword_regex(&bash_phrases),
web_search_phrase: Self::build_keyword_regex(&web_search_phrases),
}
}
pub fn shared() -> &'static PromptAnalyzer {
&SHARED
}
fn build_keyword_regex(keywords: &[&str]) -> Regex {
if keywords.is_empty() {
return Regex::new(r"(?i)\b(NEVER_MATCH_THIS)\b").expect("Failed to compile regex");
}
let pattern = keywords
.iter()
.map(|k| regex::escape(k))
.collect::<Vec<_>>()
.join("|");
Regex::new(&format!(r"(?i)\b({})\b", pattern)).expect("Failed to compile keyword regex")
}
fn build_word_regex_for_lang(keywords: &[String]) -> Regex {
let words: Vec<&str> = keywords
.iter()
.filter(|k| !k.contains(' '))
.map(|s| s.as_str())
.collect();
Self::build_keyword_regex(&words)
}
pub fn hints_for(&self, prompt: &str) -> Option<String> {
let mut transformations = Vec::new();
let lower_prompt = prompt.to_lowercase();
let lang = detect_language(prompt);
let lang_name = if std::ptr::eq(lang, &*LANG_RU) {
"ru"
} else if std::ptr::eq(lang, &*LANG_ES) {
"es"
} else if std::ptr::eq(lang, &*LANG_PT) {
"pt"
} else if std::ptr::eq(lang, &*LANG_FR) {
"fr"
} else if std::ptr::eq(lang, &*LANG_ID) {
"id"
} else {
"en"
};
let plan_word_lang = Self::build_word_regex_for_lang(&lang.plan);
let read_file_word_lang = Self::build_word_regex_for_lang(&lang.read_file);
let search_word_lang = Self::build_word_regex_for_lang(&lang.search);
let write_file_word_lang = Self::build_word_regex_for_lang(&lang.write_file);
let edit_file_word_lang = Self::build_word_regex_for_lang(&lang.edit_file);
let bash_word_lang = Self::build_word_regex_for_lang(&lang.bash);
let web_search_word_lang = Self::build_word_regex_for_lang(&lang.web_search);
let snippet = if prompt.len() > 40 {
let mut end = 40;
while end > 0 && !prompt.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &prompt[..end])
} else {
prompt.to_string()
};
let plan_match = self.plan_phrase.captures(&lower_prompt).or_else(|| {
if plan_word_lang.is_match(&lower_prompt) {
plan_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = plan_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected PLAN intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations.push(
"\n\n**CRITICAL**: The user wants a PLAN — enter the design track NOW:\n\
1. Explore first if needed (reads, search, bash are available pre-init).\n\
2. plan(operation='init', mode='design', title='...') creates the \
SESSION PLAN .md and returns its absolute path.\n\
3. Write the design INTO that .md (write_file/edit_file on that exact \
path — the only writable file) using the template: ## Context with \
**Problem:** / **Target state:** / **Intent:**, then numbered \
## Implementation steps.\n\
4. STOP and wait for the user to APPROVE the plan (/execute). Do NOT \
call start, do NOT edit project files, do NOT paste the plan in chat.\n\
The checklist is seeded automatically after Approve. Valid operations \
are EXACTLY: init, add_tasks, add_task, start, complete. There is NO \
'create' and NO 'finalize' operation, never call those.",
);
}
let read_file_match = self.read_file_phrase.captures(&lower_prompt).or_else(|| {
if read_file_word_lang.is_match(&lower_prompt) {
read_file_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = read_file_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected READ_FILE intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations
.push("\n\n**TOOL HINT**: Use the `read_file` tool to read the contents of files.");
}
let search_match = self.search_phrase.captures(&lower_prompt).or_else(|| {
if search_word_lang.is_match(&lower_prompt) {
search_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = search_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected SEARCH/GREP intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations.push(
"\n\n**TOOL HINT**: Use the `grep` tool to search for patterns in files, \
or use `glob` to find files by pattern.",
);
}
let write_file_match = self.write_file_phrase.captures(&lower_prompt).or_else(|| {
if write_file_word_lang.is_match(&lower_prompt) {
write_file_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = write_file_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected WRITE_FILE intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations
.push("\n\n**TOOL HINT**: Use the `write_file` tool to create new files.");
}
let edit_file_match = self.edit_file_phrase.captures(&lower_prompt).or_else(|| {
if edit_file_word_lang.is_match(&lower_prompt) {
edit_file_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = edit_file_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected EDIT_FILE intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations
.push("\n\n**TOOL HINT**: Use the `edit_file` tool to modify existing files.");
}
let bash_match = self.bash_phrase.captures(&lower_prompt).or_else(|| {
if bash_word_lang.is_match(&lower_prompt) {
bash_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = bash_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected BASH intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations
.push("\n\n**TOOL HINT**: Use the `bash` tool to execute shell commands.");
}
let web_search_match = self.web_search_phrase.captures(&lower_prompt).or_else(|| {
if web_search_word_lang.is_match(&lower_prompt) {
web_search_word_lang.captures(&lower_prompt)
} else {
None
}
});
if let Some(caps) = web_search_match {
let matched = caps.get(1).map_or("unknown", |m| m.as_str());
tracing::info!(
"🔍 Detected WEB_SEARCH intent: keyword='{}', lang={}, prompt='{}'",
matched,
lang_name,
snippet
);
transformations.push(
"\n\n**TOOL HINT**: Use the `web_search` tool to search the internet for \
real-time information.",
);
}
if transformations.is_empty() {
None
} else {
Some(transformations.join(""))
}
}
pub fn plan_intent(&self, prompt: &str) -> bool {
let lower_prompt = prompt.to_lowercase();
let lang = detect_language(prompt);
let plan_word_lang = Self::build_word_regex_for_lang(&lang.plan);
self.plan_phrase.is_match(&lower_prompt) || plan_word_lang.is_match(&lower_prompt)
}
pub fn analyze_and_transform(&self, prompt: &str) -> String {
match self.hints_for(prompt) {
Some(hint_section) => format!("{}{}", prompt, hint_section),
None => prompt.to_string(),
}
}
}
impl Default for PromptAnalyzer {
fn default() -> Self {
Self::new()
}
}