use anyhow::Result;
use serde_json::Value;
use crate::llmtrim::gate::{GateKind, PlanEntry, Transform};
use crate::llmtrim::ir::Request;
use crate::llmtrim::provider::Provider;
use crate::llmtrim::tokenizer::counter_for;
pub struct NgramStage {
pub max_entries: usize,
}
impl Transform for NgramStage {
fn name(&self) -> &str {
"ngram"
}
fn gate_kind(&self) -> GateKind {
GateKind::InputTokens
}
fn apply(
&self,
req: &mut Request,
provider: &dyn Provider,
_plan: &mut Vec<PlanEntry>,
) -> Result<()> {
let segs: Vec<(String, String)> =
crate::llmtrim::cache_zone::compressible_pointers(req, provider)
.into_iter()
.filter_map(|p| req.get_str(&p).map(|s| (p, s.to_string())))
.collect();
if segs.is_empty() {
return Ok(());
}
let Some(marker) = pick_marker(&segs) else {
return Ok(()); };
let prose: Vec<usize> = (0..segs.len())
.filter(|&i| !crate::llmtrim::stages::tools::is_structured_segment(&segs[i].1))
.collect();
if prose.is_empty() {
return Ok(());
}
let mut working: Vec<String> = prose.iter().map(|&i| segs[i].1.clone()).collect();
let model = req.raw().get("model").and_then(Value::as_str);
let counter = counter_for(req.kind(), model)?;
let work_refs: Vec<&str> = working.iter().map(String::as_str).collect();
let selections = crate::llmtrim::stages::ngram_sa::mine(
&work_refs,
self.max_entries,
counter.as_ref(),
|k| format!("{marker}{k}"),
);
if selections.is_empty() {
return Ok(());
}
let mut committed: Vec<(String, String)> = Vec::new();
for sel in selections {
let ph = format!("{marker}{}", committed.len() + 1);
for t in working.iter_mut() {
*t = replace_word_bounded(t, &sel.phrase, &ph);
}
committed.push((ph, sel.phrase));
}
if committed.is_empty() {
return Ok(());
}
for (wi, &i) in prose.iter().enumerate() {
req.set(&segs[i].0, Value::String(working[wi].clone()));
}
let legend = committed
.iter()
.map(|(ph, phrase)| format!("{ph}={phrase}"))
.collect::<Vec<_>>()
.join("; ");
const GLOSSARY_TMPL: &str = include_str!("../prompts/ngram_glossary.txt");
provider.add_system_instruction(req, &GLOSSARY_TMPL.replace("{terms}", &legend));
Ok(())
}
}
const MARKERS: &[&str] = &["§", "⟦", "@@", "‡"];
fn pick_marker(segs: &[(String, String)]) -> Option<&'static str> {
MARKERS
.iter()
.copied()
.find(|&m| !segs.iter().any(|(_, t)| t.contains(m)))
}
fn boundary_ok(t: &str, at: usize, before: bool) -> bool {
let adj = if before {
t[..at].chars().next_back()
} else {
t[at..].chars().next()
};
adj.is_none_or(|c| !c.is_alphanumeric())
}
#[cfg(test)]
fn count_word_bounded(t: &str, phrase: &str) -> usize {
let mut n = 0;
let mut start = 0;
while let Some(rel) = t[start..].find(phrase) {
let s = start + rel;
let e = s + phrase.len();
if boundary_ok(t, s, true) && boundary_ok(t, e, false) {
n += 1;
}
start = e; }
n
}
fn replace_word_bounded(t: &str, phrase: &str, ph: &str) -> String {
let mut out = String::with_capacity(t.len());
let mut pos = 0;
while let Some(rel) = t[pos..].find(phrase) {
let s = pos + rel;
let e = s + phrase.len();
if boundary_ok(t, s, true) && boundary_ok(t, e, false) {
out.push_str(&t[pos..s]);
out.push_str(ph);
pos = e;
} else {
let skip = t[s..].chars().next().map_or(e, |c| s + c.len_utf8());
out.push_str(&t[pos..skip]);
pos = skip;
}
}
out.push_str(&t[pos..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llmtrim::ir::ProviderKind;
use crate::llmtrim::pipeline;
use crate::llmtrim::provider::OpenAiProvider;
use crate::llmtrim::tokenizer::counter_for;
use serde_json::json;
#[test]
fn candidates_include_the_repeated_phrase() {
let phrase = "the quarterly financial report";
let text = format!(
"{phrase} grew. later {phrase} fell. again {phrase} held. then {phrase} rose. finally {phrase} dipped."
);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let sel = crate::llmtrim::stages::ngram_sa::mine(&[&text], 32, counter.as_ref(), |k| {
format!("§{k}")
});
assert!(
sel.iter().any(|s| s.phrase == phrase),
"frequent phrase is mined and selected"
);
}
#[test]
fn stage_abbreviates_repeated_boilerplate_with_legend() {
let p = "the internal configuration service endpoint";
let content = format!(
"{p} failed. retry {p}. then {p} again. {p} more. {p} keeps. {p} still. {p} yet. finally {p} ok."
);
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":content}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(NgramStage { max_entries: 32 })];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(
out.stages[0].applied,
"repeated-phrase abbreviation cuts tokens"
);
let sys = req
.raw()
.pointer("/messages/0/content")
.and_then(Value::as_str)
.unwrap();
assert!(
sys.contains("Glossary") && sys.contains(p),
"legend defines phrase"
);
let user = req
.raw()
.pointer("/messages/1/content")
.and_then(Value::as_str)
.unwrap();
assert!(user.contains('§'), "content uses the placeholder");
assert_eq!(
sys.matches('§').count(),
1,
"only the phrase that pays off is committed"
);
}
#[test]
fn replace_is_word_bounded_not_substring() {
let t = "the report says X. the report says Y. but the reporter disagreed.";
let occ = count_word_bounded(t, "the report");
assert_eq!(
occ, 2,
"only the two whole-word hits count, not 'the reporter'"
);
let out = replace_word_bounded(t, "the report", "§1");
assert!(out.contains("the reporter"), "partial-word hit untouched");
assert!(!out.contains("§1er"), "no corrupted partial replacement");
assert_eq!(out.matches("§1").count(), 2, "both whole phrases replaced");
}
#[test]
fn picks_fallback_marker_when_section_sign_present() {
let p = "die zuständige aufsichtsbehörde des landes";
let content = format!(
"Nach §1 gilt: {p} prüft. Ferner {p} entscheidet. Schließlich {p} bestätigt. Zudem {p} meldet."
);
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":content}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(NgramStage { max_entries: 32 })];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(
out.stages[0].applied,
"abbreviates despite an existing § in the text"
);
let user = req.get_str("/messages/1/content").unwrap();
assert!(user.contains('⟦'), "fallback marker used, not §");
assert!(
user.contains("§1"),
"the original §1 reference is preserved verbatim"
);
}
#[test]
fn skips_json_record_arrays() {
let content = "[{\"occupation\":\"Sales\"},{\"occupation\":\"Sales\"},{\"occupation\":\"Sales\"},{\"occupation\":\"Tech\"}]\n\nHow many records have occupation Sales? Answer with the number.";
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":content}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(NgramStage { max_entries: 32 })];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(
!out.stages[0].applied,
"structured records are not abbreviated"
);
let now = req.get_str("/messages/0/content").unwrap();
assert!(
!now.contains('§'),
"no placeholder injected into record data"
);
assert_eq!(now, content, "record segment left exactly verbatim");
}
fn toks(counter: &dyn crate::llmtrim::tokenizer::TokenCounter, s: &str) -> usize {
counter.count(s)
}
fn old_fixed_window_pick(text: &str) -> Option<String> {
use std::collections::HashMap;
let words: Vec<&str> = text.split_whitespace().collect();
let mut counts: HashMap<String, usize> = HashMap::new();
for n in 2..=6 {
if words.len() < n {
break;
}
for w in words.windows(n) {
*counts.entry(w.join(" ")).or_insert(0) += 1;
}
}
let mut cands: Vec<(String, usize)> = counts
.into_iter()
.filter(|(p, c)| *c >= 2 && p.len() >= 8)
.collect();
cands.sort_by(|a, b| {
let words = |s: &str| s.split_whitespace().count();
words(&b.0)
.cmp(&words(&a.0))
.then(b.1.cmp(&a.1))
.then(a.0.cmp(&b.0))
});
cands.into_iter().map(|(p, _)| p).next()
}
#[test]
fn captures_phrase_longer_than_the_fixed_window() {
let clause = "the parties hereby agree to indemnify and hold harmless";
assert_eq!(clause.split_whitespace().count(), 9, "fixture is 9 words");
let text = format!(
"firstly {clause} promptly. moreover {clause} fully. \
additionally {clause} jointly. lastly {clause} severally."
);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let sel = crate::llmtrim::stages::ngram_sa::mine(&[&text], 32, counter.as_ref(), |k| {
format!("§{k}")
});
let top = sel.first().expect("a phrase is selected");
assert_eq!(
top.phrase, clause,
"suffix-array miner captures the whole 9-word repeat, not a fragment"
);
let old = old_fixed_window_pick(&text).expect("fixed-window picks something");
assert!(
old.split_whitespace().count() <= 6,
"fixed window is capped at 6 words (got {})",
old.split_whitespace().count()
);
assert!(
top.phrase.split_whitespace().count() > old.split_whitespace().count(),
"new phrase is longer than the fixed-window best"
);
let occ = 4;
let new_after = occ * toks(counter.as_ref(), "§1")
+ toks(counter.as_ref(), &format!("§1={}; ", top.phrase));
let new_before = occ * toks(counter.as_ref(), top.phrase.as_str());
let old_after =
occ * toks(counter.as_ref(), "§1") + toks(counter.as_ref(), &format!("§1={old}; "));
let old_before = occ * toks(counter.as_ref(), &old);
let new_saved = new_before as i64 - new_after as i64;
let old_saved = old_before as i64 - old_after as i64;
assert!(
new_saved > old_saved,
"suffix-array savings ({new_saved}) exceed fixed-window savings ({old_saved})"
);
}
#[test]
fn overlap_no_double_claimed_spans_and_reconstructs() {
let left = "alpha bravo charlie delta";
let right = "charlie delta echo foxtrot";
let text = format!(
"{left} one. {left} two. {left} three. {left} four. \
{right} five. {right} six. {right} seven. {right} eight."
);
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":text}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(NgramStage { max_entries: 32 })];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(out.stages[0].applied, "overlapping repeats still compress");
let user = req.get_str("/messages/1/content").unwrap().to_string();
let sys = req.get_str("/messages/0/content").unwrap().to_string();
let legend = sys
.split_once(':')
.map(|(_, rest)| rest.trim())
.unwrap_or(&sys);
let mut pairs: Vec<(String, String)> = legend
.split("; ")
.filter_map(|e| {
e.split_once('=')
.map(|(k, v)| (k.trim().to_string(), v.to_string()))
})
.collect();
pairs.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then(b.0.cmp(&a.0)));
let mut restored = user.clone();
for (ph, phrase) in &pairs {
restored = restored.replace(ph.as_str(), phrase);
}
assert_eq!(
restored, text,
"legend reverses placeholders to the exact original"
);
for (ph, _) in &pairs {
assert!(
user.contains(ph.as_str()),
"every legend entry is referenced"
);
}
for tok in user.split(|c: char| !c.is_alphanumeric() && c != '§') {
if let Some(rest) = tok.strip_prefix('§')
&& rest.chars().all(|c| c.is_ascii_digit())
&& !rest.is_empty()
{
assert!(
pairs.iter().any(|(ph, _)| ph == tok),
"placeholder {tok} in body must be defined in the legend"
);
}
}
}
#[test]
fn selection_is_deterministic_across_runs() {
let p = "the internal configuration service endpoint";
let content =
format!("{p} failed. retry {p}. then {p} again. {p} more. {p} keeps. {p} still.");
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let run_once = || {
crate::llmtrim::stages::ngram_sa::mine(&[&content], 32, counter.as_ref(), |k| {
format!("§{k}")
})
.into_iter()
.map(|s| s.phrase)
.collect::<Vec<_>>()
};
let a = run_once();
let b = run_once();
let c = run_once();
assert!(!a.is_empty(), "something is selected");
assert_eq!(a, b, "mining is deterministic");
assert_eq!(b, c, "mining is deterministic");
}
#[test]
fn unicode_cjk_and_accented_repeats() {
let cjk = "请 立即 提交 季度 财务 报告"; let accented = "déclaration trimestrielle of résultats financiers";
let text = format!(
"{cjk} 一. {cjk} 二. {cjk} 三. {cjk} 四. \
{accented} A. {accented} B. {accented} C. {accented} D."
);
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":text}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(NgramStage { max_entries: 32 })];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(out.stages[0].applied, "unicode repeats compress");
let sys = req.get_str("/messages/0/content").unwrap();
assert!(
sys.contains(cjk) || sys.contains(accented),
"a unicode phrase is defined in the legend verbatim"
);
let user = req.get_str("/messages/1/content").unwrap().to_string();
let legend = sys.split_once(':').map(|(_, r)| r.trim()).unwrap_or(sys);
let mut pairs: Vec<(String, String)> = legend
.split("; ")
.filter_map(|e| {
e.split_once('=')
.map(|(k, v)| (k.trim().to_string(), v.to_string()))
})
.collect();
pairs.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then(b.0.cmp(&a.0)));
let mut restored = user.clone();
for (ph, phrase) in &pairs {
restored = restored.replace(ph.as_str(), phrase);
}
assert_eq!(
restored, text,
"unicode legend reverses to the exact original"
);
}
}