Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Run small real-API validation for dynamic silicon sampling.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| import time | |
| from urllib import request | |
| from pathlib import Path | |
| from typing import Any | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| from silicon_llm import resolve_openai_api_key, run_silicon_llm_sampling # noqa: E402 | |
| def main() -> int: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--agents", type=int, default=2) | |
| parser.add_argument("--max-cases", type=int, default=3) | |
| parser.add_argument("--model", default="gpt-4o-mini") | |
| parser.add_argument("--endpoint", default="") | |
| parser.add_argument("--out", default="") | |
| args = parser.parse_args() | |
| if not args.endpoint and not resolve_openai_api_key(): | |
| print(json.dumps({"ok": False, "error": "OPENAI_API_KEY or .env OPENAI_API_KEY is required"}, ensure_ascii=False, indent=2)) | |
| return 1 | |
| out_dir = Path(args.out) if args.out else ROOT / "runs" / f"silicon_llm_api_validation_{timestamp()}" | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| report: dict[str, Any] = {"ok": False, "model": args.model, "agents": args.agents, "maxCases": args.max_cases, "endpoint": args.endpoint, "cases": []} | |
| for case in validation_cases()[: max(1, args.max_cases)]: | |
| payload = base_payload(args.agents, case["questions"], model=args.model) | |
| started = time.time() | |
| try: | |
| result = run_payload(payload, endpoint=args.endpoint) | |
| checks = validate_result(case["case_id"], result, case["questions"], args.agents) | |
| report["cases"].append( | |
| { | |
| "case_id": case["case_id"], | |
| "ok": all(check["ok"] for check in checks), | |
| "elapsed_seconds": round(time.time() - started, 2), | |
| "questions": [{"id": item["id"], "kind": item["kind"], "scale": item.get("scale"), "options": item.get("options")} for item in case["questions"]], | |
| "checks": checks, | |
| "llmTrace": { | |
| key: value | |
| for key, value in result.get("llmTrace", {}).items() | |
| if key != "rawResponses" | |
| }, | |
| "sample_categorical_answers": result.get("categoricalAnswers", [])[:3], | |
| "sample_open_answers": result.get("openAnswers", [])[:3], | |
| "questionStats": result.get("questionStats", []), | |
| "runLog": result.get("runLog"), | |
| "runLogError": result.get("runLogError"), | |
| } | |
| ) | |
| except Exception as exc: # noqa: BLE001 | |
| report["cases"].append({"case_id": case["case_id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}) | |
| report["ok"] = bool(report["cases"]) and all(case.get("ok") for case in report["cases"]) | |
| (out_dir / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") | |
| print(json.dumps({"ok": report["ok"], "out": str(out_dir / "report.json"), "cases": report["cases"]}, ensure_ascii=False, indent=2)) | |
| return 0 if report["ok"] else 1 | |
| def run_payload(payload: dict[str, Any], *, endpoint: str) -> dict[str, Any]: | |
| if not endpoint: | |
| return run_silicon_llm_sampling(payload) | |
| url = endpoint.rstrip("/") + "/api/silicon/llm-run" | |
| data = json.dumps(payload, ensure_ascii=False).encode("utf-8") | |
| req = request.Request(url, data=data, headers={"content-type": "application/json"}, method="POST") | |
| with request.urlopen(req, timeout=180) as response: # noqa: S310 | |
| result = json.loads(response.read().decode("utf-8")) | |
| if "error" in result: | |
| raise RuntimeError(str(result["error"])) | |
| return result | |
| def validation_cases() -> list[dict[str, Any]]: | |
| return [ | |
| { | |
| "case_id": "mixed_policy_trust", | |
| "questions": [ | |
| likert("approval_5", "ํ ์ ๋ถ ๊ตญ์ ์ด์์ ์ด๋ป๊ฒ ํ๊ฐํ์ญ๋๊น?", 5), | |
| likert("medical_access_4", "๊ฑฐ์ฃผ ์ง์ญ ์๋ฃ ์ ๊ทผ์ฑ์ ๋ง์กฑํ์ญ๋๊น?", 4), | |
| categorical("life_priority", "์ถ์์ ๊ฐ์ฅ ์ค์ํ ๊ฒ์ ํ๋๋ง ๊ณ ๋ฅด์ญ์์ค.", ["๋", "๋ช ์", "๊ฑด๊ฐ"]), | |
| open_q("local_problem", "๊ฑฐ์ฃผ ์ง์ญ์์ ๊ฐ์ฅ ๋จผ์ ํด๊ฒฐํด์ผ ํ ๋ฌธ์ ๋ ๋ฌด์์ ๋๊น?"), | |
| ], | |
| }, | |
| { | |
| "case_id": "open_only_priorities", | |
| "questions": [ | |
| categorical("social_priority", "ํ๊ตญ ์ฌํ๊ฐ ๊ฐ์ฅ ์ฐ์ ํด์ผ ํ ๊ฐ์น๋ฅผ ๊ณ ๋ฅด์ญ์์ค.", ["๊ฒฝ์ ์ฑ์ฅ", "์ฌํ ์์ ", "๊ณต์ ์ฑ", "ํ๊ฒฝ"]), | |
| open_q("top_issue", "ํ๊ตญ ์ฌํ๊ฐ ๊ฐ์ฅ ๋จผ์ ํด๊ฒฐํด์ผ ํ ๋ฌธ์ ๋ ๋ฌด์์ด๋ผ๊ณ ๋ณด์ญ๋๊น?"), | |
| open_q("policy_request", "์ ์ ๋ถ๋ ์ง๋ฐฉ์์น๋จ์ฒด์ ๊ฐ์ฅ ๋ฐ๋ผ๋ ์ ์ฑ ์ ์ ์ด์ฃผ์ญ์์ค."), | |
| ], | |
| }, | |
| { | |
| "case_id": "multi_scale_likert", | |
| "questions": [ | |
| likert("vote_turnout_4", "๋ค์ ์ง๋ฐฉ์ ๊ฑฐ์ ํฌํํ ๊ฐ๋ฅ์ฑ์ด ์ผ๋ง๋ ๋์ต๋๊น?", 4), | |
| likert("party_reflects_5", "์ฃผ์ ์ ๋น์ด ๊ตญ๋ฏผ ์๊ฒฌ์ ์ ๋ฐ์ํ๋ค๊ณ ๋ณด์ญ๋๊น?", 5), | |
| categorical("vote_factor", "ํฌํํ ๋ ๊ฐ์ฅ ์ค์ํ๊ฒ ๋ณด๋ ๊ธฐ์ค์ ๊ณ ๋ฅด์ญ์์ค.", ["ํ๋ณด ์ญ๋", "์ ๋น", "๊ณต์ฝ", "์ง์ญ ํ์"]), | |
| likert("climate_cost_7", "ํ์ ๊ฐ์ถ ๋น์ฉ ๋ถ๋ด ์ ์ฑ ์ ์ด๋ ์ ๋ ๋์ํ์ญ๋๊น?", 7), | |
| ], | |
| }, | |
| { | |
| "case_id": "custom_korean_mix", | |
| "questions": [ | |
| likert("custom_transport_safety", "์ถํด๊ทผ๊ธธ ๋์ค๊ตํต ์์ ์ ๋ง์กฑํ์ญ๋๊น?", 5), | |
| categorical("custom_life_choice", "์ง๊ธ ์ํ์์ ๊ฐ์ฅ ํฌ๊ฒ ํ์ํ ๊ฒ์ ๊ณ ๋ฅด์ญ์์ค.", ["์๊ฐ", "์๋", "๊ฑด๊ฐ", "๊ด๊ณ"]), | |
| open_q("custom_one_sentence", "๋ณธ์ธ์ ์ํ์์ ๊ฐ์ฅ ํฌ๊ฒ ์ฒด๊ฐ๋๋ ์ ์ฑ ๋ฌธ์ ๋ฅผ ํ ๋ฌธ์ฅ์ผ๋ก ์ ์ด์ฃผ์ญ์์ค."), | |
| likert("custom_media_trust", "์จ๋ผ์ธ ๋ด์ค์ ์ ํ๋ธ ์ ์น ์ ๋ณด๋ฅผ ์ ๋ขฐํ์ญ๋๊น?", 5), | |
| ], | |
| }, | |
| { | |
| "case_id": "many_questions_mix", | |
| "questions": [ | |
| likert("economy_next_year", "ํฅํ 1๋ ํ๊ตญ ๊ฒฝ์ ๊ฐ ์ข์์ง ๊ฒ์ด๋ผ๊ณ ๋ณด์ญ๋๊น?", 5), | |
| likert("household_life", "ํ์ฌ ๊ฐ๊ณ ์ํ ํํธ์ ์ผ๋ง๋ ๋ง์กฑํ์ญ๋๊น?", 5), | |
| likert("institution_trust", "๊ตญํ, ์ธ๋ก , ํ์ ๋ถ ๋ฑ ๊ณต๊ณต๊ธฐ๊ด์ ์ ๋ฐ์ ์ผ๋ก ์ ๋ขฐํ์ญ๋๊น?", 5), | |
| categorical("public_spend_priority", "์ ๋ถ ์ฌ์ ์ด ์ฐ์ ํฌ์ ๋์ด์ผ ํ ๋ถ์ผ๋ฅผ ๊ณ ๋ฅด์ญ์์ค.", ["๋ณต์ง", "์ผ์๋ฆฌ", "๊ต์ก", "์๋ณด", "๊ธฐํ"]), | |
| open_q("free_reason", "๊ทธ๋ ๊ฒ ์๋ตํ ๊ฐ์ฅ ํฐ ์ด์ ๋ฅผ ์ ์ด์ฃผ์ญ์์ค."), | |
| ], | |
| }, | |
| ] | |
| def likert(question_id: str, title: str, scale: int) -> dict[str, Any]: | |
| return {"id": question_id, "title": title, "source": "API validation", "category": "๊ฒ์ฆ", "kind": "likert", "scale": scale} | |
| def open_q(question_id: str, title: str) -> dict[str, Any]: | |
| return {"id": question_id, "title": title, "source": "API validation", "category": "๊ฒ์ฆ", "kind": "open"} | |
| def categorical(question_id: str, title: str, labels: list[str]) -> dict[str, Any]: | |
| return { | |
| "id": question_id, | |
| "title": title, | |
| "source": "API validation", | |
| "category": "๊ฒ์ฆ", | |
| "kind": "categorical", | |
| "options": [{"id": f"opt_{index + 1}", "label": label} for index, label in enumerate(labels)], | |
| } | |
| def base_payload(agent_count: int, questions: list[dict[str, Any]], *, model: str) -> dict[str, Any]: | |
| return { | |
| "config": { | |
| "sampleSize": agent_count, | |
| "genders": [{"id": "male", "enabled": True, "weight": agent_count // 2}, {"id": "female", "enabled": True, "weight": agent_count - agent_count // 2}], | |
| "ages": [{"id": "30s", "enabled": True, "weight": 2}, {"id": "50s", "enabled": True, "weight": agent_count - 2}], | |
| "locations": [{"id": "seoul", "enabled": True, "weight": 3}, {"id": "busan", "enabled": True, "weight": agent_count - 3}], | |
| "locationOptions": [ | |
| {"id": "seoul", "label": "์์ธ", "parentRegion": "seoul", "level": "sido", "defaultWeight": 60, "short": "์์ธ", "group": "์๋"}, | |
| {"id": "busan", "label": "๋ถ์ฐ", "parentRegion": "busan", "level": "sido", "defaultWeight": 40, "short": "๋ถ์ฐ", "group": "์๋"}, | |
| ], | |
| "personaAttributes": [ | |
| {"id": "occ_office", "enabled": True, "weight": 2}, | |
| {"id": "occ_student", "enabled": True, "weight": agent_count - 2}, | |
| {"id": "edu_bachelor", "enabled": True, "weight": agent_count}, | |
| ], | |
| "nemotronFields": ["persona", "sex", "age", "province", "occupation", "education_level"], | |
| "questions": questions, | |
| "seed": 20260630, | |
| }, | |
| "execution": {"max_agents": agent_count, "model": model, "timeout_seconds": 90}, | |
| } | |
| def validate_result(case_id: str, result: dict[str, Any], questions: list[dict[str, Any]], agent_count: int) -> list[dict[str, Any]]: | |
| checks: list[dict[str, Any]] = [] | |
| likert_questions = [item for item in questions if item["kind"] == "likert"] | |
| categorical_questions = [item for item in questions if item["kind"] == "categorical"] | |
| open_questions = [item for item in questions if item["kind"] == "open"] | |
| checks.append(check("respondent_count", len(result.get("respondents", [])) == agent_count, {"count": len(result.get("respondents", [])), "expected": agent_count})) | |
| checks.append(check("one_llm_call_per_agent", result.get("llmTrace", {}).get("calls") == agent_count, result.get("llmTrace", {}))) | |
| checks.append(check("all_questions_in_config", [item["id"] for item in result.get("config", {}).get("questions", [])] == [item["id"] for item in questions], None)) | |
| checks.append(check("likert_answer_count", len(result.get("likertAnswers", [])) == agent_count * len(likert_questions), {"count": len(result.get("likertAnswers", [])), "expected": agent_count * len(likert_questions)})) | |
| checks.append(check("categorical_answer_count", len(result.get("categoricalAnswers", [])) == agent_count * len(categorical_questions), {"count": len(result.get("categoricalAnswers", [])), "expected": agent_count * len(categorical_questions)})) | |
| checks.append(check("open_answer_count", len(result.get("openAnswers", [])) == agent_count * len(open_questions), {"count": len(result.get("openAnswers", [])), "expected": agent_count * len(open_questions)})) | |
| checks.append(check("question_stats_count", len(result.get("questionStats", [])) == len(questions), {"count": len(result.get("questionStats", [])), "expected": len(questions)})) | |
| checks.append(check("parse_issues_empty", not result.get("llmTrace", {}).get("parseIssues"), result.get("llmTrace", {}).get("parseIssues"))) | |
| if result.get("runLog") or result.get("runLogError"): | |
| checks.append(check("run_log_recorded", bool(result.get("runLog")) and not result.get("runLogError"), result.get("runLog") or result.get("runLogError"))) | |
| for question in likert_questions: | |
| question_id = question["id"] | |
| scale = int(question.get("scale") or 5) | |
| values = [int(answer["value"]) for answer in result.get("likertAnswers", []) if answer.get("questionId") == question_id] | |
| stat = next((item for item in result.get("questionStats", []) if item.get("questionId") == question_id), {}) | |
| checks.append(check(f"{case_id}:{question_id}:value_range", len(values) == agent_count and all(1 <= value <= scale for value in values), {"values": values, "scale": scale})) | |
| checks.append(check(f"{case_id}:{question_id}:distribution_sum", sum(item.get("count", 0) for item in stat.get("distribution", [])) == agent_count, stat.get("distribution"))) | |
| rationales = [answer.get("rationale") for answer in result.get("likertAnswers", []) if answer.get("questionId") == question_id] | |
| checks.append(check(f"{case_id}:{question_id}:likert_rationale", len(rationales) == agent_count and all(rationales), rationales[:2])) | |
| for question in categorical_questions: | |
| question_id = question["id"] | |
| allowed = {option["id"] for option in question.get("options", [])} | |
| answers = [answer for answer in result.get("categoricalAnswers", []) if answer.get("questionId") == question_id] | |
| stat = next((item for item in result.get("questionStats", []) if item.get("questionId") == question_id), {}) | |
| checks.append(check(f"{case_id}:{question_id}:categorical_option", len(answers) == agent_count and all(answer.get("optionId") in allowed for answer in answers), answers[:2])) | |
| checks.append(check(f"{case_id}:{question_id}:categorical_distribution_sum", sum(item.get("count", 0) for item in stat.get("distribution", [])) == agent_count, stat.get("distribution"))) | |
| checks.append(check(f"{case_id}:{question_id}:categorical_rationale", len(answers) == agent_count and all(answer.get("rationale") for answer in answers), answers[:2])) | |
| for question in open_questions: | |
| answers = [answer for answer in result.get("openAnswers", []) if answer.get("questionId") == question["id"]] | |
| checks.append(check(f"{case_id}:{question['id']}:open_text_theme", len(answers) == agent_count and all(answer.get("text") and answer.get("theme") for answer in answers), answers[:2])) | |
| checks.append(check(f"{case_id}:{question['id']}:open_rationale", len(answers) == agent_count and all(answer.get("rationale") for answer in answers), answers[:2])) | |
| expected_primary = next((item["id"] for item in likert_questions), None) | |
| checks.append(check("primary_question_dynamic", result.get("primaryQuestionId") == expected_primary, {"actual": result.get("primaryQuestionId"), "expected": expected_primary})) | |
| return checks | |
| def check(name: str, ok: bool, detail: Any) -> dict[str, Any]: | |
| return {"name": name, "ok": bool(ok), "detail": detail} | |
| def timestamp() -> str: | |
| return time.strftime("%Y%m%d%H%M%S") | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |