ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
import json
import sys
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
import pytest
import demo
from werewolf.agent import PlayerAgent
from werewolf.game import Judge, create_players
from werewolf.human import HumanPlayerAgent, LiveVoiceSession
from werewolf.roles import Faction, Role
from werewolf.strategy_audit import evaluate_strategy, strategy_acceptance_passes, validate_strategy_result
class NoAudio:
def __init__(self):
self.private_prompts = []
def say(self, speaker, text, round_no, allow_barge_in=False):
self.private_prompts.append(text)
def test_exact_acceptance_roster_has_one_human_and_required_roles():
players = create_players(seed=42, players=7, wolves=2, human_seat=1, voice=NoAudio())
assert sum(isinstance(p, HumanPlayerAgent) for p in players) == 1
roles = [p.role for p in players]
assert roles.count(Role.WEREWOLF) == 2
assert roles.count(Role.SEER) == 1
assert roles.count(Role.WITCH) == 1
assert players[0].role in set(Role)
@pytest.mark.parametrize("seat_count", [6, 7, 8])
def test_every_allowed_live_roster_has_one_human_and_five_to_seven_ai(seat_count):
players = create_players(
seed=seat_count,
players=seat_count,
wolves=2,
human_seat=1,
voice=NoAudio(),
)
roles = [player.role for player in players]
assert sum(isinstance(player, HumanPlayerAgent) for player in players) == 1
assert sum(not isinstance(player, HumanPlayerAgent) for player in players) == seat_count - 1
assert 5 <= seat_count - 1 <= 7
assert roles.count(Role.WEREWOLF) == 2
assert roles.count(Role.SEER) == 1
assert roles.count(Role.WITCH) == 1
assert roles.count(Role.VILLAGER) == seat_count - 4
def test_spoken_player_number_parser():
candidates = ["P2", "P3", "P4"]
assert HumanPlayerAgent._spoken_target("我投三号玩家", candidates, True) == "P3"
assert HumanPlayerAgent._spoken_target("player 4", candidates, True) == "P4"
assert HumanPlayerAgent._spoken_target("I choose player three", candidates, True) == "P3"
assert HumanPlayerAgent._spoken_target("我弃票", candidates, True) is None
assert HumanPlayerAgent._spoken_target("I choose to abstain", candidates, True) is None
assert not HumanPlayerAgent._explicit_none("P1 is not")
def test_live_human_terminal_never_prints_god_view(capsys):
voice = NoAudio()
players = create_players(seed=42, players=7, wolves=2, human_seat=1, voice=voice)
judge = Judge(players, seed=42)
judge.assign_identities()
output = capsys.readouterr().out
assert "上帝视角身份表已隐藏" in output
assert "P2:" not in output
assert any("您的身份" in prompt for prompt in voice.private_prompts)
judge._print_private(["P2"], "SECRET_WOLF_ACTION")
assert "SECRET_WOLF_ACTION" not in capsys.readouterr().out
judge._print_private(["P1"], "MY_PRIVATE_ACTION")
assert "MY_PRIVATE_ACTION" in capsys.readouterr().out
def test_live_default_without_consent_stops_before_game_or_voice_construction(monkeypatch):
monkeypatch.setattr(sys, "argv", ["demo.py"])
with patch("demo.run_game") as run_game, patch(
"werewolf.human.LiveVoiceSession"
) as live_session:
with pytest.raises(SystemExit) as exc:
demo.main()
assert exc.value.code == 2
run_game.assert_not_called()
live_session.assert_not_called()
def test_human_role_is_actually_randomized_by_the_shared_shuffle():
observed = {
create_players(seed=seed, players=7, wolves=2, human_seat=1, voice=NoAudio())[0].role
for seed in range(40)
}
assert observed == set(Role)
class PassivePlayer(PlayerAgent):
def choose_target(self, prompt, candidates, players, allow_none=False):
if self.role == Role.SEER and candidates:
return candidates[0]
return None
def speak(self, players):
return "我暂时没有可验证的判断。"
def vote(self, candidates, players):
return None
def test_three_cycles_count_only_after_night_day_vote_and_round_cap_is_not_fake_win(capsys):
roles = [Role.WEREWOLF, Role.WEREWOLF, Role.SEER, Role.WITCH,
Role.VILLAGER, Role.VILLAGER, Role.VILLAGER]
players = [PassivePlayer(f"P{i + 1}", role, offline=True) for i, role in enumerate(roles)]
judge = Judge(players, seed=7, max_rounds=3)
winner = judge.run()
assert judge.completed_rounds == 3
assert len([r for r in judge.audit.records if r.category == "公开-死讯"]) == 3
assert len([r for r in judge.audit.records if r.category == "公开-放逐"]) == 3
assert winner == Faction.UNDECIDED
output = capsys.readouterr().out
assert "本局未决" in output
assert "获胜阵营:未决" not in output
def test_deterministic_winner_conditions():
roles = [Role.WEREWOLF, Role.WEREWOLF, Role.SEER, Role.WITCH,
Role.VILLAGER, Role.VILLAGER]
players = [PlayerAgent(f"P{i + 1}", role, offline=True) for i, role in enumerate(roles)]
judge = Judge(players)
players[0].alive = players[1].alive = False
assert judge._check_winner() == Faction.GOOD
players[0].alive = players[1].alive = True
players[4].alive = players[5].alive = False
assert judge._check_winner() == Faction.WEREWOLF
players[5].alive = True
assert judge._check_winner() is None
def test_strategy_acceptance_requires_all_named_criteria_and_evidence():
valid = {
"criteria": {
name: {"status": "pass", "evidence": f"quoted evidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
},
"overall_pass": True,
}
assert strategy_acceptance_passes(validate_strategy_result(valid))
valid_fail = {
"criteria": {
name: {"status": "fail", "evidence": f"counterevidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
},
"overall_pass": False,
}
checked_fail = validate_strategy_result(valid_fail)
assert checked_fail["schema_valid"] is True
assert checked_fail["overall_pass"] is False
assert not strategy_acceptance_passes(checked_fail)
malformed = {"criteria": {"role_consistency": {"status": "pass"}}, "overall_pass": True}
checked = validate_strategy_result(malformed)
assert checked["schema_valid"] is False
assert checked["overall_pass"] is False
assert not strategy_acceptance_passes(checked)
@pytest.mark.parametrize("malformed", [None, [], "not-json-object", 7])
def test_strategy_validation_fails_closed_for_non_object_provider_output(malformed):
checked = validate_strategy_result(malformed)
assert checked["schema_valid"] is False
assert checked["overall_pass"] is False
assert not strategy_acceptance_passes(checked)
def test_strategy_audit_retains_invalid_schema_and_tries_next_backend(monkeypatch):
from werewolf import strategy_audit as audit_module
criteria = {
name: {"status": "pass", "evidence": f"quoted evidence for {name}"}
for name in (
"werewolf_concealment", "seer_timing_and_evidence",
"villager_logical_reasoning", "role_consistency",
)
}
malformed = {
"criteria": dict(list(criteria.items())[:3]),
"role_consistency": criteria["role_consistency"],
}
valid = {"criteria": criteria, "overall_pass": True}
class Completion:
def __init__(self, payload, response_id):
self.payload = payload
self.response_id = response_id
def create(self, **kwargs):
return SimpleNamespace(
id=self.response_id,
model="reported-model",
usage=SimpleNamespace(model_dump=lambda: {"prompt_tokens": 10, "completion_tokens": 5}),
choices=[SimpleNamespace(message=SimpleNamespace(content=__import__("json").dumps(self.payload)))],
)
def client(payload, response_id):
return SimpleNamespace(chat=SimpleNamespace(completions=Completion(payload, response_id)))
monkeypatch.setattr(audit_module, "_backends", lambda: [
(client(malformed, "bad-schema"), "judge-a", "provider-a"),
(client(valid, "valid-schema"), "judge-b", "provider-b"),
])
judge = SimpleNamespace(
players=[SimpleNamespace(name="P1", role=Role.VILLAGER)],
action_history=[{"actor": "P1", "role": "村民", "action": "vote", "target": "P2"}],
)
result = evaluate_strategy(judge)
assert result["schema_valid"] is True
assert result["overall_pass"] is True
assert result["provider"] == "provider-b"
assert [attempt["response_id"] for attempt in result["judge_attempts"]] == [
"bad-schema", "valid-schema"
]
assert result["judge_attempts"][0]["schema_valid"] is False
# Invalid-attempt provenance must remain acyclic when attached to the
# accepted result so the retained acceptance report can be serialized.
json.dumps(result)
def test_barge_in_cancels_playback_and_transcribes_without_real_audio(monkeypatch, tmp_path):
class FakeProcess:
def __init__(self):
self.terminated = False
def poll(self):
return 0 if self.terminated else None
def terminate(self):
self.terminated = True
class FakeInputStream:
def __init__(self, **kwargs):
self.frames = iter([
np.full((1024, 1), 0.2, dtype="float32"),
np.full((1024, 1), 0.2, dtype="float32"),
np.zeros((1024, 1), dtype="float32"),
])
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self, _block):
return next(self.frames), False
session = LiveVoiceSession.__new__(LiveVoiceSession)
session.allow_interruptions = True
session.sample_rate = 1024
session.threshold = 0.05
session.silence_seconds = 1.0
session.max_utterance = 1.0
session.player = "fake-player"
events = []
session._tts = lambda *args: tmp_path / "synthetic.mp3"
session._event = lambda kind, **data: events.append((kind, data))
session._write_wav = lambda frames, path: None
session._transcribe = lambda path, kind: "我来打断"
proc = FakeProcess()
monkeypatch.setitem(
sys.modules, "sounddevice", SimpleNamespace(InputStream=FakeInputStream)
)
monkeypatch.setattr("werewolf.human.subprocess.Popen", lambda *args, **kwargs: proc)
result = session.say("P2", "一段 AI 发言", 1, allow_barge_in=True)
assert result == "我来打断"
assert proc.terminated is True
assert events == [("barge_in", {"interrupted_speaker": "P2"})]