diff --git a/app/analyzer.py b/app/analyzer.py index c5eb46f..888f71b 100644 --- a/app/analyzer.py +++ b/app/analyzer.py @@ -377,6 +377,7 @@ async def analyze_antrag( system_prompt=system_prompt, user_prompt=user_prompt, model=model, + max_tokens=8000, ) data = await bewerter.bewerte(request) diff --git a/app/embeddings.py b/app/embeddings.py index 2ca2d21..6030f68 100644 --- a/app/embeddings.py +++ b/app/embeddings.py @@ -226,8 +226,9 @@ PROGRAMME = { "gruene-rp-2021": {"name": "Grüne Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "pdf": "gruene-rp-2021.pdf"}, "fw-rp-2021": {"name": "FREIE WÄHLER Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "RP", "pdf": "fw-rp-2021.pdf"}, "fdp-rp-2021": {"name": "FDP Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "RP", "pdf": "fdp-rp-2021.pdf"}, - # Grundsatzprogramme (Bund) — Gültigkeit ab Beschluss durch den Parteitag. - # ``gueltig_bis=None`` solange das Programm nicht ersetzt wurde. + # Grundsatzprogramme (Bund) — Gültigkeit ab Beschluss-Datum durch den + # jeweils höchsten Parteitag. ``gueltig_bis=None`` solange das Programm + # nicht durch ein neueres ersetzt wurde. "spd-grundsatz": { "name": "SPD Hamburger Programm 2007", "typ": "parteiprogramm", @@ -241,8 +242,8 @@ PROGRAMME = { "typ": "parteiprogramm", "partei": "CDU", "pdf": "cdu-grundsatzprogramm.pdf", - "gueltig_ab": "2024-05-07", # Bundesparteitag Berlin (löst das - "gueltig_bis": None, # Hannoveraner Programm 2007 ab) + "gueltig_ab": "2024-05-07", # Bundesparteitag Berlin, löst das + "gueltig_bis": None, # Hannoveraner Programm 2007 ab }, "gruene-grundsatz": { "name": "Grüne Grundsatzprogramm 2020", @@ -279,7 +280,7 @@ PROGRAMME = { # Bayern — LTW 08.10.2023, WP 19 "csu-by-2023": {"name": "CSU Bayernplan 2023", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "pdf": "csu-by-2023.pdf"}, "gruene-by-2023": {"name": "Grüne Bayern Regierungsprogramm 2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "pdf": "gruene-by-2023.pdf"}, - "fw-by-2023": {"name": "FREIE WÄHLER Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "FW", "bundesland": "BY", "pdf": "fw-by-2023.pdf"}, + "fw-by-2023": {"name": "FREIE WÄHLER Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "BY", "pdf": "fw-by-2023.pdf"}, "afd-by-2023": {"name": "AfD Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BY", "pdf": "afd-by-2023.pdf"}, "spd-by-2023": {"name": "SPD Bayern Zukunftsprogramm 2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "pdf": "spd-by-2023.pdf"}, # Niedersachsen — LTW 09.10.2022, WP 19 @@ -310,15 +311,17 @@ PROGRAMME = { "spd-sn-2024": {"name": "SPD Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "pdf": "spd-sn-2024.pdf"}, "linke-sn-2024": {"name": "DIE LINKE Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "pdf": "linke-sn-2024.pdf"}, "gruene-sn-2024": {"name": "Grüne Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "pdf": "gruene-sn-2024.pdf"}, - # Bundestag — BTW 2025-Wahlprogramme. Kabinett Merz I vereidigt 06.05.2025. - "cdu-bund-2025": {"name": "CDU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "pdf": "cdu-bund-2025.pdf"}, - "csu-bund-2025": {"name": "CSU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "pdf": "csu-bund-2025.pdf"}, - "spd-bund-2025": {"name": "SPD Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "pdf": "spd-bund-2025.pdf"}, - "gruene-bund-2025":{"name": "Grüne Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "pdf": "gruene-bund-2025.pdf"}, - "fdp-bund-2025": {"name": "FDP Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "pdf": "fdp-bund-2025.pdf"}, - "afd-bund-2025": {"name": "AfD Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "pdf": "afd-bund-2025.pdf"}, - "linke-bund-2025": {"name": "DIE LINKE Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "pdf": "linke-bund-2025.pdf"}, - "bsw-bund-2025": {"name": "BSW Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BUND", "pdf": "bsw-bund-2025.pdf"}, + # Bundestagswahl 23.02.2025 — Kabinett Merz I, vereidigt 06.05.2025. + # Acht im 21. Bundestag relevante Parteien. Die Grundsatzprogramme + # oben bleiben als zweite Referenz erhalten. + "cdu-bund-2025": {"name": "CDU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "pdf": "cdu-bund-2025.pdf"}, + "csu-bund-2025": {"name": "CSU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "pdf": "csu-bund-2025.pdf"}, + "spd-bund-2025": {"name": "SPD Regierungsprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "pdf": "spd-bund-2025.pdf"}, + "gruene-bund-2025": {"name": "Grüne Regierungsprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "pdf": "gruene-bund-2025.pdf"}, + "fdp-bund-2025": {"name": "FDP Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "pdf": "fdp-bund-2025.pdf"}, + "afd-bund-2025": {"name": "AfD Bundestagswahlprogramm 2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "pdf": "afd-bund-2025.pdf"}, + "linke-bund-2025": {"name": "DIE LINKE Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "pdf": "linke-bund-2025.pdf"}, + "bsw-bund-2025": {"name": "BSW Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BUND", "pdf": "bsw-bund-2025.pdf"}, } @@ -736,7 +739,7 @@ def _chunk_pdf_url(chunk: dict) -> Optional[str]: # die URL bleibt bounded (sonst würden 500-Zeichen-Snippets in jeder # Zitat-URL stehen und das HTML-Report-JSON aufblähen). q = urllib.parse.quote_plus(text[:200]) - return f"/api/wahlprogramm-cite?pid={prog_id}&seite={seite}&q={q}" + return f"/api/wahlprogramm-cite?pid={prog_id}&seite={seite}&q={q}#page={seite}" if seite: return f"/static/referenzen/{pdf}#page={seite}" @@ -799,9 +802,14 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona rects = [] if needle: clean = needle.replace("\u00ad", "") + # LLMs ziehen h\u00e4ufig die Seitenzahl-Header (\u201e44 Gute Bildung \u2026") + # mit ins Zitat. Wenn die ersten Tokens reine Ziffern sind, + # strippen wir sie f\u00fcr die Suche \u2014 sonst matched search_for nicht. + import re as _re + clean = _re.sub(r"^\s*\d+\s+", "", clean).strip() words = clean.split() anchor = " ".join(words[:5]) if len(words) >= 5 else clean - # Versuch 1: angegebene Seite, Volltext + # Versuch 1: angegebene Seite, Volltext (gestrippt) rects = src[target_page_idx].search_for(clean) # Versuch 2: angegebene Seite, 5-Wort-Anker if not rects: @@ -814,8 +822,7 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona target_page_idx = i break - # Volles PDF mit Highlight-Annotation. Der Browser öffnet das - # vollständige Wahlprogramm; das Frontend hängt #page=N an die URL. + # Volles PDF mit Highlight-Annotation. page = src[target_page_idx] if needle and rects: for rect in rects: @@ -824,6 +831,16 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona annot.set_colors(stroke=(1.0, 0.93, 0.0)) # gelb annot.update() + # PDF-OpenAction setzen, damit der Reader direkt auf der richtigen + # Seite startet (statt Seite 1) — sonst sieht der User „PDF öffnet, + # aber falsche Seite". /Fit = passt-zur-Größe. + try: + page_xref = page.xref + catalog_xref = src.pdf_catalog() + src.xref_set_key(catalog_xref, "OpenAction", f"[{page_xref} 0 R /Fit]") + except Exception: + logger.exception("render_highlighted_page: OpenAction-Setzen fehlgeschlagen") + highlighted = bool(needle and rects) try: return src.tobytes(), target_page_idx + 1, highlighted diff --git a/app/wahlprogramm-links.yaml b/app/wahlprogramm-links.yaml index 96aed26..de71d6e 100644 --- a/app/wahlprogramm-links.yaml +++ b/app/wahlprogramm-links.yaml @@ -37,3 +37,48 @@ BB: titel: "BÜNDNIS 90/DIE GRÜNEN Brandenburg Wahlprogramm 2024 (Platzhalter — URL prüfen)" jahr: 2024 sha256: "" + +# Bundestagswahl 23.02.2025 — Kabinett Merz I, vereidigt 06.05.2025. +# Quellen: Parteiwebseiten (cdu.de, csu.de, spd.de, gruene.de, fdp.de, +# afd.de, die-linke.de, bsw-vg.de). Recherche 2026-05-07. +BUND: + CDU: + - url: https://www.cdu.de/app/uploads/2025/01/km_btw_2025_wahlprogramm_langfassung_ansicht.pdf + titel: "Politikwechsel für Deutschland — Wahlprogramm von CDU und CSU zur Bundestagswahl 2025 (Langfassung)" + jahr: 2025 + sha256: "08f751316e731b77aa2f18090b8695d88268f2942481399f37a3a47317361795" + CSU: + - url: https://www.csu.de/common/download/Wahlprogramm_2025_von_CDU_und_CSU.pdf + titel: "Politikwechsel für Deutschland — Wahlprogramm von CDU und CSU zur Bundestagswahl 2025 (CSU-Verteilungsversion)" + jahr: 2025 + sha256: "c07fe4b65404be9b4c1b99ec9c970567a96a27b86a1154754a41e9ef31d0c04f" + SPD: + - url: https://www.spd.de/fileadmin/Dokumente/Beschluesse/Programm/2025_SPD_Regierungsprogramm.pdf + titel: "Mehr für Dich. Besser für Deutschland. — Regierungsprogramm der SPD zur Bundestagswahl 2025" + jahr: 2025 + sha256: "05aeb9eb19fd423288d94de1d15cabcddbcb9bb1ecf65237657d10e4839b9d7e" + GRÜNE: + - url: https://cms.gruene.de/uploads/assets/20250205_Regierungsprogramm_DIGITAL_DINA5.pdf + titel: "Zusammen wachsen — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN zur Bundestagswahl 2025" + jahr: 2025 + sha256: "0d1f7530ddecadc8d98db15b2189f3ee4ddcccd9619df825703911f1aac18dda" + FDP: + - url: https://www.fdp.de/sites/default/files/2024-12/fdp-wahlprogramm_2025.pdf + titel: "Alles lässt sich ändern — Wahlprogramm der Freien Demokraten zur Bundestagswahl 2025" + jahr: 2025 + sha256: "a549dcb318f60fdd8a257fb2e68745d1df50151f4914910e2afde20c3b0fa039" + AfD: + - url: https://www.afd.de/wp-content/uploads/2025/02/AfD_Bundestagswahlprogramm2025_web.pdf + titel: "Programm der Alternative für Deutschland — Bundestagswahl 2025" + jahr: 2025 + sha256: "e2d0a944f54017aa432bcd0c069c7908aefb3ea65af4904e7f86ca7c7bd0d4bb" + LINKE: + - url: https://www.die-linke.de/fileadmin/user_upload/Wahlprogramm_Langfassung_Linke-BTW25_01.pdf + titel: "Du verdienst mehr — Wahlprogramm DIE LINKE zur Bundestagswahl 2025 (Langfassung)" + jahr: 2025 + sha256: "301bd30a5fcd2a7e791adc4db5294e79d8f3fd71b8c9b080b057f14bf8cae600" + BSW: + - url: https://bsw-vg.de/wp-content/themes/bsw/assets/downloads/BSW%20Wahlprogramm%202025.pdf + titel: "Unser Land verdient mehr — Wahlprogramm Bündnis Sahra Wagenknecht zur Bundestagswahl 2025" + jahr: 2025 + sha256: "bd4640aab7c6ff214becc7e44bc6dc67539ebd80992bb39fb8853839d3a7ccda" diff --git a/app/wahlprogramm-shas.lock.json b/app/wahlprogramm-shas.lock.json index 44199b3..ce534a9 100644 --- a/app/wahlprogramm-shas.lock.json +++ b/app/wahlprogramm-shas.lock.json @@ -1,7 +1,9 @@ { "afd-bb-2024.pdf": "da5cd04cc66128b2f0df35b47775fce850ed2f4145ee15d74ec8bf501ce043f1", "afd-be-2023.pdf": "d2b5997b1bc0d3fb590cc354d8ed1ac879e8de4a74518f4089436a2fa12615f1", + "afd-bund-2025.pdf": "e2d0a944f54017aa432bcd0c069c7908aefb3ea65af4904e7f86ca7c7bd0d4bb", "afd-bw-2021.pdf": "a438e09279c6c5766171a213715ed0a9d60248ff86f648227e8bb6ec59a591c7", + "afd-grundsatzprogramm.pdf": "b35026580c8610ab81c67760743497695b06e30466255586d089d4715297bead", "afd-hh-2025.pdf": "6aae3ad00cd07824bcd99473e130d1b894e2174a89fcafece51865c51fdcd4c8", "afd-lsa-2021.pdf": "dd2651af2a9423039b1c5a39760be2332025d569a878453f09e0302e252edc23", "afd-mv-2021.pdf": "953c39941a1f997233daaf0cec01bc82b1e86ba895b43e8d34b015cc72799648", @@ -9,10 +11,13 @@ "afd-rp-2021.pdf": "3ec39eb08a073244813a51f260e18fe52aab791bea26bf8079546b6e189ec2b3", "afd-th-2024.pdf": "26e61fdc3456e7ce18f7a3d2ea1eada303f93cad0b9698797f83a671574eaf51", "bsw-bb-2024.pdf": "548c9bda01af176586606fae708c9f3b3ba98e1e128f1e2ff39e482289faab42", + "bsw-bund-2025.pdf": "bd4640aab7c6ff214becc7e44bc6dc67539ebd80992bb39fb8853839d3a7ccda", "bsw-th-2024.pdf": "5ace33912083048a759ee2af9288248447363dafa21f569c5c056df22751ba69", "cdu-bb-2024.pdf": "460b1463483429f9e8b84e4ae6ef9cf878dd228e108411bed3c153169a0001e8", "cdu-be-2023.pdf": "813d0d08ac8ce7381e9a7b9472e0616aaf684b1632c9d4a7f4e940a33455f29a", + "cdu-bund-2025.pdf": "08f751316e731b77aa2f18090b8695d88268f2942481399f37a3a47317361795", "cdu-bw-2021.pdf": "a92c104c456ce06d8bad6649071551e0ec0d525a1bc0bc31e9fa6a0566da4db0", + "cdu-grundsatzprogramm.pdf": "4b4bebd40ff8d905eb3ad00975cbbf0990810c21977e2d560e3d73f2571b181d", "cdu-hh-2025.pdf": "8d29e514b8bce5c2f3f497dc5b97f6f8ab95a7bdbf619abf258e9582d57f2dbd", "cdu-lsa-2021.pdf": "63b6cf42ce97834d5d105fb7b8cc7fb7a2aa96928d4153bd3a5858c196ee0797", "cdu-mv-2021.pdf": "605a2211bef8666c2103771ebffd97a088e7cdb1545401087ef125155e7e4db2", @@ -20,7 +25,10 @@ "cdu-rp-2021.pdf": "54c50d88bdf5c5f7dee5abcc981ffb4d1cfd5c86fbf2a29f4f2f4a8a3dd4797a", "cdu-sh-2022.pdf": "39b79a22e904b300cf1bbc25752b618195683c90c31e6b10c3bc0e8408aa6a1a", "cdu-th-2024.pdf": "cde8d2222bd8ce04aee24883a38dab8a30f5d60cda115b8bb2f43ceffa08b730", + "csu-bund-2025.pdf": "c07fe4b65404be9b4c1b99ec9c970567a96a27b86a1154754a41e9ef31d0c04f", + "fdp-bund-2025.pdf": "a549dcb318f60fdd8a257fb2e68745d1df50151f4914910e2afde20c3b0fa039", "fdp-bw-2021.pdf": "bdcbb1b2e5748922c8347bd69ea6f81c954fd02cd220d448400f9a5a86ce914b", + "fdp-grundsatzprogramm.pdf": "9abb6570c3505271b1f43db2a8340c05b970a7934d0c1b2542403025affe2b13", "fdp-lsa-2021.pdf": "3d4275e36e29c0b191dcc4a29061a1072920f868cc52bee954bf81491ad15224", "fdp-mv-2021.pdf": "8dc341dd017f1d82c51608a26e1fd6c3d8acd1281dc37409e375389999b37b55", "fdp-nrw-2022.pdf": "576b42a26c29ca5d8b7469d417ae709c8d0699aed5195d4ca16dd696dcff8bea", @@ -28,7 +36,9 @@ "fdp-sh-2022.pdf": "4c49da411bb3c8e008f4b57dd20dc005104515b56056ff746cf5403529728d09", "fw-rp-2021.pdf": "c7f26d553f24c9d9fcf1c2edb1dbe558edc1ca65af68b289a1541e77f7bbeea8", "gruene-be-2023.pdf": "2b14a319cdcd2ca022399254ea285714f872eddd166f3f537861eeb2dc5ade80", + "gruene-bund-2025.pdf": "0d1f7530ddecadc8d98db15b2189f3ee4ddcccd9619df825703911f1aac18dda", "gruene-bw-2021.pdf": "9af526705cb10b91be0690b26c9c033668a8082eeefca482dc4e7a46f2d671f9", + "gruene-grundsatzprogramm.pdf": "36c68616286bc5df94cadbe176c72c515a6ed403cc246d221244a882f30b0834", "gruene-hh-2025.pdf": "4428d1cdc16b4e74588f0bd51145ab7371f9e0871a2fc9d25a1f94e4f5aeb662", "gruene-lsa-2021.pdf": "7b5cea92cd600283d7edf18dc0d358c0b7d78d7269589d9ef05de7d5f8b35998", "gruene-mv-2021.pdf": "40f0070743ef9ae7808cab319234b4c83faa53a8a098ba8a82f28023bee4d9f6", @@ -36,13 +46,17 @@ "gruene-rp-2021.pdf": "4fd68629d1560c28d61b2b913fd20ce6ad9a76b22823fd8496e51bfaf70dc19c", "gruene-sh-2022.pdf": "62870c948c9e05663125b051d3a6401d63952ea6a64e4140dcece7bd1b1aea52", "linke-be-2023.pdf": "7d6a9166f6a1d87ba26cc1a2818ae2b844ee9df6ed6668673f329dd5186fd956", + "linke-bund-2025.pdf": "301bd30a5fcd2a7e791adc4db5294e79d8f3fd71b8c9b080b057f14bf8cae600", + "linke-grundsatzprogramm.pdf": "b5b2b19f5eea4cb22816eca6f2988bcc6cffa4c0cc499411bf131e866fae4e16", "linke-hh-2025.pdf": "15e68efe3818758a7cefc0a3e3095a5a5fb191111c00a1202c563cee43ce6e40", "linke-lsa-2021.pdf": "f269c014416b213785badf7bea5928fdb847fc902e09f52ec66a140a37e03d75", "linke-mv-2021.pdf": "160dad56ab4de8f641c21f51cbf3c33953f2f3d91b4de792c4e725f3975fdfbe", "linke-th-2024.pdf": "2d8ca99ef60cbe1b59cf33b1e37320d66a057e5136c2f49aa8cde77e4a19533a", "spd-bb-2024.pdf": "4131f63fbb9d67cd8948ca7a54f1c140b47968c77454a3dabe6bcdc4384f63d3", "spd-be-2023.pdf": "4ee84e969e97894742673f940ec030883216ce852b729507327f8bced637d03b", + "spd-bund-2025.pdf": "05aeb9eb19fd423288d94de1d15cabcddbcb9bb1ecf65237657d10e4839b9d7e", "spd-bw-2021.pdf": "d888ae92bb62a61aaa4d6ac8dc22c2c98d1a2227b6ba223b6422770672825072", + "spd-grundsatzprogramm.pdf": "0290ae8870e2deeeec4f93227264fef9f6dbbc56572a2ba29e93d8bd1c0de7cd", "spd-hh-2025.pdf": "5e8c57969cb3b159b9299c173831f7863ab81bd206c2a87ae232ba96f23156ee", "spd-lsa-2021.pdf": "59140aa1921ab0ee85142d74e1d72b1af7254da3f7870a30460abd605d280333", "spd-mv-2021.pdf": "c8c671c2e60f1a4f8048bd74e379eb8edc69ab2daeb09581fe83f25f6c87d529", diff --git a/app/wahlprogramme.py b/app/wahlprogramme.py index d2bf7d8..fe08115 100644 --- a/app/wahlprogramme.py +++ b/app/wahlprogramme.py @@ -172,18 +172,6 @@ WAHLPROGRAMME: dict[str, dict[str, dict]] = { "LINKE": {"file": "linke-bund-2025.pdf", "titel": "Alle wollen regieren. Wir wollen verändern. — DIE LINKE Wahlprogramm BTW 2025", "partei": "DIE LINKE", "jahr": 2025, "seiten": 60, "regierungsbildung": "2025-05-06", "regierungsende": None}, "BSW": {"file": "bsw-bund-2025.pdf", "titel": "Unser Land verdient mehr — BSW Wahlprogramm BTW 2025", "partei": "BSW", "jahr": 2025, "seiten": 45, "regierungsbildung": "2025-05-06", "regierungsende": None}, }, - # Bundestag — keine bundesweiten Wahlprogramme im Repo, daher dienen - # die Grundsatzprogramme als Quelle. CSU + BSW haben keine Programme - # in der Registry und werden weiterhin als fehlend gemeldet. - # Diese Eintraege sind von embeddings.py separat indiziert (typ=parteiprogramm). - "BUND": { - "CDU": {"file": "cdu-grundsatzprogramm.pdf", "titel": "CDU Grundsatzprogramm 2024", "partei": "CDU", "jahr": 2024, "seiten": 64, "ist_grundsatz": True}, - "SPD": {"file": "spd-grundsatzprogramm.pdf", "titel": "SPD Hamburger Programm 2007", "partei": "SPD", "jahr": 2007, "seiten": 78, "ist_grundsatz": True}, - "GRÜNE": {"file": "gruene-grundsatzprogramm.pdf","titel": "Grüne Grundsatzprogramm 2020", "partei": "GRÜNE", "jahr": 2020, "seiten": 116, "ist_grundsatz": True}, - "FDP": {"file": "fdp-grundsatzprogramm.pdf", "titel": "FDP Karlsruher Freiheitsthesen 2012","partei": "FDP", "jahr": 2012, "seiten": 31, "ist_grundsatz": True}, - "AfD": {"file": "afd-grundsatzprogramm.pdf", "titel": "AfD Grundsatzprogramm 2016", "partei": "AfD", "jahr": 2016, "seiten": 96, "ist_grundsatz": True}, - "LINKE": {"file": "linke-grundsatzprogramm.pdf", "titel": "DIE LINKE Erfurter Programm 2011", "partei": "LINKE", "jahr": 2011, "seiten": 84, "ist_grundsatz": True}, - }, } # Pro Bundesland: Markdown-Übersichtsdatei mit Wahlprogramm-Zusammenfassungen, diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 22b9af9..e1de23f 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -317,15 +317,11 @@ class TestReconstructZitate: } out = reconstruct_zitate(data, semantic_quotes) zitate = out["wahlprogrammScores"][0]["wahlprogramm"]["zitate"] - # Beide Zitate bleiben erhalten — das nicht-matchende wird als - # unverified markiert statt gedroppt (Hybrid-Ansatz). - assert len(zitate) == 2 - # Das halluzinierte Zitat ist unverified - halluziniert = [z for z in zitate if "Rechtsextremismus" in z["text"]] - assert halluziniert[0]["verified"] is False - # Das echte Zitat ist verified - echt = [z for z in zitate if "geschlechtersensiblen" in z["text"]] - assert echt[0]["verified"] is True + # Strict-Mode (#175): das nicht-matchende halluzinierte Zitat wird + # gedroppt, das echte Zitat bleibt erhalten und ist verified. + assert len(zitate) == 1 + assert "geschlechtersensiblen" in zitate[0]["text"] + assert zitate[0]["verified"] is True def test_empty_semantic_quotes_is_noop(self): data = {"wahlprogrammScores": [{ diff --git a/tests/test_presse_generator.py b/tests/test_presse_generator.py index 4dda898..3ff439c 100644 --- a/tests/test_presse_generator.py +++ b/tests/test_presse_generator.py @@ -54,6 +54,7 @@ def db_with_antrag_and_news(tmp_path: Path) -> Path: titel TEXT NOT NULL, body TEXT NOT NULL, model TEXT NOT NULL, + style TEXT NOT NULL DEFAULT 'pm', created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) diff --git a/tests/test_report.py b/tests/test_report.py index 23afdb4..fe965b6 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -221,14 +221,18 @@ class TestGetScoreColor: class TestGetRatingSymbol: + """Skala −5..+5 (seit Modell-Migration). Schwellen: + ≥+4 → ++, +1..+3 → +, 0 → ○, −1..−3 → −, ≤−4 → −−. + """ def test_strong_positive(self): from app.report import get_rating_symbol - assert get_rating_symbol(2) == "++" + assert get_rating_symbol(4) == "++" assert get_rating_symbol(5) == "++" def test_positive(self): from app.report import get_rating_symbol assert get_rating_symbol(1) == "+" + assert get_rating_symbol(3) == "+" def test_neutral(self): from app.report import get_rating_symbol @@ -237,8 +241,9 @@ class TestGetRatingSymbol: def test_negative(self): from app.report import get_rating_symbol assert get_rating_symbol(-1) == "−" + assert get_rating_symbol(-3) == "−" def test_strong_negative(self): from app.report import get_rating_symbol - assert get_rating_symbol(-2) == "−−" + assert get_rating_symbol(-4) == "−−" assert get_rating_symbol(-5) == "−−" diff --git a/tests/test_wahlprogramme.py b/tests/test_wahlprogramme.py index cfd7d87..d0b30bd 100644 --- a/tests/test_wahlprogramme.py +++ b/tests/test_wahlprogramme.py @@ -1,4 +1,6 @@ """Tests for wahlprogramme.py — registry consistency + file existence.""" +import re + import pytest from app.wahlprogramme import ( @@ -6,6 +8,9 @@ from app.wahlprogramme import ( REFERENZEN_PATH, get_wahlprogramm, parteien_mit_wahlprogramm, + regierung_aktuell, + regierungsbildung_for, + regierungsende_for, ) @@ -42,6 +47,83 @@ class TestRegistryStructure: assert info["file"].endswith(".pdf") +# ───────────────────────────────────────────────────────────────────────────── +# Regierungsbildungs-Felder — Konsistenz pro Bundesland +# ───────────────────────────────────────────────────────────────────────────── + +class TestRegierungsbildung: + """Pro Bundesland gehören alle Wahlprogramm-Einträge zur gleichen + Legislatur — d.h. dasselbe regierungsbildung-Datum. Ausnahme: BUND, wo + Grundsatzprogramme stehen, die regierungsbildung=None tragen.""" + + _ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") + + def test_every_entry_has_regierungs_fields(self): + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + assert "regierungsbildung" in info, f"{bl}/{partei}: regierungsbildung fehlt" + assert "regierungsende" in info, f"{bl}/{partei}: regierungsende fehlt" + + def test_regierungsbildung_iso_or_none(self): + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + rb = info.get("regierungsbildung") + if rb is not None: + assert isinstance(rb, str) and self._ISO_DATE.match(rb), \ + f"{bl}/{partei}: regierungsbildung kein ISO-Datum: {rb!r}" + + def test_regierungsende_iso_or_none(self): + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + re_ = info.get("regierungsende") + if re_ is not None: + assert isinstance(re_, str) and self._ISO_DATE.match(re_), \ + f"{bl}/{partei}: regierungsende kein ISO-Datum: {re_!r}" + + def test_alle_parteien_eines_bl_haben_gleiches_datum(self): + """Alle Wahlprogramm-Einträge eines Bundeslands gehören zur selben + Regierung und müssen daher dasselbe Bildungs-/Endedatum tragen.""" + for bl, parteien in WAHLPROGRAMME.items(): + bildung = {info.get("regierungsbildung") for info in parteien.values()} + ende = {info.get("regierungsende") for info in parteien.values()} + assert len(bildung) == 1, \ + f"{bl}: regierungsbildung divergent: {bildung}" + assert len(ende) == 1, \ + f"{bl}: regierungsende divergent: {ende}" + + def test_grundsatzprogramme_haben_keine_regierung(self): + """Grundsatzprogramme (ist_grundsatz=True) tragen keine Regierungs- + bildung — sie sind zeitlos.""" + for bl, parteien in WAHLPROGRAMME.items(): + for partei, info in parteien.items(): + if info.get("ist_grundsatz"): + assert info.get("regierungsbildung") is None, \ + f"{bl}/{partei} ist Grundsatzprogramm, sollte regierungsbildung=None haben" + + +class TestRegierungsHelper: + def test_regierungsbildung_for_known_bl(self): + assert regierungsbildung_for("NRW") == "2022-06-29" + assert regierungsbildung_for("HH") == "2025-05-07" + + def test_regierungsbildung_for_bund_btw2025(self): + # BUND tragt nun die BTW-2025-Wahlprogramme; Kabinett Merz I + # vereidigt 06.05.2025. Grundsatzprogramme bleiben nur in + # embeddings.PROGRAMME als zweite Referenz. + assert regierungsbildung_for("BUND") == "2025-05-06" + + def test_regierungsbildung_for_unknown_bl(self): + assert regierungsbildung_for("XX") is None + + def test_regierung_aktuell_true_for_active_bl(self): + assert regierung_aktuell("NRW") is True + assert regierung_aktuell("BB") is True + assert regierung_aktuell("BUND") is True + + def test_regierungsende_for_active_is_none(self): + assert regierungsende_for("NRW") is None + + # ───────────────────────────────────────────────────────────────────────────── # File existence — every registered file must exist on disk # ───────────────────────────────────────────────────────────────────────────── @@ -91,6 +173,36 @@ class TestParteienMitWahlprogramm: parteien = parteien_mit_wahlprogramm("BE") assert set(parteien) == {"CDU", "SPD", "GRÜNE", "LINKE", "AfD"} + def test_bund_has_eight_parteien(self): + # BTW 2025: CDU, CSU, SPD, GRÜNE, FDP, AfD, LINKE, BSW. + parteien = parteien_mit_wahlprogramm("BUND") + assert set(parteien) == {"CDU", "CSU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE", "BSW"} + + def test_by_has_five_parteien(self): + parteien = parteien_mit_wahlprogramm("BY") + assert set(parteien) == {"CSU", "FREIE WÄHLER", "GRÜNE", "SPD", "AfD"} + + def test_hb_has_four_parteien(self): + # AfD war wegen Listenstreit nicht zur Bürgerschaftswahl 2023 zugelassen. + parteien = parteien_mit_wahlprogramm("HB") + assert set(parteien) == {"SPD", "CDU", "GRÜNE", "LINKE"} + + def test_he_has_five_parteien(self): + parteien = parteien_mit_wahlprogramm("HE") + assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"} + + def test_ni_has_four_parteien(self): + parteien = parteien_mit_wahlprogramm("NI") + assert set(parteien) == {"SPD", "CDU", "GRÜNE", "AfD"} + + def test_sl_has_three_parteien(self): + parteien = parteien_mit_wahlprogramm("SL") + assert set(parteien) == {"SPD", "CDU", "AfD"} + + def test_sn_has_six_parteien(self): + parteien = parteien_mit_wahlprogramm("SN") + assert set(parteien) == {"CDU", "SPD", "AfD", "BSW", "LINKE", "GRÜNE"} + def test_unknown_bundesland_empty_list(self): assert parteien_mit_wahlprogramm("XX") == []