import json import requests from typing import List, Dict, Any, Union import time import numpy as np import os PROMPT_TEMPLATES = { "verbatim_sentiment": { "system": ( "You are a compliance-grade policy analyst assistant. Prime directive: be faithful to the provided sources. " "Do NOT speculate. If the answer is not supported by the sources, say 'Not found in sources' and stop. " "Every non-trivial claim MUST be grounded with an inline citation in the form (filename p.X). " "Prefer 'unknown/not stated' over guessing. " "Follow this Grounding Protocol before answering: (1) read Context Sources; (2) extract exact quotes; " "(3) map each assertion to a citation; (4) list gaps and unknowns. " "Avoid hallucinations. Base everything strictly on the content provided. " "Output must be complete sentences and adequate context. " "If sentiment or coherence inputs are disabled or empty, omit those sections entirely (do not mention they were omitted)." "Do not even write anything in sentiment and coherence if it is not available" "Try to meet the user's specification as much as possible where if they only want items from a certain page only give out data from that page or if it is from a certain document please only retrieve just from that document" "Order by page" "The context is already searched, retrieved and reranked when handed to you." ), # dynamic assembly; placeholders kept for backward compatibility but sections may be removed "user_template": "DYNAMIC" }, "abstractive_summary": { "system": ( "You are a policy analyst summarizing government documents for a general audience. " "Faithfulness is mandatory: paraphrase only what is supported by the sources and cite key claims inline (filename p.X). " "Avoid quotes unless legally binding language is essential. " "Bias toward completeness over brevity; use full sentences and helpful structure. " "If critical info is absent, say 'Not found in sources'—do not infer." ), "user_template": """Query: {query} Write a comprehensive, plain-language summary with these sections: - What It Covers (scope, entities, timelines) [cite] - Key Requirements & Controls (what must be done) [cite] - Enforcement & Penalties (who enforces, how, consequences) [cite] - Deadlines & Effective Dates (explicit dates or 'not stated') [cite] - Exemptions/Thresholds (if any; otherwise 'not stated') [cite] - Risks & Open Questions (gaps/ambiguities; no speculation) - Action Checklist (practical steps derived strictly from the sources) [cite] Rules: - Use citations for non-obvious claims (filename p.X). - Avoid quotes unless a phrase is legally binding. - If the sources do not answer the query, state 'Not found in sources'. Topic hint: {topic_hint} Context DOCS: {context_block} """ }, "followup_reasoning": { "system": ( "You are an assistant that explains policy documents interactively, reasoning step-by-step. " "Be strictly faithful to the documents; if a detail is absent, say so. " "Cite document filename and page for each factual claim. " "Favor clarity and completeness over brevity; full sentences only." ), "user_template": """User query: {query} Answer step-by-step: 1) Direct Answer (what the sources actually support) with inline citations (filename p.X). 2) Why (short reasoning mapped to specific passages) with citations. 3) Edge Cases & Exceptions (only if present; otherwise 'not stated') with citations. 4) What’s Missing (explicitly note absent info; no speculation). Then list 3–6 Follow-up Questions a reader might ask, and answer each using the docs. - If a follow-up cannot be answered with the docs, respond: 'Not found in sources.' Topic: {topic_hint} DOCS: {context_block} """ }, } # --- LLM client --- def get_do_completion(api_key, model_name, messages, temperature=0.2, max_tokens=800): url = "https://inference.do-ai.run/v1/chat/completions" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } data = { "model": model_name, "messages": messages, "temperature": temperature, "max_tokens": max_tokens } try: resp = requests.post(url, headers=headers, json=data, timeout=90) resp.raise_for_status() return resp.json() except requests.exceptions.HTTPError as e: print(f"HTTP error occurred: {e}") print(f"Response body: {e.response.text if e.response is not None else ''}") return None except requests.exceptions.RequestException as e: print(f"Request error: {e}") return None except json.JSONDecodeError as e: print(f"Failed to decode JSON: {e}") print(f"Response text: {resp.text if 'resp' in locals() else ''}") return None # --- Prompt context builder --- def _clip(text: str, max_chars: int = 1400) -> str: """Trim content to limit prompt size.""" if not text: return "" text = str(text).strip() return text[:max_chars] + ("..." if len(text) > max_chars else "") def build_context_block(top_docs: List[Dict[str, Any]]) -> str: """ Formats each document with real citation: - Extracts file name from 'source' path - Uses 'page_label' or falls back to 'page' - Returns: <<>> """ blocks = [] for i, item in enumerate(top_docs): if hasattr(item, "page_content"): text = item.page_content meta = getattr(item, "metadata", {}) else: text = item.get("text") or item.get("page_content", "") meta = item.get("metadata", {}) # Get file name from path full_path = meta.get("source", "") filename = os.path.basename(full_path) if full_path else f"Document_{i+1}" # Prefer page_label if available, else fallback to raw page page_label = meta.get("page_label") or meta.get("page") or "unknown" citation = f"{filename}, p. {page_label}" blocks.append(f"<<>>\n{_clip(text)}\n") return "\n".join(blocks) # --- Message builder --- def build_messages( query: str, top_docs: List[Dict[str, Any]], task_mode: str, sentiment_rollup: Dict[str, List[str]], coherence_report: str = "", topic_hint: str = "energy policy", allowlist_meta: Dict[str, Any] = None ) -> List[Dict[str, str]]: template = PROMPT_TEMPLATES.get(task_mode) if not template: raise ValueError(f"Unknown task mode: {task_mode}") context_block = build_context_block(top_docs) sentiment_present = bool(sentiment_rollup) coherence_present = bool(coherence_report) sentiment_json = json.dumps(sentiment_rollup or {}, ensure_ascii=False) # Build user prompt dynamically to truly omit absent sections parts = [ f"Query: {query}\n", "Deliverables (omit any section whose input is empty/disabled):", "1) Quoted Policy Excerpts\n - Quote the necessary text and append citations like (filename p.X). Group by subtopic.\n - Honor any page or document restriction from the query strictly.\n - Order by page", ] if sentiment_present: parts.append("2) Sentiment Summary\n - Using the Sentiment JSON, explain tone, gaps, penalties, and enforcement clarity in plain English. Do not invent fields that aren't present.") if coherence_present: idx = 3 if sentiment_present else 2 parts.append(f"{idx}) Coherence Assessment\n - From the coherence report: on-topic vs off-topic; note coherent/off-topic/repeated sections only if present.") parts.append( "\nConstraints:\n- No external knowledge. No speculation. If a user ask is outside the sources, state 'Not found in sources.'\n- Use full sentences.\n- Each substantive statement has a citation." ) parts.append(f"\nTopic hint: {topic_hint}\n") if sentiment_present: parts.append(f"Sentiment JSON (rolled-up across top docs):\n{sentiment_json}\n") if coherence_present: parts.append(f"Coherence report:\n{coherence_report}\n") guard = "" if allowlist_meta: doc_id = allowlist_meta.get('doc_id') pages = allowlist_meta.get('pages') guard = f"[ALLOWLIST_DOCS] doc_id={doc_id}; pages={pages}\nOnly use text from chunks where doc_id={doc_id} and page_label in {pages}. If none present reply exactly: Not found in sources for page {pages} of {doc_id}. Do not use any other documents.\n" parts.append(f"{guard}Context Sources:\n{context_block}") user_prompt = "\n".join(parts) return [ {"role": "system", "content": template["system"]}, {"role": "user", "content": user_prompt} ] # --- Generation orchestrator --- def generate_policy_answer( api_key: str, model_name: str, query: str, top_docs: List[Union[Dict[str, Any], Any]], sentiment_rollup: Dict[str, List[str]], coherence_report: str = "", task_mode: str = "verbatim_sentiment", temperature: float = 0.2, max_tokens: int = 2000 ) -> str: if not top_docs: return "No documents available to answer." messages = build_messages( query=query, top_docs=top_docs, task_mode=task_mode, sentiment_rollup=sentiment_rollup, coherence_report=coherence_report ) resp = get_do_completion(api_key, model_name, messages, temperature=temperature, max_tokens=max_tokens) if resp is None: return "Upstream model error. No response." try: return resp["choices"][0]["message"]["content"].strip() except Exception: return json.dumps(resp, indent=2)