"""Example run of the Ungok P1 pier through the running web app (HTTP, so progress shows in the browser). Steps: upload the 3 DWG (copies of the RAG originals) -> check table candidates -> confirm the required sections -> run and approve G1..G5 -> make the review report draft. Confirmations and approvals are recorded under the name "예제실행(Agent)" so they are not mistaken for a person's review. Standard library only (urllib). usage: python run_example.py [pier name] [--base http://127.0.0.1:5000] """ import argparse import json import re import sys import time import urllib.parse import urllib.request import uuid from pathlib import Path APP = Path(__file__).resolve().parents[1] ROOT = APP.parents[1] sys.path.insert(0, str(APP / "web")) import store # noqa: E402 (read-only use: section lists, job files) BY = "예제실행(Agent)" class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, *a, **k): return None OPENER = urllib.request.build_opener(NoRedirect) def call(base, path, data=None, files=None): url = base + urllib.parse.quote(path) if files: b = uuid.uuid4().hex body = b"" for name, (fname, content) in files: body += (f"--{b}\r\nContent-Disposition: form-data; name=\"{name}\"; filename=\"{fname}\"\r\n" f"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8") + content + b"\r\n" body += f"--{b}--\r\n".encode() req = urllib.request.Request(url, body, {"Content-Type": f"multipart/form-data; boundary={b}"}) elif data is not None: req = urllib.request.Request(url, urllib.parse.urlencode(data, doseq=True).encode("utf-8")) else: req = urllib.request.Request(url) try: with OPENER.open(req, timeout=600) as r: return r.status, r.read().decode("utf-8") except urllib.error.HTTPError as e: # 302 redirects after POST are expected return e.code, "" def wait_job(pier, gate, limit=1800): jid = (pier.gate_status(gate)["job"] or {}).get("id") t0 = time.time() while time.time() - t0 < limit: job = pier.gate_status(gate)["job"] or {} if job.get("id") == jid and job.get("state") in ("완료", "오류"): return job time.sleep(5) return None def main(name, base): pier = store.Pier(name) print(f"[교각] {name} ({base}/pier/{name})", flush=True) dwgs = sorted((ROOT / "RAG").glob("C0050205-02*-운곡교 교각도*.dwg")) st, _ = call(base, f"/pier/{name}/upload", files=[("dwg", (p.name, p.read_bytes())) for p in dwgs]) print(f"[도면] {len(dwgs)}장 업로드 → 문자 {len(pier.texts())}개 색인 (HTTP {st})", flush=True) _, html = call(base, f"/pier/{name}/candidates") m = re.search(r"후보값 비교 \((\d+)건 · 다름 (\d+)건\)", html) print(f"[표 후보값] 비교 {m.group(1)}건, 다름 {m.group(2)}건" if m else "[표 후보값] 표를 찾지 못함", flush=True) need = sorted({(r["file"], r["key"]) for g in store.GATE_SECTIONS for r in pier.gate_requirements(g)}) for f, k in need: call(base, f"/pier/{name}/section/{f}/{k}/confirm", {"by": BY}) ok = all(r["confirmed"] for g in store.GATE_SECTIONS for r in pier.gate_requirements(g)) print(f"[확정] 필요한 묶음 {len(need)}개 확정 ({'완료' if ok else '미완료'}, 확정자 {BY})", flush=True) for g in ("G1", "G2", "G3", "G4", "G5"): t0 = time.time() call(base, f"/pier/{name}/gate/{g}/run", {}) job = wait_job(pier, g) rep = pier.gate_status(g)["report"] or {} if not job or job["state"] != "완료" or rep.get("verdict") != "PASS": print(f"[{g}] 중단: {job and job['state']} {rep.get('verdict')} {(job or {}).get('error', '')[-300:]}") return 1 s = rep["summary"] call(base, f"/pier/{name}/gate/{g}/decide", {"by": BY, "decision": "승인", "note": "예제 실행"}) print(f"[{g}] PASS {s['passed']}/{s['total']} (계획 FAIL {len(rep['planned_fails'])}) · " f"음성 {'/'.join('검출' if v['detected'] else '미검출' for v in rep['negative'].values())} · " f"재로드 {'일치' if rep['reload']['ok'] else '불일치'} · 승인 {pier.gate_status(g)['approved']} · " f"{time.time() - t0:.0f}초", flush=True) call(base, f"/pier/{name}/report", {"by": BY}) chk = json.loads((pier.run_dir / "report_check.json").read_text(encoding="utf-8")) files = sorted(p.name for p in pier.run_dir.glob("보고서_*")) print(f"[보고서] {files} · 자체 점검 {'통과' if chk['ok'] else '미통과'}", flush=True) print(f"[모델] {pier.run_dir / 'G5.3dm'}") return 0 if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("name", nargs="?", default="운곡교P1") ap.add_argument("--base", default="http://127.0.0.1:5000") a = ap.parse_args() sys.exit(main(a.name, a.base))