feat(#145): LLM-Prompts auf /methodik als Transparenz-Block

System- und User-Prompt-Template stehen jetzt collapsed unter dem
neuen Abschnitt 'LLM-Prompts'. Der User-Prompt wird auf eine eigene
Konstante USER_PROMPT_TEMPLATE umgestellt und via .format(...) gerendert,
sodass das gleiche Template auf der Methodik-Seite gezeigt werden kann
ohne den f-string-Code zu duplizieren.

Closes #145
This commit is contained in:
Dotty Dotter 2026-04-28 01:50:25 +02:00
parent 5f6bcac282
commit 0d26cad549
3 changed files with 92 additions and 34 deletions

View File

@ -71,6 +71,52 @@ def load_context_file(name: str) -> str:
return "" return ""
USER_PROMPT_TEMPLATE = """Analysiere den folgenden Antrag:
<kontext>
{bundesland_context}
</kontext>
<wahlprogramm_zitate>
{quotes_context}
</wahlprogramm_zitate>
<antrag>
{text}
</antrag>
**PFLICHT-FRAKTIONEN:** Du MUSST ALLE folgenden Fraktionen der aktuellen Wahlperiode in `wahlprogrammScores` bewerten keine auslassen:
{pflicht_fraktionen}
Bewerte nach GWÖ-Matrix 2.0 für Gemeinden:
1. GWÖ-Treue (0-10) mit Matrix-Zuordnung und Symbolen (++/+///)
2. Wahlprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
3. Parteiprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
4. Bis zu 3 Verbesserungsvorschläge in Redline-Syntax
5. Themen-Tags für Kategorisierung
**ZITATEREGEL STRIKT:** In jedem ``wahlprogrammScores[].wahlprogramm.zitate[].quelle``
und ``parteiprogrammScores[].parteiprogramm.zitate[].quelle`` musst du **wortgleich**
einen der oben in ``<wahlprogramm_zitate>`` aufgelisteten Quellen-Labels (Programm-Name +
Seite) übernehmen z.B. ``"CDU Mecklenburg-Vorpommern Wahlprogramm 2021, S. 33"``.
Erfinde keine Quellen aus deinem Trainingswissen. Nimm keine Quelle aus einem anderen
Bundesland (z.B. NRW 2022) als die hier aufgelisteten selbst wenn dir die dortigen
Programme bekannter sind. Findest du oben für eine Partei keinen passenden Chunk, lass
``zitate`` leer (``[]``) und vermerke das in der ``begruendung``.
Ausgabe als reines JSON ohne Markdown-Codeblöcke."""
def get_user_prompt_template() -> str:
"""Public Template-String fuer Transparenz-Seite (#145).
Enthaelt die Platzhalter ``{bundesland_context}``, ``{quotes_context}``,
``{text}`` und ``{pflicht_fraktionen}`` gerendert wird in
``analyze_text`` direkt via ``.format(...)``.
"""
return USER_PROMPT_TEMPLATE
def get_system_prompt() -> str: def get_system_prompt() -> str:
"""Build the system prompt with GWÖ matrix context.""" """Build the system prompt with GWÖ matrix context."""
return """Du bist ein Experte für Gemeinwohl-Ökonomie (GWÖ) und parlamentarische Analyse. Du bewertest Anträge aus Landesparlamenten systematisch nach drei Dimensionen: return """Du bist ein Experte für Gemeinwohl-Ökonomie (GWÖ) und parlamentarische Analyse. Du bewertest Anträge aus Landesparlamenten systematisch nach drei Dimensionen:
@ -316,40 +362,12 @@ async def analyze_antrag(
quotes = find_relevant_quotes(text, fraktionen, bundesland=bundesland) quotes = find_relevant_quotes(text, fraktionen, bundesland=bundesland)
quotes_context = format_quote_for_prompt(quotes) quotes_context = format_quote_for_prompt(quotes)
user_prompt = f"""Analysiere den folgenden Antrag: user_prompt = USER_PROMPT_TEMPLATE.format(
bundesland_context=bundesland_context,
<kontext> quotes_context=quotes_context if quotes_context else "Keine relevanten Zitate gefunden.",
{bundesland_context} text=text,
</kontext> pflicht_fraktionen=", ".join(BUNDESLAENDER[bundesland].landtagsfraktionen),
)
<wahlprogramm_zitate>
{quotes_context if quotes_context else "Keine relevanten Zitate gefunden."}
</wahlprogramm_zitate>
<antrag>
{text}
</antrag>
**PFLICHT-FRAKTIONEN:** Du MUSST ALLE folgenden Fraktionen der aktuellen Wahlperiode in `wahlprogrammScores` bewerten keine auslassen:
{', '.join(BUNDESLAENDER[bundesland].landtagsfraktionen)}
Bewerte nach GWÖ-Matrix 2.0 für Gemeinden:
1. GWÖ-Treue (0-10) mit Matrix-Zuordnung und Symbolen (++/+///)
2. Wahlprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
3. Parteiprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
4. Bis zu 3 Verbesserungsvorschläge in Redline-Syntax
5. Themen-Tags für Kategorisierung
**ZITATEREGEL STRIKT:** In jedem ``wahlprogrammScores[].wahlprogramm.zitate[].quelle``
und ``parteiprogrammScores[].parteiprogramm.zitate[].quelle`` musst du **wortgleich**
einen der oben in ``<wahlprogramm_zitate>`` aufgelisteten Quellen-Labels (Programm-Name +
Seite) übernehmen z.B. ``"CDU Mecklenburg-Vorpommern Wahlprogramm 2021, S. 33"``.
Erfinde keine Quellen aus deinem Trainingswissen. Nimm keine Quelle aus einem anderen
Bundesland (z.B. NRW 2022) als die hier aufgelisteten selbst wenn dir die dortigen
Programme bekannter sind. Findest du oben für eine Partei keinen passenden Chunk, lass
``zitate`` leer (``[]``) und vermerke das in der ``begruendung``.
Ausgabe als reines JSON ohne Markdown-Codeblöcke."""
# LLM-Call über den Port. Retry-Loop + Markdown-Stripping wohnen im # LLM-Call über den Port. Retry-Loop + Markdown-Stripping wohnen im
# Adapter (``QwenBewerter``). Bei exhausted retries wirft er # Adapter (``QwenBewerter``). Bei exhausted retries wirft er

View File

@ -1735,6 +1735,7 @@ async def methodik_page(request: Request, current_user: Optional[dict] = Depends
"""Transparenz-/Methodik-Seite (#96).""" """Transparenz-/Methodik-Seite (#96)."""
from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER from .bundeslaender import aktive_bundeslaender, BUNDESLAENDER
from .embeddings import get_indexing_status from .embeddings import get_indexing_status
from .analyzer import get_system_prompt, get_user_prompt_template
bl_list = [] bl_list = []
for bl in aktive_bundeslaender(): for bl in aktive_bundeslaender():
@ -1755,6 +1756,8 @@ async def methodik_page(request: Request, current_user: Optional[dict] = Depends
"programme_count": status.get("total", 0), "programme_count": status.get("total", 0),
"chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])), "chunk_count": sum(p.get("chunks", 0) for p in status.get("programmes", [])),
"bundeslaender": sorted(bl_list, key=lambda x: x["name"]), "bundeslaender": sorted(bl_list, key=lambda x: x["name"]),
"system_prompt": get_system_prompt(),
"user_prompt_template": get_user_prompt_template(),
**_v2_template_context(current_user), **_v2_template_context(current_user),
}) })

View File

@ -138,6 +138,7 @@
<a href="#was-macht">Was macht der Prüfer?</a> <a href="#was-macht">Was macht der Prüfer?</a>
<a href="#matrix">Die Matrix 2.0</a> <a href="#matrix">Die Matrix 2.0</a>
<a href="#pipeline">Analyse-Pipeline</a> <a href="#pipeline">Analyse-Pipeline</a>
<a href="#prompts">LLM-Prompts</a>
<a href="#qualitaet">Qualitätssicherung</a> <a href="#qualitaet">Qualitätssicherung</a>
<a href="#einschraenkungen">Einschränkungen</a> <a href="#einschraenkungen">Einschränkungen</a>
<a href="#datenquellen">Datenquellen</a> <a href="#datenquellen">Datenquellen</a>
@ -365,6 +366,42 @@
</div> </div>
</section> </section>
<section id="prompts">
<h2>LLM-Prompts</h2>
<div class="v2-kasten outline-blue">
<p>
Volle Transparenz: hier liegen die exakten Anweisungen, mit denen das
Sprachmodell ({{ model_name }}) jeden Antrag bewertet. Der
<strong>System-Prompt</strong> ist statisch und enthält die GWÖ-Matrix
plus Ausgabe-Schema. Der <strong>User-Prompt</strong> wird pro Antrag
dynamisch gefüllt — die Platzhalter <code>{kontext}</code>,
<code>{wahlprogramm_zitate}</code>, <code>{antrag}</code> und
<code>{pflicht_fraktionen}</code> sind unten als
<code>{...}</code> markiert.
</p>
<p style="font-size:11px;opacity:0.7;">
Quelle: <a href="https://repo.toppyr.de/tobias/gwoe-antragspruefer/src/branch/main/app/analyzer.py" target="_blank"><code>app/analyzer.py</code></a>
(<code>get_system_prompt()</code> und <code>get_user_prompt_template()</code>).
</p>
<details style="margin-top:1rem;">
<summary style="cursor:pointer;color:var(--ecg-teal);font-weight:700;padding:6px 0;font-family:var(--font-display);">
System-Prompt anzeigen
<span style="font-family:var(--font-mono);font-size:11px;opacity:0.6;font-weight:400;">({{ system_prompt|length }} Zeichen)</span>
</summary>
<pre style="background:var(--ecg-bg-subtle);border:1px solid var(--ecg-border);border-radius:4px;padding:14px 16px;margin-top:8px;font-family:var(--font-mono);font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--ecg-dark);overflow-x:auto;">{{ system_prompt }}</pre>
</details>
<details style="margin-top:0.75rem;">
<summary style="cursor:pointer;color:var(--ecg-teal);font-weight:700;padding:6px 0;font-family:var(--font-display);">
User-Prompt-Template anzeigen
<span style="font-family:var(--font-mono);font-size:11px;opacity:0.6;font-weight:400;">({{ user_prompt_template|length }} Zeichen)</span>
</summary>
<pre style="background:var(--ecg-bg-subtle);border:1px solid var(--ecg-border);border-radius:4px;padding:14px 16px;margin-top:8px;font-family:var(--font-mono);font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word;color:var(--ecg-dark);overflow-x:auto;">{{ user_prompt_template }}</pre>
</details>
</div>
</section>
<section id="qualitaet"> <section id="qualitaet">
<h2>Qualitätssicherung</h2> <h2>Qualitätssicherung</h2>
<div class="v2-kasten outline-green"> <div class="v2-kasten outline-green">