Spaces:
Running on Zero
Running on Zero
Download src/visual_doc_rag/serve.py from tungpth/visual-doc-rag: direct link, hf CLI and curl.
- Browser
- Download file 4.77 kB
-
https://huggingface.co/spaces/tungpth/visual-doc-rag/resolve/main/src/visual_doc_rag/serve.py
- Command line
-
hf download hf://spaces/tungpth/visual-doc-rag/src/visual_doc_rag/serve.py
-
curl -L -o serve.py https://huggingface.co/spaces/tungpth/visual-doc-rag/resolve/main/src/visual_doc_rag/serve.py
4.77 kB
| """Shared CLI/UI answering with persistent answer cache and bounded API usage.""" | |
| import hashlib | |
| import json | |
| import sqlite3 | |
| from collections import deque | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from threading import RLock | |
| from time import monotonic | |
| from visual_doc_rag.data import write_json | |
| from visual_doc_rag.generate import ( | |
| BASE_URL, | |
| PROMPT_VERSION, | |
| GenerationError, | |
| Generator, | |
| Settings, | |
| validate_answer, | |
| ) | |
| from visual_doc_rag.retrieve import PageRetriever | |
| def answer_cache_key(query, method, source, model, pages): | |
| fingerprint = { | |
| "query": query, | |
| "method": method, | |
| "dataset": source, | |
| "model": model, | |
| "endpoint": BASE_URL, | |
| "prompt": PROMPT_VERSION, | |
| "image_processing": "JPEG quality=90, max_side=1600", | |
| "images": [ | |
| [p["corpus_id"], hashlib.sha256(Path(p["image_path"]).read_bytes()).hexdigest()] | |
| for p in pages | |
| ], | |
| } | |
| return hashlib.sha256(json.dumps(fingerprint, sort_keys=True).encode()).hexdigest() | |
| class RequestBudget: | |
| """Count every outbound attempt, including retries/errors, across local processes.""" | |
| def __init__(self, root: Path, limit: int): | |
| root.mkdir(parents=True, exist_ok=True) | |
| self.path = root / "usage.sqlite3" | |
| self.limit = limit | |
| with sqlite3.connect(self.path) as db: | |
| db.execute("CREATE TABLE IF NOT EXISTS usage (day TEXT PRIMARY KEY, requests INTEGER)") | |
| def reserve(self): | |
| day = datetime.now(UTC).date().isoformat() | |
| with sqlite3.connect(self.path) as db: | |
| db.execute("BEGIN IMMEDIATE") | |
| row = db.execute("SELECT requests FROM usage WHERE day = ?", (day,)).fetchone() | |
| if row and row[0] >= self.limit: | |
| raise GenerationError( | |
| "Local daily API request cap reached. Try again tomorrow (UTC)." | |
| ) | |
| db.execute( | |
| "INSERT INTO usage VALUES (?, 1) ON CONFLICT(day) " | |
| "DO UPDATE SET requests = requests + 1", | |
| (day,), | |
| ) | |
| class AnswerService: | |
| def __init__( | |
| self, subset="hr", data_dir=Path("data"), settings=None, retriever=None, generator=None | |
| ): | |
| self.settings = settings or Settings.from_env() | |
| self.subset, self.data_dir = subset, data_dir | |
| self.retriever = retriever | |
| self.generator = generator or Generator(self.settings) | |
| self.cache_root = data_dir / subset / "answers" | |
| self.budget = RequestBudget(data_dir / "runtime", self.settings.daily_requests) | |
| self.sessions = {} | |
| self.lock = RLock() | |
| def ask(self, query: str, method="hybrid", session_id="cli", *, cache_only=False) -> dict: | |
| query = " ".join(query.split()) | |
| if not query or len(query) > 2000: | |
| raise GenerationError("Enter a question of 1–2,000 characters.") | |
| with self.lock: | |
| # Cache normalized whitespace, preserving case and punctuation. | |
| if self.retriever is None: | |
| self.retriever = PageRetriever(self.subset, self.data_dir) | |
| pages = self.retriever.search(query, method, top_k=3) | |
| key = answer_cache_key(query, method, self.retriever.source, self.settings.model, pages) | |
| cache = self.cache_root / f"{key}.json" | |
| if cache.exists(): | |
| try: | |
| answer = validate_answer(cache.read_text(encoding="utf-8"), pages) | |
| cached = True | |
| except (ValueError, OSError): | |
| cached = False | |
| else: | |
| cached = False | |
| if not cached: | |
| if cache_only: | |
| raise GenerationError( | |
| "No valid cached answer for this question and configuration." | |
| ) | |
| now = monotonic() | |
| self.sessions = { | |
| key: deque(t for t in times if now - t < 600) | |
| for key, times in self.sessions.items() | |
| if times and now - times[-1] < 600 | |
| } | |
| times = self.sessions.setdefault(session_id, deque()) | |
| if len(times) >= 5: | |
| raise GenerationError("Session limit reached: 5 new questions per 10 minutes.") | |
| times.append(now) | |
| answer = self.generator.generate(query, pages, self.budget.reserve) | |
| write_json(cache, answer.model_dump()) | |
| return { | |
| "query": query, | |
| "method": method, | |
| "model": self.settings.model, | |
| **answer.model_dump(), | |
| "cached": cached, | |
| "pages": pages, | |
| } | |