This commit is contained in:
Team3
2026-07-05 15:26:22 +02:00
parent 07e14fb82e
commit 250ea0b764
45 changed files with 1468 additions and 1452 deletions

View File

@@ -9,14 +9,11 @@ import asyncio
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable
from agents import run_agent, kill_process, cancel_scope, clear_scope
from config import MAX_CONCURRENT_GENERATIONS, TEMPLATES_DIR, TIMEOUTS
from database import update_guide
from jsonio import read_json_file as _json_file
from textkit import _STUFEN
log = logging.getLogger("creator.pipeline")
@@ -132,7 +129,6 @@ def _runde_schema(data, final: bool = False):
return include, rest
_RELEVANCE = ("relevant", "peripheral")
_YESNO = ("ja", "nein")
@@ -159,23 +155,13 @@ def _enum_map_schema(key: str, allowed):
return parse
_levels_schema = _enum_map_schema("levels", _STUFEN) # level ∈ beginner/advanced/expert
_relevance_schema = _enum_map_schema("relevance", _RELEVANCE) # relevance ∈ relevant/peripheral
_yesno_schema = _enum_map_schema("relevant", _YESNO) # triage gate ∈ ja/nein
from config import MAX_RESTARTS as _MAX_RESTARTS, HEDGE_NACH_S as _HEDGE_NACH_S # noqa: E402 — zentral tunebar
# Detached Nachzügler-Tasks (late-Fold): Referenz gegen GC, Aufräumen via done-callback.
_NACHZUEGLER: set[asyncio.Task] = set()
def _detached(task: asyncio.Task) -> None:
_NACHZUEGLER.add(task)
task.add_done_callback(_NACHZUEGLER.discard)
async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout: int, provider: str, on_update=None, cancelled=None, *, grace: int | None = None, min_runtime: int | None = None, max_runtime: int | None = None, late=None) -> list | None:
async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout: int, provider: str, cancelled=None, *, grace: int | None = None) -> list | None:
"""Starts all slots in parallel and collects `quorum` valid results.
Slot spec: {key, prompt, role, capabilities, payload}. `payload(result)`
@@ -188,16 +174,6 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
a timer of `grace` seconds. After it expires, running agents are only
killed if the minimum stands — otherwise the race, including restarts,
keeps running until it stands. Returns: `quorum` to `len(slots)` results.
`min_runtime` (wall-clock from start): the race does not return before it
elapses while agents are still running — gives them time to search thoroughly.
`max_runtime` (wall-clock from start): hard cap — returns whatever is collected
(or None if nothing), killing the rest. Both default off; only Research sets them.
`late(value)` (async): Nachzügler werden beim Quorum-Return NICHT gekillt, sondern
laufen detached weiter; jedes noch eintreffende valide Ergebnis geht an `late`.
Ersetzt den grace-Timer der Finder-Runden — der hielt die Runde bis 300 s offen,
nur damit die dritte Stimme zählt (gemessen: 73 s Warten pro Runde).
"""
attempts = {i: 0 for i in range(len(slots))}
tasks: dict[asyncio.Task, int] = {}
@@ -209,9 +185,6 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
# hedgten jeden gesunden langen Call — z. B. Guide-Fixes, die normal 110135 s laufen.
hedge_s = max(_HEDGE_NACH_S, timeout / 2) if _HEDGE_NACH_S else 0
loop = asyncio.get_running_loop()
start = loop.time()
min_deadline = start + min_runtime if min_runtime else None
max_deadline = start + max_runtime if max_runtime else None
deadline: float | None = None
def spawn(i: int, suffix: str = "") -> None:
@@ -227,29 +200,6 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
keys[task] = key
born[task] = loop.time()
spaet: set[int] = set() # je Slot zählt nur EIN spätes Ergebnis (Hedge-Zwilling = Echo)
def _detach_rest() -> None:
"""Quorum steht: Nachzügler an `late` übergeben statt killen (nur Erfolgs-Return)."""
if late is None:
return
for t, i in list(tasks.items()):
tasks.pop(t)
keys.pop(t, None)
born.pop(t, None)
async def _warte(t=t, i=i):
try:
r = await t
if i in spaet:
return
if r and r[0] == 0 and (val := slots[i]["payload"](r)) is not None:
spaet.add(i)
await late(val)
except (asyncio.CancelledError, Exception): # noqa: BLE001 — Nachzügler sind best-effort
pass
_detached(asyncio.create_task(_warte()))
for i in range(len(slots)):
spawn(i)
@@ -258,13 +208,7 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
while tasks:
if cancelled and cancelled():
return None
# Hard wall-clock cap: return whatever we have (None if empty), kill the rest.
if max_deadline is not None and loop.time() >= max_deadline:
_log(topic, f"{label}: max runtime {max_runtime}s reached ({len(results)} valid)")
return results or None
min_ok = min_deadline is None or loop.time() >= min_deadline
if deadline is not None and len(results) >= quorum and loop.time() >= deadline and min_ok:
_detach_rest()
if deadline is not None and len(results) >= quorum and loop.time() >= deadline:
return results
# Hedge: a slot running HEDGE_NACH_S without result gets ONE parallel twin
# (key -h) — first valid result wins. Stalled provider calls burned the full
@@ -277,14 +221,10 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
hedged.add(i)
spawn(i, suffix="-h")
_log(topic, f"{label} {i + 1}: {round(hedge_s)}s ohne Ergebnis — Hedge-Zwilling gestartet")
# Wake up for the earliest relevant deadline (grace, min, max, or next hedge).
# Wake up for the earliest relevant deadline (grace or next hedge).
waits = []
if deadline is not None and len(results) >= quorum:
waits.append(deadline - loop.time())
if min_deadline is not None:
waits.append(min_deadline - loop.time())
if max_deadline is not None:
waits.append(max_deadline - loop.time())
if hedge_s:
naechste = [born[t] + hedge_s - loop.time() for t in tasks
if tasks[t] not in hedged | fertig]
@@ -323,11 +263,7 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
if grace is not None and deadline is None:
deadline = loop.time() + grace
_log(topic, f"{label}: first result — grace {grace}s running")
if on_update:
on_update(len(results))
if (len(results) >= quorum and (grace is None or loop.time() >= deadline)
and (min_deadline is None or loop.time() >= min_deadline)):
_detach_rest()
if len(results) >= quorum and (grace is None or loop.time() >= deadline):
return results
continue
@@ -340,7 +276,6 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
if attempts[i] <= _MAX_RESTARTS and not enough and not zwilling and not (cancelled and cancelled()):
spawn(i)
if len(results) >= quorum: # all slots done, minimum stands (only reachable with grace)
_detach_rest()
return results
_log(topic, f"{label}: quorum {quorum} not reached ({len(results)} valid)")
return None