Spaces:
Runtime error
Runtime error
Download app/rag.py from capcut8128/docs-qa-backend: direct link, hf CLI and curl.
- Browser
- Download file 5.09 kB
-
https://huggingface.co/spaces/capcut8128/docs-qa-backend/resolve/main/app/rag.py
- Command line
-
hf download hf://spaces/capcut8128/docs-qa-backend/app/rag.py
-
curl -L -o rag.py https://huggingface.co/spaces/capcut8128/docs-qa-backend/resolve/main/app/rag.py
5.09 kB
| """Orchestrates: understand question -> retrieve -> ask the LLM -> return answer + sources.""" | |
| import logging | |
| from . import llm, query, retrieval | |
| from .config import settings | |
| from .registry import get_registry | |
| log = logging.getLogger("rag.rag") | |
| SYSTEM_PROMPT = """You answer questions about the user's uploaded documents using ONLY the provided context. | |
| - Cite sources inline as [1], [2] using the numbers in the context. | |
| - If the context does not contain the answer, say clearly that you could not find it in the documents. Never guess. | |
| - If the user asks what is on a specific page, describe or summarise that page from the context. | |
| - If the context lists notes about missing pages, mention them briefly. | |
| - Reply in the same language the user wrote in (English, Hindi or Hinglish). | |
| - Be concise and well organised.""" | |
| class NoReadyDocuments(Exception): | |
| pass | |
| def _merge_pages(chunks: list[dict]) -> list[dict]: | |
| """For page lookups: one source per (document, page) instead of one per chunk.""" | |
| merged: dict[tuple, dict] = {} | |
| for c in chunks: | |
| key = (c["doc_id"], c["page"]) | |
| if key in merged: | |
| merged[key]["text"] += "\n\n" + c["text"] | |
| else: | |
| merged[key] = {**c} | |
| return list(merged.values()) | |
| def _page_notes(scope: list[dict], pages: list[int], found: set[tuple]) -> list[str]: | |
| notes = [] | |
| for d in scope: | |
| for p in pages: | |
| if (d["doc_id"], p) in found: | |
| continue | |
| if d.get("pages") and p > d["pages"]: | |
| notes.append(f'"{d["filename"]}" has only {d["pages"]} pages, so page {p} does not exist.') | |
| else: | |
| notes.append( | |
| f'Page {p} of "{d["filename"]}" has no extractable text (blank page or a scanned image).' | |
| ) | |
| return notes | |
| def _build_context(sources: list[dict]) -> str: | |
| parts, used = [], 0 | |
| for i, s in enumerate(sources): | |
| block = f"[{i + 1}] ({s['filename']}, page {s['page']})\n{s['text']}" | |
| room = settings.max_context_chars - used | |
| if room <= 200: | |
| break | |
| block = block[:room] | |
| parts.append(block) | |
| used += len(block) | |
| return "\n\n".join(parts) | |
| def _shape(sources: list[dict]) -> list[dict]: | |
| return [ | |
| { | |
| "doc_id": s["doc_id"], | |
| "filename": s["filename"], | |
| "page": s["page"], | |
| "score": round(float(s.get("score", 0.0)), 4), | |
| "text": s["text"], | |
| } | |
| for s in sources | |
| ] | |
| def answer_question(question: str, doc_ids: list[str] | None = None, history: list[dict] | None = None) -> dict: | |
| history = history or [] | |
| ready = get_registry().ready() | |
| if not ready: | |
| raise NoReadyDocuments() | |
| scope = [d for d in ready if d["doc_id"] in doc_ids] if doc_ids else ready | |
| if not scope: | |
| scope = ready | |
| plan = query.analyze(question, scope, history) | |
| explicit = len(scope) == 1 | |
| if plan.doc_ids: # the question names a document -> narrow to it | |
| scope = [d for d in scope if d["doc_id"] in plan.doc_ids] or scope | |
| explicit = True | |
| scope_ids = [d["doc_id"] for d in scope] | |
| notes: list[str] = [] | |
| note_for_user = "" | |
| if plan.intent == "page_lookup" and plan.pages: | |
| pages = plan.pages | |
| if len(pages) > settings.max_lookup_pages: | |
| note_for_user = f"\n\n(I read the first {settings.max_lookup_pages} of the {len(pages)} pages you asked about.)" | |
| pages = pages[: settings.max_lookup_pages] | |
| chunks = retrieval.fetch_pages(scope_ids, pages) | |
| sources = _merge_pages(chunks) | |
| found = {(s["doc_id"], s["page"]) for s in sources} | |
| notes = _page_notes(scope, pages, found) | |
| if sources and len(scope) > 1 and not explicit: | |
| notes = [] # don't nag about documents that simply don't have that page | |
| mode = "page_lookup" | |
| shown_pages = pages | |
| else: | |
| sources = retrieval.hybrid_search(plan.search_query, scope_ids, plan.pages or None) | |
| mode = "search" | |
| shown_pages = plan.pages | |
| meta = { | |
| "mode": mode, | |
| "search_query": plan.search_query if mode == "search" else None, | |
| "pages": shown_pages or None, | |
| "documents": [d["filename"] for d in scope], | |
| } | |
| if not sources: | |
| msg = " ".join(notes) if notes else "I couldn't find anything relevant to this in the selected documents." | |
| return {"answer": msg, "sources": [], **meta} | |
| context = _build_context(sources) | |
| note_block = ("\n\nNotes:\n- " + "\n- ".join(notes)) if notes else "" | |
| messages = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| for h in history[-4:]: | |
| messages.append({"role": h["role"], "content": h["content"][:600]}) | |
| messages.append({"role": "user", "content": f"Context:\n{context}{note_block}\n\nQuestion: {question}"}) | |
| answer = llm.chat(messages) | |
| if not answer: | |
| answer = "The model returned an empty reply. Please try asking again." | |
| return {"answer": answer + note_for_user, "sources": _shape(sources), **meta} | |