diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 5c679ae..2aea02e 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -66,8 +66,8 @@ AUFTRAG: Erstelle ein strukturiertes Briefing auf {output_language} mit folgenden Abschnitten. Sei so ausführlich wie nötig, um alle Aspekte gründlich abzudecken. Verwende durchgehend Inline-Quellenverweise [1], [2], [3] etc. im Text. -## ÜBERBLICK -Kurze Einordnung des Themas (2-3 Sätze) +## ZUSAMMENFASSUNG +Kompakte Übersicht als Aufzählung (4-8 Bullet Points mit "- "). Jeder Punkt fasst einen Kernaspekt des Themas in 1-2 Sätzen zusammen. Der Leser soll nach dieser Sektion das Wesentliche erfasst haben, ohne den Rest lesen zu müssen. ## HINTERGRUND Historischer Kontext, relevante Vorgeschichte @@ -171,7 +171,7 @@ NEUE QUELLEN SEIT DEM LETZTEN UPDATE: AUFTRAG: Aktualisiere das Briefing mit den neuen Erkenntnissen. Sei so ausführlich wie nötig. Behalte die Struktur bei: -## ÜBERBLICK +## ZUSAMMENFASSUNG ## HINTERGRUND ## AKTEURE ## AKTUELLE LAGE diff --git a/src/report_generator.py b/src/report_generator.py index 32ac00c..4bdfaa0 100644 --- a/src/report_generator.py +++ b/src/report_generator.py @@ -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 = "" + else: + html = f"

{_strip_citation_numbers(zusammenfassung_raw)}

" + + 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() diff --git a/src/report_templates/report.html b/src/report_templates/report.html index da14b1a..2b2d34e 100644 --- a/src/report_templates/report.html +++ b/src/report_templates/report.html @@ -78,17 +78,27 @@ tr:nth-child(even) { background: #f8f9fa; } - + +{% if 'zusammenfassung' in sections %}
-

Executive Summary

+

{{ zusammenfassung_title }}

{{ executive_summary | safe }}
+{% endif %} + + +{% if 'bericht' in sections %} +
+

{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}

+ {% if lagebild_timestamp %}

Aktualisiert: {{ lagebild_timestamp }}

{% endif %} +
{{ lagebild_html | safe }}
+
+{% endif %} -{% if scope in ('report', 'full') %} -{% if fact_checks %} +{% if 'faktencheck' in sections and fact_checks %}

Faktencheck

@@ -106,10 +116,12 @@ tr:nth-child(even) { background: #f8f9fa; } {% endif %} - -{% if source_stats %} + +{% if 'quellen' in sections and sources %}
-

Quellenstatistik

+

Quellenverzeichnis

+ {% if source_stats %} +

Quellenstatistik

@@ -118,18 +130,8 @@ tr:nth-child(even) { background: #f8f9fa; } {% endfor %}
QuelleArtikelSprache
-
- -
-

{% if incident.type == "research" %}Recherchebericht{% else %}Lagebild{% endif %}

- {% if lagebild_timestamp %}

Aktualisiert: {{ lagebild_timestamp }}

{% endif %} -
{{ lagebild_html | safe }}
-
- - -{% if sources %} -
-

Quellenverzeichnis

+ {% endif %} +

Quellen

@@ -141,12 +143,8 @@ tr:nth-child(even) { background: #f8f9fa; } {% endif %} -{% endif %} -{% endif %} - -{% if scope == 'full' %} -{% if timeline %} +{% if 'timeline' in sections and timeline %}

Ereignis-Timeline

{% for event in timeline %} @@ -160,7 +158,7 @@ tr:nth-child(even) { background: #f8f9fa; } {% endif %} -{% if articles %} +{% if 'timeline' in sections and articles %}

Artikelverzeichnis ({{ articles | length }} Artikel)

#QuelleURL
@@ -178,7 +176,6 @@ tr:nth-child(even) { background: #f8f9fa; }
{% endif %} -{% endif %}