| """ |
| AI RUNNER ÚNICO — executa receitas YAML de QUALQUER família de IA e produz |
| saída padronizada: output/ai_runs/<stamp>_<nome>/{resultados.json, ANALISE_IA.md, ledger.sha256} |
| |
| python3 scripts/ai_runner.py --receita data/receitas_ia/llm_cego.yaml |
| python3 scripts/ai_runner.py --receita ... --dry # mostra passos sem rodar |
| |
| Princípios: zero-mock (passos reais, saída hashada) · hardware no meta · |
| falha de passo aborta a receita e registra no JSON. |
| """ |
|
|
| import argparse |
| import datetime |
| import hashlib |
| import json |
| import os |
| import platform |
| import subprocess |
| import sys |
| from pathlib import Path |
|
|
| ROOT = Path(__file__).resolve().parent.parent |
| SAIDA_RAIZ = ROOT / "output" / "ai_runs" |
|
|
|
|
| def sha256_file(p): |
| h = hashlib.sha256() |
| with open(p, "rb") as f: |
| for chunk in iter(lambda: f.read(1 << 20), b""): |
| h.update(chunk) |
| return h.hexdigest() |
|
|
|
|
| def deep_get(d, caminho): |
| cur = d |
| for parte in caminho.split("."): |
| if not isinstance(cur, dict): |
| return None |
| cur = cur.get(parte) |
| return cur |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--receita", required=True) |
| ap.add_argument("--hardware", default=None, |
| help="cpu_local | gpu_vast (default: detecta pelo local do runner)") |
| ap.add_argument("--dry", action="store_true") |
| args = ap.parse_args() |
|
|
| try: |
| import yaml |
| except ImportError: |
| sys.exit("[ERRO] pip install pyyaml") |
|
|
| rec = yaml.safe_load(Path(args.receita).read_text(encoding="utf-8")) |
| stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S") |
| hardware = args.hardware or ("gpu_vast" if os.environ.get("VAST_INSTANCE") else "cpu_local") |
| run_dir = SAIDA_RAIZ / f"{stamp}_{rec['nome']}_{hardware}" |
| run_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print("=" * 76) |
| print(f"🧪 RECEITA {rec['nome']} ({rec['familia']}) · hardware={hardware}") |
| print(f"Artefatos: {run_dir}") |
| print("=" * 76) |
|
|
| resultados = { |
| "receita": rec["nome"], "familia": rec["familia"], |
| "versao": str(rec.get("versao", "1")), "hardware": hardware, |
| "host_fingerprint": platform.node(), |
| "timestamp_utc": stamp, "passos": {}, "erro": None, |
| } |
| for passo in rec.get("passos", []): |
| pid = passo["id"] |
| cmd = " ".join(passo["cmd"].split()) |
| print(f"\n──[{pid}] {passo.get('desc','')}\n $ {cmd}") |
| if args.dry: |
| continue |
| proc = subprocess.run(cmd, shell=True, cwd=ROOT, |
| capture_output=True, text=True, timeout=60 * 60 * 12) |
| (run_dir / f"{pid}.log").write_text(proc.stdout[-20000:] + "\n|ERR|\n" + proc.stderr[-5000:]) |
| if proc.returncode != 0: |
| resultados["erro"] = f"passo {pid} falhou (rc={proc.returncode})" |
| print(f" ✗ ABORTADO — ver {run_dir}/{pid}.log") |
| break |
| saidas = {} |
| for glob_s in passo.get("saida_glob", []): |
| import glob as _g |
| hits = sorted(_g.glob(str(ROOT / glob_s))) |
| if hits: |
| mais_novo = max(hits, key=os.path.getmtime) |
| alvo = run_dir / Path(mais_novo).name |
| alvo.write_text(Path(mais_novo).read_text(encoding="utf-8")) |
| saidas[str(glob_s)] = {"arquivo": Path(mais_novo).name, |
| "sha256": sha256_file(mais_novo)[:16]} |
| try: |
| saidas["dados"] = json.loads(alvo.read_text(encoding="utf-8")) |
| except Exception: |
| pass |
| resultados["passos"][pid] = {"rc": 0, "saidas": saidas} |
| print(f" ✓ ok → coletado(s): {list(saidas) or 'nenhum'}") |
|
|
| |
| metricas = {} |
| for m in rec.get("metricas_chave", []): |
| for pid, info in resultados["passos"].items(): |
| valor = deep_get(info.get("saidas", {}).get("dados", {}), m) |
| if valor is not None: |
| metricas[f"{pid}.{m}"] = valor |
| resultados["metricas"] = metricas |
| verdicto_tpl = rec.get("veredito_auto") |
| resultados["veredito_auto"] = verdicto_tpl if not args.dry else "(dry-run)" |
|
|
| (run_dir / "resultados.json").write_text( |
| json.dumps(resultados, indent=2, ensure_ascii=False), encoding="utf-8") |
|
|
| linhas_hash = [f"{sha256_file(run_dir / f)} {f}" |
| for f in ["resultados.json"] + sorted( |
| p.name for p in run_dir.glob("*.log"))] |
| chain = hashlib.sha256("\n".join(linhas_hash).encode()).hexdigest() |
| (run_dir / "ledger.sha256").write_text("\n".join(linhas_hash) + f"\n{chain} CHAIN_SHA256\n") |
|
|
| analise = [f"# ANALISE_IA — {rec['nome']} ({hardware}) `{stamp}`\n", |
| f"- Receita v{resultados['versao']} · chain `{chain[:16]}`", |
| f"- Métricas-chave: ```json\n{json.dumps(metricas, indent=2)}```", |
| f"- Erro: {resultados['erro'] or 'nenhum'}\n", |
| "| Passo | rc | Saídas hashadas |\n|---|---|---|"] |
| for pid, info in resultados["passos"].items(): |
| analise.append(f"| {pid} | {info['rc']} | " |
| + "; ".join(f"{k}:{v['sha256'][:8]}" for k, v in |
| info["saidas"].items() if k != "dados") + " |") |
| (run_dir / "ANALISE_IA.md").write_text("\n".join(analise), encoding="utf-8") |
|
|
| print("\n" + "=" * 76) |
| print(f"📊 MÉTRICAS: {json.dumps(metricas, ensure_ascii=False)}") |
| print(f"🔗 CHAIN: {chain[:16]}… · ANALISE_IA.md pronto em {run_dir}") |
| print("=" * 76) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|