import re
import torch
from queue import Empty
from threading import Thread
from typing import Callable, Generator, List, Dict, Optional
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TextIteratorStreamer,
StoppingCriteria,
StoppingCriteriaList,
)
from prompts import REASONING_SYSTEM_PROMPT
from tokens_control import protocol as token_protocol
CHAT_CONTROL_TOKENS = token_protocol.CHAT_CONTROL_TOKENS
def strip_chat_control_tokens(text: str | None, trim: bool = True) -> str:
if text is None:
return ""
cleaned = str(text)
for token in CHAT_CONTROL_TOKENS:
cleaned = cleaned.replace(token, "")
return cleaned.strip() if trim else cleaned
class StopOnSubstrings(StoppingCriteria):
"""
Stops generation once any stop string appears in the decoded tail.
"""
def __init__(
self,
tokenizer,
stop_strings: List[str],
prompt_length: int = 0,
max_check_tokens: int = 80,
):
self.tokenizer = tokenizer
self.stop_strings = stop_strings
self.prompt_length = prompt_length
self.max_check_tokens = max_check_tokens
def __call__(self, input_ids, scores, **kwargs) -> bool:
generated_ids = input_ids[0][self.prompt_length:]
if generated_ids.numel() == 0:
return False
tail_ids = generated_ids[-self.max_check_tokens:]
tail = self.tokenizer.decode(tail_ids, skip_special_tokens=False)
return any(stop in tail for stop in self.stop_strings)
class StopOnThinkingTokenLimit(StoppingCriteria):
"""
Stops generation when the currently open block reaches a token cap.
"""
def __init__(
self,
tokenizer,
token_limit: int,
prompt_length: int = 0,
initial_in_think: bool = False,
):
self.tokenizer = tokenizer
self.token_limit = max(0, int(token_limit))
self.prompt_length = prompt_length
self.initial_in_think = initial_in_think
self.triggered = False
def __call__(self, input_ids, scores, **kwargs) -> bool:
generated_ids = input_ids[0][self.prompt_length:]
if generated_ids.numel() == 0:
return False
generated = self.tokenizer.decode(generated_ids, skip_special_tokens=False)
think_start = generated.rfind("")
think_end = generated.rfind("")
if think_start >= 0 and think_end < think_start:
content = generated[think_start + len(""):]
elif self.initial_in_think and think_end < 0:
content = generated
else:
return False
count = len(
self.tokenizer(
content,
add_special_tokens=False,
)["input_ids"]
)
if count >= self.token_limit:
self.triggered = True
return True
return False
class ResultTagSuppressor:
"""
Suppresses model-generated blocks from the visible stream.
Runtime-injected blocks are handled separately.
"""
START = ""
END = ""
TAIL = max(len(START), len(END)) - 1
def __init__(self):
self.buffer = ""
self.in_result = False
self.suppressed = False
def feed(self, chunk: str) -> str:
self.buffer += chunk
output = []
while self.buffer:
if self.in_result:
end_index = self.buffer.find(self.END)
if end_index < 0:
self.buffer = self.buffer[-self.TAIL:]
break
self.buffer = self.buffer[end_index + len(self.END):]
self.in_result = False
continue
start_index = self.buffer.find(self.START)
if start_index >= 0:
output.append(self.buffer[:start_index])
self.buffer = self.buffer[start_index + len(self.START):]
self.in_result = True
self.suppressed = True
continue
if len(self.buffer) <= self.TAIL:
break
emit_upto = len(self.buffer) - self.TAIL
output.append(self.buffer[:emit_upto])
self.buffer = self.buffer[emit_upto:]
break
return "".join(output)
def flush(self) -> str:
if self.in_result:
self.buffer = ""
self.in_result = False
return ""
output = self.buffer
self.buffer = ""
return output
class EarlyAnswerSuppressor:
"""
While retrieval has started but min_tool_rounds has not been reached,
hides early attempts from the UI.
"""
START = ""
TAIL = len(START) - 1
def __init__(self):
self.buffer = ""
self.blocked = False
def feed(self, chunk: str) -> str:
if self.blocked:
return ""
self.buffer += chunk
start_index = self.buffer.find(self.START)
if start_index >= 0:
output = self.buffer[:start_index]
self.buffer = ""
self.blocked = True
return output
if len(self.buffer) <= self.TAIL:
return ""
emit_upto = len(self.buffer) - self.TAIL
output = self.buffer[:emit_upto]
self.buffer = self.buffer[emit_upto:]
return output
def flush(self) -> str:
if self.blocked:
self.buffer = ""
return ""
output = self.buffer
self.buffer = ""
return output
class StartTagSuppressor:
"""
Suppresses a forbidden opening tag and everything generated after it.
Used after retrieval is exhausted so leftover spam never reaches UI.
"""
def __init__(self, start_tags: List[str]):
self.start_tags = start_tags
self.tail = max((len(tag) for tag in start_tags), default=1) - 1
self.buffer = ""
self.blocked = False
def feed(self, chunk: str) -> str:
if self.blocked:
return ""
self.buffer += chunk
starts = [
index
for tag in self.start_tags
for index in [self.buffer.find(tag)]
if index >= 0
]
if starts:
start_index = min(starts)
output = self.buffer[:start_index]
self.buffer = ""
self.blocked = True
return output
if len(self.buffer) <= self.tail:
return ""
emit_upto = len(self.buffer) - self.tail
output = self.buffer[:emit_upto]
self.buffer = self.buffer[emit_upto:]
return output
def flush(self) -> str:
if self.blocked:
self.buffer = ""
return ""
output = self.buffer
self.buffer = ""
return output
class MultimodalQwen:
QUERY_RE = token_protocol.QUERY_RE
QUERY_FORBIDDEN_RE = token_protocol.QUERY_FORBIDDEN_RE
QUERY_PREFIX_JUNK_RE = token_protocol.QUERY_PREFIX_JUNK_RE
QUERY_LEAK_STOP_STRINGS = token_protocol.QUERY_LEAK_STOP_STRINGS
RESULT_BLOCK_RE = token_protocol.RESULT_BLOCK_RE
PLAIN_FINAL_RE = re.compile(
r"(?is)(?:^|\n)\s*(?:final answer|answer)\s*:\s*(.+)$"
)
GENERATED_RESULT_CLAIM_RE = re.compile(
r"""(?is)
(
\b(?:the\s+)?retrieved\s+result\s+(?:is|says|shows|states)\s*:
|
\b(?:the\s+)?retrieval\s+result\s+(?:is|says|shows|states)\s*:
|
\b(?:the\s+)?document\s+(?:says|states|shows|mentions)\s*:
|
\b(?:the\s+)?source\s+(?:says|states|shows|mentions)\s*:
|
\bresult\s*:
|
\bretrieved\s+result\s*:
|
\bdocument\s*:
)
""",
re.VERBOSE,
)
FAKE_RETRIEVAL_CLAIM_RE = re.compile(
r"""(?is)
\b(
retrieved\s+(?:result|text|document|evidence)
|
retrieval\s+(?:result|text|document|evidence)
|
document
|
source
|
evidence
)\s+
(?:is|says|states|shows|mentions|contains|indicates)\b
""",
re.VERBOSE,
)
BAD_META_QUERY_RE = re.compile(
r"""(?is)^\s*
(
block\s+to\s+request\s+retrieval
|query\s+block
|request\s+retrieval
|emit\s+(?:a\s+)?query
|retrieve\s+it
|look\s+it\s+up
)
[\s.]*$
""",
re.VERBOSE,
)
BAD_META_ANSWER_RE = re.compile(
r"""(?is)
\s*
(
.*?\b(?:should|need|must)\s+(?:retrieve|query|search|look\s+up)\b.*
|
.*?\bno\s+(?:document|context|result|retrieved\s+evidence)\s+(?:is|was)\s+present\b.*
|
.*?\bno\s+retrieved\s+document\s+evidence\s+was\s+found\b.*
|
this\s+is\s+a\s+(?:fact|factual|document|external|fictional).{0,240}
)
""",
re.VERBOSE,
)
OFF_TOPIC_EVIDENCE_RE = re.compile(
r"""(?is)
(
retrieved\s+(?:result|evidence|document).{0,160}
(?:not\s+directly\s+applicable|not\s+directly\s+address|does\s+not\s+address|irrelevant|off-topic)
|
evidence\s+is\s+incomplete
|
retrieved\s+evidence\s+is\s+insufficient
)
""",
re.VERBOSE,
)
RETRIEVAL_INTENT_RE = re.compile(
r"""(?is)
(
\b(?:require|requires|required|need|needs|needed|must|should|have\s+to)\b
.{0,160}?
\b(?:retrieval|retrieve|query|lookup|search|document|documents|passage|passages|source|sources|evidence)\b
)
|
\bno\s+(?:retrieval\s+)?result\s+is\s+present\b
|
\bno\s+direct\s+evidence\s+is\s+present\b
|
\bmust\s+create\s+(?:one|a\s+(?:query|retrieval|lookup))\b
|
\bneed\s+to\s+check\s+(?:if\s+)?(?:the\s+)?documents?\b
""",
re.VERBOSE,
)
NO_RETRIEVAL_INTENT_RE = re.compile(
r"""(?is)
\b(?:do\s+not|don't|does\s+not|doesn't|no|not)\s+
(?:need|require|requires|required)\s+
.{0,80}?
\b(?:retrieval|retrieve|query|lookup|search)\b
""",
re.VERBOSE,
)
TOOL_PROMISE_RE = re.compile(
r"""(?is)
\b(
i\s+will
|i'll
|i\s+am\s+going\s+to
|i'm\s+going\s+to
|let\s+me
|i\s+should
|i\s+need\s+to
|i\s+have\s+to
)\s+
(?:now\s+|first\s+|try\s+to\s+)?
(search|retrieve|query|look\s+up|lookup|check\s+(?:the\s+)?documents?|find\s+(?:evidence|sources?))
\b
""",
re.VERBOSE,
)
PLAIN_ANSWER_ATTEMPT_RE = re.compile(
r"(?is)(?:^|\n)\s*(?:final\s+answer|answer)\s*:\s*[\s\S]*$"
)
def __init__(
self,
model_name: str = "Qwen/Qwen2.5-3B-Instruct",
device: Optional[str] = None,
torch_dtype="auto",
):
self.model_name = model_name
self.tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True,
)
if self.tokenizer.pad_token_id is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
self.model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch_dtype,
trust_remote_code=True,
).to(self.device)
self.model.eval()
self.prompt = REASONING_SYSTEM_PROMPT
# ---------------------------------------------------------------------
# Basic helpers
# ---------------------------------------------------------------------
def _stop_strings(self, stop_strings: Optional[List[str]] = None) -> List[str]:
return list(stop_strings or [])
def _eos_token_ids(self):
return self.tokenizer.eos_token_id
def coerce_plain_final_answer(self, text: str) -> tuple[str, bool]:
"""
Do not fabricate in Python.
The model must emit protocol tags itself.
"""
return text, False
@staticmethod
def has_open_tag(text: str, tag: str) -> bool:
return text.rfind(f"<{tag}>") > text.rfind(f"{tag}>")
@staticmethod
def first_terminal_tag(text: str) -> tuple[str | None, int]:
"""
Returns the first completed terminal protocol block.
If model emits:
real queryjunk
runtime keeps only:
real query
and starts retrieval immediately.
"""
text = text or ""
candidates: list[tuple[int, str]] = []
for close_tag, kind in (
("", "query"),
("", "answer"),
):
index = text.find(close_tag)
if index >= 0:
candidates.append((index + len(close_tag), kind))
if not candidates:
return None, -1
end_index, kind = min(candidates, key=lambda item: item[0])
return kind, end_index
def _slice_to_first_terminal(self, text: str) -> tuple[str, str | None]:
terminal_kind, terminal_end = self.first_terminal_tag(text)
if terminal_kind is None:
return text, None
return text[:terminal_end], terminal_kind
# ---------------------------------------------------------------------
# Prompt formatting
# ---------------------------------------------------------------------
def _messages(
self,
user_prompt: str,
system_prompt: Optional[str] = None,
) -> List[Dict[str, str]]:
active_prompt = str(system_prompt).strip() if system_prompt is not None else ""
active_prompt = active_prompt or self.prompt
return [
{"role": "system", "content": active_prompt},
{"role": "user", "content": strip_chat_control_tokens(user_prompt)},
]
def _apply_chat_template(self, messages: List[Dict[str, str]]):
inputs = self.tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
)
return {k: v.to(self.device) for k, v in inputs.items()}
def _build_raw_prompt(
self,
user_prompt: str,
system_prompt: Optional[str] = None,
) -> str:
return self.tokenizer.apply_chat_template(
self._messages(user_prompt, system_prompt=system_prompt),
add_generation_prompt=True,
tokenize=False,
)
def _tokenize_raw(self, raw_prompt: str):
inputs = self.tokenizer(
raw_prompt,
return_tensors="pt",
add_special_tokens=False,
)
return {k: v.to(self.device) for k, v in inputs.items()}
# ---------------------------------------------------------------------
# Basic generation
# ---------------------------------------------------------------------
@torch.inference_mode()
def generate(
self,
prompt: str,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
system_prompt: Optional[str] = None,
) -> str:
messages = self._messages(prompt, system_prompt=system_prompt)
inputs = self._apply_chat_template(messages)
do_sample = temperature > 0
output_ids = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else None,
top_p=top_p if do_sample else None,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self._eos_token_ids(),
)
new_ids = output_ids[0][inputs["input_ids"].shape[-1]:]
return strip_chat_control_tokens(
self.tokenizer.decode(new_ids, skip_special_tokens=True)
)
@torch.inference_mode()
def _generate_raw(
self,
raw_prompt: str,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
stop_strings: Optional[List[str]] = None,
) -> str:
inputs = self._tokenize_raw(raw_prompt)
do_sample = temperature > 0
stop_strings = self._stop_strings(stop_strings)
stopping_criteria = None
if stop_strings:
stopping_criteria = StoppingCriteriaList([
StopOnSubstrings(
self.tokenizer,
stop_strings,
prompt_length=inputs["input_ids"].shape[-1],
)
])
output_ids = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else None,
top_p=top_p if do_sample else None,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self._eos_token_ids(),
stopping_criteria=stopping_criteria,
)
new_ids = output_ids[0][inputs["input_ids"].shape[-1]:]
return strip_chat_control_tokens(
self.tokenizer.decode(new_ids, skip_special_tokens=True)
)
def stream_raw(
self,
raw_prompt: str,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
stop_strings: Optional[List[str]] = None,
thinking_token_limit: Optional[int] = None,
thinking_limit_state: Optional[dict] = None,
) -> Generator[str, None, None]:
inputs = self._tokenize_raw(raw_prompt)
streamer = TextIteratorStreamer(
self.tokenizer,
skip_prompt=True,
skip_special_tokens=True,
timeout=1.0,
)
do_sample = temperature > 0
stop_strings = self._stop_strings(stop_strings)
stopping_items = []
if stop_strings:
stopping_items.append(
StopOnSubstrings(
self.tokenizer,
stop_strings,
prompt_length=inputs["input_ids"].shape[-1],
)
)
thinking_stopper = None
if thinking_token_limit is not None:
thinking_stopper = StopOnThinkingTokenLimit(
self.tokenizer,
thinking_token_limit,
prompt_length=inputs["input_ids"].shape[-1],
initial_in_think=self.has_open_tag(raw_prompt, "think"),
)
stopping_items.append(thinking_stopper)
stopping_criteria = StoppingCriteriaList(stopping_items) if stopping_items else None
kwargs = dict(
**inputs,
streamer=streamer,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else None,
top_p=top_p if do_sample else None,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self._eos_token_ids(),
stopping_criteria=stopping_criteria,
)
generation_error = []
def run_generation():
try:
with torch.inference_mode():
self.model.generate(**kwargs)
except BaseException as exc:
generation_error.append(exc)
try:
streamer.on_finalized_text("", stream_end=True)
except Exception:
pass
thread = Thread(target=run_generation, daemon=True)
thread.start()
while True:
try:
chunk = next(streamer)
except StopIteration:
break
except Empty:
if not thread.is_alive():
break
continue
chunk = strip_chat_control_tokens(chunk, trim=False)
if chunk:
yield chunk
thread.join()
if thinking_limit_state is not None:
thinking_limit_state["triggered"] = bool(
thinking_stopper and thinking_stopper.triggered
)
if generation_error:
raise RuntimeError("Model generation failed") from generation_error[0]
def stream(
self,
prompt: str,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
thinking_token_limit: Optional[int] = None,
system_prompt: Optional[str] = None,
) -> Generator[str, None, None]:
raw_prompt = self._build_raw_prompt(prompt, system_prompt=system_prompt)
thinking_state: dict = {}
generated = ""
for chunk in self.stream_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
thinking_token_limit=thinking_token_limit,
thinking_limit_state=thinking_state,
):
generated += chunk
yield chunk
raw_prompt += generated
if thinking_state.get("triggered") and self.has_open_tag(raw_prompt, "think"):
close_think = "\n\n"
raw_prompt += close_think
yield close_think
forced = (
"[Runtime instruction: Thinking budget reached. "
"Produce the final ... now.]\n"
)
raw_prompt += forced
yield forced
for chunk in self.stream_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=["", "", "", "", "", ""],
):
yield chunk
# ---------------------------------------------------------------------
# Protocol parsing / sanitizing
# ---------------------------------------------------------------------
def extract_query(self, text: str) -> Optional[str]:
return token_protocol.extract_query(text)
def extract_open_query(self, text: str) -> Optional[str]:
"""
Only catches a truly unclosed final block.
"""
text = text or ""
lower = text.lower()
last_open = lower.rfind("")
last_close = lower.rfind("")
if last_open < 0:
return None
if last_close > last_open:
return None
return text[last_open + len(""):].strip()
def sanitize_query_text(self, query: str) -> str:
query = str(query or "").strip()
# Remove repeated/nested query openings.
query = re.sub(r"(?is)(?:\s*)+", "", query).strip()
query = re.split(
r"(?is)?think>|?answer>|?result>|||final\s+answer\s*:|answer\s*:",
query,
maxsplit=1,
)[0].strip()
query = re.sub(
r"(?is)\b(?:retrieved\s+evidence\s+only|evidence\s+only|use\s+retrieved\s+evidence\s+only)\b\.?",
"",
query,
).strip()
query = self.QUERY_PREFIX_JUNK_RE.sub("", query).strip()
query = re.sub(r"(?is)^(query|search|lookup)\s*:\s*", "", query).strip()
return query
def has_malformed_query(self, text: str) -> bool:
open_query = self.extract_open_query(text)
if open_query is None:
return False
clean_query = self.sanitize_query_text(open_query)
if not clean_query:
return True
if self.QUERY_FORBIDDEN_RE.search(open_query):
return True
return False
@staticmethod
def malformed_query_correction() -> str:
return token_protocol.malformed_query_correction()
def strip_generated_result_blocks(self, text: str) -> tuple[str, bool]:
cleaned, count = self.RESULT_BLOCK_RE.subn("", text)
return cleaned, count > 0
def sanitize_result(self, result: str) -> str:
if result is None:
result = ""
result = str(result)
result = strip_chat_control_tokens(result)
replacements = {
"": "[think]",
"": "[/think]",
"": "[query]",
"": "[/query]",
"": "[result]",
"": "[/result]",
"": "[answer]",
"": "[/answer]",
}
for old, new in replacements.items():
result = result.replace(old, new)
return result.strip()
def format_query_result(self, query: str, result: str) -> str:
safe_query = self.sanitize_result(query)
safe_result = self.sanitize_result(result)
if not safe_result:
safe_result = "No retrieval results found."
return (
"Query:\n"
f"{safe_query}\n\n"
"Retrieved Result:\n"
f"{safe_result}"
)
def retrieval_limit_result(self, max_tool_rounds: int) -> str:
return self.format_query_result(
query="retrieval limit",
result=(
f"Retrieval limit reached ({max_tool_rounds}). "
"No more retrieval documents will be provided for this answer. "
"Use the evidence already shown, say when it is insufficient, "
"and finish with answer."
),
)
def is_bad_meta_query(self, query: str) -> bool:
return self.BAD_META_QUERY_RE.search(query or "") is not None
def has_bad_meta_answer(self, text: str) -> bool:
return self.BAD_META_ANSWER_RE.search(text or "") is not None
def says_evidence_off_topic_or_incomplete(self, text: str) -> bool:
return self.OFF_TOPIC_EVIDENCE_RE.search(text or "") is not None
def has_generated_result_claim(self, text: str, tool_rounds: int) -> bool:
if not text:
return False
if tool_rounds > 0:
return False
if "" in text or "" in text:
return False
return self.GENERATED_RESULT_CLAIM_RE.search(text) is not None
def strip_generated_result_claim(self, text: str) -> str:
match = self.GENERATED_RESULT_CLAIM_RE.search(text or "")
if not match:
return text
return text[:match.start()].rstrip()
def has_fake_retrieval_claim(self, text: str, tool_rounds: int) -> bool:
if tool_rounds > 0:
return False
if "" in text or "" in text:
return False
return self.FAKE_RETRIEVAL_CLAIM_RE.search(text or "") is not None
def strip_fake_retrieval_claim(self, text: str) -> str:
match = self.FAKE_RETRIEVAL_CLAIM_RE.search(text or "")
if not match:
return text
return text[:match.start()].rstrip()
@staticmethod
def retrieval_exhausted_answer_instruction(max_tool_rounds: int) -> str:
return (
"\n[Runtime instruction: Retrieval budget is exhausted. "
f"Maximum retrieval results already reached: {max_tool_rounds}. "
"Do not emit again. Do not say you will search, retrieve, look up, "
"check documents, or request more evidence. "
"Produce exactly one final ... now, using only the existing "
" evidence. If the evidence is insufficient, say it is insufficient.]\n"
)
@staticmethod
def protocol_correction(force_answer: bool = False) -> str:
if force_answer:
return (
"\n[Runtime instruction: No more retrieval results are available. "
"Answer from the evidence already shown.]\n"
)
return (
"\n[Runtime instruction: Do not create retrieval results yourself. "
"Either request one focused lookup or answer directly.]\n"
)
@staticmethod
def fake_retrieval_claim_correction() -> str:
return (
"\n[Runtime instruction: You claimed retrieval/document evidence exists, "
"but no block has been provided by runtime. Do not invent retrieved content. "
"Emit exactly one focused ... next.]\n"
)
@staticmethod
def generated_result_claim_correction() -> str:
return (
"\n[Runtime instruction: You cannot create or describe retrieved results yourself. "
"Only the runtime may provide retrieval results inside .... "
"If evidence is needed, emit exactly one focused .... "
"If evidence is not needed, answer directly without mentioning retrieved results.]\n"
)
@staticmethod
def force_query_correction() -> str:
return (
"\n[Runtime instruction: You promised or stated that retrieval/search is needed, "
"but you did not emit a valid block. Natural-language promises like "
"'I will search' do not execute tools. Emit exactly one concrete "
"... next.]\n"
)
def minimum_retrieval_correction(self, tool_rounds: int, min_tool_rounds: int) -> str:
remaining = max(0, min_tool_rounds - tool_rounds)
return (
"\n[Runtime instruction: Retrieval has started, but not enough retrieval results "
"have been collected yet. "
f"Current retrieval results: {tool_rounds}. "
f"Minimum required retrieval results: {min_tool_rounds}. "
f"Remaining retrieval result(s): {remaining}. "
"Request another focused retrieval query next.]\n"
)
def declares_retrieval_intent(self, text: str) -> bool:
if not text:
return False
if self.extract_query(text) is not None:
return False
if self.NO_RETRIEVAL_INTENT_RE.search(text):
return False
return (
self.RETRIEVAL_INTENT_RE.search(text) is not None
or self.TOOL_PROMISE_RE.search(text) is not None
)
def strip_answer_attempts(self, text: str) -> str:
text = re.sub(r"(?is)[\s\S]*?(?:|$)", "", text)
text = self.PLAIN_ANSWER_ATTEMPT_RE.sub("", text)
return text.rstrip()
# ---------------------------------------------------------------------
# Non-streaming query loop
# ---------------------------------------------------------------------
def generate_with_query(
self,
prompt: str,
query_fn: Callable[[str], str],
max_tool_rounds: int = 4,
min_tool_rounds: int = 2,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
system_prompt: Optional[str] = None,
) -> str:
raw_prompt = self._build_raw_prompt(prompt, system_prompt=system_prompt)
transcript = ""
tool_rounds = 0
retrieval_started = False
force_answer = False
illegal_result_count = 0
min_tool_rounds = max(0, int(min_tool_rounds))
max_tool_rounds = max(min_tool_rounds, int(max_tool_rounds))
max_iterations = max_tool_rounds + 8
def inject_query_result(query: str) -> str:
nonlocal tool_rounds, retrieval_started
tool_rounds += 1
retrieval_started = True
result = self.format_query_result(query, query_fn(query))
return (
"\n"
f"{result}\n"
"\n"
)
for _ in range(max_iterations):
need_more_retrieval = retrieval_started and tool_rounds < min_tool_rounds
stop_strings = token_protocol.query_generation_stop_strings(
need_more_retrieval=need_more_retrieval,
)
if self.has_open_tag(raw_prompt, "think"):
stop_strings = list(dict.fromkeys([
"",
"",
"",
"",
*stop_strings,
]))
generated = self._generate_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=stop_strings,
)
generated, had_illegal_result = self.strip_generated_result_blocks(generated)
generated, terminal_kind = self._slice_to_first_terminal(generated)
# Query wins immediately after .
if terminal_kind == "query":
query = self.extract_query(generated) or ""
clean_query = self.sanitize_query_text(query)
if not clean_query or self.is_bad_meta_query(clean_query):
cleaned = generated.split("", 1)[0].rstrip()
raw_prompt += cleaned
transcript += cleaned
correction = (
"\n[Runtime instruction: The query was malformed or not a real lookup. "
"Emit exactly one concrete ... containing the subject and missing fact.]\n"
)
raw_prompt += correction
transcript += correction
continue
if force_answer or tool_rounds >= max_tool_rounds:
cleaned = generated.split("", 1)[0].rstrip()
raw_prompt += cleaned
transcript += cleaned
injected = self.retrieval_exhausted_answer_instruction(max_tool_rounds)
raw_prompt += injected
transcript += injected
force_answer = True
continue
raw_prompt += generated
transcript += generated
injected = inject_query_result(clean_query)
raw_prompt += injected
transcript += injected
continue
# Completed answer.
if terminal_kind == "answer":
if self.has_bad_meta_answer(generated) or self.declares_retrieval_intent(generated):
cleaned = self.strip_answer_attempts(generated)
raw_prompt += cleaned
transcript += cleaned
correction = self.force_query_correction()
raw_prompt += correction
transcript += correction
continue
if retrieval_started and tool_rounds < min_tool_rounds:
cleaned = self.strip_answer_attempts(generated)
raw_prompt += cleaned
transcript += cleaned
correction = self.minimum_retrieval_correction(
tool_rounds=tool_rounds,
min_tool_rounds=min_tool_rounds,
)
raw_prompt += correction
transcript += correction
continue
raw_prompt += generated
transcript += generated
break
# Open query / malformed query.
if self.has_malformed_query(generated):
raw_query = self.extract_open_query(generated) or ""
clean_query = self.sanitize_query_text(raw_query)
raw_prompt += re.sub(r"(?is)[\s\S]*$", "", generated).rstrip()
transcript += re.sub(r"(?is)[\s\S]*$", "", generated).rstrip()
if clean_query and not self.is_bad_meta_query(clean_query):
normalized = f"{clean_query}"
raw_prompt += normalized
transcript += normalized
if not force_answer and tool_rounds < max_tool_rounds:
injected = inject_query_result(clean_query)
raw_prompt += injected
transcript += injected
continue
correction = self.malformed_query_correction()
raw_prompt += correction
transcript += correction
continue
if self.has_fake_retrieval_claim(generated, tool_rounds):
generated = self.strip_fake_retrieval_claim(generated)
raw_prompt += generated
transcript += generated
correction = self.fake_retrieval_claim_correction()
raw_prompt += correction
transcript += correction
continue
if self.has_generated_result_claim(generated, tool_rounds):
generated = self.strip_generated_result_claim(generated)
raw_prompt += generated
transcript += generated
correction = self.generated_result_claim_correction()
raw_prompt += correction
transcript += correction
continue
if self.declares_retrieval_intent(generated):
generated = self.strip_answer_attempts(generated)
raw_prompt += generated
transcript += generated
correction = self.force_query_correction()
raw_prompt += correction
transcript += correction
continue
if self.has_open_tag(raw_prompt, "think") and "" in generated:
before_answer = generated.split("", 1)[0].rstrip()
raw_prompt += before_answer
transcript += before_answer
raw_prompt += "\n\n"
transcript += "\n\n"
raw_prompt += self.protocol_correction(force_answer=True)
transcript += self.protocol_correction(force_answer=True)
continue
if had_illegal_result:
illegal_result_count += 1
force_answer = force_answer or illegal_result_count >= 2
correction = self.protocol_correction(force_answer=force_answer)
raw_prompt += correction
transcript += correction
continue
raw_prompt += generated
transcript += generated
if generated.strip().endswith(""):
if retrieval_started and tool_rounds >= min_tool_rounds:
instruction = (
"\n[Runtime instruction: Retrieved evidence is available. "
"Produce the final ... now.]\n"
)
raw_prompt += instruction
transcript += instruction
continue
continue
if retrieval_started and tool_rounds >= min_tool_rounds:
instruction = (
"\n[Runtime instruction: Retrieved evidence is available. "
"Produce the final ... now.]\n"
)
raw_prompt += instruction
transcript += instruction
continue
if not retrieval_started:
break
else:
correction = (
"\n[Runtime instruction: The retrieval loop reached its safety limit. "
"Do not request more retrieval. Emit a final ... now, "
"using only the evidence already shown.]\n"
)
raw_prompt += correction
transcript += correction
generated = self._generate_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=["", "", "", "", ""],
)
generated, _ = self._slice_to_first_terminal(generated)
generated = generated.split("", 1)[0]
generated = generated.split("", 1)[0]
raw_prompt += generated
transcript += generated
return transcript
# ---------------------------------------------------------------------
# Streaming query loop
# ---------------------------------------------------------------------
def stream_with_query(
self,
prompt: str,
query_fn: Callable[[str], str],
max_tool_rounds: int = 3,
min_tool_rounds: int = 2,
max_new_tokens: int = 512,
temperature: float = 0.2,
top_p: float = 0.9,
show_injected_result: bool = True,
thinking_token_limit: Optional[int] = None,
system_prompt: Optional[str] = None,
) -> Generator[str, None, None]:
raw_prompt = self._build_raw_prompt(prompt, system_prompt=system_prompt)
min_tool_rounds = max(0, int(min_tool_rounds))
max_tool_rounds = max(min_tool_rounds, int(max_tool_rounds))
tool_rounds = 0
retrieval_started = False
force_answer = False
final_answer_requested = False
illegal_result_count = 0
last_query: str | None = None
max_iterations = max_tool_rounds + 8
def close_open_think_if_needed(text: str) -> str:
if self.has_open_tag(text, "think"):
return "\n\n"
if not text.endswith("\n"):
return "\n"
return ""
def visible_result_block(result: str) -> str:
return f"{result}\n"
def inject_results_for_query(query: str, minimum_fill: bool) -> tuple[str, str]:
nonlocal tool_rounds
prompt_parts = []
visible_parts = []
needed = max(1, min_tool_rounds - tool_rounds) if minimum_fill else 1
can_fetch = max(0, max_tool_rounds - tool_rounds)
fetch_count = min(needed, can_fetch)
for _ in range(fetch_count):
tool_rounds += 1
result = self.format_query_result(query, query_fn(query))
block = visible_result_block(result)
prompt_parts.append(block)
visible_parts.append(block)
return "".join(prompt_parts), "".join(visible_parts)
def request_final_answer_instruction() -> str:
return (
"\n[Runtime instruction: Retrieved evidence is available. "
"Do not request more retrieval unless a genuinely different lookup is required. "
"Continue with a normal reasoning block if needed, then produce the final "
"....]\n"
)
for _ in range(max_iterations):
generated = ""
thinking_state: dict = {}
suppressor = ResultTagSuppressor()
query_guard = StartTagSuppressor([""]) if force_answer else None
block_early_answer = retrieval_started and tool_rounds < min_tool_rounds
answer_guard = EarlyAnswerSuppressor() if block_early_answer else None
need_more_retrieval = retrieval_started and tool_rounds < min_tool_rounds
if force_answer:
# Answer-only mode. Stop as soon as the model tries to open another query.
stop_strings = ["", "", "", ""]
else:
stop_strings = token_protocol.query_generation_stop_strings(
need_more_retrieval=need_more_retrieval,
)
if self.has_open_tag(raw_prompt, "think"):
stop_strings = list(dict.fromkeys([
"",
"",
"",
"",
*stop_strings,
]))
for chunk in self.stream_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=stop_strings,
thinking_token_limit=thinking_token_limit,
thinking_limit_state=thinking_state,
):
filtered_chunk = suppressor.feed(chunk)
if not filtered_chunk:
continue
if query_guard is not None:
filtered_chunk = query_guard.feed(filtered_chunk)
if not filtered_chunk:
continue
if answer_guard is not None:
visible_chunk = answer_guard.feed(filtered_chunk)
else:
visible_chunk = filtered_chunk
if visible_chunk:
generated += visible_chunk
yield visible_chunk
filtered_tail = suppressor.flush()
if filtered_tail:
if query_guard is not None:
filtered_tail = query_guard.feed(filtered_tail)
if answer_guard is not None:
visible_tail = answer_guard.feed(filtered_tail)
else:
visible_tail = filtered_tail
if visible_tail:
generated += visible_tail
yield visible_tail
if query_guard is not None:
visible_tail = query_guard.flush()
if visible_tail:
generated += visible_tail
yield visible_tail
if answer_guard is not None:
visible_tail = answer_guard.flush()
if visible_tail:
generated += visible_tail
yield visible_tail
think_limit_hit = bool(thinking_state.get("triggered"))
if think_limit_hit and self.has_open_tag(raw_prompt + generated, "think"):
close_think = "\n\n"
generated += close_think
yield close_think
generated, had_illegal_result = self.strip_generated_result_blocks(generated)
generated, terminal_kind = self._slice_to_first_terminal(generated)
if force_answer and (("" in generated) or (query_guard is not None and query_guard.blocked)):
before_query = generated.split("", 1)[0].rstrip()
if before_query:
raw_prompt += before_query
raw_prompt += close_open_think_if_needed(raw_prompt)
raw_prompt += self.retrieval_exhausted_answer_instruction(max_tool_rounds)
final_answer_requested = True
continue
# -------------------------------------------------------------
# CRITICAL FIX:
# If is the first completed terminal block,
# start retrieval immediately. Do not inspect later junk.
# -------------------------------------------------------------
if terminal_kind == "query":
query = self.extract_query(generated) or ""
clean_query = self.sanitize_query_text(query)
if force_answer or tool_rounds >= max_tool_rounds:
before_query = generated.split("", 1)[0].rstrip()
if before_query:
raw_prompt += before_query
raw_prompt += close_open_think_if_needed(raw_prompt)
raw_prompt += self.retrieval_exhausted_answer_instruction(max_tool_rounds)
force_answer = True
final_answer_requested = True
continue
raw_prompt += generated
if not clean_query or self.is_bad_meta_query(clean_query):
raw_prompt += (
"\n[Runtime instruction: The query was malformed or not a real lookup. "
"Emit exactly one concrete ... containing the subject and missing fact.]\n"
)
continue
retrieval_started = True
last_query = clean_query
close_think = close_open_think_if_needed(raw_prompt)
prompt_injected, visible_injected = inject_results_for_query(
clean_query,
minimum_fill=(tool_rounds < min_tool_rounds),
)
prompt_injected = close_think + prompt_injected + "\n"
visible_injected = close_think + visible_injected + "\n"
raw_prompt += prompt_injected
if show_injected_result and visible_injected.strip():
yield visible_injected
continue
# Completed answer.
if terminal_kind == "answer":
if self.has_bad_meta_answer(generated) or self.declares_retrieval_intent(generated):
cleaned = self.strip_answer_attempts(generated)
raw_prompt += cleaned
raw_prompt += self.force_query_correction()
continue
if retrieval_started and tool_rounds < min_tool_rounds:
cleaned = self.strip_answer_attempts(generated)
raw_prompt += cleaned
if last_query and tool_rounds < max_tool_rounds:
prompt_injected, visible_injected = inject_results_for_query(
last_query,
minimum_fill=True,
)
raw_prompt += prompt_injected
if show_injected_result and visible_injected:
yield visible_injected
continue
raw_prompt += self.minimum_retrieval_correction(
tool_rounds=tool_rounds,
min_tool_rounds=min_tool_rounds,
)
continue
raw_prompt += generated
break
# Open query / malformed query.
if self.has_malformed_query(generated):
raw_query = self.extract_open_query(generated) or ""
clean_query = self.sanitize_query_text(raw_query)
generated_without_open_query = re.sub(
r"(?is)[\s\S]*$",
"",
generated,
).rstrip()
raw_prompt += generated_without_open_query
if clean_query and not self.is_bad_meta_query(clean_query):
normalized = f"{clean_query}"
raw_prompt += normalized
yield normalized
if not force_answer and tool_rounds < max_tool_rounds:
retrieval_started = True
last_query = clean_query
close_think = close_open_think_if_needed(raw_prompt)
prompt_injected, visible_injected = inject_results_for_query(
clean_query,
minimum_fill=(tool_rounds < min_tool_rounds),
)
prompt_injected = close_think + prompt_injected + "\n"
visible_injected = close_think + visible_injected + "\n"
raw_prompt += prompt_injected
if show_injected_result and visible_injected.strip():
yield visible_injected
continue
raw_prompt += self.malformed_query_correction()
continue
# Fake retrieval claims before runtime provided results.
if self.has_fake_retrieval_claim(generated, tool_rounds):
generated = self.strip_fake_retrieval_claim(generated)
raw_prompt += generated
raw_prompt += self.fake_retrieval_claim_correction()
continue
if self.has_generated_result_claim(generated, tool_rounds):
generated = self.strip_generated_result_claim(generated)
raw_prompt += generated
raw_prompt += self.generated_result_claim_correction()
continue
# Retrieval intent without a valid query.
if self.declares_retrieval_intent(generated):
cleaned = self.strip_answer_attempts(generated)
raw_prompt += cleaned
raw_prompt += self.force_query_correction()
continue
# If answer leaked inside a runtime-opened think block, repair it.
if self.has_open_tag(raw_prompt, "think") and "" in generated:
before_answer = generated.split("", 1)[0].rstrip()
raw_prompt += before_answer
raw_prompt += "\n\n"
raw_prompt += self.retrieval_exhausted_answer_instruction(max_tool_rounds)
final_answer_requested = True
continue
if had_illegal_result or suppressor.suppressed:
illegal_result_count += 1
force_answer = force_answer or illegal_result_count >= 2
raw_prompt += self.protocol_correction(force_answer=force_answer)
continue
if think_limit_hit:
raw_prompt += generated
raw_prompt += (
"\n[Runtime instruction: Thinking budget reached. "
"Produce the final ... now.]\n"
)
final_answer_requested = True
continue
raw_prompt += generated
if generated.strip().endswith(""):
if retrieval_started and tool_rounds >= min_tool_rounds:
raw_prompt += request_final_answer_instruction()
final_answer_requested = True
continue
# Before retrieval starts, do not force answer.
continue
if retrieval_started and tool_rounds >= min_tool_rounds and not final_answer_requested:
if force_answer or tool_rounds >= max_tool_rounds:
raw_prompt += self.retrieval_exhausted_answer_instruction(max_tool_rounds)
else:
raw_prompt += request_final_answer_instruction()
final_answer_requested = True
continue
if not retrieval_started:
break
if not final_answer_requested:
raw_prompt += request_final_answer_instruction()
final_answer_requested = True
continue
# Final fallback pass.
fallback_guard = StartTagSuppressor(["", ""])
for chunk in self.stream_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=["", "", "", ""],
):
visible_chunk = fallback_guard.feed(chunk)
if visible_chunk:
yield visible_chunk
visible_tail = fallback_guard.flush()
if visible_tail:
yield visible_tail
break
else:
raw_prompt += request_final_answer_instruction()
fallback_guard = StartTagSuppressor(["", ""])
for chunk in self.stream_raw(
raw_prompt=raw_prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
stop_strings=["", "", "", "", ""],
):
visible_chunk = fallback_guard.feed(chunk)
if visible_chunk:
yield visible_chunk
visible_tail = fallback_guard.flush()
if visible_tail:
yield visible_tail