Export: Zusammenfassung-Sektion, Checkbox-Auswahl, neue Reihenfolge

Research-Briefings:
- Neue Sektion ZUSAMMENFASSUNG mit Bullet-Points als erstes Element
- UEBERBLICK entfernt, durch ZUSAMMENFASSUNG ersetzt
- Inkrementelles Briefing ebenfalls angepasst

Export-System:
- Zusammenfassung wird direkt aus dem Bericht extrahiert (kein
  separater KI-Aufruf mehr fuer Research-Lagen)
- Reihenfolge: Zusammenfassung > Recherchebericht > Faktencheck > Quellen > Timeline
- Sections-basiert statt scope-basiert (rueckwaertskompatibel)
- Checkbox-Dialog statt Radio-Buttons im Frontend
- Bereiche: Zusammenfassung, Recherchebericht, Faktencheck, Quellen, Timeline, Karte
- PDF und DOCX Templates angepasst
- Backend akzeptiert sections-Parameter (kommagetrennt)
Dieser Commit ist enthalten in:
claude-dev
2026-04-11 20:56:04 +00:00
Ursprung 89cc920bdc
Commit fa12d4cfd6
7 geänderte Dateien mit 188 neuen und 66 gelöschten Zeilen

Datei anzeigen

@@ -171,6 +171,55 @@ def _strip_citation_numbers(text: str) -> str:
def _extract_zusammenfassung(summary_text: str) -> tuple[str, str]:
"""Extrahiert die ZUSAMMENFASSUNG-Sektion aus einem Research-Briefing.
Returns:
(zusammenfassung_html, remaining_summary)
zusammenfassung_html: HTML-formatierte Bullet Points
remaining_summary: Der Rest des Berichts ohne die Zusammenfassung
"""
if not summary_text:
return "", summary_text
# Suche nach ## ZUSAMMENFASSUNG ... bis zur naechsten ## Ueberschrift
pattern = r"(## ZUSAMMENFASSUNG\s*\n)(.*?)(?=\n## |\Z)"
match = re.search(pattern, summary_text, re.DOTALL)
if not match:
return "", summary_text
zusammenfassung_raw = match.group(2).strip()
# Rest des Berichts ohne die Zusammenfassung-Sektion
remaining = summary_text[:match.start()] + summary_text[match.end():]
remaining = remaining.strip()
# Bullet Points als HTML formatieren
lines = []
for line in zusammenfassung_raw.split("\n"):
stripped = line.strip()
if stripped.startswith("- "):
clean = _strip_citation_numbers(stripped[2:].strip())
if clean:
lines.append(clean)
elif stripped.startswith("* "):
clean = _strip_citation_numbers(stripped[2:].strip())
if clean:
lines.append(clean)
elif stripped and not stripped.startswith("#"):
clean = _strip_citation_numbers(stripped)
if clean:
lines.append(clean)
if lines:
html = "<ul>\n" + "\n".join(f"<li>{line}</li>" for line in lines) + "\n</ul>"
else:
html = f"<p>{_strip_citation_numbers(zusammenfassung_raw)}</p>"
return html, remaining
async def generate_executive_summary(summary_text: str) -> str:
"""KI-verdichtetes Executive Summary aus dem Lagebild."""
if not summary_text or len(summary_text.strip()) < 50:
@@ -246,8 +295,31 @@ LAGEBILD:
async def generate_pdf(
incident: dict, articles: list, fact_checks: list, snapshots: list,
scope: str, creator: str, executive_summary_html: str,
sections: set[str] | None = None,
) -> bytes:
"""PDF-Report via WeasyPrint generieren."""
# Sections aus scope ableiten wenn nicht explizit angegeben
if sections is None:
if scope == "summary":
sections = {"zusammenfassung"}
elif scope == "report":
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen"}
else: # full
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
is_research = incident.get("type") == "research"
zusammenfassung_html = executive_summary_html
bericht_summary = incident.get("summary", "")
zusammenfassung_title = "Executive Summary"
if is_research and bericht_summary:
extracted_html, remaining = _extract_zusammenfassung(bericht_summary)
if extracted_html:
zusammenfassung_html = extracted_html
zusammenfassung_title = "Zusammenfassung"
bericht_summary = remaining
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
template = env.get_template("report.html")
@@ -260,13 +332,12 @@ async def generate_pdf(
report_date=now.strftime("%d.%m.%Y, %H:%M Uhr"),
creator=creator,
logo_base64=_get_logo_base64(),
executive_summary=executive_summary_html,
executive_summary=zusammenfassung_html,
zusammenfassung_title=zusammenfassung_title,
sections=sections,
scope=scope,
lagebild_html=_markdown_to_html(
_strip_citation_numbers(
_truncate_lagebild(incident.get("summary", ""), 4000) if scope == "report"
else incident.get("summary", "")
)
_strip_citation_numbers(bericht_summary)
),
lagebild_timestamp=(incident.get("updated_at") or "")[:16].replace("T", " "),
sources=_prepare_sources(incident)[:30] if scope == "report" else _prepare_sources(incident),
@@ -292,10 +363,33 @@ async def generate_pdf(
async def generate_docx(
incident: dict, articles: list, fact_checks: list, snapshots: list,
scope: str, creator: str, executive_summary_text: str,
sections: set[str] | None = None,
) -> bytes:
"""Word-Report via python-docx generieren."""
doc = Document()
# Sections aus scope ableiten wenn nicht explizit angegeben
if sections is None:
if scope == "summary":
sections = {"zusammenfassung"}
elif scope == "report":
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen"}
else: # full
sections = {"zusammenfassung", "bericht", "faktencheck", "quellen", "timeline"}
# Fuer Research-Lagen: Zusammenfassung aus dem Bericht extrahieren
is_research = incident.get("type") == "research"
zusammenfassung_text = executive_summary_text
bericht_summary = incident.get("summary") or "Keine Zusammenfassung verfuegbar."
zusammenfassung_title = "Executive Summary"
if is_research and bericht_summary:
extracted_html, remaining = _extract_zusammenfassung(bericht_summary)
if extracted_html:
zusammenfassung_text = extracted_html
zusammenfassung_title = "Zusammenfassung"
bericht_summary = remaining
# Styles
style = doc.styles['Normal']
style.font.size = Pt(10)
@@ -347,23 +441,21 @@ async def generate_docx(
doc.add_page_break()
# --- Executive Summary ---
doc.add_heading("Executive Summary", level=1)
# --- Zusammenfassung / Executive Summary ---
if "zusammenfassung" in sections:
doc.add_heading(zusammenfassung_title, level=1)
# HTML-Tags entfernen und als Bullet Points
clean_text = re.sub(r'<[^>]+>', '', executive_summary_text)
lines = [line.strip().lstrip("- ").lstrip("* ") for line in clean_text.strip().split("\n") if line.strip()]
for line in lines:
if line:
doc.add_paragraph(line, style='List Bullet')
# HTML-Tags entfernen und als Bullet Points
clean_text = re.sub(r'<[^>]+>', '', zusammenfassung_text)
lines = [line.strip().lstrip("- ").lstrip("* ") for line in clean_text.strip().split("\n") if line.strip()]
for line in lines:
if line:
doc.add_paragraph(line, style='List Bullet')
if scope in ("report", "full"):
# --- Lagebild ---
doc.add_heading("Recherchebericht" if incident.get("type") == "research" else "Lagebild", level=1)
raw_summary = incident.get("summary") or "Keine Zusammenfassung verfügbar."
summary = _strip_citation_numbers(
_truncate_lagebild(raw_summary, 4000) if scope == "report" else raw_summary
)
if "bericht" in sections:
# --- Lagebild / Recherchebericht ---
doc.add_heading("Recherchebericht" if is_research else "Lagebild", level=1)
summary = _strip_citation_numbers(bericht_summary)
# Markdown-Formatierung entfernen
clean_summary = re.sub(r'\*\*(.+?)\*\*', r'\1', summary)
clean_summary = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', clean_summary)
@@ -379,8 +471,9 @@ async def generate_docx(
else:
doc.add_paragraph(para_text)
if "faktencheck" in sections:
# --- Faktencheck ---
report_fcs = fact_checks[:20] if scope == 'report' else fact_checks
report_fcs = fact_checks
if report_fcs:
doc.add_heading("Faktencheck", level=1)
table = doc.add_table(rows=1, cols=3)
@@ -400,10 +493,9 @@ async def generate_docx(
row[1].text = FC_STATUS_LABELS.get(fc.get("status", ""), fc.get("status", ""))
row[2].text = str(fc.get("sources_count", 0))
if "quellen" in sections:
# --- Quellenstatistik ---
source_stats = _prepare_source_stats(articles)
if scope == 'report':
source_stats = source_stats[:20]
if source_stats:
doc.add_heading("Quellenstatistik", level=1)
table = doc.add_table(rows=1, cols=3)
@@ -423,7 +515,7 @@ async def generate_docx(
row[1].text = str(stat["count"])
row[2].text = stat["languages"]
if scope == "full":
if "timeline" in sections:
# --- Artikelverzeichnis ---
if articles:
doc.add_page_break()