Resilient LLM runner: model fallback chain, provider-death blacklist, cooldown, ANSI-repair
- llm.py: single retry/fallback/cooldown authority used by analyze/verify/pipeline - provider [404] blacklists model + switches; rate limit escalates then cooldown - unparsable model output (bad JSON/ANSI noise) skips to next model - verify.py: DS_VERIFY_TIMEOUT, true rate-limit handling, honest FAILED reporting - pipeline_all.py: honors model, reports verify status accurately - aggregate.py: skip epNNNN_corrected.json strays (was duplicating ep 304) - gitignore data outputs; drop stray analysis/analyst/analysts dirs
This commit is contained in:
@@ -5,6 +5,8 @@ Spots "strange assumptions" like attributing a 2023 cohost seat to Maddox.
|
||||
Usage:
|
||||
python verify.py # verify ALL existing analyses
|
||||
python verify.py 361 373 # verify specific episodes
|
||||
|
||||
Retries, model fallback and provider cooldown live in llm.py (shared).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -18,12 +20,14 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from llm import run as llm_run
|
||||
from llm import RateLimitError, ProviderError
|
||||
from llm import VERIFY_TIMEOUT
|
||||
from config import BASE_DIR
|
||||
from feed import load_episodes
|
||||
from download import slug as ep_slug
|
||||
from facts import DOSSIER_PATH, CAST_SHEET
|
||||
from analyze import (ANALYSIS_DIR, OPENCODE, run_opencode, strip_to_json,
|
||||
validate)
|
||||
from analyze import (ANALYSIS_DIR, OPENCODE, strip_to_json, validate)
|
||||
from export_txt import txt_path
|
||||
|
||||
VERIFY_PROMPT = """You are the fact-checking archivist for The Dick Show project. Three files are attached:
|
||||
@@ -77,72 +81,54 @@ def verify_episode(ep: dict, force: bool = False) -> dict | None:
|
||||
return None
|
||||
|
||||
prompt = build_verify_prompt()
|
||||
from analyze import RateLimitError
|
||||
waits = [30, 120, 300, 600]
|
||||
attempt = 0
|
||||
last_err = None
|
||||
while attempt < len(waits):
|
||||
attempt += 1
|
||||
try:
|
||||
raw = run_opencode_multi(prompt, [attach_txt, DOSSIER_PATH, apath])
|
||||
verdict = strip_to_json(raw)
|
||||
v = verdict.get("verdict")
|
||||
if v not in ("pass", "fix"):
|
||||
raise ValueError(f"bad verdict: {v!r}")
|
||||
if v == "pass":
|
||||
current.setdefault("_verification", {
|
||||
"verdict": "pass", "checked_at": _now(), "issues": []})
|
||||
_save(apath, current)
|
||||
print(f" verified OK (no issues)", flush=True)
|
||||
return current
|
||||
fixed = verdict.get("corrected_analysis")
|
||||
|
||||
def parse_verdict(raw: str) -> dict:
|
||||
v = strip_to_json(raw)
|
||||
verdict = v.get("verdict")
|
||||
if verdict not in ("pass", "fix"):
|
||||
raise ValueError(f"bad verdict: {verdict!r}")
|
||||
if verdict == "fix":
|
||||
fixed = v.get("corrected_analysis")
|
||||
if not isinstance(fixed, dict):
|
||||
raise ValueError("verdict=fix but no corrected_analysis")
|
||||
validate(fixed)
|
||||
issues = verdict.get("issues", [])
|
||||
fixed["_verification"] = {"verdict": "fix", "checked_at": _now(),
|
||||
"issues": issues}
|
||||
fixed["ep_num"] = ep["ep_num"] if ep["ep_num"] else fixed.get("ep_num")
|
||||
_save(apath, fixed)
|
||||
print(f" FIXED {len(issues)} issue(s):", flush=True)
|
||||
for i in issues[:6]:
|
||||
print(f" - [{i.get('field')}] {str(i.get('issue'))[:90]}",
|
||||
flush=True)
|
||||
return fixed
|
||||
except RateLimitError as e:
|
||||
last_err = e
|
||||
wait = waits[min(attempt, len(waits) - 1)]
|
||||
print(f" verify rate-limited (attempt {attempt}); "
|
||||
f"sleeping {wait}s ...", flush=True)
|
||||
time.sleep(wait)
|
||||
except Exception as e: # noqa: BLE001
|
||||
last_err = e
|
||||
wait = waits[min(attempt - 1, len(waits) - 1)]
|
||||
print(f" verify attempt {attempt} failed: {e}; "
|
||||
f"retry in {wait}s", flush=True)
|
||||
time.sleep(wait)
|
||||
print(f" verification FAILED for {ep['title']}: {last_err}", flush=True)
|
||||
return None
|
||||
return v
|
||||
|
||||
from datetime import datetime, timezone as _tz
|
||||
try:
|
||||
verdict = llm_run(prompt, [attach_txt, DOSSIER_PATH, apath],
|
||||
timeout=VERIFY_TIMEOUT, label="verify",
|
||||
parse=parse_verdict)
|
||||
except (RateLimitError, ProviderError) as e:
|
||||
print(f" verification NOT RUN for {ep['title']}: {e}", flush=True)
|
||||
return None
|
||||
|
||||
v = verdict.get("verdict")
|
||||
if v == "pass":
|
||||
current.setdefault("_verification", {
|
||||
"verdict": "pass",
|
||||
"checked_at": datetime.now(_tz.utc).isoformat(), "issues": []})
|
||||
_save(apath, current)
|
||||
print(f" verified OK (no issues)", flush=True)
|
||||
return current
|
||||
fixed = verdict["corrected_analysis"]
|
||||
issues = verdict.get("issues", [])
|
||||
fixed["_verification"] = {"verdict": "fix",
|
||||
"checked_at": datetime.now(_tz.utc).isoformat(),
|
||||
"issues": issues}
|
||||
fixed["ep_num"] = ep["ep_num"] if ep["ep_num"] else fixed.get("ep_num")
|
||||
_save(apath, fixed)
|
||||
print(f" FIXED {len(issues)} issue(s):", flush=True)
|
||||
for i in issues[:6]:
|
||||
print(f" - [{i.get('field')}] {str(i.get('issue'))[:90]}",
|
||||
flush=True)
|
||||
return fixed
|
||||
|
||||
|
||||
def run_opencode_multi(prompt: str, attachments: list[str]) -> str:
|
||||
from analyze import OPENCODE_MODEL
|
||||
cmd = [OPENCODE, "run", "--auto"]
|
||||
for a in attachments:
|
||||
cmd += ["-f", a]
|
||||
cmd += ["--dir", BASE_DIR]
|
||||
if OPENCODE_MODEL:
|
||||
cmd += ["-m", OPENCODE_MODEL]
|
||||
cmd.append(prompt)
|
||||
import subprocess
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace",
|
||||
cwd=BASE_DIR, timeout=900)
|
||||
out = proc.stdout or ""
|
||||
if not out.strip() and proc.stderr:
|
||||
raise RuntimeError(f"opencode produced no stdout: "
|
||||
f"{proc.stderr[-400:]}")
|
||||
return out
|
||||
# compat shim: raw output only, JSON parsed by the caller
|
||||
return llm_run(prompt, attachments, timeout=VERIFY_TIMEOUT,
|
||||
label="verify")
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
@@ -174,4 +160,4 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in New Issue
Block a user