Initial commit: AegisSight-Monitor (OSINT-Monitoringsystem)

Dieser Commit ist enthalten in:
claude-dev
2026-03-04 17:53:18 +01:00
Commit 8312d24912
51 geänderte Dateien mit 19355 neuen und 0 gelöschten Zeilen

169
src/agents/analyzer.py Normale Datei
Datei anzeigen

@@ -0,0 +1,169 @@
"""Analyzer-Agent: Analysiert, übersetzt und fasst Meldungen zusammen."""
import asyncio
import json
import logging
import re
from datetime import datetime
from config import TIMEZONE
from agents.claude_client import call_claude, ClaudeUsage
logger = logging.getLogger("osint.analyzer")
ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language}
VORFALL: {title}
KONTEXT: {description}
VORHANDENE MELDUNGEN:
{articles_text}
AUFTRAG:
1. Erstelle eine neutrale, faktenbasierte Zusammenfassung auf {output_language} (max. 500 Wörter)
2. Verwende Inline-Quellenverweise [1], [2], [3] etc. im Zusammenfassungstext
3. Liste die bestätigten Kernfakten auf
4. Übersetze fremdsprachige Überschriften und Inhalte in die Ausgabesprache
STRUKTUR:
- Wenn die Meldungen thematisch klar einen einzelnen Strang behandeln: Fließtext ohne Überschriften
- Wenn verschiedene Aspekte oder Themenfelder aufkommen (z.B. Ereignis + Reaktionen + Hintergrund): Gliedere mit kurzen Markdown-Zwischenüberschriften (##)
- Die Entscheidung liegt bei dir — Überschriften nur wenn sie dem Leser helfen, verschiedene Themenstränge auseinanderzuhalten
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
- Nur gesicherte Informationen in die Zusammenfassung
- Bei widersprüchlichen Angaben beide Seiten erwähnen
- Quellen immer mit [Nr] referenzieren
- Jede verwendete Quelle MUSS im sources-Array aufgelistet sein
- Nummeriere die Quellen fortlaufend ab [1]
- Ältere Quellen zeitlich einordnen (z.B. "laut einem Bericht vom Januar", "Anfang Februar berichtete...")
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
- "summary": Zusammenfassung auf {output_language} mit Quellenverweisen [1], [2] etc. im Text (Markdown-Überschriften ## erlaubt wenn sinnvoll)
- "sources": Array von Quellenobjekten, je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array von bestätigten Kernfakten (Strings, in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für fremdsprachige Artikel)
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
BRIEFING_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
Du erstellst ein strukturiertes Briefing für eine Hintergrundrecherche.
HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language}
THEMA: {title}
KONTEXT: {description}
VORLIEGENDE QUELLEN:
{articles_text}
AUFTRAG:
Erstelle ein strukturiertes Briefing (max. 800 Wörter) auf {output_language} mit folgenden Abschnitten.
Verwende durchgehend Inline-Quellenverweise [1], [2], [3] etc. im Text.
## ÜBERBLICK
Kurze Einordnung des Themas (2-3 Sätze)
## HINTERGRUND
Historischer Kontext, relevante Vorgeschichte
## AKTEURE
Beteiligte Personen, Organisationen, Institutionen und ihre Rollen
## AKTUELLE LAGE
Was ist der aktuelle Stand? Welche Entwicklungen gibt es?
## EINSCHÄTZUNG
Sachliche Bewertung der Situation, mögliche Entwicklungen
## QUELLENQUALITÄT
Kurze Bewertung der Informationslage: Wie belastbar sind die vorliegenden Quellen?
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
- Nur gesicherte Informationen verwenden
- Bei widersprüchlichen Angaben beide Seiten erwähnen
- Quellen immer mit [Nr] referenzieren
- Jede verwendete Quelle MUSS im sources-Array aufgelistet sein
- Nummeriere die Quellen fortlaufend ab [1]
- Ältere Quellen zeitlich einordnen (z.B. "laut einem Bericht vom Januar", "Anfang Februar berichtete...")
- Markdown-Überschriften (##) für die Abschnitte verwenden
- Fettdruck (**) für Schlüsselbegriffe erlaubt
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
- "summary": Das strukturierte Briefing als Markdown-Text mit Quellenverweisen [1], [2] etc.
- "sources": Array von Quellenobjekten, je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array von gesicherten Kernfakten (Strings, in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für fremdsprachige Artikel)
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
class AnalyzerAgent:
"""Analysiert und übersetzt Meldungen über Claude CLI."""
async def analyze(self, title: str, description: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[dict | None, ClaudeUsage | None]:
"""Analysiert alle Meldungen zu einem Vorfall."""
if not articles:
return None, None
# Artikel-Text für Prompt aufbereiten
articles_text = ""
for i, article in enumerate(articles[:30]): # Max 30 Artikel um Prompt-Länge zu begrenzen
articles_text += f"\n--- Meldung {i+1} (ID: {article.get('id', 'neu')}) ---\n"
articles_text += f"Quelle: {article.get('source', 'Unbekannt')}\n"
url = article.get('source_url', '')
if url:
articles_text += f"URL: {url}\n"
articles_text += f"Sprache: {article.get('language', 'de')}\n"
published = article.get('published_at', '')
if published:
articles_text += f"Veröffentlicht: {published}\n"
headline = article.get('headline_de') or article.get('headline', '')
articles_text += f"Überschrift: {headline}\n"
content = article.get('content_de') or article.get('content_original', '')
if content:
articles_text += f"Inhalt: {content[:500]}\n"
from config import OUTPUT_LANGUAGE
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
template = BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else ANALYSIS_PROMPT_TEMPLATE
prompt = template.format(
title=title,
description=description or "Keine weiteren Details",
articles_text=articles_text,
today=today,
output_language=OUTPUT_LANGUAGE,
)
try:
result, usage = await call_claude(prompt)
analysis = self._parse_response(result)
if analysis:
logger.info(f"Analyse abgeschlossen: {len(analysis.get('sources', []))} Quellen referenziert")
return analysis, usage
except Exception as e:
logger.error(f"Analyse-Fehler: {e}")
return None, None
def _parse_response(self, response: str) -> dict | None:
"""Parst die Claude-Antwort als JSON-Objekt."""
try:
data = json.loads(response)
if isinstance(data, dict):
return data
except json.JSONDecodeError:
pass
match = re.search(r'\{.*\}', response, re.DOTALL)
if match:
try:
data = json.loads(match.group())
if isinstance(data, dict):
return data
except json.JSONDecodeError:
pass
logger.warning("Konnte Analyse-Antwort nicht als JSON parsen")
return None