diff --git a/app/database.py b/app/database.py index 6f17556..716f770 100644 --- a/app/database.py +++ b/app/database.py @@ -200,6 +200,17 @@ async def get_assessment(drucksache: str) -> Optional[dict]: return None +async def delete_assessment(drucksache: str) -> bool: + """Delete an assessment by drucksache ID. Used by the cite-endpoint + to trigger re-analysis of Pre-#60 hallucinated assessments.""" + async with aiosqlite.connect(settings.db_path) as db: + cursor = await db.execute( + "DELETE FROM assessments WHERE drucksache = ?", (drucksache,) + ) + await db.commit() + return cursor.rowcount > 0 + + async def get_all_assessments(bundesland: str = None) -> list[dict]: """Get all assessments from database, optionally filtered by Bundesland. diff --git a/app/embeddings.py b/app/embeddings.py index ff82426..b0b006c 100644 --- a/app/embeddings.py +++ b/app/embeddings.py @@ -592,10 +592,10 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona ``add_highlight_annot``. Returns the serialized PDF bytes, or None if the programme/page can't be resolved. - Returns a tuple ``(pdf_bytes, found_page)`` where ``found_page`` is the - 1-indexed page where the highlight was placed (may differ from ``seite`` - if the text was found on a different page). Returns ``(None, 0)`` if the - programme/page can't be resolved. + Returns a tuple ``(pdf_bytes, found_page, highlighted)`` where + ``found_page`` is the 1-indexed page number and ``highlighted`` is + True if the text was found and annotated. Returns ``(None, 0, False)`` + if the programme/page can't be resolved. Args: programm_id: Key into PROGRAMME registry — validated by caller. @@ -607,22 +607,22 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona """ info = PROGRAMME.get(programm_id) if not info: - return None, 0 + return None, 0, False pdf_filename = info.get("pdf") if not pdf_filename: - return None, 0 + return None, 0, False referenzen = Path(__file__).parent / "static" / "referenzen" pdf_path = referenzen / pdf_filename if not pdf_path.exists(): - return None, 0 + return None, 0, False needle = (query or "").strip()[:200] src = fitz.open(str(pdf_path)) try: if seite < 1 or seite > len(src): - return None, 0 + return None, 0, False # Suche den Needle auf der angegebenen Seite. Falls dort nichts # gefunden wird (Pre-#60-Assessments haben oft falsche Seiten- @@ -657,28 +657,9 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona if annot is not None: annot.set_colors(stroke=(1.0, 0.93, 0.0)) # gelb annot.update() - elif needle: - # Kein Match — halluziniertes Zitat aus Pre-#60-Assessment. - # Sichtbare Notiz-Annotation am Seitenkopf, damit der User - # versteht warum nichts markiert ist. - note_rect = fitz.Rect(20, 20, 400, 55) - note_text = ( - "Textstelle nicht im Dokument auffindbar — " - "das Zitat wurde möglicherweise vom LLM paraphrasiert. " - "Eine Re-Analyse des Antrags würde verifizierte Zitate erzeugen." - ) - annot = page.add_freetext_annot( - note_rect, - note_text, - fontsize=9, - fontname="helv", - text_color=(0.5, 0.2, 0.0), - fill_color=(1.0, 0.95, 0.8), - ) - if annot is not None: - annot.update() - return src.tobytes(), target_page_idx + 1 + highlighted = bool(needle and rects) + return src.tobytes(), target_page_idx + 1, highlighted finally: src.close() diff --git a/app/main.py b/app/main.py index 3852ced..c0449ed 100644 --- a/app/main.py +++ b/app/main.py @@ -32,8 +32,9 @@ logger = logging.getLogger(__name__) from .config import settings from .database import ( init_db, get_job, create_job, update_job, - get_all_assessments, get_assessment, upsert_assessment, import_json_assessments, - search_assessments + get_all_assessments, get_assessment, delete_assessment, + upsert_assessment, import_json_assessments, + search_assessments, ) from .parlamente import get_adapter, ADAPTERS from .bundeslaender import alle_bundeslaender @@ -596,7 +597,12 @@ async def quellen_page(request: Request): @app.get("/api/wahlprogramm-cite") -async def wahlprogramm_cite(pid: str = "", pdf: str = "", seite: int = 1, q: str = ""): +async def wahlprogramm_cite( + request: Request, + background_tasks: BackgroundTasks, + pid: str = "", pdf: str = "", seite: int = 1, q: str = "", + ds: str = "", bl: str = "", +): """Render eine Wahlprogramm-Seite mit gelb hervorgehobener Zitat-Stelle. Issue #47: Klick auf eine Zitat-Quelle im Report soll direkt zur @@ -640,13 +646,50 @@ async def wahlprogramm_cite(pid: str = "", pdf: str = "", seite: int = 1, q: str if seite < 1 or seite > 2000: raise HTTPException(status_code=400, detail="Ungültige Seitennummer") - pdf_bytes, found_page = render_highlighted_page(pid, seite, q) + pdf_bytes, found_page, highlighted = render_highlighted_page(pid, seite, q) if pdf_bytes is None: raise HTTPException( status_code=404, detail="Wahlprogramm-PDF oder Seite nicht verfügbar", ) + # Issue #47: Wenn das Zitat nicht im PDF auffindbar ist UND wir die + # Drucksache kennen, ist das Assessment wahrscheinlich ein Pre-#60- + # Halluzinations-Opfer. Automatische Re-Analyse triggern und dem + # User eine Warte-Seite zeigen statt ein PDF ohne Highlights. + if not highlighted and q and ds and bl: + existing = await get_assessment(ds) + if existing: + adapter = get_adapter(bl) + if adapter: + # Altes Assessment löschen und neu analysieren + await delete_assessment(ds) + job_id = str(uuid.uuid4()) + await create_job(job_id, f"Re-Analyse {ds} (Zitat nicht verifizierbar)", bl, "qwen-plus") + text = await adapter.download_text(ds) + if text: + doc = await adapter.get_document(ds) + background_tasks.add_task( + run_drucksache_analysis, + job_id, ds, text, bl, "qwen-plus", doc, + ) + # HTML-Warte-Seite mit Auto-Redirect zurück zum Assessment + return HTMLResponse(f""" +
+ +Der Antrag {ds} wird mit der aktuellen Pipeline
+neu analysiert, um verifizierte Zitate zu erzeugen.
Automatische Weiterleitung in 15 Sekunden…
+