Initial commit: AegisSight-Monitor (OSINT-Monitoringsystem)
Dieser Commit ist enthalten in:
169
src/agents/analyzer.py
Normale Datei
169
src/agents/analyzer.py
Normale Datei
@@ -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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren