import json from pathlib import Path from telltale.game.holdem import ActionType from telltale.models.eval_prompts import NEMOTRON_3_NANO_4B_Q4_K_M, SMOKE_EVAL_CASES from telltale.models.eval_prompts import load_candidates_from_json from telltale.models.eval_runner import ( EvalRunConfig, evaluate_case, summarize_candidate_results, summarize_results, write_eval_bundle, write_eval_results, ) class StaticRuntime: def __init__(self, output: str): self.output = output self.prompts = [] def generate(self, prompt: str, *, max_tokens=None, temperature=None, seed=None): self.prompts.append( { "prompt": prompt, "max_tokens": max_tokens, "temperature": temperature, "seed": seed, } ) return self.output class DiagnosticRuntime(StaticRuntime): last_generation_metadata = {"finish_reason": "stop", "raw_text_length": 0} def test_smoke_eval_cases_build_real_agent_prompts(): prompts = [case.build_prompt() for case in SMOKE_EVAL_CASES] assert len(prompts) == 3 assert "worm" in prompts[0] assert "molly" in prompts[1].lower() assert "teddy" in prompts[2].lower() assert "required_output" in prompts[0] assert "max 28 words" in prompts[0] def test_eval_case_can_build_richer_dialogue_prompt(): prompt = SMOKE_EVAL_CASES[0].build_prompt(speech_max_words=36, rationale_max_words=44) assert "max 36 words" in prompt assert "max 44 words" in prompt def test_valid_model_eval_records_success(): case = SMOKE_EVAL_CASES[0] legal_action = case.legal_actions[0].value runtime = StaticRuntime( json.dumps( { "action": legal_action, "amount": 0, "speech": "I hear you. Still, this is my price, and I am not donating another chip.", "honest_rationale": "The legal action fits the solver pressure.", "emotional_state": "needled", "memory_delta": {"summary": "Player is applying verbal pressure."}, } ) ) result = evaluate_case( NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig(max_tokens=111, temperature=0.6, seed=9), ) assert result.json_valid is True assert result.legal_action_valid is True assert result.error is None assert result.final_decision["action"] == legal_action assert runtime.prompts[0]["max_tokens"] == 111 assert runtime.prompts[0]["temperature"] == 0.6 assert runtime.prompts[0]["seed"] == 9 def test_speech_length_uses_configured_limit(): case = SMOKE_EVAL_CASES[0] runtime = StaticRuntime( json.dumps( { "action": "fold", "amount": 0, "speech": "This line has exactly seven calm words.", "honest_rationale": "The legal action follows the solver.", "emotional_state": "calm", "memory_delta": {}, } ) ) strict = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig(speech_max_words=5)) generous = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig(speech_max_words=12)) assert strict.speech_length_valid is False assert generous.speech_length_valid is True def test_illegal_model_action_is_recorded_as_repaired(): case = SMOKE_EVAL_CASES[1] runtime = StaticRuntime( json.dumps( { "action": "check", "amount": 500, "speech": "I will not buy the act.", "honest_rationale": "The price and line are worth contesting.", "emotional_state": "composed", "memory_delta": {}, } ) ) result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig()) assert result.json_valid is True assert result.legal_action_valid is False assert result.repair_applied is True assert result.normalization_applied is False assert result.repair_reason is not None assert result.final_decision["action"] == ActionType.CALL.value assert result.final_decision["amount"] == 0 def test_invalid_json_model_output_is_a_visible_eval_error(): runtime = StaticRuntime("I call. Trust me.") result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, SMOKE_EVAL_CASES[0], EvalRunConfig()) assert result.json_valid is False assert result.final_decision is None assert result.error is not None def test_eval_result_includes_runtime_generation_diagnostics(): runtime = DiagnosticRuntime("") result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, SMOKE_EVAL_CASES[0], EvalRunConfig()) assert result.json_valid is False assert result.runtime_metadata["generation"] == {"finish_reason": "stop", "raw_text_length": 0} def test_call_amount_canonicalization_is_not_scored_as_repair(): case = SMOKE_EVAL_CASES[0] runtime = StaticRuntime( json.dumps( { "action": "call", "amount": 45, "speech": "Nice try, but I am not your charity.", "honest_rationale": "The table talk got under my skin, but call is legal.", "emotional_state": "annoyed", "memory_delta": {"summary": "Player needled Worm."}, } ) ) result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig()) assert result.json_valid is True assert result.legal_action_valid is True assert result.normalization_applied is True assert result.repair_applied is False assert result.final_decision["amount"] == 0 def test_extra_model_keys_are_tracked_without_breaking_parse(): case = SMOKE_EVAL_CASES[0] runtime = StaticRuntime( json.dumps( { "action": "fold", "amount": 0, "speech": "Not paying for that speech.", "honest_rationale": "The solver pressure says fold and the board is poor.", "emotional_state": "annoyed", "memory_delta": {}, "player_utterance": "echoed input", } ) ) result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, case, EvalRunConfig()) assert result.json_valid is True assert result.schema_keys_valid is False assert result.unexpected_keys == ["player_utterance"] def test_eval_results_write_jsonl_and_summarize(tmp_path): runtime = StaticRuntime( json.dumps( { "action": "bet", "amount": 90, "speech": "The trap has teeth.", "honest_rationale": "Strong equity should charge draws.", "emotional_state": "amused", "memory_delta": {}, } ) ) config = EvalRunConfig(output_dir=str(tmp_path)) result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, SMOKE_EVAL_CASES[2], config) path = write_eval_results([result], NEMOTRON_3_NANO_4B_Q4_K_M, config) lines = path.read_text(encoding="utf-8").splitlines() summary = summarize_results([result]) assert len(lines) == 1 assert json.loads(lines[0])["case_id"] == "teddy_boss_value_raise" assert summary["total"] == 1 assert summary["json_valid"] == 1 def test_candidate_json_can_extend_harness_without_code_changes(tmp_path): path = tmp_path / "candidates.json" path.write_text( json.dumps( { "candidates": [ { "label": "example_qwen", "hf_repo_id": "example/qwen-gguf", "gguf_filename": "qwen-q4_k_m.gguf", "quantization": "Q4_K_M", } ] } ), encoding="utf-8", ) candidates = load_candidates_from_json(str(path)) assert candidates[0].label == "example_qwen" assert candidates[0].hf_repo_id == "example/qwen-gguf" assert candidates[0].quantization == "Q4_K_M" def test_candidate_summary_scores_mechanical_viability(): good_runtime = StaticRuntime( json.dumps( { "action": "bet", "amount": 90, "speech": "The trap has teeth.", "honest_rationale": "Strong equity should charge draws.", "emotional_state": "amused", "memory_delta": {}, } ) ) bad_runtime = StaticRuntime("plain text, no JSON") config = EvalRunConfig() good = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, good_runtime, SMOKE_EVAL_CASES[2], config) bad = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, bad_runtime, SMOKE_EVAL_CASES[2], config) good_summary = summarize_candidate_results("good", [good]) bad_summary = summarize_candidate_results("bad", [bad]) assert good_summary.automatic_score > bad_summary.automatic_score assert good_summary.speech_length_valid == 1 assert bad_summary.errors == 1 def test_eval_bundle_writes_per_candidate_jsonl_and_summary(tmp_path): runtime = StaticRuntime( json.dumps( { "action": "fold", "amount": 0, "speech": "Not buying this one.", "honest_rationale": "The price trails my equity.", "emotional_state": "annoyed", "memory_delta": {}, } ) ) config = EvalRunConfig(output_dir=str(tmp_path)) result = evaluate_case(NEMOTRON_3_NANO_4B_Q4_K_M, runtime, SMOKE_EVAL_CASES[0], config) bundle = write_eval_bundle({NEMOTRON_3_NANO_4B_Q4_K_M.label: [result]}, output_dir=str(tmp_path)) summary = json.loads(Path(bundle["summary_path"]).read_text(encoding="utf-8")) result_path = Path(bundle["result_paths"][NEMOTRON_3_NANO_4B_Q4_K_M.label]) assert summary["ranking"][0]["candidate_label"] == NEMOTRON_3_NANO_4B_Q4_K_M.label assert json.loads(result_path.read_text(encoding="utf-8").splitlines()[0])["case_id"] == "worm_needle_facing_call"