feat(studio): Phase 2 - Studio-Bausteine, Backend-Kern (Sammeln/Analyse/Faktencheck)
Backend fuer die modularen Studio-Pipeline-Bausteine, alles ueber Claude.
DB (additiv, idempotent):
- Tabellen incident_events + fact_check_runs (+ Indizes)
- Funktion log_incident_event()
- Spalten incidents.summary_at, incidents.executive_summary, articles.geoparsed_at
agents/stage_runners.py (neu): isolierte Bausteine analyze + factcheck auf dem
vorhandenen Bestand, komplett neu, Historie bleibt (Snapshot/fact_check_runs).
orchestrator.py: collect_only in die Lane-Multi-Tenant-Queue eingebaut (enqueue_refresh
4-Tupel, _worker-Entpacken, Multi-Pass-Gate, _run_refresh-Signatur + Analyse/FC-Skip).
Zusaetzlich summary_at beim Schreiben des Lagebilds gestempelt (Studio-Freshness).
incidents.py: Endpunkte POST /{id}/run/{stage} (collect|analyze|factcheck),
GET /{id}/run-status, GET /{id}/freshness, GET /{id}/factcheck-runs[/{run_id}].
Busy-Check auf Online-Orchestrator (_current_tasks), Tenant-Zugriffspruefung.
Kein lokaler LLM-Unterbau, kein lokales Modell. /ask + /events folgen in Phase 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -458,9 +458,13 @@ class AgentOrchestrator:
|
||||
task.cancel()
|
||||
logger.info("Agenten-Orchestrator gestoppt")
|
||||
|
||||
async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual", user_id: int = None) -> bool:
|
||||
async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual", user_id: int = None, collect_only: bool = False) -> bool:
|
||||
"""Refresh-Auftrag in die Lane der Organisation stellen. Gibt False zurueck
|
||||
wenn die Lage dort bereits wartet oder gerade laeuft."""
|
||||
wenn die Lage dort bereits wartet oder gerade laeuft.
|
||||
|
||||
collect_only=True (Studio-Baustein "Sammeln"): sammelt nur Artikel, ueberspringt
|
||||
Analyse und Faktencheck.
|
||||
"""
|
||||
if incident_id in self._queued_ids or incident_id in self._current_tasks:
|
||||
logger.info(f"Refresh fuer Lage {incident_id} uebersprungen: bereits aktiv/in Queue")
|
||||
return False
|
||||
@@ -476,7 +480,7 @@ class AgentOrchestrator:
|
||||
self._lane_workers[lane_key] = asyncio.create_task(self._worker(lane_key, queue))
|
||||
logger.info(f"Neue Worker-Lane fuer Organisation {lane_key} gestartet")
|
||||
self._queued_ids.add(incident_id)
|
||||
queue.put_nowait((incident_id, trigger_type, user_id))
|
||||
queue.put_nowait((incident_id, trigger_type, user_id, collect_only))
|
||||
queue_size = queue.qsize()
|
||||
logger.info(f"Refresh fuer Lage {incident_id} eingereiht (Lane {lane_key}, Queue: {queue_size}, Trigger: {trigger_type})")
|
||||
|
||||
@@ -582,11 +586,15 @@ class AgentOrchestrator:
|
||||
return
|
||||
continue
|
||||
|
||||
if len(item) == 3:
|
||||
if len(item) == 4:
|
||||
incident_id, trigger_type, user_id, collect_only = item
|
||||
elif len(item) == 3:
|
||||
incident_id, trigger_type, user_id = item
|
||||
collect_only = False
|
||||
else:
|
||||
incident_id, trigger_type = item
|
||||
user_id = None
|
||||
collect_only = False
|
||||
self._queued_ids.discard(incident_id)
|
||||
# Session-Start EINMAL setzen — bleibt ueber Multi-Pass/Retry hinweg stabil
|
||||
self._current_tasks[incident_id] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
@@ -610,14 +618,14 @@ class AgentOrchestrator:
|
||||
try:
|
||||
# Research-Lagen: Automatisch 3 Durchläufe nur beim ersten Refresh
|
||||
incident_type, has_summary = await self._get_incident_info(incident_id)
|
||||
use_multi_pass = incident_type == "research" and not has_summary
|
||||
use_multi_pass = incident_type == "research" and not has_summary and not collect_only
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
if use_multi_pass:
|
||||
await self._run_research_multi_pass(incident_id, trigger_type=trigger_type, user_id=user_id)
|
||||
else:
|
||||
await self._run_refresh(incident_id, trigger_type=trigger_type, retry_count=attempt, user_id=user_id)
|
||||
await self._run_refresh(incident_id, trigger_type=trigger_type, retry_count=attempt, user_id=user_id, collect_only=collect_only)
|
||||
last_error = None
|
||||
break # Erfolg
|
||||
except asyncio.CancelledError:
|
||||
@@ -783,7 +791,7 @@ class AgentOrchestrator:
|
||||
await db.close()
|
||||
return visibility, created_by, tenant_id
|
||||
|
||||
async def _run_refresh(self, incident_id: int, trigger_type: str = "manual", retry_count: int = 0, user_id: int = None, _suppress_complete: bool = False, _pass_info: dict = None):
|
||||
async def _run_refresh(self, incident_id: int, trigger_type: str = "manual", retry_count: int = 0, user_id: int = None, _suppress_complete: bool = False, _pass_info: dict = None, collect_only: bool = False):
|
||||
"""Führt einen kompletten Refresh-Zyklus durch."""
|
||||
import aiosqlite
|
||||
from database import get_db
|
||||
@@ -1433,7 +1441,8 @@ class AgentOrchestrator:
|
||||
logger.warning(f"Quellen-Statistiken konnten nicht aktualisiert werden: {e}")
|
||||
|
||||
# Schritt 3+4: Analyse und Faktencheck PARALLEL
|
||||
if new_count > 0 or not previous_summary:
|
||||
# collect_only (Studio-Baustein "Sammeln"): ueberspringt Analyse/Faktencheck.
|
||||
if (new_count > 0 or not previous_summary) and not collect_only:
|
||||
is_first_summary = not previous_summary
|
||||
|
||||
# Snapshot des alten Lagebilds sichern BEVOR parallele Verarbeitung startet
|
||||
@@ -1775,9 +1784,14 @@ class AgentOrchestrator:
|
||||
pass
|
||||
sources_json = json.dumps(sources, ensure_ascii=False) if sources else previous_sources_json
|
||||
|
||||
# summary_at haelt fest, WANN das Lagebild entstand (Studio-Freshness).
|
||||
# JETZT stempeln, nicht 'now' vom Lauf-Beginn: sonst saehen die in diesem
|
||||
# Lauf gesammelten Artikel neuer aus als der Bericht, der sie schon enthaelt.
|
||||
summary_now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
|
||||
await db.execute(
|
||||
"UPDATE incidents SET summary = ?, sources_json = ?, executive_summary = NULL, updated_at = ? WHERE id = ?",
|
||||
(new_summary, sources_json, now, incident_id),
|
||||
"UPDATE incidents SET summary = ?, sources_json = ?, executive_summary = NULL, "
|
||||
"updated_at = ?, summary_at = ? WHERE id = ?",
|
||||
(new_summary, sources_json, summary_now, summary_now, incident_id),
|
||||
)
|
||||
|
||||
# Beim ersten Refresh: Snapshot des neuen Lagebilds erstellen
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren