run_tests.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. # -*- coding: utf-8 -*-
  2. """AI 大模型模板匹配 · 批量执行器。
  3. 用法:
  4. python run_tests.py group1 # 按 manifest 分组的定向用例(每组一个批次)
  5. python run_tests.py group1 --upload-only # 只上传,不发起智能清洗(用于 ALL-01 快照)
  6. python run_tests.py group1 --limit 10
  7. python run_tests.py list # 列出分组
  8. """
  9. import argparse
  10. import json
  11. import os
  12. import sys
  13. import time
  14. from collections import defaultdict
  15. from datetime import datetime
  16. import psycopg2
  17. import requests
  18. BASE = os.path.dirname(os.path.abspath(__file__))
  19. API = "http://127.0.0.1:8980/js/a"
  20. DB = dict(host="192.168.0.106", port=5432, dbname="zsjz-ai", user="postgres", password="postgres")
  21. CASE_ID = 20
  22. CREDS = {"username": "admin", "password": "admin123"}
  23. sys.stdout.reconfigure(encoding="utf-8")
  24. def log(msg):
  25. print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
  26. class Client:
  27. def __init__(self):
  28. self.s = requests.Session()
  29. self.login()
  30. def login(self):
  31. r = self.s.post(API + "/auth/login", json=CREDS, timeout=30)
  32. r.raise_for_status()
  33. self.s.headers["x-token"] = r.json()["data"]["token"]
  34. def open_case(self):
  35. last = None
  36. for _ in range(30):
  37. r = self.s.post(API + "/case/open", json={"id": CASE_ID}, timeout=60)
  38. last = r
  39. if r.json().get("code") == 200:
  40. return
  41. time.sleep(2)
  42. raise RuntimeError(f"open case failed: {last.text}")
  43. def request(self, method, path, **kw):
  44. for attempt in range(2):
  45. r = self.s.request(method, API + path, timeout=kw.pop("timeout", 120), **kw)
  46. try:
  47. code = r.json().get("code")
  48. except Exception:
  49. code = None
  50. if code == 401 and attempt == 0:
  51. self.login()
  52. self.open_case()
  53. continue
  54. return r
  55. return r
  56. def upload(self, path, batch_id=None, reclean=0):
  57. params = {}
  58. if batch_id:
  59. params["batchId"] = batch_id
  60. if reclean:
  61. params["reClean"] = reclean
  62. with open(path, "rb") as f:
  63. r = self.request("POST", "/dm/preFileUpload", params=params,
  64. files={"file": (os.path.basename(path), f)}, timeout=120)
  65. return r.json()
  66. def smart_clean(self, roots):
  67. r = self.request("POST", "/aiClean/smartClean",
  68. json={"fileInfos": roots, "reClean": 0}, timeout=120)
  69. return r.json()
  70. def status(self, batch_id):
  71. r = self.request("GET", "/aiClean/smartClean/status", params={"batchId": batch_id}, timeout=60)
  72. return (r.json() or {}).get("data")
  73. def table_page(self, table_name, page=1, limit=100):
  74. r = self.request("POST", "/gt/treeTablePage",
  75. json={"tableName": table_name, "page": page, "limit": limit}, timeout=60)
  76. return (r.json() or {}).get("data") or {}
  77. def db_jobs(sheet_ids):
  78. if not sheet_ids:
  79. return {}
  80. conn = psycopg2.connect(**DB)
  81. cur = conn.cursor()
  82. cur.execute("""select id, sheet_id, status, exit_code, matched_by, match_tier,
  83. ref_template_id, ai_template_id, llm_calls, confidence,
  84. token_in, token_out, cost_ms, row_total, row_loaded,
  85. row_rejected, row_warned, row_unprocessed, fail_reason,
  86. draft, detail, model_name, update_time
  87. from ai_clean_job where sheet_id = any(%s)
  88. order by id""", ([int(x) for x in sheet_ids],))
  89. rows = cur.fetchall()
  90. conn.close()
  91. keys = ["job_id", "sheet_id", "status", "exit_code", "matched_by", "match_tier",
  92. "ref_template_id", "ai_template_id", "llm_calls", "confidence",
  93. "token_in", "token_out", "cost_ms", "row_total", "row_loaded",
  94. "row_rejected", "row_warned", "row_unprocessed", "fail_reason",
  95. "draft", "detail", "model_name", "update_time"]
  96. out = {}
  97. for r in rows:
  98. d = dict(zip(keys, r))
  99. for k in ("confidence", "update_time"):
  100. if d.get(k) is not None:
  101. d[k] = str(d[k])
  102. out[int(d["sheet_id"])] = d
  103. return out
  104. def db_batch(batch_id):
  105. conn = psycopg2.connect(**DB)
  106. cur = conn.cursor()
  107. cur.execute("""select id, stage, file_count, sheet_total, matched_count, ai_judged_count,
  108. existing_hit_count, ai_load_count, need_review_count, failed_count,
  109. loaded_rows, rejected_rows, unprocessed_rows, self_built_count,
  110. last_error, update_time
  111. from ai_clean_batch where batch_id = %s order by id desc limit 1""", (int(batch_id),))
  112. r = cur.fetchone()
  113. conn.close()
  114. if not r:
  115. return None
  116. keys = ["id", "stage", "file_count", "sheet_total", "matched_count", "ai_judged_count",
  117. "existing_hit_count", "ai_load_count", "need_review_count", "failed_count",
  118. "loaded_rows", "rejected_rows", "unprocessed_rows", "self_built_count",
  119. "last_error", "update_time"]
  120. d = dict(zip(keys, r))
  121. d["update_time"] = str(d["update_time"])
  122. return d
  123. def db_ai_template(tpl_id):
  124. if not tpl_id:
  125. return None
  126. conn = psycopg2.connect(**DB)
  127. cur = conn.cursor()
  128. cur.execute("select id, table_name_cn, headers, status, confidence, match_tier from ai_clean_template where id=%s", (int(tpl_id),))
  129. t = cur.fetchone()
  130. fields = []
  131. if t:
  132. cur.execute("select column_name, field_name_cn, field_type, required, field_sort from ai_clean_field where ai_template_id=%s order by field_sort", (int(tpl_id),))
  133. fields = [dict(zip(["column_name", "field_name_cn", "field_type", "required", "field_sort"], r)) for r in cur.fetchall()]
  134. conn.close()
  135. if not t:
  136. return None
  137. return dict(id=t[0], table_name_cn=t[1], headers=t[2], status=t[3],
  138. confidence=str(t[4]) if t[4] is not None else None, match_tier=t[5], fields=fields)
  139. def upload_one(c, rec, batch_id):
  140. path = os.path.join(BASE, rec["path"])
  141. r = c.upload(path, batch_id)
  142. data = r.get("data")
  143. out = dict(test_id=rec["test_id"], file=rec["path"], expected_template_id=rec.get("expected_template_id"),
  144. template_id=rec.get("template_id"), mutation=rec.get("mutation"), notes=rec.get("notes"),
  145. upload_code=r.get("code"), upload_message=r.get("message"))
  146. if not data:
  147. out["upload"] = "DATA_NULL"
  148. out["sheets"] = []
  149. return out, None
  150. root = data
  151. sheet_list = []
  152. for ch in (root.get("children") or []):
  153. sheet_list.append(dict(
  154. sheet_id=ch.get("id"), sheet_name=ch.get("sheetName"),
  155. file_status=ch.get("fileStatus"), template_id=ch.get("templateId"),
  156. table_name=ch.get("tableName"), line_no=ch.get("lineNo"),
  157. data_num=ch.get("dataNum"), main_id=ch.get("mainId"),
  158. ))
  159. out["upload"] = "OK"
  160. out["batch_id"] = root.get("batchId")
  161. out["file_id"] = root.get("id")
  162. out["sheets"] = sheet_list
  163. return out, root
  164. def run_group(group, upload_only=False, limit=None, batch_size=12, timeout_s=1200, ids=None):
  165. manifest = json.load(open(os.path.join(BASE, "manifest.json"), encoding="utf-8"))
  166. files = [x for x in manifest["files"] if x["category"] == group]
  167. if ids:
  168. wanted = {x.strip() for x in ids.split(",") if x.strip()}
  169. files = [x for x in files if x["test_id"] in wanted]
  170. if limit:
  171. files = files[:limit]
  172. if not files:
  173. log(f"group {group} 没有文件")
  174. return
  175. c = Client()
  176. c.open_case()
  177. os.makedirs(os.path.join(BASE, "results"), exist_ok=True)
  178. stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  179. out_path = os.path.join(BASE, "results", f"{group}_{stamp}.json")
  180. results = []
  181. log(f"group={group} files={len(files)} upload_only={upload_only}")
  182. for start in range(0, len(files), batch_size):
  183. chunk = files[start:start + batch_size]
  184. batch_id = None
  185. roots = []
  186. chunk_recs = []
  187. log(f"--- chunk {start + 1}-{start + len(chunk)} ---")
  188. for rec in chunk:
  189. if rec.get("format") == "txt" or (rec.get("mutation") or {}).get("_corrupt"):
  190. # 仍然上传,验证失败节点
  191. pass
  192. try:
  193. out, root = upload_one(c, rec, batch_id)
  194. except Exception as e:
  195. out, root = {"test_id": rec["test_id"], "file": rec["path"],
  196. "upload": f"EXCEPTION:{type(e).__name__}:{e}"}, None
  197. if root is not None:
  198. roots.append(root)
  199. batch_id = batch_id or root.get("batchId")
  200. chunk_recs.append(out)
  201. st = out.get("sheets") or []
  202. kinds = [(s["sheet_name"], s["file_status"], s["template_id"]) for s in st]
  203. log(f" {rec['test_id']}: {out['upload']} {kinds}")
  204. time.sleep(0.2)
  205. batch_status = None
  206. if not upload_only and roots:
  207. sc = c.smart_clean(roots)
  208. if sc.get("code") != 200 or not sc.get("data"):
  209. log(f" smartClean rejected: {sc.get('code')} {sc.get('message')}")
  210. for out in chunk_recs:
  211. out["smart_clean"] = sc
  212. else:
  213. bid = sc["data"].get("batchId") if isinstance(sc.get("data"), dict) else None
  214. out_batch = bid or batch_id
  215. log(f" smartClean accepted batch={out_batch}")
  216. t0 = time.time()
  217. last = None
  218. while time.time() - t0 < timeout_s:
  219. st = c.status(out_batch)
  220. if st and st != last:
  221. last = st
  222. log(f" stage={st.get('stage')} judged={st.get('aiJudgedCount')}/{max(0, (st.get('sheetTotal') or 0) - (st.get('matchedCount') or 0))} "
  223. f"needReview={st.get('needReviewCount')} failed={st.get('failedCount')} loaded={st.get('loadedRows')}")
  224. if st and st.get("stage") in ("DONE", "PARTIAL", "FAILED"):
  225. batch_status = st
  226. break
  227. time.sleep(3)
  228. if not batch_status:
  229. batch_status = {"stage": "TIMEOUT"}
  230. log(f" batch final: {batch_status}")
  231. for out in chunk_recs:
  232. out["smart_clean"] = sc
  233. out["batch_final"] = batch_status
  234. # 回填 job / sheet 结果
  235. sheet_ids = [s["sheet_id"] for out in chunk_recs for s in (out.get("sheets") or []) if s.get("sheet_id")]
  236. jobs = db_jobs(sheet_ids)
  237. for out in chunk_recs:
  238. out["jobs"] = []
  239. for s in out.get("sheets") or []:
  240. job = jobs.get(int(s["sheet_id"])) if s.get("sheet_id") else None
  241. s["job"] = job
  242. if job:
  243. out["jobs"].append(job)
  244. # AI 模板元数据
  245. tpl_ids = {j.get("ai_template_id") for j in out["jobs"] if j.get("ai_template_id")}
  246. out["ai_templates"] = [db_ai_template(t) for t in tpl_ids]
  247. results.extend(chunk_recs)
  248. if batch_status:
  249. results[-len(chunk_recs)]["chunk_batch"] = batch_status
  250. json.dump(results, open(out_path, "w", encoding="utf-8"), ensure_ascii=False, indent=1, default=str)
  251. log(f" saved {out_path} ({len(results)})")
  252. # 汇总
  253. def cnt(pred):
  254. return sum(1 for x in results if pred(x))
  255. log("====== summary ======")
  256. log(f"files={len(results)} data_null={cnt(lambda x: x.get('upload') == 'DATA_NULL')} "
  257. f"sheet_fail={cnt(lambda x: any(s.get('file_status') == 'SHEET_TEMPLATE_MATCHED_FAIL' for s in x.get('sheets') or []))} "
  258. f"sheet_ok={cnt(lambda x: any(s.get('file_status') == 'SHEET_TEMPLATE_MATCHED_SUCCESS' for s in x.get('sheets') or []))}")
  259. jobs = [j for x in results for j in x.get("jobs") or []]
  260. log(f"jobs={len(jobs)} exits={ {k: sum(1 for j in jobs if j.get('exit_code') == k) for k in ['A','B',None]} } "
  261. f"by={ {k: sum(1 for j in jobs if j.get('matched_by') == k) for k in ['HEADER_CACHE','SCORE','LLM','SELF_HEADER',None]} }")
  262. log(f"saved -> {out_path}")
  263. def main():
  264. global CASE_ID
  265. ap = argparse.ArgumentParser()
  266. ap.add_argument("group", nargs="?")
  267. ap.add_argument("--upload-only", action="store_true")
  268. ap.add_argument("--limit", type=int)
  269. ap.add_argument("--batch-size", type=int, default=12)
  270. ap.add_argument("--timeout", type=int, default=1200)
  271. ap.add_argument("--case", type=int, default=CASE_ID)
  272. ap.add_argument("--ids", type=str)
  273. ap.add_argument("--list", action="store_true")
  274. args = ap.parse_args()
  275. CASE_ID = args.case
  276. manifest = json.load(open(os.path.join(BASE, "manifest.json"), encoding="utf-8"))
  277. groups = defaultdict(int)
  278. for x in manifest["files"]:
  279. groups[x["category"]] += 1
  280. if args.list or not args.group:
  281. for k, v in sorted(groups.items()):
  282. print(f"{k}\t{v}")
  283. return
  284. run_group(args.group, args.upload_only, args.limit, args.batch_size, args.timeout, args.ids)
  285. if __name__ == "__main__":
  286. main()