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:
@@ -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()
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren