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
@@ -0,0 +1,221 @@
import json
import sys
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import demo
from werewolf import agent as agent_module
from werewolf.game import Judge, create_players
from werewolf.roles import Role
from werewolf.agent import PlayerAgent
from werewolf.simulator import SimulatedUserPlayerAgent, SimulatedVoiceSession
class FakeVoice:
def __init__(self, transcripts=None):
self.events = []
self.transcripts = iter(transcripts or [])
self.spoken = []
def say(self, speaker, text, round_no, allow_barge_in=False):
self.spoken.append((speaker, text, round_no))
def record_llm_decision(self, **data):
self.events.append({"type": "simulator_llm_tool", **data})
def roundtrip_user(self, speaker, text, round_no):
self.spoken.append((speaker, text, round_no))
return next(self.transcripts)
def _event(self, type_, **data):
self.events.append({"type": type_, **data})
def tool_response(name, arguments):
call = SimpleNamespace(
function=SimpleNamespace(name=name, arguments=json.dumps(arguments, ensure_ascii=False))
)
message = SimpleNamespace(tool_calls=[call])
return SimpleNamespace(
id="response-real-shape",
model="provider-model",
usage=SimpleNamespace(model_dump=lambda: {"prompt_tokens": 10, "completion_tokens": 3}),
choices=[SimpleNamespace(message=message)],
)
def test_simulated_user_is_one_randomized_protected_user_seat():
voice = FakeVoice()
players = create_players(
seed=42,
players=7,
wolves=2,
simulated_user_seat=1,
voice=voice,
)
assert sum(isinstance(player, SimulatedUserPlayerAgent) for player in players) == 1
assert sum(getattr(player, "is_user", False) for player in players) == 1
assert players[0].role in set(Role)
judge = Judge(players, seed=42)
judge.assign_identities()
assert any("您的身份" in text for _, text, _ in voice.spoken)
def test_simulator_uses_private_context_tool_and_only_asr_speech(monkeypatch):
voice = FakeVoice(transcripts=["ASR 后的公开发言"])
player = SimulatedUserPlayerAgent("P1", Role.VILLAGER, voice, model="test/model")
player.observe("仅 P1 可见的记忆")
captured = {}
response = tool_response("speak_publicly", {"utterance": "LLM 原始公开发言"})
monkeypatch.setattr(agent_module, "get_client", lambda: object())
def fake_create(_client, **kwargs):
captured.update(kwargs)
return response
monkeypatch.setattr(agent_module, "_safe_create", fake_create)
speech = player.speak(["P1", "P2"])
assert speech == "ASR 后的公开发言"
assert "仅 P1 可见的记忆" in captured["messages"][1]["content"]
assert captured["tool_choice"]["function"]["name"] == "speak_publicly"
assert captured["tools"][0]["function"]["parameters"]["additionalProperties"] is False
assert voice.spoken[-1][1] == "LLM 原始公开发言"
assert voice.events[0]["tool"] == "speak_publicly"
def test_simulator_fails_closed_when_asr_changes_selected_action(monkeypatch):
voice = FakeVoice(transcripts=["I choose player three"])
player = SimulatedUserPlayerAgent("P1", Role.VILLAGER, voice)
response = tool_response("choose_player", {"target": "P2", "reason": "公开证据"})
monkeypatch.setattr(agent_module, "get_client", lambda: object())
monkeypatch.setattr(agent_module, "_safe_create", lambda _client, **kwargs: response)
with pytest.raises(RuntimeError, match="speech boundary changed"):
player.vote(["P2", "P3"], ["P1", "P2", "P3"])
mismatch = voice.events[-1]
assert mismatch["type"] == "simulator_action_mismatch"
assert mismatch["tool_target"] == "P2"
assert mismatch["parsed_target"] == "P3"
def test_simulator_fails_closed_when_abstention_is_not_explicit_in_asr(monkeypatch):
voice = FakeVoice(transcripts=["P1 is not"])
player = SimulatedUserPlayerAgent("P1", Role.VILLAGER, voice)
response = tool_response("choose_player", {"target": "none", "reason": "not enough evidence"})
monkeypatch.setattr(agent_module, "get_client", lambda: object())
monkeypatch.setattr(agent_module, "_safe_create", lambda _client, **kwargs: response)
with pytest.raises(RuntimeError, match="speech boundary changed"):
player.vote(["P2", "P3"], ["P1", "P2", "P3"])
assert voice.spoken[-1][1] == "I choose to abstain."
assert voice.events[-1]["type"] == "simulator_action_mismatch"
def test_judge_retains_llm_decision_reason_in_action_evidence(monkeypatch):
voice = FakeVoice(transcripts=["I choose player two"])
simulator = SimulatedUserPlayerAgent("P1", Role.VILLAGER, voice)
response = tool_response(
"choose_player", {"target": "P2", "reason": "P2 contradicted the public vote record"}
)
monkeypatch.setattr(agent_module, "get_client", lambda: object())
monkeypatch.setattr(agent_module, "_safe_create", lambda _client, **kwargs: response)
target = simulator.vote(["P2"], ["P1", "P2"])
record = Judge._decision_record(
simulator, round=1, phase="vote", actor="P1", role="村民",
action="vote", target=target,
)
assert record["reason"] == "P2 contradicted the public vote record"
assert simulator.last_decision_reason is None
def test_good_faction_vote_prompt_prioritizes_uncontested_seer_evidence(monkeypatch):
player = PlayerAgent("P7", Role.VILLAGER)
captured = {}
def fake_chat(instruction, players, max_tokens, json_mode=False):
captured["instruction"] = instruction
return '{"target": "P5", "reason": "P4 is uncontested and checked P5 as a wolf"}'
monkeypatch.setattr(player, "_chat", fake_chat)
assert player.vote(["P4", "P5"], ["P4", "P5", "P7"]) == "P5"
assert "不得投该声明者" in captured["instruction"]
assert "被查杀者仅仅否认并不构成" in captured["instruction"]
def test_reasoning_model_empty_content_retries_with_larger_bounded_budget(monkeypatch):
player = PlayerAgent("P7", Role.VILLAGER)
calls = []
responses = iter([
SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content=""))]),
SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="visible speech"))]),
])
monkeypatch.setattr(agent_module, "get_client", lambda: object())
def fake_create(_client, **kwargs):
calls.append(kwargs)
return next(responses)
monkeypatch.setattr(agent_module, "_safe_create", fake_create)
assert player._chat("speak", ["P7"], max_tokens=180) == "visible speech"
assert calls[0]["max_tokens"] == 512
assert calls[1]["max_tokens"] == 2048
def test_simulator_vote_prompt_uses_the_same_evidence_priority(monkeypatch):
voice = FakeVoice(transcripts=["I choose player five"])
player = SimulatedUserPlayerAgent("P1", Role.VILLAGER, voice)
captured = {}
response = tool_response(
"choose_player", {"target": "P5", "reason": "P4 is the only Seer and checked P5"}
)
monkeypatch.setattr(agent_module, "get_client", lambda: object())
def fake_create(_client, **kwargs):
captured.update(kwargs)
return response
monkeypatch.setattr(agent_module, "_safe_create", fake_create)
assert player.vote(["P4", "P5"], ["P1", "P4", "P5"]) == "P5"
instruction = captured["messages"][1]["content"]
assert "不得投该声明者" in instruction
assert "被查杀者仅仅否认不构成" in instruction
def test_system_speech_falls_back_to_macos_say(monkeypatch, tmp_path):
from werewolf import simulator as simulator_module
paths = {"espeak-ng": None, "espeak": None, "say": "/usr/bin/say", "ffmpeg": "/opt/bin/ffmpeg"}
monkeypatch.setattr(simulator_module.shutil, "which", lambda name: paths.get(name))
monkeypatch.setenv("OPENROUTER_API_KEY", "configured-for-test")
def fake_run(command, **kwargs):
if command[0] == "/usr/bin/say":
output = command[command.index("-o") + 1]
else:
output = command[-1]
from pathlib import Path
Path(output).write_bytes(b"real-audio-shaped-bytes")
return SimpleNamespace(returncode=0)
monkeypatch.setattr(simulator_module.subprocess, "run", fake_run)
session = SimulatedVoiceSession(str(tmp_path), provider="openrouter-system")
path = session._synthesize("P1", "I choose player four.", 1)
assert path.read_bytes() == b"real-audio-shaped-bytes"
assert session.events[0]["model"] == "macos-say-Samantha"
def test_simulated_user_cli_needs_no_human_consent(monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "configured-for-test")
monkeypatch.setenv("GEMINI_API_KEY", "configured-for-test")
monkeypatch.setattr(sys, "argv", ["demo.py", "--simulate-user"])
with patch("demo.run_game", return_value=True) as run_game:
demo.main()
assert run_game.call_args.args[0].simulate_user is True
assert run_game.call_args.args[0].confirm_human_consent is False