core: Router bleibt bei Embedding-Ausfall lauffähig (Stufe 0) und erholt sich selbst
ci / compose (push) Failing after 3s
ci / test (push) Failing after 13m22s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 00:55:25 +02:00
co-authored by Claude Opus 5.5
parent f27c63b711
commit 96209d684d
3 changed files with 45 additions and 5 deletions
+2 -1
View File
@@ -71,7 +71,8 @@ def create_app(system: System | None = None, start_prober: bool | None = None) -
@app.get("/health")
def health() -> dict[str, Any]:
return {"status": "ok", "skills": len(system.library.all()), "dry_run": system.settings.dry_run}
return {"status": "ok", "skills": len(system.library.all()), "dry_run": system.settings.dry_run,
"embedding_index": "ok" if system.router.index_error is None else system.router.index_error}
@app.get("/metrics")
def metrics() -> Response:
+21 -4
View File
@@ -79,11 +79,18 @@ class Router:
self.config = config or RouterConfig()
self.rng = rng or random.Random()
self.index = SkillIndex(embedder)
self.index_error: str | None = None
self.refresh()
def refresh(self) -> None:
"""Index neu aufbauen (nach Skill-Änderungen)."""
self.index.rebuild({s.name: s.index_texts() for s in self.library.all()})
def refresh(self) -> bool:
"""Index neu aufbauen (nach Skill-Änderungen). Fällt das Embedding aus, bleibt der Service lauffähig."""
try:
self.index.rebuild({s.name: s.index_texts() for s in self.library.all()})
except Exception as exc: # noqa: BLE001 – Embedding-Endpunkt nicht erreichbar
self.index_error = f"{type(exc).__name__}: {exc}"
return False
self.index_error = None
return True
# --- Routing ----------------------------------------------------------------------------
def route(self, text: str, hint_skill: str | None = None) -> RouteDecision:
@@ -123,7 +130,17 @@ class Router:
reasons.append("Keine routbaren Skills")
signals.append(Signal.NO_SKILL)
return None, None
qvec = self.embedder.embed([text])[0]
if self.index_error is not None and not self.refresh():
reasons.append(f"Embedding nicht verfügbar ({self.index_error}) – Stufe 0")
signals.append(Signal.NO_SKILL)
return None, None
try:
qvec = self.embedder.embed([text])[0]
except Exception as exc: # noqa: BLE001
self.index_error = f"{type(exc).__name__}: {exc}"
reasons.append(f"Embedding nicht verfügbar ({self.index_error}) – Stufe 0")
signals.append(Signal.NO_SKILL)
return None, None
hits = self.index.search(qvec, k=self.config.top_k, only=routable)
if not hits:
signals.append(Signal.NO_SKILL)
+22
View File
@@ -131,3 +131,25 @@ def test_conflict_with_reranker_trusts_reranker(registry, library, embedder):
assert Signal.SKILL_CONFLICT in d.signals
assert "Reranking aktiv" in d.reasons
assert d.skill == "rechnung-extrahieren" and d.stage == 1
class FlakyEmbedder:
def __init__(self, inner):
self.inner = inner
self.down = True
def embed(self, texts):
if self.down:
raise ConnectionError("embed-default weg")
return self.inner.embed(texts)
def test_embedding_outage_degrades_to_strong_and_recovers(registry, library, embedder):
flaky = FlakyEmbedder(embedder)
r = Router(registry, library, flaky) # Start ohne Embedding darf nicht abstürzen
assert r.index_error
d = r.route(RECHNUNG)
assert d.stage == 0 and any("Embedding nicht verfügbar" in x for x in d.reasons)
flaky.down = False
d = r.route(RECHNUNG)
assert d.stage == 1 and r.index_error is None