feat(eu-umbau): staan-Recherche-Schleife und Doppelspur (Phase 2)

Im EU-Modus laeuft die Recherche jetzt europaeisch: agents/eu_researcher.py
steuert eine Schleife aus Bedrock-Planung (Opus, EU-Profile) und staan-Suchen
(services/staan_client.py, Suche + Volltexte via full_content=markdown,
Pflicht-Domain-Ausschlussliste je Anfrage). Bildet die 4-Phasen-Tiefenrecherche
nach und liefert exakt das JSON der bisherigen CLI-WebSearch, Parsing und
Filter in researcher.search bleiben unveraendert. Anti-Halluzination: nur URLs
aus echten staan-Treffern werden akzeptiert; published_at nur wenn aus Inhalt
oder URL ableitbar (staan liefert keine Daten).

Doppelspur im Orchestrator: Lane-Schluessel ist jetzt (Organisation, Backend),
Aufloesung zentral in _resolve_ai_backend (Lage > Org-Setting > ENV). CLI- und
EU-Lauf derselben Organisation laufen gleichzeitig, gleiche Backends seriell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-30 20:12:31 +00:00
Ursprung e0edac2a18
Commit a7f1aa29ab
6 geänderte Dateien mit 589 neuen und 20 gelöschten Zeilen

Datei anzeigen

@@ -470,7 +470,12 @@ class AgentOrchestrator:
return False
visibility, created_by, tenant_id = await self._get_incident_visibility(incident_id)
lane_key = tenant_id if tenant_id else self.PUBLIC_LANE
# Doppelspur (EU-Umbau Phase 2). Eine Lane je Organisation UND KI-Weg.
# Ein CLI-Lauf und ein EU-Lauf derselben Organisation laufen dadurch
# wirklich gleichzeitig (getrennte Ressourcen, Abo gegen AWS), während
# Läufe über denselben Weg seriell bleiben (Rate-Limit-Schutz).
ai_backend = await self._resolve_ai_backend(incident_id)
lane_key = (tenant_id if tenant_id else self.PUBLIC_LANE, ai_backend)
async with self._lanes_lock:
queue = self._lanes.get(lane_key)
@@ -792,6 +797,35 @@ class AgentOrchestrator:
await db.close()
return visibility, created_by, tenant_id
async def _resolve_ai_backend(self, incident_id: int) -> str:
"""Löst das KI-Backend einer Lage auf (EU-Umbau).
Lage (incidents.ai_backend) gewinnt vor dem Org-Setting 'ai_backend',
das vor dem globalen Default AI_BACKEND aus config. Wird beim Einreihen
(Lane-Wahl) und beim Refresh-Start (ContextVar) identisch genutzt."""
from config import AI_BACKEND
from database import get_db
from services.org_settings import get_org_setting
incident_backend = None
tenant_id = None
db = await get_db()
try:
cursor = await db.execute(
"SELECT ai_backend, tenant_id FROM incidents WHERE id = ?", (incident_id,)
)
row = await cursor.fetchone()
if row:
incident_backend = row["ai_backend"] if "ai_backend" in row.keys() else None
tenant_id = row["tenant_id"] if "tenant_id" in row.keys() else None
org_backend = await get_org_setting(db, tenant_id, "ai_backend", default=None) if tenant_id else None
finally:
await db.close()
ai_backend = (incident_backend or org_backend or AI_BACKEND or "cli").strip().lower()
if ai_backend not in ("cli", "bedrock"):
logger.warning("Unbekanntes ai_backend '%s' für Lage %s, fallback 'cli'", ai_backend, incident_id)
ai_backend = "cli"
return ai_backend
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
@@ -829,18 +863,11 @@ class AgentOrchestrator:
from services.org_settings import (
get_org_language, language_display, get_research_language,
get_source_language_whitelist, get_translator_enabled,
get_org_setting,
)
# KI-Backend für diesen Refresh auflösen (EU-Umbau Phase 1).
# Lage gewinnt vor Organisation, die vor dem globalen Default.
# Gilt über das ContextVar für alle call_claude-Aufrufe darunter.
from config import AI_BACKEND
incident_backend = incident["ai_backend"] if "ai_backend" in incident.keys() else None
org_backend = await get_org_setting(db, tenant_id, "ai_backend", default=None) if tenant_id else None
ai_backend = (incident_backend or org_backend or AI_BACKEND or "cli").strip().lower()
if ai_backend not in ("cli", "bedrock"):
logger.warning("Unbekanntes ai_backend '%s' für Lage %s, fallback 'cli'", ai_backend, incident_id)
ai_backend = "cli"
# KI-Backend für diesen Refresh auflösen und per ContextVar setzen
# (EU-Umbau, gemeinsame Auflösung mit der Lane-Wahl beim Einreihen,
# siehe _resolve_ai_backend).
ai_backend = await self._resolve_ai_backend(incident_id)
_ai_backend_var.set(ai_backend)
if ai_backend != "cli":
logger.info("Lage %s läuft über KI-Backend '%s'", incident_id, ai_backend)