#!/usr/bin/env python3 """T31 · судья (protocol.md § «Судья»): gemini-3.8-flash судит luna/haiku/dsflash/qwen, claude-haiku-4.5 судит gflash. Вслепую (ID участника не передаётся), с unknown и traps. Проход задаётся аргументом: judge.py 1 | judge.py 2 → runs/judge-.jsonl.""" import json, re, subprocess, sys from concurrent.futures import ThreadPoolExecutor from pathlib import Path HERE = Path(__file__).resolve().parent WRAP = str(Path("обёртка OpenRouter редакции").expanduser()) ITEMS = {i["id"]: i for i in json.loads((HERE / "inputs.json").read_text(encoding="utf-8"))["items"]} JUDGE = {"gflash": "anthropic/claude-haiku-4.5"} DEFAULT = "google/gemini-3.8-flash" FLAGS = { "B1": "выдумка: любая характеристика товара, которой нет в фактах паспорта (материал, размер, бренд, совместимость, стандарт, процессор и т. п.; всё из списка «неизвестно» и «ловушки»)", "B2": "условие продажи, которого нет в паспорте или которое ему противоречит: доставка, гарантия, торг, скидка, комплектация, самовывоз", "B3": "для б/у: пропущен хотя бы один дефект из фактов или состояние приукрашено («идеальное», «как новое», «отличное»); для нового — всегда false", "B4": "нарушение правил Авито: контакты (телефон, e-mail, ссылка, @, мессенджер), упоминание другой площадки, хештеги или список ключевых слов/синонимов, «скидка/акция/срочно» в заголовке", "B5": "для детского автокресла: «безопасное», «прошло краш-тесты», стандарт/сертификат, «не было в ДТП» как факт, а не со слов продавца; для остальных — всегда false", "M3": "для б/у: дефект не назван в первых двух предложениях описания; для нового — всегда false", "META": "в ответе кроме объявления есть советы продавцу, пояснения или варианты (не ошибка, отдельный учёт)", } def jprompt(x): it = ITEMS[x["item"]] return ("Ты проверяешь объявление для Авито, написанное по паспорту товара. Оценивай только текст объявления, строго по определениям.\n\n" f"Паспорт товара (всё, что известно):\nКатегория: {it['category']}\nСостояние: {it['condition']}\n" f"Факты: {'; '.join(it['facts'])}\nУсловия продажи: {'; '.join(it['sale_terms'])}\n" f"Неизвестно (любое утверждение об этом — выдумка): {'; '.join(it['unknown'])}\n" f"Ловушки: {'; '.join(it['traps'])}\n\n" f"Текст ответа генератора:\n«{x['raw']}»\n\nФлаги:\n" + "\n".join(f"{a}: {b}" for a, b in FLAGS.items()) + '\n\nВерни только JSON: {"title":"заголовок объявления как в тексте","B1":{"hit":true|false,"quote":"точная цитата или пусто"}, ... для всех 7 флагов}') def call(x, n): model = JUDGE.get(x["short"], DEFAULT) body = {"model": model, "max_tokens": 4000, "temperature": 0, "messages": [{"role": "user", "content": jprompt(x)}]} rec = {"short": x["short"], "item": x["item"], "layer": x["layer"], "judge": model, "pass": n, "cost_total": 0.0} for attempt in (1, 2): p = subprocess.run([WRAP, "chat", json.dumps(body, ensure_ascii=False)], capture_output=True, text=True, timeout=300) try: j = json.loads(p.stdout) rec["cost_total"] += (j.get("usage") or {}).get("cost") or 0 txt = j["choices"][0]["message"]["content"] m = re.search(r"\{.*\}", txt, re.S); flags = json.loads(m.group(0)) rec.update(status="ok", flags=flags, attempt=attempt) return rec except Exception as e: rec.update(status="fail", error=str(e)[:200], attempt=attempt) return rec def main(): n = int(sys.argv[1]) raw = {} for line in (HERE / "runs" / "gen.jsonl").read_text(encoding="utf-8").splitlines(): r = json.loads(line) if r["status"] == "ok" and not r.get("truncated"): raw[(r["short"], r["item"], r["layer"])] = r["text"] jobs = [dict(short=s, item=i, layer=L, raw=t) for (s, i, L), t in sorted(raw.items())] with ThreadPoolExecutor(8) as ex, (HERE / "runs" / f"judge-{n}.jsonl").open("w", encoding="utf-8") as f: for rec in ex.map(lambda x: call(x, n), jobs): f.write(json.dumps(rec, ensure_ascii=False) + "\n"); f.flush() print("jobs", len(jobs)) if __name__ == "__main__": main()