silicon-sampling-lab / scripts /test_silicon_llm_runtime.py
kyu823's picture
Use HF dataset logs and improve survey outputs
f84a7bc verified
Raw
History Blame Contribute Delete
8 kB
#!/usr/bin/env python3
"""Unit tests for dynamic LLM-backed silicon sampling runtime."""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from silicon_llm import ( # noqa: E402
build_silicon_llm_system_prompt,
build_silicon_prompt_preview,
build_silicon_llm_response_contract,
parse_silicon_agent_response,
run_silicon_llm_sampling,
)
QUESTIONS = [
{
"id": "q_likert_5",
"title": "ν˜„ μ •λΆ€ κ΅­μ • μš΄μ˜μ„ μ–΄λ–»κ²Œ ν‰κ°€ν•˜μ‹­λ‹ˆκΉŒ?",
"kind": "likert",
"scale": 5,
"lowLabel": "맀우 λΆ€μ •",
"highLabel": "맀우 긍정",
},
{
"id": "q_likert_4",
"title": "μ§€μ—­ 의료 접근성에 λ§Œμ‘±ν•˜μ‹­λ‹ˆκΉŒ?",
"kind": "likert",
"scale": 4,
},
{
"id": "q_open",
"title": "κ°€μž₯ λ¨Όμ € ν•΄κ²°ν•΄μ•Ό ν•  λ¬Έμ œλŠ” λ¬΄μ—‡μž…λ‹ˆκΉŒ?",
"kind": "open",
},
{
"id": "q_categorical",
"title": "μ‚Άμ—μ„œ κ°€μž₯ μ€‘μš”ν•œ 것을 ν•˜λ‚˜λ§Œ κ³ λ₯΄μ‹­μ‹œμ˜€.",
"kind": "categorical",
"options": [
{"id": "money", "label": "돈"},
{"id": "honor", "label": "λͺ…μ˜ˆ"},
{"id": "health", "label": "건강"},
],
},
]
class FakeLLM:
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
def complete_agent(self, *, agent: dict[str, Any], questions: list[dict[str, Any]], response_contract: dict[str, Any]) -> str:
self.calls.append({"agent": agent, "questions": questions, "response_contract": response_contract})
return json.dumps(
{
"answers": {
"q_likert_5": {"value": 4, "reason": "μ •μ±… ν‰κ°€λŠ” λŒ€μ²΄λ‘œ κΈμ •μ μž…λ‹ˆλ‹€."},
"q_likert_4": {"value": 2, "reason": "의료 접근성은 지역에 따라 λΆ€μ‘±ν•©λ‹ˆλ‹€."},
"q_open": {"text": "물가와 의료 μ ‘κ·Όμ„± κ°œμ„ μ΄ κ°€μž₯ μ‹œκΈ‰ν•©λ‹ˆλ‹€.", "theme": "의료", "rationale": "μƒν™œλΉ„μ™€ 의료 이용 κ²½ν—˜μ„ ν•¨κ»˜ κ³ λ €ν–ˆμŠ΅λ‹ˆλ‹€."},
"q_categorical": {"optionId": "health", "label": "건강", "rationale": "κ³ λ Ή κ°€μ‘± λŒλ΄„ κ²½ν—˜ λ•Œλ¬Έμ— 건강을 μš°μ„ ν–ˆμŠ΅λ‹ˆλ‹€."},
}
},
ensure_ascii=False,
)
class SiliconLLMRuntimeTests(unittest.TestCase):
def test_contract_is_dynamic_for_question_mix(self) -> None:
contract = build_silicon_llm_response_contract(QUESTIONS)
answer_properties = contract["schema"]["properties"]["answers"]["properties"]
self.assertEqual(set(answer_properties), {"q_likert_5", "q_likert_4", "q_open", "q_categorical"})
self.assertEqual(answer_properties["q_likert_5"]["properties"]["value"]["minimum"], 1)
self.assertEqual(answer_properties["q_likert_5"]["properties"]["value"]["maximum"], 5)
self.assertEqual(answer_properties["q_likert_4"]["properties"]["value"]["maximum"], 4)
self.assertIn("rationale", answer_properties["q_likert_4"]["required"])
self.assertIn("text", answer_properties["q_open"]["required"])
self.assertIn("rationale", answer_properties["q_open"]["required"])
self.assertEqual(answer_properties["q_categorical"]["properties"]["optionId"]["enum"], ["money", "honor", "health"])
def test_prompt_preview_uses_personalized_backend_prompt(self) -> None:
system_prompt = build_silicon_llm_system_prompt()
self.assertIn("1인칭", system_prompt)
self.assertIn("μ΅œμ†Œ 두 κ°€μ§€", system_prompt)
self.assertIn("일반둠", system_prompt)
preview = build_silicon_prompt_preview(
{
"config": {
"sampleSize": 1,
"genders": [{"id": "female", "enabled": True, "weight": 1}],
"ages": [{"id": "40s", "enabled": True, "weight": 1}],
"locations": [{"id": "seoul", "enabled": True, "weight": 1}],
"locationOptions": [{"id": "seoul", "label": "μ„œμšΈ", "parentRegion": "seoul", "level": "sido"}],
"questions": QUESTIONS,
}
}
)
self.assertEqual(preview["systemContent"], system_prompt)
self.assertIn("required_json_contract", preview["userContent"])
self.assertIn("q_categorical", preview["userContent"])
def test_parser_extracts_all_dynamic_answers_from_fenced_json(self) -> None:
raw = """
생각 과정은 μƒλž΅ν•©λ‹ˆλ‹€.
```json
{
"answers": {
"q_likert_5": {"value": "5", "reason": "κ°•ν•œ 긍정"},
"q_likert_4": {"score": 3, "reason": "보톡 이상"},
"q_open": {"answer": "ꡐ톡과 λŒλ΄„μ΄ ν•„μš”ν•©λ‹ˆλ‹€", "topic": "ꡐ톡"},
"q_categorical": {"label": "건강", "rationale": "μƒν™œ μ•ˆμ •μ˜ μ „μ œλΌκ³  λ³΄μ•˜μŠ΅λ‹ˆλ‹€."}
}
}
```
"""
parsed = parse_silicon_agent_response(raw, QUESTIONS, respondent_id="R0001")
self.assertEqual(len(parsed["likertAnswers"]), 2)
self.assertEqual(len(parsed["categoricalAnswers"]), 1)
self.assertEqual(len(parsed["openAnswers"]), 1)
self.assertEqual(parsed["likertAnswers"][0]["value"], 5)
self.assertEqual(parsed["likertAnswers"][1]["value"], 3)
self.assertEqual(parsed["likertAnswers"][0]["rationale"], "κ°•ν•œ 긍정")
self.assertEqual(parsed["categoricalAnswers"][0]["optionId"], "health")
self.assertEqual(parsed["categoricalAnswers"][0]["label"], "건강")
self.assertEqual(parsed["openAnswers"][0]["theme"], "ꡐ톡")
self.assertTrue(parsed["openAnswers"][0]["rationale"])
def test_runtime_calls_one_llm_agent_for_all_questions_and_builds_stats(self) -> None:
fake = FakeLLM()
payload = {
"config": {
"sampleSize": 2,
"genders": [{"id": "male", "enabled": True, "weight": 1}, {"id": "female", "enabled": True, "weight": 1}],
"ages": [{"id": "30s", "enabled": True, "weight": 2}],
"locations": [{"id": "seoul", "enabled": True, "weight": 2}],
"locationOptions": [{"id": "seoul", "label": "μ„œμšΈ", "parentRegion": "seoul", "level": "sido", "defaultWeight": 100, "short": "μ„œμšΈ", "group": "μ‹œλ„"}],
"personaAttributes": [{"id": "occ_office", "enabled": True, "weight": 2}],
"nemotronFields": ["persona", "sex", "age"],
"questions": QUESTIONS,
"seed": 42,
},
"execution": {"max_agents": 2},
}
result = run_silicon_llm_sampling(payload, llm=fake)
self.assertEqual(len(fake.calls), 2)
self.assertTrue(all(len(call["questions"]) == 4 for call in fake.calls))
self.assertEqual(len(result["respondents"]), 2)
self.assertEqual(len(result["likertAnswers"]), 4)
self.assertEqual(len(result["categoricalAnswers"]), 2)
self.assertEqual(len(result["openAnswers"]), 2)
self.assertTrue(all(answer.get("rationale") for answer in result["likertAnswers"]))
self.assertTrue(all(answer.get("rationale") for answer in result["categoricalAnswers"]))
self.assertTrue(all(answer.get("rationale") for answer in result["openAnswers"]))
self.assertEqual([stat["questionId"] for stat in result["questionStats"]], ["q_likert_5", "q_likert_4", "q_open", "q_categorical"])
self.assertEqual(result["questionStats"][0]["distribution"][3]["count"], 2)
self.assertEqual(result["questionStats"][3]["distribution"][2]["count"], 2)
self.assertEqual(result["regionStats"][0]["respondents"], 2)
self.assertEqual(result["llmTrace"]["calls"], 2)
if __name__ == "__main__":
unittest.main(verbosity=2)