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"") @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)|||||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