| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import re |
| from typing import TYPE_CHECKING |
|
|
| if TYPE_CHECKING: |
| from core.generator import ChatGenerator |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
|
|
| def _call_small(system: str, user: str, generator: "ChatGenerator") -> str: |
| """ |
| Make a lightweight LLM call via the generator's existing client. |
| Caps at 256 tokens β rewrites and hypothetical passages are short. |
| Strips <think>β¦</think> tags that reasoning models emit. |
| """ |
| from config import IS_HF_SPACES |
| try: |
| if IS_HF_SPACES: |
| |
| original_create = generator._client.chat.completions.create |
|
|
| def _create_short(**kwargs): |
| kwargs["max_tokens"] = 256 |
| return original_create(**kwargs) |
|
|
| generator._client.chat.completions.create = _create_short |
| result = generator._call_hf(system=system, user=user) |
| generator._client.chat.completions.create = original_create |
| else: |
| result = generator._call_ollama(system=system, user=user) |
|
|
| |
| result = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip() |
| return result |
| except Exception as exc: |
| logger.warning("QueryRewriter LLM call failed: %s", exc) |
| return "" |
|
|
|
|
| |
|
|
| def rewrite_query(original: str, generator: "ChatGenerator") -> str: |
| """ |
| Rephrase the user's question to improve document retrieval. |
| |
| Strategy: ask the LLM to restate the question using different vocabulary β |
| particularly substituting domain terms, expanding acronyms, and adding |
| synonyms that are more likely to appear in a formal document. |
| |
| Returns the rewritten query, or the original if the rewrite fails or is |
| suspiciously short / identical. |
| |
| Parameters |
| ---------- |
| original : str β the user's raw question |
| generator : ChatGenerator β existing generator instance (no new client) |
| """ |
| system = ( |
| "You are a search-query optimizer. " |
| "Your only job is to rewrite a user's question so it matches the " |
| "vocabulary used in formal documents, reports, and articles. " |
| "Rules:\n" |
| "1. Output ONLY the rewritten query β no explanation, no quotes, no preamble.\n" |
| "2. Keep it under 30 words.\n" |
| "3. Preserve the original intent exactly.\n" |
| "4. Expand acronyms, replace informal phrases with formal equivalents, " |
| " and add one or two relevant synonyms if helpful.\n" |
| "5. If the query is already clear and formal, return it unchanged." |
| ) |
| user = f"Rewrite this search query:\n\n{original}" |
|
|
| rewritten = _call_small(system=system, user=user, generator=generator) |
|
|
| |
| if ( |
| not rewritten |
| or len(rewritten) < 5 |
| or rewritten.lower() == original.lower() |
| or len(rewritten) > len(original) * 4 |
| ): |
| logger.debug("Query rewrite skipped (bad output); using original.") |
| return original |
|
|
| logger.info("Query rewritten: %r β %r", original, rewritten) |
| return rewritten |
|
|
|
|
| def hyde_query(original: str, generator: "ChatGenerator") -> str: |
| """ |
| Generate a Hypothetical Document Embedding (HyDE) passage. |
| |
| The LLM writes a short (3-5 sentence) hypothetical document passage that |
| would *answer* the user's question. That passage β which looks like real |
| document text β is then used as the embedding query instead of the question. |
| This dramatically narrows the distance in embedding space between the query |
| and relevant chunks. |
| |
| Returns the hypothetical passage, or the original question if generation |
| fails. |
| |
| Parameters |
| ---------- |
| original : str β the user's raw question |
| generator : ChatGenerator β existing generator instance (no new client) |
| |
| References |
| ---------- |
| Gao et al., 2022 β "Precise Zero-Shot Dense Retrieval without Relevance Labels" |
| https://arxiv.org/abs/2212.10496 |
| """ |
| system = ( |
| "You are an expert document writer. " |
| "Given a question, write a short passage (3-5 sentences) from a " |
| "formal document, report, or article that would DIRECTLY ANSWER the " |
| "question. The passage should:\n" |
| "1. Sound like it was extracted from a real document.\n" |
| "2. Use formal, domain-appropriate vocabulary.\n" |
| "3. Contain the key facts or explanations that answer the question.\n" |
| "4. Be 50-120 words long.\n" |
| "Output ONLY the passage β no heading, no preamble, no explanation." |
| ) |
| user = f"Write a document passage that answers this question:\n\n{original}" |
|
|
| passage = _call_small(system=system, user=user, generator=generator) |
|
|
| if not passage or len(passage) < 20: |
| logger.debug("HyDE generation failed; falling back to original query.") |
| return original |
|
|
| logger.info( |
| "HyDE passage generated (%d chars) for query: %r", |
| len(passage), |
| original, |
| ) |
| return passage |
|
|