update
This commit is contained in:
@@ -155,11 +155,6 @@ QUELLE_RELEVANZ_SNIPPET = 800 # body characters per page in the prompt (URL i
|
||||
QA_GATE_NOTE = 9.5 # 0 = gate off; quota-based, so the tolerated finding count scales with topic size
|
||||
QA_GATE_LLM = True # include the LLM samples (Echtheit/Dubletten) in the gate run
|
||||
|
||||
# Guide section length per relevant sub (ausführlich part) — QA detector AND the
|
||||
# deterministic readability-stage trigger share these bounds (writers overshot 2.7–4.1×).
|
||||
GUIDE_LAENGE_MIN = 150
|
||||
GUIDE_LAENGE_MAX = 1200
|
||||
|
||||
# Inline evidence for judge agents: corpus excerpts go INTO the prompt instead of letting
|
||||
# every judge re-search the source folder (measured: ~10 tool turns/judge, 82 % of the
|
||||
# run's tokens were cache reads from those loops).
|
||||
@@ -177,10 +172,6 @@ RESEARCH_READERS = 2 # reader agents per batch/section (consensus ≥2)
|
||||
RESEARCH_THEMA_AGENTS = 5 # web mode (source "thema")
|
||||
RESEARCH_SECTION_CHARS = 12000 # uni/projekt section size (lost-in-the-middle guard)
|
||||
RESEARCH_RUNTIME = 900 # one research agent, one round (tail ingests live)
|
||||
SUBBLOCK_CAP = 900 # subblock find loop per chunk (seconds)
|
||||
SUBBLOCK_MIN = 5 # below this consensus count → focused catch-up rounds
|
||||
SUBBLOCK_EXTRA_ROUNDS = 2 # max catch-up rounds
|
||||
SUBBLOCK_MAX_ROUNDS = 3 # hard round cap (rounds 4–5 burned 29 % of finders for ~0 gain)
|
||||
CONSOLIDATION_CHUNK = 600 # up to here ONE global judge (fallback path)
|
||||
DEDUP_PAIR_FLOOR = 0.6 # min cosine for a candidate pair
|
||||
DEDUP_PAIRS_CHUNK = 40 # pairs per judge package
|
||||
@@ -201,7 +192,7 @@ GEN_PANEL = 2 # unabhängige Generator-Calls pro Block
|
||||
VERIFY_PANEL = 2 # unabhängige Prüfer-Calls pro Block (+ Ersatz bei 1 Ausfall)
|
||||
ART_SPLIT_SUBS = 20 # Artefakt-Generator splittet ab so vielen Subs in 2 parallele Calls
|
||||
FILTER_RECHECK_PANEL = 3 # judges in the survivor-recheck
|
||||
GATE_FIX_MIN = 3 # fact-gate: unbelegt-claims below this → log only (falsch fixt immer)
|
||||
GATE_FIX_MIN = 3 # fact-gate: unbelegt-claims below min(this, relevante Subs) → log only (falsch fixt immer)
|
||||
WRITER_SPLIT_SUBS = 30 # guide writer splits sections above this sub count
|
||||
KANBAN_BATCH = 5 # cards a worker pulls per micro-batch
|
||||
MAX_CARD_RETRIES = 3 # failures per card → dead-letter
|
||||
@@ -209,8 +200,9 @@ RETRY_BACKOFF = 30.0 # base seconds; backoff = base · 2^(retries-1)
|
||||
MAX_RESTARTS = 2 # agent restart cap per race slot
|
||||
# Stall-Hedge: läuft ein Race-Slot so lange ohne Ergebnis, startet parallel ein Zwilling
|
||||
# (key -h), der erste valide gewinnt. Gemessen (kanban-smoke): 4 Panel-Stalls à 160–230 s
|
||||
# verlängerten den kritischen Pfad um ~5 min — gesunde Judge-Calls liegen bei p90 ≤ 105 s.
|
||||
# 0 = aus.
|
||||
# verlängerten den kritischen Pfad um ~5 min. UNTERGRENZE: effektiv gilt
|
||||
# max(HEDGE_NACH_S, halbes Call-Timeout) — pauschale 90 s hedgten jeden gesunden
|
||||
# Fix-/Gate-Call (die laufen normal 110–135 s). 0 = aus.
|
||||
HEDGE_NACH_S = 90
|
||||
JUDGE_CHUNK = 40 # repair: findings per judge call
|
||||
EVIDENCE_PER_BLOCK = 6000 # repair: excerpt chars per fremd candidate
|
||||
|
||||
@@ -22,8 +22,9 @@ import re
|
||||
import database as db
|
||||
import readability
|
||||
from blocks import _sink_json
|
||||
from config import (FORMAT_PURPOSE, GUIDE_LAENGE_MAX, GUIDE_LAENGE_MIN, READABILITY_ACTIVE,
|
||||
from config import (FORMAT_PURPOSE, READABILITY_ACTIVE,
|
||||
TEMPLATES_DIR, MAX_CONCURRENT_AGENTS_PER_TOPIC)
|
||||
from guide_qa import block_budget
|
||||
from fsutil import atomic_write_json
|
||||
from jsonio import read_json_file as _json_file
|
||||
from pipeline import (CANCELLED, FAILED, OK, GenContext, _extra, _log, _prompt,
|
||||
@@ -274,12 +275,6 @@ def _merge_split_sections(sec_a: dict, sec_b: dict) -> str:
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
def _writer_budget(n_subs: int, sockel: int = 800) -> int:
|
||||
"""Length guideline (chars) for the detailed version — unguided sections measured 2–4×
|
||||
too long (23k) or, after the readability fix, far too thin (180 chars/sub)."""
|
||||
return sockel + 400 * max(n_subs, 1)
|
||||
|
||||
|
||||
async def _write_split(env: _Env, card: dict, ziele_text: str):
|
||||
"""First draft in two halves (parallel), merged into one section.
|
||||
→ merged text | None (failed) | False (cancelled)."""
|
||||
@@ -316,7 +311,7 @@ async def _write_split(env: _Env, card: dict, ziele_text: str):
|
||||
examples=await _card_examples(env, norm, parts[i],
|
||||
include_unmatched=(i == 0)),
|
||||
gaps="\n" + hints[i] + "\n",
|
||||
budget=_writer_budget(len(parts[i]), sockel=400),
|
||||
budget=block_budget(parts[i]),
|
||||
spec=env.spec, out_path=path, extra=_extra(env.instructions)),
|
||||
role="guide", capabilities="files", payload=_payload,
|
||||
timeout=_timeout("writer", 1))
|
||||
@@ -360,7 +355,7 @@ async def _stage_writer(env: _Env, card: dict) -> bool:
|
||||
facts=_card_facts(env, card["block"]),
|
||||
examples=await _card_examples(env, norm, env.subs_by_title.get(card["block"], [])),
|
||||
gaps="", spec=env.spec,
|
||||
budget=_writer_budget(len(env.subs_by_title.get(card["block"], []))),
|
||||
budget=block_budget(env.subs_by_title.get(card["block"], [])),
|
||||
out_path=path, extra=_extra(env.instructions)),
|
||||
role="guide", capabilities="files", payload=_payload,
|
||||
timeout=_timeout("writer", 1))
|
||||
@@ -373,22 +368,27 @@ async def _stage_writer(env: _Env, card: dict) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _n_rel(env: _Env, card: dict) -> int:
|
||||
return sum(1 for s in env.subs_by_title.get(card["block"], [])
|
||||
if s.get("relevance") != "peripheral")
|
||||
|
||||
|
||||
def _det_hinweise(env: _Env, card: dict, sec: dict) -> list[str]:
|
||||
"""Deterministische Befunde (extern geerdet, kein LLM): Readability-Modell +
|
||||
Längen-Rahmen — dieselbe Formel wie der QA-Detektor. Gehen direkt in den Fix
|
||||
Längenbudget — dieselbe Formel wie der QA-Detektor (guide_qa.block_budget), nur mit
|
||||
engerem Band, damit der Fix VOR der QA-Grenze greift. Gehen direkt in den Fix
|
||||
und als „nicht wiederholen"-Notiz in den Prüfer-Prompt."""
|
||||
out: list[str] = []
|
||||
subs_all = env.subs_by_title.get(card["block"], [])
|
||||
n_rel = max(sum(1 for s in subs_all if s.get("relevance") != "peripheral"), 1)
|
||||
if not any(s.get("relevance") != "peripheral" for s in subs_all):
|
||||
return out # kein Inventar als Budget-Basis → kein Längen-Urteil (wie der QA-Detektor)
|
||||
budget = block_budget(subs_all)
|
||||
aus = re.split(r"<!--\s*ausführlich\s*-->", sec["md"], maxsplit=1)
|
||||
pro_sub = len(aus[1] if len(aus) == 2 else sec["md"]) / n_rel
|
||||
if not (GUIDE_LAENGE_MIN <= pro_sub <= GUIDE_LAENGE_MAX * 0.9):
|
||||
# Ziel nach der QA-Formel (Zeichen ÷ relevante Subs ≤ MAX), nicht dem Writer-Budget —
|
||||
# die zwei Formeln liefen auseinander und der Fix verfehlte den QA-Rahmen
|
||||
ziel = n_rel * round(GUIDE_LAENGE_MAX * 0.75)
|
||||
zeichen = len(aus[1] if len(aus) == 2 else sec["md"])
|
||||
if not (0.5 * budget <= zeichen <= 1.2 * budget):
|
||||
out.append(
|
||||
f"Länge {round(pro_sub)} Zeichen/Sub (Rahmen {GUIDE_LAENGE_MIN}–{round(GUIDE_LAENGE_MAX * 0.9)}): "
|
||||
f"schreibe den ausführlich-Teil auf etwa {ziel} Zeichen GESAMT um — Sockel-Prosa und "
|
||||
f"Länge {zeichen} Zeichen (Budget {budget}, erlaubt {round(0.5 * budget)}–{round(1.2 * budget)}): "
|
||||
f"schreibe den ausführlich-Teil auf etwa {budget} Zeichen GESAMT um — Sockel-Prosa und "
|
||||
f"Wiederholungen streichen, alle Sub-Marker und Beispiele behalten")
|
||||
return out
|
||||
|
||||
@@ -400,13 +400,15 @@ async def _det_readability(sec: dict) -> list[str]:
|
||||
return [hints[1]] if hints.get(1) else []
|
||||
|
||||
|
||||
def _auftraege(verdict: dict, det: list[str]) -> tuple[list[str], bool]:
|
||||
def _auftraege(verdict: dict, det: list[str], n_rel: int = 0) -> tuple[list[str], bool]:
|
||||
"""Prüfer-Verdikt → Fix-Auftragszeilen. kritisch = falsch-Claims oder Lücken
|
||||
(nur die rechtfertigen den Re-Prüfer-Pass — Fakten/Coverage sind der Qualitätskern).
|
||||
Claims-Schwelle wie beim alten Gate: 1–2 nur-„unbelegt" lohnen keinen Fix-Pass."""
|
||||
Claims-Schwelle: wenige nur-„unbelegt" lohnen keinen Fix-Pass — bei kleinen Sektionen
|
||||
sinkt sie auf die Sub-Zahl (2 unbelegte Claims in 2 Subs sind viel, nicht wenig)."""
|
||||
claims = verdict["claims"]
|
||||
falsch = [c for c in claims if c["urteil"] == "falsch"]
|
||||
if claims and not falsch and len(claims) < GATE_FIX_MIN:
|
||||
schwelle = min(GATE_FIX_MIN, n_rel) if n_rel else GATE_FIX_MIN
|
||||
if claims and not falsch and len(claims) < schwelle:
|
||||
claims = []
|
||||
zeilen = [f"- CLAIM ({c['urteil']}): {c['text']}" + (f" — {c['grund']}" if c['grund'] else "")
|
||||
for c in claims]
|
||||
@@ -465,7 +467,7 @@ async def _stage_pruefer(env: _Env, card: dict) -> bool:
|
||||
# gehen trotzdem in den Fix
|
||||
_log(env.topic, f"Prüfer {card['block']}: kein Ergebnis — nur deterministische Checks")
|
||||
verdict = {"claims": [], "ziele": {}, "luecken": [], "ballast": [], "lese_probleme": []}
|
||||
zeilen, kritisch = _auftraege(verdict, det)
|
||||
zeilen, kritisch = _auftraege(verdict, det, _n_rel(env, card))
|
||||
if not zeilen:
|
||||
await _set(env, card, md=card["md"], stage="done", status="ok", gate_info="")
|
||||
return True
|
||||
@@ -524,7 +526,7 @@ async def _stage_fix(env: _Env, card: dict) -> bool:
|
||||
if verdict is None and is_guide_cancelled(env.guide_id):
|
||||
return False
|
||||
if verdict:
|
||||
zeilen, _k = _auftraege(verdict, [])
|
||||
zeilen, _k = _auftraege(verdict, [], _n_rel(env, card))
|
||||
if zeilen:
|
||||
rest = "Rest-Befunde nach Fix:\n" + "\n".join(zeilen)
|
||||
_log(env.topic, f"Re-Prüfer {card['block']}: {len(zeilen)} Rest-Befund(e) bleiben")
|
||||
|
||||
@@ -10,6 +10,7 @@ Report: storage/qa/<topic>/guide-<ts>.json + Konsolen-Digest.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
@@ -20,8 +21,6 @@ import readability
|
||||
from fsutil import atomic_write_json
|
||||
from textkit import _norm_title
|
||||
|
||||
from config import GUIDE_LAENGE_MAX as LAENGE_MAX, GUIDE_LAENGE_MIN as LAENGE_MIN
|
||||
|
||||
JACCARD_ABSATZ = 0.6 # Wort-Jaccard, ab dem zwei Absätze als Doppel gelten
|
||||
ABSATZ_MIN_CHARS = 200 # kürzere Absätze sind Übergänge — kein Dubletten-Signal
|
||||
LLM_SECTION_CHARS = 2500 # Section-Auszug je Judge-Item
|
||||
@@ -71,13 +70,40 @@ def ziel_ohne_anker(cards: list[dict], ziele: list[dict]) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def laengen_ausreisser(cards: list[dict], subs_rel: dict[str, set]) -> list[dict]:
|
||||
# Längenbudget je Sub aus der Inventar-Substanz — ersetzt den festen Rahmen 150–1200/Sub:
|
||||
# ein dichter Sub (viele key_points, Fakten, Beispiel) trägt mehr Text als ein Einzeiler.
|
||||
# Die Pipeline (Writer-Vorgabe, Prüfer-Trigger, Fix-Ziel) nutzt DIESELBE Formel mit engerem
|
||||
# Band — Messlatte und Fix-Auftrag müssen übereinstimmen, sonst sind Befunde unfixbar.
|
||||
BUDGET_BASIS = 200 # Einstieg/Übergang je Sub
|
||||
BUDGET_KEY_POINT = 160 # ~1–2 Sätze Erklärung je key_point
|
||||
BUDGET_FAKT = 60 # zitierter Fakt, in den Text eingewoben
|
||||
BUDGET_BEISPIEL = 250 # ausgearbeitetes Beispiel
|
||||
LAENGE_BAND = (0.35, 1.5) # QA-Toleranz um das Blockbudget
|
||||
|
||||
|
||||
def sub_budget(facts: dict) -> int:
|
||||
"""Zeichenbudget für den ausführlich-Teil EINES Subs (facts = Inventar-JSON des Subs)."""
|
||||
kp = len(facts.get("key_points") or [])
|
||||
cf = len(facts.get("cited_facts") or [])
|
||||
ex = 1 if str(facts.get("example_idea") or "").strip() else 0
|
||||
return BUDGET_BASIS + BUDGET_KEY_POINT * kp + BUDGET_FAKT * cf + BUDGET_BEISPIEL * ex
|
||||
|
||||
|
||||
def block_budget(subs: list[dict]) -> int:
|
||||
"""Budget einer Section: Summe über die relevanten Subs ({relevance, facts}-Dicts)."""
|
||||
return max(BUDGET_BASIS, sum(sub_budget(s.get("facts") or {}) for s in subs
|
||||
if s.get("relevance") != "peripheral"))
|
||||
|
||||
|
||||
def laengen_ausreisser(cards: list[dict], budget_by_norm: dict[str, int]) -> list[dict]:
|
||||
out = []
|
||||
for c in cards:
|
||||
n = max(len(subs_rel.get(c["block_norm"], set())), 1)
|
||||
pro_sub = len(_ausfuehrlich(c["md"])) / n
|
||||
if not (LAENGE_MIN <= pro_sub <= LAENGE_MAX):
|
||||
out.append({"block": c["block"], "zeichen_pro_sub": round(pro_sub)})
|
||||
budget = budget_by_norm.get(c["block_norm"])
|
||||
if not budget:
|
||||
continue
|
||||
zeichen = len(_ausfuehrlich(c["md"]))
|
||||
if not (LAENGE_BAND[0] * budget <= zeichen <= LAENGE_BAND[1] * budget):
|
||||
out.append({"block": c["block"], "zeichen": zeichen, "budget": budget})
|
||||
return out
|
||||
|
||||
|
||||
@@ -148,14 +174,23 @@ async def guide_qa_report(topic: str, llm: bool = False) -> dict | None:
|
||||
print(f"Keine Guide-Karten für '{topic}' — Guide noch nicht gebaut?")
|
||||
return None
|
||||
subs_rel: dict[str, set] = {}
|
||||
subs_by_norm: dict[str, list[dict]] = {}
|
||||
for r in await db.list_subblocks(topic):
|
||||
if r["status"] == "consensus" and r["relevance"] != "peripheral":
|
||||
if r["status"] != "consensus":
|
||||
continue
|
||||
try:
|
||||
facts = json.loads(r["facts"]) if r["facts"] else {}
|
||||
except (ValueError, TypeError):
|
||||
facts = {}
|
||||
subs_by_norm.setdefault(r["block_norm"], []).append(
|
||||
{"relevance": r["relevance"], "facts": facts if isinstance(facts, dict) else {}})
|
||||
if r["relevance"] != "peripheral":
|
||||
subs_rel.setdefault(r["block_norm"], set()).add(r["sub_norm"])
|
||||
ziele = [dict(r) for r in await db.list_lernziele(topic)]
|
||||
|
||||
mf = marker_fehlend(cards, subs_rel)
|
||||
za = ziel_ohne_anker(cards, ziele)
|
||||
la = laengen_ausreisser(cards, subs_rel)
|
||||
la = laengen_ausreisser(cards, {n: block_budget(s) for n, s in subs_by_norm.items()})
|
||||
rd = redundanz(cards)
|
||||
lb = lesbarkeit(cards)
|
||||
falsch = await _fachlich_falsch(topic, cards) if llm else None
|
||||
|
||||
@@ -205,6 +205,9 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
|
||||
born: dict[asyncio.Task, float] = {}
|
||||
hedged: set[int] = set() # slot got its one twin — no hedge cascades
|
||||
fertig: set[int] = set() # slot delivered a valid result (late twins are ignored)
|
||||
# Hedge-Schwelle relativ zum Call-Timeout (HEDGE_NACH_S = Untergrenze): pauschale 90 s
|
||||
# hedgten jeden gesunden langen Call — z. B. Guide-Fixes, die normal 110–135 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
|
||||
@@ -266,14 +269,14 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
|
||||
# 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
|
||||
# timeout cap before the restart even began (measured: 160–230 s per stall).
|
||||
if _HEDGE_NACH_S:
|
||||
if hedge_s:
|
||||
now = loop.time()
|
||||
for t in [t for t in list(tasks) if tasks[t] not in hedged | fertig
|
||||
and now - born[t] >= _HEDGE_NACH_S]:
|
||||
and now - born[t] >= hedge_s]:
|
||||
i = tasks[t]
|
||||
hedged.add(i)
|
||||
spawn(i, suffix="-h")
|
||||
_log(topic, f"{label} {i + 1}: {_HEDGE_NACH_S}s ohne Ergebnis — Hedge-Zwilling gestartet")
|
||||
_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).
|
||||
waits = []
|
||||
if deadline is not None and len(results) >= quorum:
|
||||
@@ -282,8 +285,8 @@ async def _race(topic: str, label: str, slots: list[dict], quorum: int, timeout:
|
||||
waits.append(min_deadline - loop.time())
|
||||
if max_deadline is not None:
|
||||
waits.append(max_deadline - loop.time())
|
||||
if _HEDGE_NACH_S:
|
||||
naechste = [born[t] + _HEDGE_NACH_S - loop.time() for t in tasks
|
||||
if hedge_s:
|
||||
naechste = [born[t] + hedge_s - loop.time() for t in tasks
|
||||
if tasks[t] not in hedged | fertig]
|
||||
if naechste:
|
||||
waits.append(max(0.0, min(naechste)))
|
||||
|
||||
@@ -318,7 +318,7 @@ async def test_writer_prompt_traegt_budget(testdb, tmp_path, monkeypatch):
|
||||
|
||||
monkeypatch.setattr(gb, "run_single_slot", fake_slot)
|
||||
await gb._stage_writer(env, card)
|
||||
assert str(gb._writer_budget(2)) in seen["prompt"] # 800 + 2×400
|
||||
assert str(gb.block_budget(env.subs_by_title["Alpha"])) in seen["prompt"] # 2× Basis-Budget
|
||||
|
||||
|
||||
async def test_pruefer_claims_schwelle(testdb, tmp_path, monkeypatch):
|
||||
@@ -416,7 +416,8 @@ async def test_laengen_trigger_startet_fix(testdb, tmp_path, monkeypatch):
|
||||
assert (await db.list_guide_cards(TOPIC, FMT))[0]["stage"] == "fix"
|
||||
card.update(stage="fix", gate_info=(await db.list_guide_cards(TOPIC, FMT))[0]["gate_info"])
|
||||
assert await gb._stage_fix(env, card)
|
||||
assert "Länge" in seen["auftraege"] and str(round(gb.GUIDE_LAENGE_MAX * 0.75)) in seen["auftraege"]
|
||||
budget = gb.block_budget(env.subs_by_title["Alpha"])
|
||||
assert "Länge" in seen["auftraege"] and f"etwa {budget} Zeichen GESAMT" in seen["auftraege"]
|
||||
|
||||
|
||||
async def test_repair_karten_setzt_befundkarten_auf_pruefer(testdb, tmp_path, monkeypatch):
|
||||
|
||||
@@ -35,10 +35,24 @@ def test_ziel_ohne_anker():
|
||||
|
||||
|
||||
def test_laengen_ausreisser():
|
||||
"""Budget-Band statt Festrahmen: zu dünn und zu dick fallen auf, ohne Budget kein Urteil."""
|
||||
duenn = _card("Alpha", "<!-- ausführlich -->\nkurz")
|
||||
ok = _card("Beta", "<!-- ausführlich -->\n" + "x" * 500)
|
||||
out = gq.laengen_ausreisser([duenn, ok], {"alpha": {"s1"}, "beta": {"s1"}})
|
||||
assert [x["block"] for x in out] == ["Alpha"]
|
||||
dick = _card("Gamma", "<!-- ausführlich -->\n" + "x" * 2000)
|
||||
ohne = _card("Delta", "<!-- ausführlich -->\nkurz")
|
||||
budgets = {"alpha": 500, "beta": 500, "gamma": 500} # delta: kein Inventar → übersprungen
|
||||
out = gq.laengen_ausreisser([duenn, ok, dick, ohne], budgets)
|
||||
assert [x["block"] for x in out] == ["Alpha", "Gamma"]
|
||||
assert out[1]["zeichen"] >= 2000 and out[1]["budget"] == 500
|
||||
|
||||
|
||||
def test_budget_aus_substanz():
|
||||
"""Dichte Subs bekommen mehr Budget; periphere zählen nicht."""
|
||||
dicht = {"relevance": "relevant", "facts": {"key_points": ["a", "b"], "cited_facts": [{}], "example_idea": "x"}}
|
||||
duenn = {"relevance": "relevant", "facts": {}}
|
||||
peripher = {"relevance": "peripheral", "facts": {"key_points": ["a"] * 9}}
|
||||
assert gq.sub_budget(dicht["facts"]) > gq.sub_budget(duenn["facts"])
|
||||
assert gq.block_budget([dicht, duenn, peripher]) == gq.block_budget([dicht, duenn])
|
||||
|
||||
|
||||
def test_redundanz_findet_absatz_doppel():
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_hedge_zwilling_rettet_stall(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||||
monkeypatch.setattr(pipeline, "kill_process", lambda k: killed.append(k))
|
||||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 0.1, "claude")
|
||||
assert res == ["zwilling"]
|
||||
assert calls == ["k1", "k1-h"]
|
||||
assert "k1" in killed # das hängende Original läuft nicht weiter
|
||||
@@ -43,7 +43,7 @@ async def test_hedge_original_gewinnt_zwilling_wird_gekillt(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||||
monkeypatch.setattr(pipeline, "kill_process", lambda k: killed.append(k))
|
||||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 0.1, "claude")
|
||||
assert res == ["k1"]
|
||||
assert "k1-h" in killed
|
||||
|
||||
@@ -59,7 +59,25 @@ async def test_hedge_aus_bei_null(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0)
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 0.1, "claude")
|
||||
assert res == ["ok"]
|
||||
assert calls == ["k1"]
|
||||
|
||||
|
||||
async def test_hedge_schwelle_skaliert_mit_timeout(monkeypatch):
|
||||
"""Die Schwelle ist max(HEDGE_NACH_S, timeout/2): ein gesunder Call, der länger als
|
||||
die Untergrenze, aber kürzer als das halbe Timeout läuft, bekommt KEINEN Zwilling
|
||||
(pauschale 90 s hedgten jeden normalen Fix-Call)."""
|
||||
calls = []
|
||||
|
||||
async def fake_agent(key, prompt, timeout, **kw):
|
||||
calls.append(key)
|
||||
await asyncio.sleep(0.15) # > Untergrenze 0.05, < timeout/2 = 0.5
|
||||
return (0, "ok", "")
|
||||
|
||||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 1.0, "claude")
|
||||
assert res == ["ok"]
|
||||
assert calls == ["k1"]
|
||||
|
||||
@@ -133,6 +151,6 @@ async def test_hedge_zwilling_ersetzt_restart(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "run_agent", fake_agent)
|
||||
monkeypatch.setattr(pipeline, "kill_process", lambda k: None)
|
||||
monkeypatch.setattr(pipeline, "_HEDGE_NACH_S", 0.05)
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 60, "claude")
|
||||
res = await pipeline._race("t", "Test", [_slot()], 1, 0.1, "claude")
|
||||
assert res == ["zwilling"]
|
||||
assert calls == ["k1", "k1-h"] # kein dritter Spawn
|
||||
|
||||
Reference in New Issue
Block a user