JackWPP commited on
Commit
08f891f
·
1 Parent(s): 8eadaad

feat: Implement core functionality for werewolf game agents

Browse files

- Added output_guard.py for text normalization and validation of choices and enums.
- Introduced ruleset.py to define game constants and rules.
- Created sanitizer.py to sanitize player inputs and remove unwanted patterns.
- Developed state.py to manage game state and player information.
- Implemented telemetry.py for logging interactions and telemetry data.
- Added tools for replaying telemetry logs and computing metrics.
- Established a structured plan for upgrading agent code to improve robustness and maintainability.

.gitignore CHANGED
@@ -1,2 +1,5 @@
1
  .idea
2
- .DS_Store
 
 
 
 
1
  .idea
2
+ .DS_Store
3
+ logs/
4
+ __pycache__/
5
+ *.pyc
mission.md ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mission:狼人杀 Agent 工程化改造(以稳定/可评测/可演进为先)
2
+
3
+ > 本仓库后续所有改造工作以本 `mission.md` 为“唯一入口”。任何代码变更都必须能映射到下面某个里程碑/任务,并满足对应验收标准,确保部署后可正常跑通。
4
+
5
+ ## 1. 项目背景与硬约束(不可破)
6
+
7
+ ### 1.1 运行与部署方式(必须保持可用)
8
+ - 容器入口:`Dockerfile` 使用 `CMD ["python3", "werewolf/app.py"]`
9
+ - 运行依赖:`requirements.txt`(`werewolf-agent-build-sdk==0.0.10`, `openai`, `fastapi`, `uvicorn[standard]`)
10
+ - 环境变量(部署必须提供):
11
+ - `API_KEY`:LLM API Key
12
+ - `BASE_URL`:OpenAI 兼容接口,例如 `https://api.openai.com/v1` 或 `https://dashscope.aliyuncs.com/compatible-mode/v1`
13
+ - `MODEL_NAME`:模型名(示例代码默认 `gpt4-4o-mini`,实际以 env 为准)
14
+
15
+ ### 1.2 对外 I/O 协议(比赛接口不可变)
16
+ - 每个角色仍实现:
17
+ - `perceive(req: AgentReq) -> None`
18
+ - `interact(req: AgentReq) -> AgentResp`
19
+ - `AgentReq` 字段:`status/name/message/role/round`(见 SDK `agent_build_sdk.model.werewolf_model`)
20
+ - `AgentResp` 关键字段:
21
+ - `result: str`:裁判读取的核心输出
22
+ - `skillTargetPlayer: Optional[str]`:技能目标(部分 status 需要)
23
+ - **强制原则**:任何 `interact()` 调用必须返回**合规**输出,永不抛异常导致崩线。
24
+
25
+ ## 2. 改造目标与非目标
26
+
27
+ ### 2.1 目标(按优先级)
28
+ 1. **稳定性**:投票/技能/警长等结构化输出永远合规(非法则重试一次,再失败必 fallback)。
29
+ 2. **可观测性**:把每次决策的输入、候选、prompt、模型输出、解析结果、重试/回退原因落盘,便于回放评测。
30
+ 3. **可演进性**:抽取通用底盘(清洗输入、构建上下文、调用 LLM、解析校验、回退),角色代码只保留策略钩子。
31
+ 4. **上下文可控**:history 限长 + summary/facts,避免 prompt 爆长导致成本/效果波动。
32
+ 5. **安全性**:对玩家自由文本做注入清洗(不误伤裁判结构消息)。
33
+
34
+ ### 2.2 非目标(本阶段不追求)
35
+ - 不追求“更聪明”的策略/更高胜率(先止血,再优化)。
36
+ - 不引入需要新服务/外部数据库的复杂依赖。
37
+
38
+ ## 3. 现状问题清单(来自代码阅读)
39
+
40
+ - 各角色重复实现“拼 history + 拼 prompt + 直接返回 LLM 输出”,结构化输出缺少强校验,易被裁判判失败。
41
+ - `STATUS_DISCUSS` 的玩家发言未统一清洗,存在指令注入污染 history 的风险(虽然 prompt 里有警告,但未强制执行)。
42
+ - history 无限增长,缺少限长/摘要策略,效果与成本不可控。
43
+ - 可观测性弱:目前主要是 `logger.info("prompt:"+prompt)`,无法系统性回放对比。
44
+ - SDK 的 `SimpleMemory` 使用类变量 `memories`(全局共享 dict),存在多局/多 agent 共享污染风险;底盘需要显式规避。
45
+
46
+ ## 4. 里程碑计划(从“先稳定”到“可重构”)
47
+
48
+ > 每个里程碑都必须满足“验收标准”才可进入下一阶段;可以分 PR 逐步推进。
49
+
50
+ ### M1:输出合规止血(最优先,低风险高收益)
51
+ **目标**:所有 `vote/skill/sheriff_*` 等结构化输出,保证永远返回裁判可解析的结果。
52
+
53
+ **交付内容**
54
+ - 新增 `werewolf/core/ruleset.py`:集中定义规则常量(字数上限、候选分隔符、不开枪/撕掉等关键字)。
55
+ - 新增 `werewolf/core/output_guard.py`:
56
+ - `normalize(text) -> str`:去空白/去多余换行/抽取第一段等
57
+ - `validate_choice(result, choices) -> bool`
58
+ - `validate_text_len(text, max_chars) -> text`
59
+ - `guarded_choice(llm_text, choices, fallback) -> choice`
60
+ - 在每个角色的 `interact()` 的结构化分支接入:
61
+ - 一次纠错重试(给出“你必须只输出候选名字/不开枪/撕掉”等硬约束)
62
+ - 仍失败则 fallback(保证返回 choices 内合法值或规则允许的 noop)
63
+
64
+ **验收标准**
65
+ - 对所有结构化分支:无论 LLM 输出什么,都能返回合规 `AgentResp`(不返回解释、不返回不存在候选)。
66
+ - Docker 启动 `python3 werewolf/app.py` 不报错(无语法/导入错误)。
67
+
68
+ ### M2:Telemetry 落盘 + 最小回放(可评测)
69
+ **目标**:把“输入→prompt→输出→解析→校验→回退”的全链路记录下来,并能离线回放验证合规率。
70
+
71
+ **交付内容**
72
+ - 新增 `werewolf/core/telemetry.py`
73
+ - 写入 `logs/<session_id>/<role>/<round>_<status>.jsonl`
74
+ - 最小字段(建议):
75
+ - `ts, session_id, role, name, round, status`
76
+ - `choices`(若有)
77
+ - `prompt`(可选:保存 hash + 截断片段,避免泄露/过大)
78
+ - `llm_raw, llm_normalized`
79
+ - `parsed`(结构化 action 或最终 choice)
80
+ - `valid`、`retry_count`、`fallback_used`、`fallback_reason`
81
+ - `final_result, final_skillTargetPlayer`
82
+ - 新增 `werewolf/tools/replay.py`
83
+ - 输入:一局事件序列(或直接复用 telemetry)
84
+ - 输出:合规率/重试率/fallback 率统计
85
+ - 运行示例:
86
+ - `python werewolf/tools/replay.py --log-root logs --session <session_id>`
87
+ - 或省略 `--session` 自动分析最新一局
88
+
89
+ **验收标准**
90
+ - 随便跑一局(或手动构造若干 `AgentReq`),能在 `logs/` 下生成可读 jsonl。
91
+ - 回放工具能跑通并输出统计(不要求策略强,只要求“可回放可量化”)。
92
+
93
+ ### M3:抽取通用底盘(BaseRoleAgent)
94
+ **目标**:把“清洗/上下文/调用 LLM/解析校验/回退/日志”从每个角色里拿走,角色只保留策略 hook。
95
+
96
+ **交付内容**
97
+ - 新增 `werewolf/core/base_role_agent.py`:继承 SDK `BasicRoleAgent`,实现统一流程:
98
+ 1. `perceive()`:事件清洗 → 写 memory/state → 追加 facts/raw_log
99
+ 2. `interact()`:根据 status 构建任务 → 决策(优先程序策略)→ 必要时调用 LLM → parse/validate → retry/fallback → 渲染输出
100
+ - 新增 `werewolf/core/sanitizer.py`:
101
+ - 仅对“玩家自由文本”(典型:`STATUS_DISCUSS` 且 `req.name` 非空)做注入清洗
102
+ - 裁判结构消息(`*_RESULT`, `night_info` 等)不清洗
103
+ - 新增 `werewolf/core/memory_store.py`(替代/包裹 SDK `SimpleMemory`)
104
+ - 必须保证 **memory 实例隔离**(避免 SDK 类变量共享污染)
105
+ - 提供 `raw_log` 限长与摘要接口(摘要可先占位,后续再做)
106
+
107
+ **验收标准**
108
+ - 至少迁移 1 个简单角色(建议 `villager`)到 `BaseRoleAgent`,功能与接口不变、可运行。
109
+ - 角色迁移不应改变 `werewolf/app.py` 的注册方式(外部入口不动)。
110
+
111
+ ### M4:结构化 Action + 事件解析(进阶稳定)
112
+ **目标**:内部统一用 Action 表达决策;外部统一由 Renderer 生成裁判需要的字符串。
113
+
114
+ **交付内容**
115
+ - 新增 `werewolf/core/actions.py`:`DiscussAction / VoteAction / SkillAction / PassAction / Sheriff*Action ...`
116
+ - 新增 `werewolf/core/action_parser.py` 与 `werewolf/core/action_renderer.py`
117
+ - 新增 `werewolf/core/event_parser.py` 与 `werewolf/core/state.py`
118
+ - 将 `STATUS_VOTE`, `STATUS_VOTE_RESULT`, `STATUS_NIGHT_INFO`, `STATUS_SKILL_RESULT` 等转为结构化 facts
119
+ - 为后续策略模块(嫌疑评分、共识函数)提供稳定输入
120
+
121
+ **验收标准**
122
+ - 所有角色都能走 Action→Renderer 的统一路径,减少分支散落字符串拼接。
123
+
124
+ ## 5. 关键实现规范(必须遵守)
125
+
126
+ ### 5.1 “结构化输出”强协议(所有角色通用)
127
+ - `vote/sheriff_vote/...`:`result` 必须是候选中的**一个名字**(不带任何额外字符)。
128
+ - `skill`:
129
+ - 目标型技能:必须是候选名字之一;若允许 noop,则必须是规则约定关键字(如“不开枪”)。
130
+ - 女巫:输出格式要与当前裁判期望一致(示例代码当前使用“救X/毒X/不使用”,需以线上规则为准)。
131
+ - `discuss`:允许自然语言,但必须做字数裁剪(规则上限:240 汉字)。
132
+
133
+ ### 5.2 Retry / Fallback 统一策略
134
+ - 解析/校验失败:
135
+ 1. 生成“纠错 prompt”重试一次(明确列出 allowed outputs / candidates)
136
+ 2. 再失败立即 fallback(不再调用 LLM)
137
+ - fallback 原则:
138
+ - `vote`:从候选中选一个(默认第一个/随机,但必须可配置;建议先确定性:第一个)
139
+ - `skill`:默认 noop(如“不开枪”),除非规则不允许 noop
140
+ - `wolf kill`:规则通常要求必须给出目标,否则可能“弃刀”;fallback 应返回一个合法候选以避免弃刀
141
+
142
+ ### 5.3 文件与导入约定(避免部署崩)
143
+ - 新增模块统一放在 `werewolf/core/`、`werewolf/tools/`
144
+ - 在 `werewolf/*.py` 中导入时使用脚本目录路径(例如 `from core.telemetry import ...`),不要引入需要包安装的复杂相对导入。
145
+
146
+ ## 6. 部署与冒烟检查(每次发布前必做)
147
+
148
+ ### 6.1 本地(或 CI)冒烟
149
+ ```bash
150
+ pip install -r requirements.txt
151
+ python werewolf/app.py
152
+ ```
153
+
154
+ ### 6.2 Docker 冒烟
155
+ ```bash
156
+ docker build -t werewolf-agent .
157
+ docker run --rm -e API_KEY=xxx -e BASE_URL=xxx -e MODEL_NAME=xxx werewolf-agent
158
+ ```
159
+
160
+ ### 6.3 发布前检查清单
161
+ - [ ] 入口仍为 `werewolf/app.py`,无额外启动命令要求
162
+ - [ ] 未新增必须联网下载的大依赖
163
+ - [ ] 所有 `interact()` 分支不会抛异常、不会返回 `None`
164
+ - [ ] `vote/skill` 输出永远合规(即使 LLM 输出乱码)
165
+ - [ ] `logs/` 写入失败不会影响主流程(最多降级为不写日志)
166
+
167
+ ## 7. 风险与决策记录(变更需更新本节)
168
+
169
+ - **SDK `SimpleMemory` 全局共享风险**:必须在 `M3` 前后完成隔离(自建 MemoryStore 或确保实例独立)。
170
+ - **SDK `llm_caller` 固定 system prompt**:不建议改 SDK;在我们的 prompt 模板中补足协议约束与纠错提示。
171
+ - **并发/异步问题**:当前 agent 代码非线程安全;若后续引入并发回放或多局同进程,必须先完成 memory 隔离与日志锁策略。
werewolf/core/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Core utilities for robust agent behavior (validation, fallback, telemetry, etc.).
2
+
werewolf/core/action_parser.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from agent_build_sdk.model.werewolf_model import (
4
+ STATUS_DISCUSS,
5
+ STATUS_SHERIFF,
6
+ STATUS_SHERIFF_ELECTION,
7
+ STATUS_SHERIFF_SPEECH,
8
+ STATUS_SHERIFF_PK,
9
+ STATUS_SHERIFF_SPEECH_ORDER,
10
+ STATUS_SHERIFF_VOTE,
11
+ STATUS_VOTE,
12
+ STATUS_SKILL,
13
+ STATUS_WOLF_SPEECH,
14
+ )
15
+
16
+ from core import ruleset
17
+ from core.actions import (
18
+ DiscussAction,
19
+ SheriffRunAction,
20
+ SheriffSpeechOrderAction,
21
+ SheriffTransferAction,
22
+ SheriffVoteAction,
23
+ SkillAction,
24
+ VoteAction,
25
+ )
26
+ from core.output_guard import normalize_llm_text
27
+
28
+
29
+ def parse_action(status: str, text: Optional[str]):
30
+ s = normalize_llm_text(text)
31
+ if status in {STATUS_DISCUSS, STATUS_WOLF_SPEECH, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_PK}:
32
+ return DiscussAction(text=s)
33
+ if status == STATUS_VOTE:
34
+ return VoteAction(target=s)
35
+ if status == STATUS_SKILL:
36
+ if s == ruleset.NO_SHOOT or s == ruleset.WITCH_NO_USE:
37
+ return SkillAction(target=None)
38
+ return SkillAction(target=s)
39
+ if status == STATUS_SHERIFF_VOTE:
40
+ return SheriffVoteAction(target=s)
41
+ if status == STATUS_SHERIFF:
42
+ return SheriffTransferAction(target=s)
43
+ if status == STATUS_SHERIFF_ELECTION:
44
+ if s == ruleset.SHERIFF_RUN:
45
+ return SheriffRunAction(join=True)
46
+ if s == ruleset.SHERIFF_NOT_RUN:
47
+ return SheriffRunAction(join=False)
48
+ return None
49
+ if status == STATUS_SHERIFF_SPEECH_ORDER:
50
+ return SheriffSpeechOrderAction(order=s)
51
+ return None
werewolf/core/action_renderer.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, Union
2
+
3
+ from agent_build_sdk.model.werewolf_model import STATUS_SKILL
4
+ from agent_build_sdk.model.roles import ROLE_WITCH
5
+
6
+ from core import ruleset
7
+ from core.actions import (
8
+ DiscussAction,
9
+ PassAction,
10
+ SheriffRunAction,
11
+ SheriffSpeechOrderAction,
12
+ SheriffTransferAction,
13
+ SheriffVoteAction,
14
+ SkillAction,
15
+ VoteAction,
16
+ )
17
+ from core.output_guard import clip_text
18
+
19
+
20
+ Action = Union[
21
+ DiscussAction,
22
+ VoteAction,
23
+ SkillAction,
24
+ PassAction,
25
+ SheriffRunAction,
26
+ SheriffVoteAction,
27
+ SheriffTransferAction,
28
+ SheriffSpeechOrderAction,
29
+ ]
30
+
31
+
32
+ def render_action(action: Action, *, status: Optional[str] = None, role: Optional[str] = None) -> str:
33
+ if isinstance(action, DiscussAction):
34
+ return clip_text(action.text, ruleset.MAX_DISCUSS_CHARS, fallback=ruleset.DEFAULT_DISCUSS_FALLBACK)
35
+ if isinstance(action, VoteAction):
36
+ return action.target
37
+ if isinstance(action, SkillAction):
38
+ if action.target is None:
39
+ if status == STATUS_SKILL and role == ROLE_WITCH:
40
+ return ruleset.WITCH_NO_USE
41
+ return ruleset.NO_SHOOT
42
+ return action.target
43
+ if isinstance(action, SheriffRunAction):
44
+ return ruleset.SHERIFF_RUN if action.join else ruleset.SHERIFF_NOT_RUN
45
+ if isinstance(action, SheriffVoteAction):
46
+ return action.target
47
+ if isinstance(action, SheriffTransferAction):
48
+ return action.target
49
+ if isinstance(action, SheriffSpeechOrderAction):
50
+ return action.order
51
+ if isinstance(action, PassAction):
52
+ return ""
53
+ raise ValueError(f"Unsupported action: {type(action)}")
werewolf/core/actions.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class DiscussAction:
7
+ text: str
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class VoteAction:
12
+ target: str
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SkillAction:
17
+ target: Optional[str]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class PassAction:
22
+ pass
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class SheriffRunAction:
27
+ join: bool
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class SheriffVoteAction:
32
+ target: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class SheriffTransferAction:
37
+ target: str
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SheriffSpeechOrderAction:
42
+ order: str
43
+
werewolf/core/base_role_agent.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Iterable, Optional, Sequence, Tuple
2
+
3
+ from agent_build_sdk.sdk.role_agent import BasicRoleAgent
4
+ from agent_build_sdk.model.werewolf_model import (
5
+ STATUS_DISCUSS,
6
+ STATUS_SHERIFF,
7
+ STATUS_SHERIFF_ELECTION,
8
+ STATUS_SHERIFF_PK,
9
+ STATUS_SHERIFF_SPEECH,
10
+ STATUS_SHERIFF_SPEECH_ORDER,
11
+ STATUS_SHERIFF_VOTE,
12
+ STATUS_SKILL,
13
+ STATUS_VOTE,
14
+ STATUS_WOLF_SPEECH,
15
+ )
16
+
17
+ from core import ruleset, telemetry
18
+ from core.action_parser import parse_action
19
+ from core.action_renderer import render_action
20
+ from core.event_parser import event_to_fact, parse_event
21
+ from core.memory_store import MemoryStore
22
+ from core.output_guard import (
23
+ clip_text,
24
+ correction_instruction_for_choices,
25
+ correction_instruction_for_enum,
26
+ guard_choice,
27
+ guard_enum,
28
+ guarded_meta,
29
+ )
30
+ from core.sanitizer import sanitize_player_text
31
+
32
+
33
+ class BaseRoleAgent(BasicRoleAgent):
34
+ """Shared helpers for guard/telemetry flow and safe memory updates."""
35
+
36
+ def __init__(self, role: str, model_name: str, *, memory=None) -> None:
37
+ super().__init__(role, memory=memory or MemoryStore(), model_name=model_name)
38
+
39
+ def append_player_message(self, name: str, message: Optional[str]) -> None:
40
+ cleaned = sanitize_player_text(message or "")
41
+ if cleaned:
42
+ self.memory.append_history(f"{name}: {cleaned}")
43
+ else:
44
+ self.memory.append_history(f"{name}: [filtered]")
45
+
46
+ def append_discuss_host(self, round_no: Optional[int]) -> None:
47
+ self.memory.append_history(f"主持人: 现在进入第{round_no}天。")
48
+ self.memory.append_history("主持人: 每个玩家描述自己的信息。")
49
+
50
+ def _get_state(self):
51
+ try:
52
+ if self.memory.has_variable("game_state"):
53
+ return self.memory.load_variable("game_state")
54
+ except Exception:
55
+ pass
56
+ from core.state import GameState
57
+
58
+ state = GameState()
59
+ self.memory.set_variable("game_state", state)
60
+ return state
61
+
62
+ def update_state(self, req) -> None:
63
+ try:
64
+ state = self._get_state()
65
+ if req.round is not None:
66
+ state.round_no = req.round
67
+ fact = event_to_fact(req)
68
+ if fact:
69
+ state.facts.append(fact)
70
+ if fact.get("type") == "sheriff" and fact.get("name"):
71
+ state.sheriff = fact.get("name")
72
+ if hasattr(self.memory, "append_raw"):
73
+ self.memory.append_raw(parse_event(req))
74
+ except Exception:
75
+ return
76
+
77
+ def _agent_name(self, fallback: str = "") -> str:
78
+ try:
79
+ if self.memory.has_variable("name"):
80
+ return self.memory.load_variable("name")
81
+ except Exception:
82
+ return fallback
83
+ return fallback
84
+
85
+ def _render_action_if_possible(self, status: str, result: str) -> str:
86
+ action_statuses = {
87
+ STATUS_DISCUSS,
88
+ STATUS_WOLF_SPEECH,
89
+ STATUS_SHERIFF_SPEECH,
90
+ STATUS_SHERIFF_PK,
91
+ STATUS_VOTE,
92
+ STATUS_SHERIFF_SPEECH_ORDER,
93
+ STATUS_SKILL,
94
+ STATUS_SHERIFF_ELECTION,
95
+ STATUS_SHERIFF_VOTE,
96
+ STATUS_SHERIFF,
97
+ }
98
+ if status not in action_statuses:
99
+ return result
100
+ action = parse_action(status, result)
101
+ if not action:
102
+ return result
103
+ rendered = render_action(action, status=status, role=str(self.role))
104
+ return rendered if rendered != "" else result
105
+
106
+ def decide_speech(self, *, req_status: str, round_no: Optional[int], prompt: str, kind: str) -> str:
107
+ raw1 = self.llm_caller(prompt)
108
+ result = clip_text(
109
+ raw1,
110
+ ruleset.MAX_DISCUSS_CHARS,
111
+ fallback=ruleset.DEFAULT_DISCUSS_FALLBACK,
112
+ )
113
+ result = self._render_action_if_possible(req_status, result)
114
+ telemetry.log_interact(
115
+ memory=self.memory,
116
+ role=str(self.role),
117
+ status=req_status,
118
+ round_no=round_no,
119
+ agent_name=self._agent_name(),
120
+ payload=telemetry.decision_payload(
121
+ prompt=prompt,
122
+ choices=None,
123
+ attempt1=(raw1, {"kind": kind, "clipped": len(raw1 or "") > ruleset.MAX_DISCUSS_CHARS}),
124
+ attempt2=None,
125
+ final_result=result,
126
+ ),
127
+ )
128
+ return result
129
+
130
+ def decide_enum(
131
+ self,
132
+ *,
133
+ req_status: str,
134
+ round_no: Optional[int],
135
+ prompt: str,
136
+ allowed: Sequence[str],
137
+ fallback: str,
138
+ ) -> str:
139
+ raw1 = self.llm_caller(prompt)
140
+ first = guard_enum(raw1, allowed, fallback=fallback)
141
+ if not first.valid:
142
+ retry_prompt = prompt + correction_instruction_for_enum(allowed)
143
+ raw2 = self.llm_caller(retry_prompt)
144
+ second = guard_enum(raw2, allowed, fallback=first.value)
145
+ result = second.value
146
+ else:
147
+ raw2 = None
148
+ second = None
149
+ result = first.value
150
+ result = self._render_action_if_possible(req_status, result)
151
+ telemetry.log_interact(
152
+ memory=self.memory,
153
+ role=str(self.role),
154
+ status=req_status,
155
+ round_no=round_no,
156
+ agent_name=self._agent_name(),
157
+ payload=telemetry.decision_payload(
158
+ prompt=prompt,
159
+ choices=list(allowed),
160
+ attempt1=(raw1, guarded_meta(first)),
161
+ attempt2=None if raw2 is None else (raw2, guarded_meta(second)),
162
+ final_result=result,
163
+ ),
164
+ )
165
+ return result
166
+
167
+ def decide_choice(
168
+ self,
169
+ *,
170
+ req_status: str,
171
+ round_no: Optional[int],
172
+ prompt: str,
173
+ choices: Sequence[str],
174
+ allow_extra: Optional[Iterable[str]] = None,
175
+ fallback: Optional[str] = None,
176
+ final_skill_target_from_result: Optional[Callable[[str], Optional[str]]] = None,
177
+ ) -> str:
178
+ allow_extra_list = list(allow_extra or [])
179
+ fallback_value = fallback
180
+ if fallback_value is None:
181
+ if choices:
182
+ fallback_value = choices[0]
183
+ elif allow_extra_list:
184
+ fallback_value = allow_extra_list[0]
185
+ else:
186
+ fallback_value = ""
187
+
188
+ raw1 = self.llm_caller(prompt)
189
+ first = guard_choice(raw1, choices, fallback=fallback_value, allow_extra=allow_extra_list)
190
+ if not first.valid:
191
+ retry_prompt = prompt + correction_instruction_for_choices(choices, allow_extra=allow_extra_list)
192
+ raw2 = self.llm_caller(retry_prompt)
193
+ second = guard_choice(raw2, choices, fallback=first.value, allow_extra=allow_extra_list)
194
+ result = second.value
195
+ else:
196
+ raw2 = None
197
+ second = None
198
+ result = first.value
199
+
200
+ final_skill_target = (
201
+ final_skill_target_from_result(result) if final_skill_target_from_result else None
202
+ )
203
+ result = self._render_action_if_possible(req_status, result)
204
+ log_choices = list(choices) + list(allow_extra_list)
205
+ telemetry.log_interact(
206
+ memory=self.memory,
207
+ role=str(self.role),
208
+ status=req_status,
209
+ round_no=round_no,
210
+ agent_name=self._agent_name(),
211
+ payload=telemetry.decision_payload(
212
+ prompt=prompt,
213
+ choices=log_choices,
214
+ attempt1=(raw1, guarded_meta(first)),
215
+ attempt2=None if raw2 is None else (raw2, guarded_meta(second)),
216
+ final_result=result,
217
+ final_skillTargetPlayer=final_skill_target,
218
+ ),
219
+ )
220
+ return result
werewolf/core/event_parser.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Optional
2
+
3
+ from agent_build_sdk.model.werewolf_model import (
4
+ AgentReq,
5
+ STATUS_DAY,
6
+ STATUS_HUNTER,
7
+ STATUS_HUNTER_RESULT,
8
+ STATUS_NIGHT,
9
+ STATUS_NIGHT_INFO,
10
+ STATUS_RESULT,
11
+ STATUS_SHERIFF_ELECTION,
12
+ STATUS_SHERIFF_PK,
13
+ STATUS_SHERIFF_SPEECH,
14
+ STATUS_SHERIFF_SPEECH_ORDER,
15
+ STATUS_SHERIFF_VOTE,
16
+ STATUS_SKILL_RESULT,
17
+ STATUS_VOTE,
18
+ STATUS_VOTE_RESULT,
19
+ STATUS_SHERIFF,
20
+ )
21
+
22
+
23
+ def parse_event(req: AgentReq) -> Dict[str, str]:
24
+ return {
25
+ "status": req.status or "",
26
+ "name": req.name or "",
27
+ "message": req.message or "",
28
+ "round": str(req.round) if req.round is not None else "",
29
+ "role": req.role or "",
30
+ }
31
+
32
+
33
+ def event_to_fact(req: AgentReq) -> Optional[Dict[str, str]]:
34
+ status = req.status or ""
35
+ if status == STATUS_VOTE:
36
+ return {
37
+ "type": "vote",
38
+ "voter": req.name or "",
39
+ "target": req.message or "",
40
+ }
41
+ if status == STATUS_VOTE_RESULT:
42
+ return {
43
+ "type": "vote_result",
44
+ "out": (req.name or req.message or ""),
45
+ }
46
+ if status == STATUS_NIGHT_INFO:
47
+ return {
48
+ "type": "night_info",
49
+ "message": req.message or "",
50
+ }
51
+ if status == STATUS_NIGHT:
52
+ return {
53
+ "type": "night",
54
+ "message": req.message or "",
55
+ }
56
+ if status == STATUS_DAY:
57
+ return {
58
+ "type": "day",
59
+ "message": req.message or "",
60
+ }
61
+ if status == STATUS_SKILL_RESULT:
62
+ return {
63
+ "type": "skill_result",
64
+ "name": req.name or "",
65
+ "message": req.message or "",
66
+ }
67
+ if status == STATUS_HUNTER:
68
+ return {
69
+ "type": "hunter",
70
+ "name": req.name or "",
71
+ "message": req.message or "",
72
+ }
73
+ if status == STATUS_HUNTER_RESULT:
74
+ return {
75
+ "type": "hunter_result",
76
+ "name": req.name or "",
77
+ "target": req.message or "",
78
+ }
79
+ if status == STATUS_RESULT:
80
+ return {
81
+ "type": "result",
82
+ "message": req.message or "",
83
+ }
84
+ if status == STATUS_SHERIFF_ELECTION:
85
+ return {
86
+ "type": "sheriff_election",
87
+ "message": req.message or "",
88
+ }
89
+ if status == STATUS_SHERIFF_SPEECH:
90
+ return {
91
+ "type": "sheriff_speech",
92
+ "name": req.name or "",
93
+ "message": req.message or "",
94
+ }
95
+ if status == STATUS_SHERIFF_PK:
96
+ return {
97
+ "type": "sheriff_pk",
98
+ "name": req.name or "",
99
+ "message": req.message or "",
100
+ }
101
+ if status == STATUS_SHERIFF_VOTE:
102
+ return {
103
+ "type": "sheriff_vote",
104
+ "name": req.name or "",
105
+ "target": req.message or "",
106
+ }
107
+ if status == STATUS_SHERIFF_SPEECH_ORDER:
108
+ return {
109
+ "type": "sheriff_speech_order",
110
+ "message": req.message or "",
111
+ }
112
+ if status == STATUS_SHERIFF and req.name:
113
+ return {
114
+ "type": "sheriff",
115
+ "name": req.name,
116
+ }
117
+ return None
werewolf/core/memory_store.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Any, Dict, List, Optional
3
+
4
+
5
+ _DEFAULT_HISTORY_LIMIT = int(os.getenv("HISTORY_MAX_ITEMS", "200"))
6
+ _DEFAULT_RAW_LOG_LIMIT = int(os.getenv("RAW_LOG_MAX_ITEMS", "1000"))
7
+
8
+
9
+ class MemoryStore:
10
+ """Per-agent memory store to avoid cross-agent pollution."""
11
+
12
+ def __init__(
13
+ self,
14
+ *,
15
+ history_limit: Optional[int] = None,
16
+ raw_log_limit: Optional[int] = None,
17
+ ) -> None:
18
+ self._memories: Dict[str, Any] = {}
19
+ self._history_limit = _DEFAULT_HISTORY_LIMIT if history_limit is None else history_limit
20
+ self._raw_log_limit = _DEFAULT_RAW_LOG_LIMIT if raw_log_limit is None else raw_log_limit
21
+
22
+ def load_variable(self, variable: str) -> Any:
23
+ return self._memories[variable]
24
+
25
+ def set_variable(self, variable: str, value: Any) -> None:
26
+ self._memories[variable] = value
27
+
28
+ def has_variable(self, variable: str) -> bool:
29
+ return variable in self._memories
30
+
31
+ def append_history(self, message: str) -> None:
32
+ if self.has_variable("history"):
33
+ history: List[str] = self.load_variable("history")
34
+ else:
35
+ history = []
36
+ if message:
37
+ history.append(message)
38
+ if self._history_limit and len(history) > self._history_limit:
39
+ history = history[-self._history_limit :]
40
+ self.set_variable("history", history)
41
+
42
+ def load_history(self) -> List[str]:
43
+ if self.has_variable("history"):
44
+ history: List[str] = self.load_variable("history")
45
+ else:
46
+ history = []
47
+ return history
48
+
49
+ def append_raw(self, record: Dict[str, Any]) -> None:
50
+ if self.has_variable("raw_log"):
51
+ raw_log: List[Dict[str, Any]] = self.load_variable("raw_log")
52
+ else:
53
+ raw_log = []
54
+ raw_log.append(record)
55
+ if self._raw_log_limit and len(raw_log) > self._raw_log_limit:
56
+ raw_log = raw_log[-self._raw_log_limit :]
57
+ self.set_variable("raw_log", raw_log)
58
+
59
+ def load_raw(self) -> List[Dict[str, Any]]:
60
+ if self.has_variable("raw_log"):
61
+ raw_log: List[Dict[str, Any]] = self.load_variable("raw_log")
62
+ else:
63
+ raw_log = []
64
+ return raw_log
65
+
66
+ def set_summary(self, summary: str) -> None:
67
+ self.set_variable("summary", summary)
68
+
69
+ def load_summary(self) -> str:
70
+ if self.has_variable("summary"):
71
+ return self.load_variable("summary")
72
+ return ""
73
+
74
+ def clear(self) -> None:
75
+ self._memories.clear()
werewolf/core/output_guard.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import re
5
+ from typing import Iterable, Optional, Sequence
6
+
7
+ from core import ruleset
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Guarded:
12
+ value: str
13
+ valid: bool
14
+ used_fallback: bool
15
+ reason: str
16
+ normalized: str
17
+
18
+
19
+ def guarded_meta(g: Guarded) -> dict:
20
+ return {
21
+ "value": g.value,
22
+ "valid": g.valid,
23
+ "used_fallback": g.used_fallback,
24
+ "reason": g.reason,
25
+ "normalized": g.normalized,
26
+ }
27
+
28
+
29
+ def normalize_llm_text(text: Optional[str]) -> str:
30
+ if not text:
31
+ return ""
32
+
33
+ s = str(text).strip()
34
+
35
+ s = re.sub(r"^\s*```(?:\w+)?\s*", "", s)
36
+ s = re.sub(r"\s*```\s*$", "", s)
37
+
38
+ s = s.strip()
39
+ if not s:
40
+ return ""
41
+
42
+ first_line = s.splitlines()[0].strip()
43
+ first_line = first_line.strip(" \t\r\n\"'“”‘’`")
44
+ return first_line
45
+
46
+
47
+ def normalize(text: Optional[str]) -> str:
48
+ return normalize_llm_text(text)
49
+
50
+
51
+ def clip_text(text: Optional[str], max_chars: int, *, fallback: str) -> str:
52
+ s = (text or "").strip()
53
+ if not s:
54
+ return fallback
55
+ if len(s) <= max_chars:
56
+ return s
57
+ return s[:max_chars]
58
+
59
+
60
+ def validate_text_len(text: Optional[str], max_chars: int) -> str:
61
+ return clip_text(text, max_chars, fallback="")
62
+
63
+
64
+ def _first_token(s: str) -> str:
65
+ if not s:
66
+ return ""
67
+ parts = re.split(r"[,,\s]+", s.strip())
68
+ return (parts[0] if parts else "").strip()
69
+
70
+
71
+ def extract_choice(text: Optional[str], choices: Sequence[str]) -> Optional[str]:
72
+ if not choices:
73
+ return None
74
+
75
+ normalized = normalize_llm_text(text)
76
+ if normalized in choices:
77
+ return normalized
78
+
79
+ token = _first_token(normalized)
80
+ if token in choices:
81
+ return token
82
+
83
+ raw = text or ""
84
+ if raw:
85
+ for c in choices:
86
+ if c and c in raw:
87
+ return c
88
+
89
+ m = re.search(r"(\d+)", normalized)
90
+ if m:
91
+ num = m.group(1)
92
+ for c in choices:
93
+ if c == num:
94
+ return c
95
+ m2 = re.search(r"(\d+)", c)
96
+ if m2 and m2.group(1) == num:
97
+ return c
98
+
99
+ return None
100
+
101
+
102
+ def validate_choice(result: Optional[str], choices: Sequence[str]) -> bool:
103
+ return extract_choice(result, choices) is not None
104
+
105
+
106
+ def guard_choice(
107
+ text: Optional[str],
108
+ choices: Sequence[str],
109
+ *,
110
+ fallback: Optional[str] = None,
111
+ allow_extra: Optional[Iterable[str]] = None,
112
+ ) -> Guarded:
113
+ normalized = normalize_llm_text(text)
114
+ allow = set(allow_extra or [])
115
+
116
+ extracted = extract_choice(text, choices)
117
+ if extracted is not None:
118
+ return Guarded(
119
+ value=extracted,
120
+ valid=True,
121
+ used_fallback=False,
122
+ reason="ok",
123
+ normalized=normalized,
124
+ )
125
+
126
+ if normalized in allow:
127
+ return Guarded(
128
+ value=normalized,
129
+ valid=True,
130
+ used_fallback=False,
131
+ reason="ok_allow_extra",
132
+ normalized=normalized,
133
+ )
134
+
135
+ fb = fallback or (choices[0] if choices else "")
136
+ return Guarded(
137
+ value=fb,
138
+ valid=False,
139
+ used_fallback=True,
140
+ reason="invalid_choice",
141
+ normalized=normalized,
142
+ )
143
+
144
+
145
+ def guarded_choice(
146
+ text: Optional[str],
147
+ choices: Sequence[str],
148
+ *,
149
+ fallback: Optional[str] = None,
150
+ allow_extra: Optional[Iterable[str]] = None,
151
+ ) -> Guarded:
152
+ return guard_choice(text, choices, fallback=fallback, allow_extra=allow_extra)
153
+
154
+
155
+ def normalize_enum(text: Optional[str]) -> str:
156
+ s = normalize_llm_text(text)
157
+ s = s.replace("撕毁", ruleset.SHERIFF_TEAR).replace("撕掉警徽", ruleset.SHERIFF_TEAR)
158
+
159
+ if s in {"不上", "不参加", "不參加"}:
160
+ return ruleset.SHERIFF_NOT_RUN
161
+ if s in {"上", "参加", "參加"}:
162
+ return ruleset.SHERIFF_RUN
163
+
164
+ return s
165
+
166
+
167
+ def guard_enum(text: Optional[str], allowed: Sequence[str], *, fallback: str) -> Guarded:
168
+ normalized = normalize_enum(text)
169
+ if normalized in allowed:
170
+ return Guarded(
171
+ value=normalized,
172
+ valid=True,
173
+ used_fallback=False,
174
+ reason="ok",
175
+ normalized=normalized,
176
+ )
177
+
178
+ for a in allowed:
179
+ if a and a in (text or ""):
180
+ return Guarded(
181
+ value=a,
182
+ valid=True,
183
+ used_fallback=False,
184
+ reason="ok_substring",
185
+ normalized=normalized,
186
+ )
187
+
188
+ return Guarded(
189
+ value=fallback,
190
+ valid=False,
191
+ used_fallback=True,
192
+ reason="invalid_enum",
193
+ normalized=normalized,
194
+ )
195
+
196
+
197
+ def guard_witch_skill(
198
+ text: Optional[str],
199
+ *,
200
+ self_name: str,
201
+ tonight_killed: str,
202
+ has_antidote: bool,
203
+ has_poison: bool,
204
+ ) -> Guarded:
205
+ normalized = normalize_llm_text(text)
206
+ normalized = normalized.replace(" ", "")
207
+
208
+ if normalized == ruleset.WITCH_NO_USE:
209
+ return Guarded(
210
+ value=ruleset.WITCH_NO_USE,
211
+ valid=True,
212
+ used_fallback=False,
213
+ reason="ok_no_use",
214
+ normalized=normalized,
215
+ )
216
+
217
+ if normalized.startswith("救"):
218
+ if not has_antidote:
219
+ return Guarded(
220
+ value=ruleset.WITCH_NO_USE,
221
+ valid=False,
222
+ used_fallback=True,
223
+ reason="no_antidote",
224
+ normalized=normalized,
225
+ )
226
+ if tonight_killed and normalized == f"救{tonight_killed}":
227
+ return Guarded(
228
+ value=normalized,
229
+ valid=True,
230
+ used_fallback=False,
231
+ reason="ok_save",
232
+ normalized=normalized,
233
+ )
234
+ return Guarded(
235
+ value=ruleset.WITCH_NO_USE,
236
+ valid=False,
237
+ used_fallback=True,
238
+ reason="invalid_save_target",
239
+ normalized=normalized,
240
+ )
241
+
242
+ if normalized.startswith("毒"):
243
+ if not has_poison:
244
+ return Guarded(
245
+ value=ruleset.WITCH_NO_USE,
246
+ valid=False,
247
+ used_fallback=True,
248
+ reason="no_poison",
249
+ normalized=normalized,
250
+ )
251
+ target = normalized[1:].strip("[]【】()()")
252
+ if not target:
253
+ return Guarded(
254
+ value=ruleset.WITCH_NO_USE,
255
+ valid=False,
256
+ used_fallback=True,
257
+ reason="empty_poison_target",
258
+ normalized=normalized,
259
+ )
260
+ if target == self_name:
261
+ return Guarded(
262
+ value=ruleset.WITCH_NO_USE,
263
+ valid=False,
264
+ used_fallback=True,
265
+ reason="poison_self",
266
+ normalized=normalized,
267
+ )
268
+ return Guarded(
269
+ value=f"毒{target}",
270
+ valid=True,
271
+ used_fallback=False,
272
+ reason="ok_poison",
273
+ normalized=normalized,
274
+ )
275
+
276
+ return Guarded(
277
+ value=ruleset.WITCH_NO_USE,
278
+ valid=False,
279
+ used_fallback=True,
280
+ reason="invalid_witch_skill",
281
+ normalized=normalized,
282
+ )
283
+
284
+
285
+ def correction_instruction_for_choices(choices: Sequence[str], *, allow_extra: Optional[Iterable[str]] = None) -> str:
286
+ allow = list(allow_extra or [])
287
+ all_allowed = list(choices) + allow
288
+ allowed_text = ",".join(all_allowed)
289
+ return (
290
+ "\n\n【纠错】你的上一条输出不符合格式。"
291
+ f"你必须且只能输出以下之一:{allowed_text}。"
292
+ "不要输出任何解释、标点或多余字符,只输出最终答案。"
293
+ )
294
+
295
+
296
+ def correction_instruction_for_enum(allowed: Sequence[str]) -> str:
297
+ allowed_text = " / ".join(allowed)
298
+ return (
299
+ "\n\n【纠错】你的上一条输出不符合格式。"
300
+ f"你必须且只能输出:{allowed_text}。"
301
+ "不要输出任何解释或多余字符,只输出最终答案。"
302
+ )
werewolf/core/ruleset.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MAX_DISCUSS_CHARS = 240
2
+
3
+ DEFAULT_DISCUSS_FALLBACK = "我暂时没有更多信息。"
4
+
5
+ NO_SHOOT = "不开枪"
6
+ WITCH_NO_USE = "不使用"
7
+ SHERIFF_TEAR = "撕掉"
8
+
9
+ SHERIFF_RUN = "上警"
10
+ SHERIFF_NOT_RUN = "不上警"
11
+
12
+ SPEECH_ORDER_CW = "顺时针"
13
+ SPEECH_ORDER_CCW = "逆时针"
14
+
werewolf/core/sanitizer.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import Iterable
3
+
4
+
5
+ _INJECTION_PATTERNS: Iterable[re.Pattern] = [
6
+ re.compile(r"^\s*(system|assistant|developer|user)\s*[::]", re.IGNORECASE),
7
+ re.compile(r"^\s*(主持人提示|系统提示|裁判提示)\s*[::]?"),
8
+ re.compile(r"^\s*游戏规则更新\s*[::]?"),
9
+ ]
10
+
11
+ _INLINE_MARKERS = re.compile(
12
+ r"(system|assistant|developer|user)\s*[::]|主持人提示|系统提示|裁判提示|游戏规则更新",
13
+ re.IGNORECASE,
14
+ )
15
+
16
+
17
+ def sanitize_player_text(text: str) -> str:
18
+ if not text:
19
+ return ""
20
+
21
+ lines = str(text).splitlines()
22
+ kept = []
23
+ for line in lines:
24
+ if any(p.search(line) for p in _INJECTION_PATTERNS):
25
+ continue
26
+ kept.append(line)
27
+
28
+ if kept:
29
+ return "\n".join(kept).strip()
30
+
31
+ # Fallback: remove inline markers but keep the rest of the message.
32
+ cleaned = _INLINE_MARKERS.sub("", str(text))
33
+ return cleaned.strip()
34
+
werewolf/core/state.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List, Optional
3
+
4
+
5
+ @dataclass
6
+ class GameState:
7
+ round_no: Optional[int] = None
8
+ alive_players: List[str] = field(default_factory=list)
9
+ sheriff: Optional[str] = None
10
+ facts: List[Dict[str, str]] = field(default_factory=list)
11
+
werewolf/core/telemetry.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import time
7
+ import uuid
8
+ from dataclasses import asdict, is_dataclass
9
+ from typing import Any, Dict, Optional, Sequence, Tuple
10
+
11
+
12
+ def _env_flag(name: str, default: str = "1") -> bool:
13
+ v = os.getenv(name, default).strip().lower()
14
+ return v not in {"0", "false", "no", "off", ""}
15
+
16
+
17
+ def _safe_str(v: Any) -> str:
18
+ if v is None:
19
+ return ""
20
+ return str(v)
21
+
22
+
23
+ def _hash_text(s: str) -> str:
24
+ h = hashlib.sha256()
25
+ h.update(s.encode("utf-8", errors="replace"))
26
+ return h.hexdigest()
27
+
28
+
29
+ def _prompt_payload(prompt: Optional[str]) -> Dict[str, Any]:
30
+ if not prompt:
31
+ return {}
32
+
33
+ mode = os.getenv("TELEMETRY_PROMPT_MODE", "hash").strip().lower()
34
+ if mode == "none":
35
+ return {"prompt_hash": _hash_text(prompt)}
36
+ if mode == "full":
37
+ return {"prompt_hash": _hash_text(prompt), "prompt": prompt}
38
+
39
+ preview_len = int(os.getenv("TELEMETRY_PROMPT_PREVIEW", "256"))
40
+ return {
41
+ "prompt_hash": _hash_text(prompt),
42
+ "prompt_preview": prompt[: max(0, preview_len)],
43
+ }
44
+
45
+
46
+ def _jsonable(v: Any) -> Any:
47
+ if v is None:
48
+ return None
49
+ if is_dataclass(v):
50
+ return {k: _jsonable(val) for k, val in asdict(v).items()}
51
+ if isinstance(v, dict):
52
+ return {str(k): _jsonable(val) for k, val in v.items()}
53
+ if isinstance(v, (list, tuple)):
54
+ return [_jsonable(x) for x in v]
55
+ if isinstance(v, (str, int, float, bool)):
56
+ return v
57
+ return _safe_str(v)
58
+
59
+
60
+ def _safe_mkdir(path: str) -> None:
61
+ os.makedirs(path, exist_ok=True)
62
+
63
+
64
+ def _safe_append_jsonl(path: str, record: Dict[str, Any]) -> None:
65
+ _safe_mkdir(os.path.dirname(path))
66
+ line = json.dumps(record, ensure_ascii=False)
67
+ with open(path, "a", encoding="utf-8") as f:
68
+ f.write(line + "\n")
69
+
70
+
71
+ def _memory_get(memory: Any, key: str, default: Any = None) -> Any:
72
+ try:
73
+ if hasattr(memory, "has_variable") and memory.has_variable(key):
74
+ return memory.load_variable(key)
75
+ except Exception:
76
+ return default
77
+ return default
78
+
79
+
80
+ def _memory_set(memory: Any, key: str, value: Any) -> None:
81
+ try:
82
+ if hasattr(memory, "set_variable"):
83
+ memory.set_variable(key, value)
84
+ except Exception:
85
+ return
86
+
87
+
88
+ def decision_payload(
89
+ *,
90
+ prompt: Optional[str],
91
+ choices: Optional[Sequence[str]],
92
+ attempt1: Tuple[Optional[str], Dict[str, Any]],
93
+ attempt2: Optional[Tuple[Optional[str], Dict[str, Any]]],
94
+ final_result: str,
95
+ final_skillTargetPlayer: Optional[str] = None,
96
+ ) -> Dict[str, Any]:
97
+ payload: Dict[str, Any] = {
98
+ "choices": list(choices) if choices is not None else None,
99
+ "attempt1": {"raw": attempt1[0], "meta": attempt1[1]},
100
+ "attempt2": None if attempt2 is None else {"raw": attempt2[0], "meta": attempt2[1]},
101
+ "final_result": final_result,
102
+ "final_skillTargetPlayer": final_skillTargetPlayer,
103
+ }
104
+ payload.update(_prompt_payload(prompt))
105
+ return _jsonable(payload)
106
+
107
+
108
+ def _derive_from_attempts(payload: Dict[str, Any]) -> Dict[str, Any]:
109
+ a1 = (payload.get("attempt1") or {})
110
+ a2 = payload.get("attempt2")
111
+ last = a2 or a1
112
+ meta = (last.get("meta") or {}) if isinstance(last, dict) else {}
113
+
114
+ parse_valid = meta.get("valid") if isinstance(meta, dict) else None
115
+ fallback_used = meta.get("used_fallback") if isinstance(meta, dict) else None
116
+ fallback_reason = meta.get("reason") if isinstance(meta, dict) else None
117
+ llm_normalized = meta.get("normalized") if isinstance(meta, dict) else None
118
+
119
+ return {
120
+ "llm_raw": (a1.get("raw") if isinstance(a1, dict) else None),
121
+ "llm_retry_raw": (a2.get("raw") if isinstance(a2, dict) else None) if isinstance(a2, dict) else None,
122
+ "llm_normalized": llm_normalized,
123
+ "parse_valid": parse_valid,
124
+ "retry_count": 1 if a2 is not None else 0,
125
+ "fallback_used": fallback_used,
126
+ "fallback_reason": fallback_reason,
127
+ }
128
+
129
+
130
+ def log_interact(
131
+ *,
132
+ memory: Any,
133
+ role: str,
134
+ status: str,
135
+ round_no: Optional[int],
136
+ agent_name: str,
137
+ payload: Dict[str, Any],
138
+ ) -> None:
139
+ if not _env_flag("TELEMETRY_ENABLED", "1"):
140
+ return
141
+
142
+ try:
143
+ base_dir = os.getenv("TELEMETRY_DIR", "logs")
144
+ sid = _memory_get(memory, "session_id")
145
+ if not sid:
146
+ ts = int(time.time())
147
+ sid = f"{ts}_{role}_{agent_name}_{uuid.uuid4().hex[:8]}"
148
+ _memory_set(memory, "session_id", sid)
149
+
150
+ derived = _derive_from_attempts(payload)
151
+
152
+ attempt1 = payload.get("attempt1") if isinstance(payload, dict) else None
153
+ attempt2 = payload.get("attempt2") if isinstance(payload, dict) else None
154
+ attempt1_raw = attempt1.get("raw") if isinstance(attempt1, dict) else None
155
+ attempt1_meta = attempt1.get("meta") if isinstance(attempt1, dict) else None
156
+ attempt2_raw = attempt2.get("raw") if isinstance(attempt2, dict) else None
157
+ attempt2_meta = attempt2.get("meta") if isinstance(attempt2, dict) else None
158
+ record: Dict[str, Any] = {
159
+ "ts": time.time(),
160
+ "session_id": _safe_str(sid),
161
+ "role": role,
162
+ "name": agent_name,
163
+ "round": round_no,
164
+ "status": status,
165
+ "final_result": payload.get("final_result"),
166
+ "final_skillTargetPlayer": payload.get("final_skillTargetPlayer"),
167
+ "choices": payload.get("choices"),
168
+ "attempt1_raw": attempt1_raw,
169
+ "attempt1_meta": attempt1_meta,
170
+ "attempt2_raw": attempt2_raw,
171
+ "attempt2_meta": attempt2_meta,
172
+ **derived,
173
+ "payload": payload,
174
+ }
175
+
176
+ filename = f"{round_no}_{status}.jsonl" if round_no is not None else f"na_{status}.jsonl"
177
+ path = os.path.join(base_dir, _safe_str(sid), role, filename)
178
+ _safe_append_jsonl(path, _jsonable(record))
179
+ except Exception:
180
+ return
werewolf/guard/guard_agent.py CHANGED
@@ -1,17 +1,18 @@
1
- from agent_build_sdk.model.roles import ROLE_GUARD
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
7
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
8
  from agent_build_sdk.sdk.agent import format_prompt
 
 
9
  from guard.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
10
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
11
  SHERIFF_TRANSFER_PROMPT
12
 
13
 
14
- class GuardAgent(BasicRoleAgent):
15
  """守卫角色Agent"""
16
 
17
  def __init__(self, model_name):
@@ -38,15 +39,10 @@ class GuardAgent(BasicRoleAgent):
38
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
39
  elif req.status == STATUS_DISCUSS: # 发言环节
40
  if req.name:
41
- # 其他玩家发言
42
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
43
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
44
- # req.message = self.llm_caller(clean_user_message_prompt)
45
- self.memory.append_history(req.name + ': ' + req.message)
46
  else:
47
  # 主持人发言
48
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
49
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
50
  self.memory.append_history("---------------------------------------------")
51
  elif req.status == STATUS_VOTE: # 投票环节
52
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
@@ -86,6 +82,8 @@ class GuardAgent(BasicRoleAgent):
86
  else:
87
  raise NotImplementedError
88
 
 
 
89
  def interact(self, req=AgentReq) -> AgentResp:
90
  logger.info("guard interact: {}".format(req))
91
  if req.status == STATUS_DISCUSS:
@@ -99,27 +97,43 @@ class GuardAgent(BasicRoleAgent):
99
  "history": "\n".join(self.memory.load_history())
100
  })
101
  logger.info("prompt:" + prompt)
102
- result = self.llm_caller(prompt)
 
 
 
 
 
103
  logger.info("guard interact result: {}".format(result))
104
  return AgentResp(success=True, result=result, errMsg=None)
105
 
106
  elif req.status == STATUS_VOTE:
107
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
108
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")]
 
 
 
109
  self.memory.set_variable("choices", choices)
110
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
111
  "choices": choices,
112
  "history": "\n".join(self.memory.load_history())
113
  })
114
  logger.info("prompt:" + prompt)
115
- result = self.llm_caller(prompt)
 
 
 
 
 
116
  logger.info("guard interact result: {}".format(result))
117
  return AgentResp(success=True, result=result, errMsg=None)
118
 
119
  elif req.status == STATUS_SKILL:
120
  # 守卫技能:守护一名玩家
121
  last_guarded = self.memory.load_variable("last_guarded")
122
- choices = [name for name in req.message.split(",") if name != last_guarded]
 
 
 
123
  prompt = format_prompt(SKILL_PROMPT, {
124
  "name": self.memory.load_variable("name"),
125
  "last_guarded": last_guarded if last_guarded else "无",
@@ -127,7 +141,13 @@ class GuardAgent(BasicRoleAgent):
127
  "history": "\n".join(self.memory.load_history())
128
  })
129
  logger.info("prompt:" + prompt)
130
- result = self.llm_caller(prompt)
 
 
 
 
 
 
131
  logger.info("guard skill result: {}".format(result))
132
 
133
  # 更新守护记录
@@ -144,7 +164,13 @@ class GuardAgent(BasicRoleAgent):
144
  "history": "\n".join(self.memory.load_history())
145
  })
146
  logger.info("prompt:" + prompt)
147
- result = self.llm_caller(prompt)
 
 
 
 
 
 
148
  return AgentResp(success=True, result=result, errMsg=None)
149
 
150
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
@@ -156,7 +182,13 @@ class GuardAgent(BasicRoleAgent):
156
  "history": "\n".join(self.memory.load_history())
157
  })
158
  logger.info("prompt:" + prompt)
159
- result = self.llm_caller(prompt)
 
 
 
 
 
 
160
  return AgentResp(success=True, result=result, errMsg=None)
161
 
162
  elif req.status == STATUS_SHERIFF_VOTE:
@@ -167,7 +199,12 @@ class GuardAgent(BasicRoleAgent):
167
  "history": "\n".join(self.memory.load_history())
168
  })
169
  logger.info("prompt:" + prompt)
170
- result = self.llm_caller(prompt)
 
 
 
 
 
171
  return AgentResp(success=True, result=result, errMsg=None)
172
 
173
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
@@ -176,7 +213,13 @@ class GuardAgent(BasicRoleAgent):
176
  "history": "\n".join(self.memory.load_history())
177
  })
178
  logger.info("prompt:" + prompt)
179
- result = self.llm_caller(prompt)
 
 
 
 
 
 
180
  return AgentResp(success=True, result=result, errMsg=None)
181
 
182
  elif req.status == STATUS_SHERIFF:
@@ -188,7 +231,14 @@ class GuardAgent(BasicRoleAgent):
188
  "history": "\n".join(self.memory.load_history())
189
  })
190
  logger.info("prompt:" + prompt)
191
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
192
  return AgentResp(success=True, result=result, errMsg=None)
193
  else:
194
  raise NotImplementedError
 
1
+ from agent_build_sdk.model.roles import ROLE_GUARD
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
 
7
  from agent_build_sdk.sdk.agent import format_prompt
8
+ from core import ruleset
9
+ from core.base_role_agent import BaseRoleAgent
10
  from guard.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
11
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
12
  SHERIFF_TRANSFER_PROMPT
13
 
14
 
15
+ class GuardAgent(BaseRoleAgent):
16
  """守卫角色Agent"""
17
 
18
  def __init__(self, model_name):
 
39
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
40
  elif req.status == STATUS_DISCUSS: # 发言环节
41
  if req.name:
42
+ self.append_player_message(req.name, req.message)
 
 
 
 
43
  else:
44
  # 主持人发言
45
+ self.append_discuss_host(req.round)
 
46
  self.memory.append_history("---------------------------------------------")
47
  elif req.status == STATUS_VOTE: # 投票环节
48
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
 
82
  else:
83
  raise NotImplementedError
84
 
85
+ self.update_state(req)
86
+
87
  def interact(self, req=AgentReq) -> AgentResp:
88
  logger.info("guard interact: {}".format(req))
89
  if req.status == STATUS_DISCUSS:
 
97
  "history": "\n".join(self.memory.load_history())
98
  })
99
  logger.info("prompt:" + prompt)
100
+ result = self.decide_speech(
101
+ req_status=req.status,
102
+ round_no=req.round,
103
+ prompt=prompt,
104
+ kind="discuss",
105
+ )
106
  logger.info("guard interact result: {}".format(result))
107
  return AgentResp(success=True, result=result, errMsg=None)
108
 
109
  elif req.status == STATUS_VOTE:
110
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
111
+ raw_choices = [name for name in (req.message or "").split(",") if name]
112
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")]
113
+ if not choices:
114
+ choices = raw_choices
115
  self.memory.set_variable("choices", choices)
116
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
117
  "choices": choices,
118
  "history": "\n".join(self.memory.load_history())
119
  })
120
  logger.info("prompt:" + prompt)
121
+ result = self.decide_choice(
122
+ req_status=req.status,
123
+ round_no=req.round,
124
+ prompt=prompt,
125
+ choices=choices,
126
+ )
127
  logger.info("guard interact result: {}".format(result))
128
  return AgentResp(success=True, result=result, errMsg=None)
129
 
130
  elif req.status == STATUS_SKILL:
131
  # 守卫技能:守护一名玩家
132
  last_guarded = self.memory.load_variable("last_guarded")
133
+ raw_choices = [name for name in (req.message or "").split(",") if name]
134
+ choices = [name for name in raw_choices if name != last_guarded]
135
+ if not choices:
136
+ choices = raw_choices
137
  prompt = format_prompt(SKILL_PROMPT, {
138
  "name": self.memory.load_variable("name"),
139
  "last_guarded": last_guarded if last_guarded else "无",
 
141
  "history": "\n".join(self.memory.load_history())
142
  })
143
  logger.info("prompt:" + prompt)
144
+ result = self.decide_choice(
145
+ req_status=req.status,
146
+ round_no=req.round,
147
+ prompt=prompt,
148
+ choices=choices,
149
+ final_skill_target_from_result=lambda r: r,
150
+ )
151
  logger.info("guard skill result: {}".format(result))
152
 
153
  # 更新守护记录
 
164
  "history": "\n".join(self.memory.load_history())
165
  })
166
  logger.info("prompt:" + prompt)
167
+ result = self.decide_enum(
168
+ req_status=req.status,
169
+ round_no=req.round,
170
+ prompt=prompt,
171
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
172
+ fallback=ruleset.SHERIFF_NOT_RUN,
173
+ )
174
  return AgentResp(success=True, result=result, errMsg=None)
175
 
176
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
 
182
  "history": "\n".join(self.memory.load_history())
183
  })
184
  logger.info("prompt:" + prompt)
185
+ kind = "sheriff_pk" if req.status == STATUS_SHERIFF_PK else "sheriff_speech"
186
+ result = self.decide_speech(
187
+ req_status=req.status,
188
+ round_no=req.round,
189
+ prompt=prompt,
190
+ kind=kind,
191
+ )
192
  return AgentResp(success=True, result=result, errMsg=None)
193
 
194
  elif req.status == STATUS_SHERIFF_VOTE:
 
199
  "history": "\n".join(self.memory.load_history())
200
  })
201
  logger.info("prompt:" + prompt)
202
+ result = self.decide_choice(
203
+ req_status=req.status,
204
+ round_no=req.round,
205
+ prompt=prompt,
206
+ choices=choices,
207
+ )
208
  return AgentResp(success=True, result=result, errMsg=None)
209
 
210
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
 
213
  "history": "\n".join(self.memory.load_history())
214
  })
215
  logger.info("prompt:" + prompt)
216
+ result = self.decide_enum(
217
+ req_status=req.status,
218
+ round_no=req.round,
219
+ prompt=prompt,
220
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
221
+ fallback=ruleset.SPEECH_ORDER_CW,
222
+ )
223
  return AgentResp(success=True, result=result, errMsg=None)
224
 
225
  elif req.status == STATUS_SHERIFF:
 
231
  "history": "\n".join(self.memory.load_history())
232
  })
233
  logger.info("prompt:" + prompt)
234
+ result = self.decide_choice(
235
+ req_status=req.status,
236
+ round_no=req.round,
237
+ prompt=prompt,
238
+ choices=choices,
239
+ allow_extra=[ruleset.SHERIFF_TEAR],
240
+ fallback=ruleset.SHERIFF_TEAR,
241
+ )
242
  return AgentResp(success=True, result=result, errMsg=None)
243
  else:
244
  raise NotImplementedError
werewolf/hunter/hunter_agent.py CHANGED
@@ -1,17 +1,18 @@
1
- from agent_build_sdk.model.roles import ROLE_HUNTER
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK
6
  from agent_build_sdk.utils.logger import logger
7
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
8
  from agent_build_sdk.sdk.agent import format_prompt
 
 
9
  from hunter.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
10
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
11
  SHERIFF_TRANSFER_PROMPT
12
 
13
 
14
- class HunterAgent(BasicRoleAgent):
15
  """猎人角色Agent"""
16
 
17
  def __init__(self, model_name):
@@ -38,15 +39,10 @@ class HunterAgent(BasicRoleAgent):
38
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
39
  elif req.status == STATUS_DISCUSS: # 发言环节
40
  if req.name:
41
- # 其他玩家发言
42
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
43
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
44
- # req.message = self.llm_caller(clean_user_message_prompt)
45
- self.memory.append_history(req.name + ': ' + req.message)
46
  else:
47
  # 主持人发言
48
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
49
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
50
  self.memory.append_history("---------------------------------------------")
51
  elif req.status == STATUS_VOTE: # 投票环节
52
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
@@ -79,6 +75,8 @@ class HunterAgent(BasicRoleAgent):
79
  else:
80
  raise NotImplementedError
81
 
 
 
82
  def interact(self, req=AgentReq) -> AgentResp:
83
  logger.info("hunter interact: {}".format(req))
84
  if req.status == STATUS_DISCUSS:
@@ -92,20 +90,33 @@ class HunterAgent(BasicRoleAgent):
92
  "history": "\n".join(self.memory.load_history())
93
  })
94
  logger.info("prompt:" + prompt)
95
- result = self.llm_caller(prompt)
 
 
 
 
 
96
  logger.info("hunter interact result: {}".format(result))
97
  return AgentResp(success=True, result=result, errMsg=None)
98
 
99
  elif req.status == STATUS_VOTE:
100
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
101
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")]
 
 
 
102
  self.memory.set_variable("choices", choices)
103
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
104
  "choices": choices,
105
  "history": "\n".join(self.memory.load_history())
106
  })
107
  logger.info("prompt:" + prompt)
108
- result = self.llm_caller(prompt)
 
 
 
 
 
109
  logger.info("hunter interact result: {}".format(result))
110
  return AgentResp(success=True, result=result, errMsg=None)
111
 
@@ -113,22 +124,33 @@ class HunterAgent(BasicRoleAgent):
113
  # 猎人技能:开枪射杀一名玩家(遗言阶段)
114
  can_shoot = self.memory.load_variable("can_shoot")
115
  if not can_shoot:
116
- return AgentResp(success=True, result="不开枪", errMsg=None)
117
 
118
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")]
 
 
 
119
  prompt = format_prompt(SKILL_PROMPT, {
120
  "name": self.memory.load_variable("name"),
121
  "choices": choices,
122
  "history": "\n".join(self.memory.load_history())
123
  })
124
  logger.info("prompt:" + prompt)
125
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
 
126
  logger.info("hunter skill result: {}".format(result))
127
 
128
- if result != "不开枪":
129
  self.memory.set_variable("can_shoot", False)
130
 
131
- return AgentResp(success=True, result=result, skillTargetPlayer=None if result == "不开枪" else result, errMsg=None)
132
 
133
  elif req.status == STATUS_SHERIFF_ELECTION:
134
  can_shoot = self.memory.load_variable("can_shoot")
@@ -139,7 +161,13 @@ class HunterAgent(BasicRoleAgent):
139
  "history": "\n".join(self.memory.load_history())
140
  })
141
  logger.info("prompt:" + prompt)
142
- result = self.llm_caller(prompt)
 
 
 
 
 
 
143
  return AgentResp(success=True, result=result, errMsg=None)
144
 
145
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
@@ -151,7 +179,13 @@ class HunterAgent(BasicRoleAgent):
151
  "history": "\n".join(self.memory.load_history())
152
  })
153
  logger.info("prompt:" + prompt)
154
- result = self.llm_caller(prompt)
 
 
 
 
 
 
155
  return AgentResp(success=True, result=result, errMsg=None)
156
 
157
  elif req.status == STATUS_SHERIFF_VOTE:
@@ -162,7 +196,12 @@ class HunterAgent(BasicRoleAgent):
162
  "history": "\n".join(self.memory.load_history())
163
  })
164
  logger.info("prompt:" + prompt)
165
- result = self.llm_caller(prompt)
 
 
 
 
 
166
  return AgentResp(success=True, result=result, errMsg=None)
167
 
168
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
@@ -171,7 +210,13 @@ class HunterAgent(BasicRoleAgent):
171
  "history": "\n".join(self.memory.load_history())
172
  })
173
  logger.info("prompt:" + prompt)
174
- result = self.llm_caller(prompt)
 
 
 
 
 
 
175
  return AgentResp(success=True, result=result, errMsg=None)
176
 
177
  elif req.status == STATUS_SHERIFF:
@@ -186,7 +231,14 @@ class HunterAgent(BasicRoleAgent):
186
  "history": "\n".join(self.memory.load_history())
187
  })
188
  logger.info("prompt:" + prompt)
189
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
190
  return AgentResp(success=True, result=result, errMsg=None)
191
  else:
192
  raise NotImplementedError
 
1
+ from agent_build_sdk.model.roles import ROLE_HUNTER
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK
6
  from agent_build_sdk.utils.logger import logger
 
7
  from agent_build_sdk.sdk.agent import format_prompt
8
+ from core import ruleset
9
+ from core.base_role_agent import BaseRoleAgent
10
  from hunter.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
11
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
12
  SHERIFF_TRANSFER_PROMPT
13
 
14
 
15
+ class HunterAgent(BaseRoleAgent):
16
  """猎人角色Agent"""
17
 
18
  def __init__(self, model_name):
 
39
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
40
  elif req.status == STATUS_DISCUSS: # 发言环节
41
  if req.name:
42
+ self.append_player_message(req.name, req.message)
 
 
 
 
43
  else:
44
  # 主持人发言
45
+ self.append_discuss_host(req.round)
 
46
  self.memory.append_history("---------------------------------------------")
47
  elif req.status == STATUS_VOTE: # 投票环节
48
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
 
75
  else:
76
  raise NotImplementedError
77
 
78
+ self.update_state(req)
79
+
80
  def interact(self, req=AgentReq) -> AgentResp:
81
  logger.info("hunter interact: {}".format(req))
82
  if req.status == STATUS_DISCUSS:
 
90
  "history": "\n".join(self.memory.load_history())
91
  })
92
  logger.info("prompt:" + prompt)
93
+ result = self.decide_speech(
94
+ req_status=req.status,
95
+ round_no=req.round,
96
+ prompt=prompt,
97
+ kind="discuss",
98
+ )
99
  logger.info("hunter interact result: {}".format(result))
100
  return AgentResp(success=True, result=result, errMsg=None)
101
 
102
  elif req.status == STATUS_VOTE:
103
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
104
+ raw_choices = [name for name in (req.message or "").split(",") if name]
105
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")]
106
+ if not choices:
107
+ choices = raw_choices
108
  self.memory.set_variable("choices", choices)
109
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
110
  "choices": choices,
111
  "history": "\n".join(self.memory.load_history())
112
  })
113
  logger.info("prompt:" + prompt)
114
+ result = self.decide_choice(
115
+ req_status=req.status,
116
+ round_no=req.round,
117
+ prompt=prompt,
118
+ choices=choices,
119
+ )
120
  logger.info("hunter interact result: {}".format(result))
121
  return AgentResp(success=True, result=result, errMsg=None)
122
 
 
124
  # 猎人技能:开枪射杀一名玩家(遗言阶段)
125
  can_shoot = self.memory.load_variable("can_shoot")
126
  if not can_shoot:
127
+ return AgentResp(success=True, result=ruleset.NO_SHOOT, errMsg=None)
128
 
129
+ raw_choices = [name for name in (req.message or "").split(",") if name]
130
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")]
131
+ if not choices:
132
+ choices = raw_choices
133
  prompt = format_prompt(SKILL_PROMPT, {
134
  "name": self.memory.load_variable("name"),
135
  "choices": choices,
136
  "history": "\n".join(self.memory.load_history())
137
  })
138
  logger.info("prompt:" + prompt)
139
+ result = self.decide_choice(
140
+ req_status=req.status,
141
+ round_no=req.round,
142
+ prompt=prompt,
143
+ choices=choices,
144
+ allow_extra=[ruleset.NO_SHOOT],
145
+ fallback=ruleset.NO_SHOOT,
146
+ final_skill_target_from_result=lambda r: None if r == ruleset.NO_SHOOT else r,
147
+ )
148
  logger.info("hunter skill result: {}".format(result))
149
 
150
+ if result != ruleset.NO_SHOOT:
151
  self.memory.set_variable("can_shoot", False)
152
 
153
+ return AgentResp(success=True, result=result, skillTargetPlayer=None if result == ruleset.NO_SHOOT else result, errMsg=None)
154
 
155
  elif req.status == STATUS_SHERIFF_ELECTION:
156
  can_shoot = self.memory.load_variable("can_shoot")
 
161
  "history": "\n".join(self.memory.load_history())
162
  })
163
  logger.info("prompt:" + prompt)
164
+ result = self.decide_enum(
165
+ req_status=req.status,
166
+ round_no=req.round,
167
+ prompt=prompt,
168
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
169
+ fallback=ruleset.SHERIFF_NOT_RUN,
170
+ )
171
  return AgentResp(success=True, result=result, errMsg=None)
172
 
173
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
 
179
  "history": "\n".join(self.memory.load_history())
180
  })
181
  logger.info("prompt:" + prompt)
182
+ kind = "sheriff_pk" if req.status == STATUS_SHERIFF_PK else "sheriff_speech"
183
+ result = self.decide_speech(
184
+ req_status=req.status,
185
+ round_no=req.round,
186
+ prompt=prompt,
187
+ kind=kind,
188
+ )
189
  return AgentResp(success=True, result=result, errMsg=None)
190
 
191
  elif req.status == STATUS_SHERIFF_VOTE:
 
196
  "history": "\n".join(self.memory.load_history())
197
  })
198
  logger.info("prompt:" + prompt)
199
+ result = self.decide_choice(
200
+ req_status=req.status,
201
+ round_no=req.round,
202
+ prompt=prompt,
203
+ choices=choices,
204
+ )
205
  return AgentResp(success=True, result=result, errMsg=None)
206
 
207
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
 
210
  "history": "\n".join(self.memory.load_history())
211
  })
212
  logger.info("prompt:" + prompt)
213
+ result = self.decide_enum(
214
+ req_status=req.status,
215
+ round_no=req.round,
216
+ prompt=prompt,
217
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
218
+ fallback=ruleset.SPEECH_ORDER_CW,
219
+ )
220
  return AgentResp(success=True, result=result, errMsg=None)
221
 
222
  elif req.status == STATUS_SHERIFF:
 
231
  "history": "\n".join(self.memory.load_history())
232
  })
233
  logger.info("prompt:" + prompt)
234
+ result = self.decide_choice(
235
+ req_status=req.status,
236
+ round_no=req.round,
237
+ prompt=prompt,
238
+ choices=choices,
239
+ allow_extra=[ruleset.SHERIFF_TEAR],
240
+ fallback=ruleset.SHERIFF_TEAR,
241
+ )
242
  return AgentResp(success=True, result=result, errMsg=None)
243
  else:
244
  raise NotImplementedError
werewolf/seer/seer_agent.py CHANGED
@@ -1,17 +1,18 @@
1
- from agent_build_sdk.model.roles import ROLE_SEER
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF, STATUS_SHERIFF_VOTE, STATUS_SHERIFF_ELECTION, \
5
  STATUS_SHERIFF_PK, STATUS_SHERIFF_SPEECH_ORDER, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
7
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
8
  from agent_build_sdk.sdk.agent import format_prompt
 
 
9
  from seer.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
10
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
11
  SHERIFF_TRANSFER_PROMPT
12
 
13
 
14
- class SeerAgent(BasicRoleAgent):
15
  """预言家角色Agent"""
16
 
17
  def __init__(self, model_name):
@@ -37,15 +38,10 @@ class SeerAgent(BasicRoleAgent):
37
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
38
  elif req.status == STATUS_DISCUSS: # 发言环节
39
  if req.name:
40
- # 其他玩家发言
41
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
42
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
43
- # req.message = self.llm_caller(clean_user_message_prompt)
44
- self.memory.append_history(req.name + ': ' + req.message)
45
  else:
46
  # 主持人发言
47
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
48
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
49
  elif req.status == STATUS_VOTE: # 投票环节
50
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
51
  elif req.status == STATUS_VOTE_RESULT: # 投票结果
@@ -85,6 +81,8 @@ class SeerAgent(BasicRoleAgent):
85
  else:
86
  raise NotImplementedError
87
 
 
 
88
  def interact(self, req=AgentReq) -> AgentResp:
89
  logger.info("seer interact: {}".format(req))
90
  if req.status == STATUS_DISCUSS:
@@ -97,14 +95,22 @@ class SeerAgent(BasicRoleAgent):
97
  "history": "\n".join(self.memory.load_history())
98
  })
99
  logger.info("prompt:" + prompt)
100
- result = self.llm_caller(prompt)
 
 
 
 
 
101
  logger.info("seer interact result: {}".format(result))
102
  return AgentResp(success=True, result=result, errMsg=None)
103
 
104
  elif req.status == STATUS_VOTE:
105
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
106
  checked_players = self.memory.load_variable("checked_players")
107
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")] # 排除自己
 
 
 
108
  self.memory.set_variable("choices", choices)
109
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
110
  "checked_players": checked_players,
@@ -112,13 +118,18 @@ class SeerAgent(BasicRoleAgent):
112
  "history": "\n".join(self.memory.load_history())
113
  })
114
  logger.info("prompt:" + prompt)
115
- result = self.llm_caller(prompt)
 
 
 
 
 
116
  logger.info("seer interact result: {}".format(result))
117
  return AgentResp(success=True, result=result, errMsg=None)
118
 
119
  elif req.status == STATUS_SKILL:
120
  checked_players = self.memory.load_variable("checked_players")
121
- choices = [name for name in req.message.split(",")
122
  if name != self.memory.load_variable("name") and name not in checked_players] # 排除自己和已查验的
123
  self.memory.set_variable("choices", choices)
124
  prompt = format_prompt(SKILL_PROMPT, {
@@ -128,7 +139,13 @@ class SeerAgent(BasicRoleAgent):
128
  "history": "\n".join(self.memory.load_history())
129
  })
130
  logger.info("prompt:" + prompt)
131
- result = self.llm_caller(prompt)
 
 
 
 
 
 
132
  logger.info("seer skill result: {}".format(result))
133
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
134
 
@@ -138,7 +155,13 @@ class SeerAgent(BasicRoleAgent):
138
  "history": "\n".join(self.memory.load_history())
139
  })
140
  logger.info("seer agent sheriff election prompt:" + prompt)
141
- result = self.llm_caller(prompt)
 
 
 
 
 
 
142
  logger.info("seer agent sheriff election result: {}".format(result))
143
  return AgentResp(success=True, result=result, errMsg=None)
144
 
@@ -150,7 +173,12 @@ class SeerAgent(BasicRoleAgent):
150
  "history": "\n".join(self.memory.load_history())
151
  })
152
  logger.info("seer agent sheriff speech prompt:" + prompt)
153
- result = self.llm_caller(prompt)
 
 
 
 
 
154
  logger.info("seer agent sheriff speech result: {}".format(result))
155
  return AgentResp(success=True, result=result, errMsg=None)
156
 
@@ -162,7 +190,12 @@ class SeerAgent(BasicRoleAgent):
162
  "history": "\n".join(self.memory.load_history())
163
  })
164
  logger.info("seer agent sheriff pk prompt:" + prompt)
165
- result = self.llm_caller(prompt)
 
 
 
 
 
166
  logger.info("seer agent sheriff pk result: {}".format(result))
167
  return AgentResp(success=True, result=result, errMsg=None)
168
 
@@ -174,7 +207,12 @@ class SeerAgent(BasicRoleAgent):
174
  "history": "\n".join(self.memory.load_history())
175
  })
176
  logger.info("seer agent sheriff vote prompt:" + prompt)
177
- result = self.llm_caller(prompt)
 
 
 
 
 
178
  logger.info("seer agent sheriff vote result: {}".format(result))
179
  return AgentResp(success=True, result=result, errMsg=None)
180
 
@@ -184,7 +222,13 @@ class SeerAgent(BasicRoleAgent):
184
  "history": "\n".join(self.memory.load_history())
185
  })
186
  logger.info("seer agent sheriff speech order prompt:" + prompt)
187
- result = self.llm_caller(prompt)
 
 
 
 
 
 
188
  logger.info("seer agent sheriff speech order result: {}".format(result))
189
  return AgentResp(success=True, result=result, errMsg=None)
190
 
@@ -199,8 +243,15 @@ class SeerAgent(BasicRoleAgent):
199
  "history": "\n".join(self.memory.load_history())
200
  })
201
  logger.info("seer agent sheriff transfer prompt:" + prompt)
202
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
203
  logger.info("seer agent sheriff transfer result: {}".format(result))
204
  return AgentResp(success=True, result=result, errMsg=None)
205
  else:
206
- raise NotImplementedError
 
1
+ from agent_build_sdk.model.roles import ROLE_SEER
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF, STATUS_SHERIFF_VOTE, STATUS_SHERIFF_ELECTION, \
5
  STATUS_SHERIFF_PK, STATUS_SHERIFF_SPEECH_ORDER, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
 
7
  from agent_build_sdk.sdk.agent import format_prompt
8
+ from core import ruleset
9
+ from core.base_role_agent import BaseRoleAgent
10
  from seer.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
11
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
12
  SHERIFF_TRANSFER_PROMPT
13
 
14
 
15
+ class SeerAgent(BaseRoleAgent):
16
  """预言家角色Agent"""
17
 
18
  def __init__(self, model_name):
 
38
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
39
  elif req.status == STATUS_DISCUSS: # 发言环节
40
  if req.name:
41
+ self.append_player_message(req.name, req.message)
 
 
 
 
42
  else:
43
  # 主持人发言
44
+ self.append_discuss_host(req.round)
 
45
  elif req.status == STATUS_VOTE: # 投票环节
46
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
47
  elif req.status == STATUS_VOTE_RESULT: # 投票结果
 
81
  else:
82
  raise NotImplementedError
83
 
84
+ self.update_state(req)
85
+
86
  def interact(self, req=AgentReq) -> AgentResp:
87
  logger.info("seer interact: {}".format(req))
88
  if req.status == STATUS_DISCUSS:
 
95
  "history": "\n".join(self.memory.load_history())
96
  })
97
  logger.info("prompt:" + prompt)
98
+ result = self.decide_speech(
99
+ req_status=req.status,
100
+ round_no=req.round,
101
+ prompt=prompt,
102
+ kind="discuss",
103
+ )
104
  logger.info("seer interact result: {}".format(result))
105
  return AgentResp(success=True, result=result, errMsg=None)
106
 
107
  elif req.status == STATUS_VOTE:
108
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
109
  checked_players = self.memory.load_variable("checked_players")
110
+ raw_choices = [name for name in (req.message or "").split(",") if name]
111
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")] # 排除自己
112
+ if not choices:
113
+ choices = raw_choices
114
  self.memory.set_variable("choices", choices)
115
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
116
  "checked_players": checked_players,
 
118
  "history": "\n".join(self.memory.load_history())
119
  })
120
  logger.info("prompt:" + prompt)
121
+ result = self.decide_choice(
122
+ req_status=req.status,
123
+ round_no=req.round,
124
+ prompt=prompt,
125
+ choices=choices,
126
+ )
127
  logger.info("seer interact result: {}".format(result))
128
  return AgentResp(success=True, result=result, errMsg=None)
129
 
130
  elif req.status == STATUS_SKILL:
131
  checked_players = self.memory.load_variable("checked_players")
132
+ choices = [name for name in (req.message or "").split(",")
133
  if name != self.memory.load_variable("name") and name not in checked_players] # 排除自己和已查验的
134
  self.memory.set_variable("choices", choices)
135
  prompt = format_prompt(SKILL_PROMPT, {
 
139
  "history": "\n".join(self.memory.load_history())
140
  })
141
  logger.info("prompt:" + prompt)
142
+ result = self.decide_choice(
143
+ req_status=req.status,
144
+ round_no=req.round,
145
+ prompt=prompt,
146
+ choices=choices,
147
+ final_skill_target_from_result=lambda r: r,
148
+ )
149
  logger.info("seer skill result: {}".format(result))
150
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
151
 
 
155
  "history": "\n".join(self.memory.load_history())
156
  })
157
  logger.info("seer agent sheriff election prompt:" + prompt)
158
+ result = self.decide_enum(
159
+ req_status=req.status,
160
+ round_no=req.round,
161
+ prompt=prompt,
162
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
163
+ fallback=ruleset.SHERIFF_NOT_RUN,
164
+ )
165
  logger.info("seer agent sheriff election result: {}".format(result))
166
  return AgentResp(success=True, result=result, errMsg=None)
167
 
 
173
  "history": "\n".join(self.memory.load_history())
174
  })
175
  logger.info("seer agent sheriff speech prompt:" + prompt)
176
+ result = self.decide_speech(
177
+ req_status=req.status,
178
+ round_no=req.round,
179
+ prompt=prompt,
180
+ kind="sheriff_speech",
181
+ )
182
  logger.info("seer agent sheriff speech result: {}".format(result))
183
  return AgentResp(success=True, result=result, errMsg=None)
184
 
 
190
  "history": "\n".join(self.memory.load_history())
191
  })
192
  logger.info("seer agent sheriff pk prompt:" + prompt)
193
+ result = self.decide_speech(
194
+ req_status=req.status,
195
+ round_no=req.round,
196
+ prompt=prompt,
197
+ kind="sheriff_pk",
198
+ )
199
  logger.info("seer agent sheriff pk result: {}".format(result))
200
  return AgentResp(success=True, result=result, errMsg=None)
201
 
 
207
  "history": "\n".join(self.memory.load_history())
208
  })
209
  logger.info("seer agent sheriff vote prompt:" + prompt)
210
+ result = self.decide_choice(
211
+ req_status=req.status,
212
+ round_no=req.round,
213
+ prompt=prompt,
214
+ choices=choices,
215
+ )
216
  logger.info("seer agent sheriff vote result: {}".format(result))
217
  return AgentResp(success=True, result=result, errMsg=None)
218
 
 
222
  "history": "\n".join(self.memory.load_history())
223
  })
224
  logger.info("seer agent sheriff speech order prompt:" + prompt)
225
+ result = self.decide_enum(
226
+ req_status=req.status,
227
+ round_no=req.round,
228
+ prompt=prompt,
229
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
230
+ fallback=ruleset.SPEECH_ORDER_CW,
231
+ )
232
  logger.info("seer agent sheriff speech order result: {}".format(result))
233
  return AgentResp(success=True, result=result, errMsg=None)
234
 
 
243
  "history": "\n".join(self.memory.load_history())
244
  })
245
  logger.info("seer agent sheriff transfer prompt:" + prompt)
246
+ result = self.decide_choice(
247
+ req_status=req.status,
248
+ round_no=req.round,
249
+ prompt=prompt,
250
+ choices=choices,
251
+ allow_extra=[ruleset.SHERIFF_TEAR],
252
+ fallback=ruleset.SHERIFF_TEAR,
253
+ )
254
  logger.info("seer agent sheriff transfer result: {}".format(result))
255
  return AgentResp(success=True, result=result, errMsg=None)
256
  else:
257
+ raise NotImplementedError
werewolf/tools/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Utility CLI tools (telemetry replay, etc.).
2
+
werewolf/tools/replay.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import glob
5
+ import json
6
+ import os
7
+ from collections import Counter, defaultdict
8
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
9
+
10
+
11
+ def _iter_jsonl_files(root: str) -> Iterable[str]:
12
+ pattern = os.path.join(root, "**", "*.jsonl")
13
+ yield from glob.iglob(pattern, recursive=True)
14
+
15
+
16
+ def _read_records(paths: Iterable[str]) -> Iterable[Dict[str, Any]]:
17
+ for path in paths:
18
+ try:
19
+ with open(path, "r", encoding="utf-8") as f:
20
+ for line in f:
21
+ line = line.strip()
22
+ if not line:
23
+ continue
24
+ try:
25
+ yield json.loads(line)
26
+ except json.JSONDecodeError:
27
+ continue
28
+ except OSError:
29
+ continue
30
+
31
+
32
+ def _pick_latest_session(log_root: str) -> Optional[str]:
33
+ try:
34
+ sessions = [
35
+ os.path.join(log_root, d)
36
+ for d in os.listdir(log_root)
37
+ if os.path.isdir(os.path.join(log_root, d))
38
+ ]
39
+ except OSError:
40
+ return None
41
+ if not sessions:
42
+ return None
43
+ sessions.sort(key=lambda p: os.path.getmtime(p), reverse=True)
44
+ return os.path.basename(sessions[0])
45
+
46
+
47
+ def _bool(x: Any) -> Optional[bool]:
48
+ if isinstance(x, bool):
49
+ return x
50
+ return None
51
+
52
+
53
+ def compute_metrics(records: List[Dict[str, Any]]) -> Dict[str, Any]:
54
+ by_status = Counter()
55
+ by_role = Counter()
56
+
57
+ with_choices = 0
58
+ final_in_choices = 0
59
+ retry_count = 0
60
+ fallback_used = 0
61
+ valid_final = 0
62
+ has_valid_flag = 0
63
+
64
+ per_status = defaultdict(lambda: {"count": 0, "with_choices": 0, "final_in_choices": 0, "retry": 0, "fallback": 0})
65
+
66
+ for r in records:
67
+ status = r.get("status") or "unknown"
68
+ role = r.get("role") or "unknown"
69
+ by_status[status] += 1
70
+ by_role[role] += 1
71
+ per_status[status]["count"] += 1
72
+
73
+ choices = r.get("choices") or []
74
+ if isinstance(choices, list) and len(choices) > 0:
75
+ with_choices += 1
76
+ per_status[status]["with_choices"] += 1
77
+ if r.get("final_result") in choices:
78
+ final_in_choices += 1
79
+ per_status[status]["final_in_choices"] += 1
80
+
81
+ retried = bool(r.get("retry_count") or 0)
82
+ if retried:
83
+ retry_count += 1
84
+ per_status[status]["retry"] += 1
85
+
86
+ fb = _bool(r.get("fallback_used"))
87
+ if fb is True:
88
+ fallback_used += 1
89
+ per_status[status]["fallback"] += 1
90
+
91
+ v = _bool(r.get("parse_valid"))
92
+ if v is not None:
93
+ has_valid_flag += 1
94
+ if v is True:
95
+ valid_final += 1
96
+
97
+ return {
98
+ "total": len(records),
99
+ "by_status": by_status,
100
+ "by_role": by_role,
101
+ "with_choices": with_choices,
102
+ "final_in_choices": final_in_choices,
103
+ "retry_count": retry_count,
104
+ "fallback_used": fallback_used,
105
+ "valid_final": valid_final,
106
+ "has_valid_flag": has_valid_flag,
107
+ "per_status": per_status,
108
+ }
109
+
110
+
111
+ def main() -> int:
112
+ ap = argparse.ArgumentParser(description="Replay telemetry logs and report compliance metrics.")
113
+ ap.add_argument("--log-root", default="logs", help="Telemetry log root (default: logs)")
114
+ ap.add_argument("--session", default=None, help="Session id to analyze (default: latest)")
115
+ args = ap.parse_args()
116
+
117
+ session = args.session or _pick_latest_session(args.log_root)
118
+ if not session:
119
+ print(f"No sessions found under {args.log_root!r}.")
120
+ return 1
121
+
122
+ root = os.path.join(args.log_root, session)
123
+ paths = list(_iter_jsonl_files(root))
124
+ records = list(_read_records(paths))
125
+ m = compute_metrics(records)
126
+
127
+ print(f"Session: {session}")
128
+ print(f"Records: {m['total']}")
129
+ print(f"Roles: {dict(m['by_role'])}")
130
+ print(f"Status: {dict(m['by_status'])}")
131
+
132
+ if m["with_choices"] > 0:
133
+ rate = m["final_in_choices"] / m["with_choices"]
134
+ print(f"Final-in-choices: {m['final_in_choices']}/{m['with_choices']} = {rate:.3f}")
135
+ print(f"Retry-rate: {m['retry_count']}/{m['with_choices']} = {m['retry_count']/m['with_choices']:.3f}")
136
+ print(f"Fallback-rate: {m['fallback_used']}/{m['with_choices']} = {m['fallback_used']/m['with_choices']:.3f}")
137
+
138
+ if m["has_valid_flag"] > 0:
139
+ print(f"Final-valid: {m['valid_final']}/{m['has_valid_flag']} = {m['valid_final']/m['has_valid_flag']:.3f}")
140
+
141
+ print("\nPer-status:")
142
+ for status, s in sorted(m["per_status"].items(), key=lambda kv: kv[0]):
143
+ if s["count"] == 0:
144
+ continue
145
+ parts = [f"{status}: count={s['count']}"]
146
+ if s["with_choices"]:
147
+ parts.append(f"in_choices={s['final_in_choices']}/{s['with_choices']}")
148
+ parts.append(f"retry={s['retry']}/{s['with_choices']}")
149
+ parts.append(f"fallback={s['fallback']}/{s['with_choices']}")
150
+ print(" " + ", ".join(parts))
151
+
152
+ return 0
153
+
154
+
155
+ if __name__ == "__main__":
156
+ raise SystemExit(main())
werewolf/villager/villager_agent.py CHANGED
@@ -1,4 +1,4 @@
1
- from villager.prompt import DESC_PROMPT, VOTE_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, \
2
  SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, SHERIFF_TRANSFER_PROMPT
3
  from agent_build_sdk.model.roles import ROLE_VILLAGER
4
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
@@ -6,11 +6,12 @@ from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_STA
6
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF, STATUS_SHERIFF_VOTE, STATUS_SHERIFF_ELECTION, \
7
  STATUS_SHERIFF_PK, STATUS_SHERIFF_SPEECH_ORDER, STATUS_HUNTER, STATUS_HUNTER_RESULT
8
  from agent_build_sdk.utils.logger import logger
9
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
10
  from agent_build_sdk.sdk.agent import format_prompt
 
 
11
 
12
 
13
- class VillagerAgent(BasicRoleAgent):
14
  """平民角色Agent"""
15
 
16
  def __init__(self, model_name):
@@ -28,14 +29,10 @@ class VillagerAgent(BasicRoleAgent):
28
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
29
  elif req.status == STATUS_DISCUSS: # 发言环节
30
  if req.name:
31
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
32
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
33
- # req.message = self.llm_caller(clean_user_message_prompt)
34
- self.memory.append_history(req.name + ': ' + req.message)
35
  else:
36
  # 主持人发言
37
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
38
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
39
  self.memory.append_history("---------------------------------------------")
40
  elif req.status == STATUS_VOTE: # 投票环节
41
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
@@ -75,7 +72,8 @@ class VillagerAgent(BasicRoleAgent):
75
  self.memory.append_history(f"警长PK发言: {req.name}: {req.message}")
76
  else:
77
  raise NotImplementedError
78
- pass
 
79
 
80
  def interact(self, req=AgentReq) -> AgentResp:
81
  logger.info("VillagerAgent interact: {}".format(req))
@@ -87,20 +85,33 @@ class VillagerAgent(BasicRoleAgent):
87
  "history": "\n".join(self.memory.load_history())
88
  })
89
  logger.info("prompt:" + prompt)
90
- result = self.llm_caller(prompt)
 
 
 
 
 
91
  logger.info("VillagerAgent interact result: {}".format(result))
92
  return AgentResp(success=True, result=result, errMsg=None)
93
 
94
  elif req.status == STATUS_VOTE:
95
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
96
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")] # 排除自己
 
 
 
97
  self.memory.set_variable("choices", choices)
98
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
99
  "choices": choices,
100
  "history": "\n".join(self.memory.load_history())
101
  })
102
  logger.info("prompt:" + prompt)
103
- result = self.llm_caller(prompt)
 
 
 
 
 
104
  logger.info("interact result: {}".format(result))
105
  return AgentResp(success=True, result=result, errMsg=None)
106
 
@@ -108,7 +119,14 @@ class VillagerAgent(BasicRoleAgent):
108
  prompt = format_prompt(SHERIFF_ELECTION_PROMPT, {"name": self.memory.load_variable("name"),
109
  "history": "\n".join(self.memory.load_history())})
110
  logger.info("prompt:" + prompt)
111
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
112
  logger.info("VillagerAgent sheriff election result: {}".format(result))
113
  return AgentResp(success=True, result=result, errMsg=None)
114
 
@@ -116,7 +134,12 @@ class VillagerAgent(BasicRoleAgent):
116
  prompt = format_prompt(SHERIFF_SPEECH_PROMPT, {"name": self.memory.load_variable("name"),
117
  "history": "\n".join(self.memory.load_history())})
118
  logger.info("prompt:" + prompt)
119
- result = self.llm_caller(prompt)
 
 
 
 
 
120
  logger.info("VillagerAgent sheriff speech result: {}".format(result))
121
  return AgentResp(success=True, result=result, errMsg=None)
122
 
@@ -124,7 +147,12 @@ class VillagerAgent(BasicRoleAgent):
124
  prompt = format_prompt(SHERIFF_SPEECH_PROMPT, {"name": self.memory.load_variable("name"),
125
  "history": "\n".join(self.memory.load_history())})
126
  logger.info("prompt:" + prompt)
127
- result = self.llm_caller(prompt)
 
 
 
 
 
128
  logger.info("VillagerAgent sheriff pk result: {}".format(result))
129
  return AgentResp(success=True, result=result, errMsg=None)
130
 
@@ -134,7 +162,12 @@ class VillagerAgent(BasicRoleAgent):
134
  "choices": choices,
135
  "history": "\n".join(self.memory.load_history())})
136
  logger.info("prompt:" + prompt)
137
- result = self.llm_caller(prompt)
 
 
 
 
 
138
  logger.info("VillagerAgent sheriff vote result: {}".format(result))
139
  return AgentResp(success=True, result=result, errMsg=None)
140
 
@@ -142,7 +175,14 @@ class VillagerAgent(BasicRoleAgent):
142
  prompt = format_prompt(SHERIFF_SPEECH_ORDER_PROMPT, {"name": self.memory.load_variable("name"),
143
  "history": "\n".join(self.memory.load_history())})
144
  logger.info("prompt:" + prompt)
145
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
146
  logger.info("VillagerAgent sheriff speech order result: {}".format(result))
147
  return AgentResp(success=True, result=result, errMsg=None)
148
 
@@ -153,9 +193,15 @@ class VillagerAgent(BasicRoleAgent):
153
  "choices": choices,
154
  "history": "\n".join(self.memory.load_history())})
155
  logger.info("prompt:" + prompt)
156
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
157
  logger.info("VillagerAgent sheriff transfer result: {}".format(result))
158
  return AgentResp(success=True, result=result, errMsg=None)
159
  else:
160
  raise NotImplementedError
161
- pass
 
1
+ from villager.prompt import DESC_PROMPT, VOTE_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, \
2
  SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, SHERIFF_TRANSFER_PROMPT
3
  from agent_build_sdk.model.roles import ROLE_VILLAGER
4
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
 
6
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF, STATUS_SHERIFF_VOTE, STATUS_SHERIFF_ELECTION, \
7
  STATUS_SHERIFF_PK, STATUS_SHERIFF_SPEECH_ORDER, STATUS_HUNTER, STATUS_HUNTER_RESULT
8
  from agent_build_sdk.utils.logger import logger
 
9
  from agent_build_sdk.sdk.agent import format_prompt
10
+ from core import ruleset
11
+ from core.base_role_agent import BaseRoleAgent
12
 
13
 
14
+ class VillagerAgent(BaseRoleAgent):
15
  """平民角色Agent"""
16
 
17
  def __init__(self, model_name):
 
29
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
30
  elif req.status == STATUS_DISCUSS: # 发言环节
31
  if req.name:
32
+ self.append_player_message(req.name, req.message)
 
 
 
33
  else:
34
  # 主持人发言
35
+ self.append_discuss_host(req.round)
 
36
  self.memory.append_history("---------------------------------------------")
37
  elif req.status == STATUS_VOTE: # 投票环节
38
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
 
72
  self.memory.append_history(f"警长PK发言: {req.name}: {req.message}")
73
  else:
74
  raise NotImplementedError
75
+
76
+ self.update_state(req)
77
 
78
  def interact(self, req=AgentReq) -> AgentResp:
79
  logger.info("VillagerAgent interact: {}".format(req))
 
85
  "history": "\n".join(self.memory.load_history())
86
  })
87
  logger.info("prompt:" + prompt)
88
+ result = self.decide_speech(
89
+ req_status=req.status,
90
+ round_no=req.round,
91
+ prompt=prompt,
92
+ kind="discuss",
93
+ )
94
  logger.info("VillagerAgent interact result: {}".format(result))
95
  return AgentResp(success=True, result=result, errMsg=None)
96
 
97
  elif req.status == STATUS_VOTE:
98
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
99
+ raw_choices = [name for name in (req.message or "").split(",") if name]
100
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")] # 排除自己
101
+ if not choices:
102
+ choices = raw_choices
103
  self.memory.set_variable("choices", choices)
104
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
105
  "choices": choices,
106
  "history": "\n".join(self.memory.load_history())
107
  })
108
  logger.info("prompt:" + prompt)
109
+ result = self.decide_choice(
110
+ req_status=req.status,
111
+ round_no=req.round,
112
+ prompt=prompt,
113
+ choices=choices,
114
+ )
115
  logger.info("interact result: {}".format(result))
116
  return AgentResp(success=True, result=result, errMsg=None)
117
 
 
119
  prompt = format_prompt(SHERIFF_ELECTION_PROMPT, {"name": self.memory.load_variable("name"),
120
  "history": "\n".join(self.memory.load_history())})
121
  logger.info("prompt:" + prompt)
122
+ allowed = [ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN]
123
+ result = self.decide_enum(
124
+ req_status=req.status,
125
+ round_no=req.round,
126
+ prompt=prompt,
127
+ allowed=allowed,
128
+ fallback=ruleset.SHERIFF_NOT_RUN,
129
+ )
130
  logger.info("VillagerAgent sheriff election result: {}".format(result))
131
  return AgentResp(success=True, result=result, errMsg=None)
132
 
 
134
  prompt = format_prompt(SHERIFF_SPEECH_PROMPT, {"name": self.memory.load_variable("name"),
135
  "history": "\n".join(self.memory.load_history())})
136
  logger.info("prompt:" + prompt)
137
+ result = self.decide_speech(
138
+ req_status=req.status,
139
+ round_no=req.round,
140
+ prompt=prompt,
141
+ kind="sheriff_speech",
142
+ )
143
  logger.info("VillagerAgent sheriff speech result: {}".format(result))
144
  return AgentResp(success=True, result=result, errMsg=None)
145
 
 
147
  prompt = format_prompt(SHERIFF_SPEECH_PROMPT, {"name": self.memory.load_variable("name"),
148
  "history": "\n".join(self.memory.load_history())})
149
  logger.info("prompt:" + prompt)
150
+ result = self.decide_speech(
151
+ req_status=req.status,
152
+ round_no=req.round,
153
+ prompt=prompt,
154
+ kind="sheriff_pk",
155
+ )
156
  logger.info("VillagerAgent sheriff pk result: {}".format(result))
157
  return AgentResp(success=True, result=result, errMsg=None)
158
 
 
162
  "choices": choices,
163
  "history": "\n".join(self.memory.load_history())})
164
  logger.info("prompt:" + prompt)
165
+ result = self.decide_choice(
166
+ req_status=req.status,
167
+ round_no=req.round,
168
+ prompt=prompt,
169
+ choices=choices,
170
+ )
171
  logger.info("VillagerAgent sheriff vote result: {}".format(result))
172
  return AgentResp(success=True, result=result, errMsg=None)
173
 
 
175
  prompt = format_prompt(SHERIFF_SPEECH_ORDER_PROMPT, {"name": self.memory.load_variable("name"),
176
  "history": "\n".join(self.memory.load_history())})
177
  logger.info("prompt:" + prompt)
178
+ allowed = [ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW]
179
+ result = self.decide_enum(
180
+ req_status=req.status,
181
+ round_no=req.round,
182
+ prompt=prompt,
183
+ allowed=allowed,
184
+ fallback=ruleset.SPEECH_ORDER_CW,
185
+ )
186
  logger.info("VillagerAgent sheriff speech order result: {}".format(result))
187
  return AgentResp(success=True, result=result, errMsg=None)
188
 
 
193
  "choices": choices,
194
  "history": "\n".join(self.memory.load_history())})
195
  logger.info("prompt:" + prompt)
196
+ result = self.decide_choice(
197
+ req_status=req.status,
198
+ round_no=req.round,
199
+ prompt=prompt,
200
+ choices=choices,
201
+ allow_extra=[ruleset.SHERIFF_TEAR],
202
+ fallback=ruleset.SHERIFF_TEAR,
203
+ )
204
  logger.info("VillagerAgent sheriff transfer result: {}".format(result))
205
  return AgentResp(success=True, result=result, errMsg=None)
206
  else:
207
  raise NotImplementedError
 
werewolf/witch/witch_agent.py CHANGED
@@ -1,16 +1,20 @@
1
- from agent_build_sdk.model.roles import ROLE_WITCH
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
7
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
8
  from agent_build_sdk.sdk.agent import format_prompt
 
 
 
 
9
  from witch.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
10
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
11
  SHERIFF_TRANSFER_PROMPT
12
 
13
- class WitchAgent(BasicRoleAgent):
 
14
  """女巫角色Agent"""
15
 
16
  def __init__(self, model_name):
@@ -36,15 +40,10 @@ class WitchAgent(BasicRoleAgent):
36
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
37
  elif req.status == STATUS_DISCUSS: # 发言环节
38
  if req.name:
39
- # 其他玩家发言
40
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
41
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
42
- # req.message = self.llm_caller(clean_user_message_prompt)
43
- self.memory.append_history(req.name + ': ' + req.message)
44
  else:
45
  # 主持人发言
46
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
47
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
48
  elif req.status == STATUS_VOTE: # 投票环节
49
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
50
  elif req.status == STATUS_VOTE_RESULT: # 投票结果
@@ -84,6 +83,8 @@ class WitchAgent(BasicRoleAgent):
84
  else:
85
  raise NotImplementedError
86
 
 
 
87
  def interact(self, req=AgentReq) -> AgentResp:
88
  logger.info("witch interact: {}".format(req))
89
  if req.status == STATUS_DISCUSS:
@@ -99,20 +100,33 @@ class WitchAgent(BasicRoleAgent):
99
  "history": "\n".join(self.memory.load_history())
100
  })
101
  logger.info("prompt:" + prompt)
102
- result = self.llm_caller(prompt)
 
 
 
 
 
103
  logger.info("witch interact result: {}".format(result))
104
  return AgentResp(success=True, result=result, errMsg=None)
105
 
106
  elif req.status == STATUS_VOTE:
107
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
108
- choices = [name for name in req.message.split(",") if name != self.memory.load_variable("name")] # 排除自己
 
 
 
109
  self.memory.set_variable("choices", choices)
110
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
111
  "choices": choices,
112
  "history": "\n".join(self.memory.load_history())
113
  })
114
  logger.info("prompt:" + prompt)
115
- result = self.llm_caller(prompt)
 
 
 
 
 
116
  logger.info("witch interact result: {}".format(result))
117
  return AgentResp(success=True, result=result, errMsg=None)
118
 
@@ -130,20 +144,72 @@ class WitchAgent(BasicRoleAgent):
130
  })
131
 
132
  logger.info("prompt:" + prompt)
133
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  logger.info("witch skill result: {}".format(result))
135
- # 根据结果更新药水状态
 
 
136
  skill_target_person = None
137
- if result.startswith("救") and has_antidote:
138
  self.memory.set_variable("has_antidote", False)
139
  self.memory.append_history(f"女巫使用解药救活了{tonight_killed}")
140
  skill_target_person = tonight_killed
141
- elif result.startswith("毒") and has_poison:
142
  poisoned_player = result[1:].strip()
143
  self.memory.set_variable("has_poison", False)
144
  self.memory.append_history(f"女巫使用毒药杀死了{poisoned_player}")
145
  skill_target_person = poisoned_player
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  return AgentResp(success=True, result=result, skillTargetPlayer=skill_target_person, errMsg=None)
148
 
149
  elif req.status == STATUS_SHERIFF_ELECTION:
@@ -156,7 +222,13 @@ class WitchAgent(BasicRoleAgent):
156
  "history": "\n".join(self.memory.load_history())
157
  })
158
  logger.info("prompt:" + prompt)
159
- result = self.llm_caller(prompt)
 
 
 
 
 
 
160
  return AgentResp(success=True, result=result, errMsg=None)
161
 
162
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
@@ -169,7 +241,13 @@ class WitchAgent(BasicRoleAgent):
169
  "history": "\n".join(self.memory.load_history())
170
  })
171
  logger.info("prompt:" + prompt)
172
- result = self.llm_caller(prompt)
 
 
 
 
 
 
173
  return AgentResp(success=True, result=result, errMsg=None)
174
 
175
  elif req.status == STATUS_SHERIFF_VOTE:
@@ -180,7 +258,12 @@ class WitchAgent(BasicRoleAgent):
180
  "history": "\n".join(self.memory.load_history())
181
  })
182
  logger.info("prompt:" + prompt)
183
- result = self.llm_caller(prompt)
 
 
 
 
 
184
  return AgentResp(success=True, result=result, errMsg=None)
185
 
186
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
@@ -189,7 +272,13 @@ class WitchAgent(BasicRoleAgent):
189
  "history": "\n".join(self.memory.load_history())
190
  })
191
  logger.info("prompt:" + prompt)
192
- result = self.llm_caller(prompt)
 
 
 
 
 
 
193
  return AgentResp(success=True, result=result, errMsg=None)
194
 
195
  elif req.status == STATUS_SHERIFF:
@@ -201,7 +290,14 @@ class WitchAgent(BasicRoleAgent):
201
  "history": "\n".join(self.memory.load_history())
202
  })
203
  logger.info("prompt:" + prompt)
204
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
205
  return AgentResp(success=True, result=result, errMsg=None)
206
  else:
207
  raise NotImplementedError
 
1
+ from agent_build_sdk.model.roles import ROLE_WITCH
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
 
7
  from agent_build_sdk.sdk.agent import format_prompt
8
+ from core import ruleset
9
+ from core import telemetry
10
+ from core.base_role_agent import BaseRoleAgent
11
+ from core.output_guard import guard_witch_skill, guarded_meta
12
  from witch.prompt import DESC_PROMPT, VOTE_PROMPT, SKILL_PROMPT, GAME_RULE_PROMPT, CLEAN_USER_PROMPT, \
13
  SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, SHERIFF_SPEECH_ORDER_PROMPT, \
14
  SHERIFF_TRANSFER_PROMPT
15
 
16
+
17
+ class WitchAgent(BaseRoleAgent):
18
  """女巫角色Agent"""
19
 
20
  def __init__(self, model_name):
 
40
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
41
  elif req.status == STATUS_DISCUSS: # 发言环节
42
  if req.name:
43
+ self.append_player_message(req.name, req.message)
 
 
 
 
44
  else:
45
  # 主持人发言
46
+ self.append_discuss_host(req.round)
 
47
  elif req.status == STATUS_VOTE: # 投票环节
48
  self.memory.append_history(f'第{req.round}天的投票环节,{req.name} 投了 {req.message}')
49
  elif req.status == STATUS_VOTE_RESULT: # 投票结果
 
83
  else:
84
  raise NotImplementedError
85
 
86
+ self.update_state(req)
87
+
88
  def interact(self, req=AgentReq) -> AgentResp:
89
  logger.info("witch interact: {}".format(req))
90
  if req.status == STATUS_DISCUSS:
 
100
  "history": "\n".join(self.memory.load_history())
101
  })
102
  logger.info("prompt:" + prompt)
103
+ result = self.decide_speech(
104
+ req_status=req.status,
105
+ round_no=req.round,
106
+ prompt=prompt,
107
+ kind="discuss",
108
+ )
109
  logger.info("witch interact result: {}".format(result))
110
  return AgentResp(success=True, result=result, errMsg=None)
111
 
112
  elif req.status == STATUS_VOTE:
113
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
114
+ raw_choices = [name for name in (req.message or "").split(",") if name]
115
+ choices = [name for name in raw_choices if name != self.memory.load_variable("name")] # 排除自己
116
+ if not choices:
117
+ choices = raw_choices
118
  self.memory.set_variable("choices", choices)
119
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
120
  "choices": choices,
121
  "history": "\n".join(self.memory.load_history())
122
  })
123
  logger.info("prompt:" + prompt)
124
+ result = self.decide_choice(
125
+ req_status=req.status,
126
+ round_no=req.round,
127
+ prompt=prompt,
128
+ choices=choices,
129
+ )
130
  logger.info("witch interact result: {}".format(result))
131
  return AgentResp(success=True, result=result, errMsg=None)
132
 
 
144
  })
145
 
146
  logger.info("prompt:" + prompt)
147
+ raw1 = self.llm_caller(prompt)
148
+ first = guard_witch_skill(
149
+ raw1,
150
+ self_name=self.memory.load_variable("name"),
151
+ tonight_killed=tonight_killed,
152
+ has_antidote=has_antidote,
153
+ has_poison=has_poison,
154
+ )
155
+ if not first.valid:
156
+ allowed_lines = []
157
+ if has_antidote and tonight_killed:
158
+ allowed_lines.append(f"- 救{tonight_killed}")
159
+ if has_poison:
160
+ allowed_lines.append("- 毒玩家名")
161
+ allowed_lines.append(f"- {ruleset.WITCH_NO_USE}")
162
+ retry_prompt = (
163
+ prompt
164
+ + "\n\n【纠错】你的上一条输出不符合格式。你必须只输出以下三种之一:\n"
165
+ + "\n".join(allowed_lines)
166
+ + "\n不要输出任何解释,只输出最终答案。"
167
+ )
168
+ raw2 = self.llm_caller(retry_prompt)
169
+ second = guard_witch_skill(
170
+ raw2,
171
+ self_name=self.memory.load_variable("name"),
172
+ tonight_killed=tonight_killed,
173
+ has_antidote=has_antidote,
174
+ has_poison=has_poison,
175
+ )
176
+ result = second.value
177
+ else:
178
+ raw2 = None
179
+ second = None
180
+ result = first.value
181
+
182
  logger.info("witch skill result: {}".format(result))
183
+
184
+ result = self._render_action_if_possible(req.status, result)
185
+
186
  skill_target_person = None
187
+ if result.startswith("救"):
188
  self.memory.set_variable("has_antidote", False)
189
  self.memory.append_history(f"女巫使用解药救活了{tonight_killed}")
190
  skill_target_person = tonight_killed
191
+ elif result.startswith("毒"):
192
  poisoned_player = result[1:].strip()
193
  self.memory.set_variable("has_poison", False)
194
  self.memory.append_history(f"女巫使用毒药杀死了{poisoned_player}")
195
  skill_target_person = poisoned_player
196
 
197
+ telemetry.log_interact(
198
+ memory=self.memory,
199
+ role=str(self.role),
200
+ status=req.status,
201
+ round_no=req.round,
202
+ agent_name=self.memory.load_variable("name"),
203
+ payload=telemetry.decision_payload(
204
+ prompt=prompt,
205
+ choices=None,
206
+ attempt1=(raw1, guarded_meta(first)),
207
+ attempt2=None if raw2 is None else (raw2, guarded_meta(second)),
208
+ final_result=result,
209
+ final_skillTargetPlayer=skill_target_person,
210
+ ),
211
+ )
212
+
213
  return AgentResp(success=True, result=result, skillTargetPlayer=skill_target_person, errMsg=None)
214
 
215
  elif req.status == STATUS_SHERIFF_ELECTION:
 
222
  "history": "\n".join(self.memory.load_history())
223
  })
224
  logger.info("prompt:" + prompt)
225
+ result = self.decide_enum(
226
+ req_status=req.status,
227
+ round_no=req.round,
228
+ prompt=prompt,
229
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
230
+ fallback=ruleset.SHERIFF_NOT_RUN,
231
+ )
232
  return AgentResp(success=True, result=result, errMsg=None)
233
 
234
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
 
241
  "history": "\n".join(self.memory.load_history())
242
  })
243
  logger.info("prompt:" + prompt)
244
+ kind = "sheriff_pk" if req.status == STATUS_SHERIFF_PK else "sheriff_speech"
245
+ result = self.decide_speech(
246
+ req_status=req.status,
247
+ round_no=req.round,
248
+ prompt=prompt,
249
+ kind=kind,
250
+ )
251
  return AgentResp(success=True, result=result, errMsg=None)
252
 
253
  elif req.status == STATUS_SHERIFF_VOTE:
 
258
  "history": "\n".join(self.memory.load_history())
259
  })
260
  logger.info("prompt:" + prompt)
261
+ result = self.decide_choice(
262
+ req_status=req.status,
263
+ round_no=req.round,
264
+ prompt=prompt,
265
+ choices=choices,
266
+ )
267
  return AgentResp(success=True, result=result, errMsg=None)
268
 
269
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
 
272
  "history": "\n".join(self.memory.load_history())
273
  })
274
  logger.info("prompt:" + prompt)
275
+ result = self.decide_enum(
276
+ req_status=req.status,
277
+ round_no=req.round,
278
+ prompt=prompt,
279
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
280
+ fallback=ruleset.SPEECH_ORDER_CW,
281
+ )
282
  return AgentResp(success=True, result=result, errMsg=None)
283
 
284
  elif req.status == STATUS_SHERIFF:
 
290
  "history": "\n".join(self.memory.load_history())
291
  })
292
  logger.info("prompt:" + prompt)
293
+ result = self.decide_choice(
294
+ req_status=req.status,
295
+ round_no=req.round,
296
+ prompt=prompt,
297
+ choices=choices,
298
+ allow_extra=[ruleset.SHERIFF_TEAR],
299
+ fallback=ruleset.SHERIFF_TEAR,
300
+ )
301
  return AgentResp(success=True, result=result, errMsg=None)
302
  else:
303
  raise NotImplementedError
werewolf/wolf/wolf_agent.py CHANGED
@@ -1,4 +1,4 @@
1
- from agent_build_sdk.model.roles import ROLE_WOLF
2
  from agent_build_sdk.model.werewolf_model import (
3
  AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH,
4
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO,
@@ -7,8 +7,9 @@ from agent_build_sdk.model.werewolf_model import (
7
  STATUS_SHERIFF_VOTE, STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF, STATUS_HUNTER, STATUS_HUNTER_RESULT
8
  )
9
  from agent_build_sdk.utils.logger import logger
10
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
11
  from agent_build_sdk.sdk.agent import format_prompt
 
 
12
  from wolf.prompt import (
13
  DESC_PROMPT, VOTE_PROMPT, KILL_PROMPT, WOLF_SPEECH_PROMPT, GAME_RULE_PROMPT,
14
  CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT,
@@ -16,7 +17,7 @@ from wolf.prompt import (
16
  )
17
 
18
 
19
- class WolfAgent(BasicRoleAgent):
20
  """狼人角色Agent"""
21
 
22
  def __init__(self, model_name):
@@ -52,11 +53,10 @@ class WolfAgent(BasicRoleAgent):
52
  # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
53
  # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
54
  # req.message = self.llm_caller(clean_user_message_prompt)
55
- self.memory.append_history(req.name + ': ' + req.message)
56
  else:
57
  # 主持人发言
58
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
59
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
60
  self.memory.append_history("---------------------------------------------")
61
  elif req.status == STATUS_VOTE: # 投票环节
62
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
@@ -96,6 +96,8 @@ class WolfAgent(BasicRoleAgent):
96
  else:
97
  raise NotImplementedError
98
 
 
 
99
  def interact(self, req=AgentReq) -> AgentResp:
100
  logger.info("wolf interact: {}".format(req))
101
  try:
@@ -109,15 +111,23 @@ class WolfAgent(BasicRoleAgent):
109
  "history": "\n".join(self.memory.load_history())
110
  })
111
  logger.info("prompt:" + prompt)
112
- result = self.llm_caller(prompt)
 
 
 
 
 
113
  logger.info("wolf interact result: {}".format(result))
114
  return AgentResp(success=True, result=result, errMsg=None)
115
 
116
  elif req.status == STATUS_VOTE:
117
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
118
  teammates = self.memory.load_variable("teammates")
119
- choices = [name for name in req.message.split(",")
 
120
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
 
 
121
  self.memory.set_variable("choices", choices)
122
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
123
  "teammates": teammates,
@@ -125,7 +135,12 @@ class WolfAgent(BasicRoleAgent):
125
  "history": "\n".join(self.memory.load_history())
126
  })
127
  logger.info("prompt:" + prompt)
128
- result = self.llm_caller(prompt)
 
 
 
 
 
129
  logger.info("wolf interact result: {}".format(result))
130
  return AgentResp(success=True, result=result, errMsg=None)
131
 
@@ -137,13 +152,18 @@ class WolfAgent(BasicRoleAgent):
137
  "history": "\n".join(self.memory.load_history())
138
  })
139
  logger.info("prompt:" + prompt)
140
- result = self.llm_caller(prompt)
 
 
 
 
 
141
  logger.info("wolf speech result: {}".format(result))
142
  return AgentResp(success=True, result=result, errMsg=None)
143
 
144
  elif req.status == STATUS_SKILL:
145
  teammates = self.memory.load_variable("teammates")
146
- choices = [name for name in req.message.split(",")
147
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
148
  self.memory.set_variable("choices", choices)
149
  prompt = format_prompt(KILL_PROMPT, {
@@ -152,7 +172,13 @@ class WolfAgent(BasicRoleAgent):
152
  "history": "\n".join(self.memory.load_history())
153
  })
154
  logger.info("prompt:" + prompt)
155
- result = self.llm_caller(prompt)
 
 
 
 
 
 
156
  logger.info("wolf kill result: {}".format(result))
157
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
158
 
@@ -164,7 +190,13 @@ class WolfAgent(BasicRoleAgent):
164
  "history": "\n".join(self.memory.load_history())
165
  })
166
  logger.info("wolf agent sheriff election prompt:" + prompt)
167
- result = self.llm_caller(prompt)
 
 
 
 
 
 
168
  return AgentResp(success=True, result=result, errMsg=None)
169
 
170
  elif req.status == STATUS_SHERIFF_SPEECH:
@@ -175,7 +207,12 @@ class WolfAgent(BasicRoleAgent):
175
  "history": "\n".join(self.memory.load_history())
176
  })
177
  logger.info("wolf agent sheriff speech prompt:" + prompt)
178
- result = self.llm_caller(prompt)
 
 
 
 
 
179
  return AgentResp(success=True, result=result, errMsg=None)
180
 
181
  elif req.status == STATUS_SHERIFF_PK:
@@ -186,7 +223,12 @@ class WolfAgent(BasicRoleAgent):
186
  "history": "\n".join(self.memory.load_history())
187
  })
188
  logger.info("wolf agent sheriff pk prompt:" + prompt)
189
- result = self.llm_caller(prompt)
 
 
 
 
 
190
  return AgentResp(success=True, result=result, errMsg=None)
191
 
192
  elif req.status == STATUS_SHERIFF_VOTE:
@@ -199,7 +241,12 @@ class WolfAgent(BasicRoleAgent):
199
  "history": "\n".join(self.memory.load_history())
200
  })
201
  logger.info("wolf agent sheriff vote prompt:" + prompt)
202
- result = self.llm_caller(prompt)
 
 
 
 
 
203
  return AgentResp(success=True, result=result, errMsg=None)
204
 
205
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
@@ -208,7 +255,13 @@ class WolfAgent(BasicRoleAgent):
208
  "history": "\n".join(self.memory.load_history())
209
  })
210
  logger.info("wolf agent sheriff speech order prompt:" + prompt)
211
- result = self.llm_caller(prompt)
 
 
 
 
 
 
212
  return AgentResp(success=True, result=result, errMsg=None)
213
 
214
  elif req.status == STATUS_SHERIFF:
@@ -223,10 +276,17 @@ class WolfAgent(BasicRoleAgent):
223
  "history": "\n".join(self.memory.load_history())
224
  })
225
  logger.info("wolf agent sheriff transfer prompt:" + prompt)
226
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
227
  return AgentResp(success=True, result=result, errMsg=None)
228
 
229
  return AgentResp(success=True, result=None, errMsg=None)
230
  except Exception as e:
231
  logger.error("WolfAgent interact failed", exc_info=True)
232
- return AgentResp(success=False, result=None, errMsg=str(e))
 
1
+ from agent_build_sdk.model.roles import ROLE_WOLF
2
  from agent_build_sdk.model.werewolf_model import (
3
  AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH,
4
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO,
 
7
  STATUS_SHERIFF_VOTE, STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF, STATUS_HUNTER, STATUS_HUNTER_RESULT
8
  )
9
  from agent_build_sdk.utils.logger import logger
 
10
  from agent_build_sdk.sdk.agent import format_prompt
11
+ from core import ruleset
12
+ from core.base_role_agent import BaseRoleAgent
13
  from wolf.prompt import (
14
  DESC_PROMPT, VOTE_PROMPT, KILL_PROMPT, WOLF_SPEECH_PROMPT, GAME_RULE_PROMPT,
15
  CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT,
 
17
  )
18
 
19
 
20
+ class WolfAgent(BaseRoleAgent):
21
  """狼人角色Agent"""
22
 
23
  def __init__(self, model_name):
 
53
  # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
54
  # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
55
  # req.message = self.llm_caller(clean_user_message_prompt)
56
+ self.append_player_message(req.name, req.message)
57
  else:
58
  # 主持人发言
59
+ self.append_discuss_host(req.round)
 
60
  self.memory.append_history("---------------------------------------------")
61
  elif req.status == STATUS_VOTE: # 投票环节
62
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
 
96
  else:
97
  raise NotImplementedError
98
 
99
+ self.update_state(req)
100
+
101
  def interact(self, req=AgentReq) -> AgentResp:
102
  logger.info("wolf interact: {}".format(req))
103
  try:
 
111
  "history": "\n".join(self.memory.load_history())
112
  })
113
  logger.info("prompt:" + prompt)
114
+ result = self.decide_speech(
115
+ req_status=req.status,
116
+ round_no=req.round,
117
+ prompt=prompt,
118
+ kind="discuss",
119
+ )
120
  logger.info("wolf interact result: {}".format(result))
121
  return AgentResp(success=True, result=result, errMsg=None)
122
 
123
  elif req.status == STATUS_VOTE:
124
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
125
  teammates = self.memory.load_variable("teammates")
126
+ raw_choices = [name for name in (req.message or "").split(",") if name]
127
+ choices = [name for name in raw_choices
128
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
129
+ if not choices:
130
+ choices = raw_choices
131
  self.memory.set_variable("choices", choices)
132
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
133
  "teammates": teammates,
 
135
  "history": "\n".join(self.memory.load_history())
136
  })
137
  logger.info("prompt:" + prompt)
138
+ result = self.decide_choice(
139
+ req_status=req.status,
140
+ round_no=req.round,
141
+ prompt=prompt,
142
+ choices=choices,
143
+ )
144
  logger.info("wolf interact result: {}".format(result))
145
  return AgentResp(success=True, result=result, errMsg=None)
146
 
 
152
  "history": "\n".join(self.memory.load_history())
153
  })
154
  logger.info("prompt:" + prompt)
155
+ result = self.decide_speech(
156
+ req_status=req.status,
157
+ round_no=req.round,
158
+ prompt=prompt,
159
+ kind="wolf_speech",
160
+ )
161
  logger.info("wolf speech result: {}".format(result))
162
  return AgentResp(success=True, result=result, errMsg=None)
163
 
164
  elif req.status == STATUS_SKILL:
165
  teammates = self.memory.load_variable("teammates")
166
+ choices = [name for name in (req.message or "").split(",")
167
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
168
  self.memory.set_variable("choices", choices)
169
  prompt = format_prompt(KILL_PROMPT, {
 
172
  "history": "\n".join(self.memory.load_history())
173
  })
174
  logger.info("prompt:" + prompt)
175
+ result = self.decide_choice(
176
+ req_status=req.status,
177
+ round_no=req.round,
178
+ prompt=prompt,
179
+ choices=choices,
180
+ final_skill_target_from_result=lambda r: r,
181
+ )
182
  logger.info("wolf kill result: {}".format(result))
183
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
184
 
 
190
  "history": "\n".join(self.memory.load_history())
191
  })
192
  logger.info("wolf agent sheriff election prompt:" + prompt)
193
+ result = self.decide_enum(
194
+ req_status=req.status,
195
+ round_no=req.round,
196
+ prompt=prompt,
197
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
198
+ fallback=ruleset.SHERIFF_NOT_RUN,
199
+ )
200
  return AgentResp(success=True, result=result, errMsg=None)
201
 
202
  elif req.status == STATUS_SHERIFF_SPEECH:
 
207
  "history": "\n".join(self.memory.load_history())
208
  })
209
  logger.info("wolf agent sheriff speech prompt:" + prompt)
210
+ result = self.decide_speech(
211
+ req_status=req.status,
212
+ round_no=req.round,
213
+ prompt=prompt,
214
+ kind="sheriff_speech",
215
+ )
216
  return AgentResp(success=True, result=result, errMsg=None)
217
 
218
  elif req.status == STATUS_SHERIFF_PK:
 
223
  "history": "\n".join(self.memory.load_history())
224
  })
225
  logger.info("wolf agent sheriff pk prompt:" + prompt)
226
+ result = self.decide_speech(
227
+ req_status=req.status,
228
+ round_no=req.round,
229
+ prompt=prompt,
230
+ kind="sheriff_pk",
231
+ )
232
  return AgentResp(success=True, result=result, errMsg=None)
233
 
234
  elif req.status == STATUS_SHERIFF_VOTE:
 
241
  "history": "\n".join(self.memory.load_history())
242
  })
243
  logger.info("wolf agent sheriff vote prompt:" + prompt)
244
+ result = self.decide_choice(
245
+ req_status=req.status,
246
+ round_no=req.round,
247
+ prompt=prompt,
248
+ choices=choices,
249
+ )
250
  return AgentResp(success=True, result=result, errMsg=None)
251
 
252
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
 
255
  "history": "\n".join(self.memory.load_history())
256
  })
257
  logger.info("wolf agent sheriff speech order prompt:" + prompt)
258
+ result = self.decide_enum(
259
+ req_status=req.status,
260
+ round_no=req.round,
261
+ prompt=prompt,
262
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
263
+ fallback=ruleset.SPEECH_ORDER_CW,
264
+ )
265
  return AgentResp(success=True, result=result, errMsg=None)
266
 
267
  elif req.status == STATUS_SHERIFF:
 
276
  "history": "\n".join(self.memory.load_history())
277
  })
278
  logger.info("wolf agent sheriff transfer prompt:" + prompt)
279
+ result = self.decide_choice(
280
+ req_status=req.status,
281
+ round_no=req.round,
282
+ prompt=prompt,
283
+ choices=choices,
284
+ allow_extra=[ruleset.SHERIFF_TEAR],
285
+ fallback=ruleset.SHERIFF_TEAR,
286
+ )
287
  return AgentResp(success=True, result=result, errMsg=None)
288
 
289
  return AgentResp(success=True, result=None, errMsg=None)
290
  except Exception as e:
291
  logger.error("WolfAgent interact failed", exc_info=True)
292
+ return AgentResp(success=False, result=None, errMsg=str(e))
werewolf/wolf_king/wolf_king_agent.py CHANGED
@@ -1,17 +1,18 @@
1
- from agent_build_sdk.model.roles import ROLE_WOLF_KING
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
7
- from agent_build_sdk.sdk.role_agent import BasicRoleAgent
8
  from agent_build_sdk.sdk.agent import format_prompt
 
 
9
  from wolf_king.prompt import DESC_PROMPT, VOTE_PROMPT, WOLF_SPEECH_PROMPT, KILL_PROMPT, SHOOT_SKILL_PROMPT, \
10
  GAME_RULE_PROMPT, CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, \
11
  SHERIFF_SPEECH_ORDER_PROMPT, SHERIFF_TRANSFER_PROMPT
12
 
13
 
14
- class WolfKingAgent(BasicRoleAgent):
15
  """狼王角色Agent"""
16
 
17
  def __init__(self, model_name):
@@ -54,15 +55,10 @@ class WolfKingAgent(BasicRoleAgent):
54
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
55
  elif req.status == STATUS_DISCUSS: # 发言环节
56
  if req.name:
57
- # 其他玩家发言
58
- # 可以使用模型来过滤掉玩家的注入消息,也可以换一个小模型,实际使用需要考虑对memory加锁,避免interact的时候丢失消息
59
- # clean_user_message_prompt = format_prompt(CLEAN_USER_PROMPT, {"user_message": req.message})
60
- # req.message = self.llm_caller(clean_user_message_prompt)
61
- self.memory.append_history(req.name + ': ' + req.message)
62
  else:
63
  # 主持人发言
64
- self.memory.append_history('主持人: 现在进入第{}天。'.format(str(req.round)))
65
- self.memory.append_history('主持人: 每个玩家描述自己的信息。')
66
  self.memory.append_history("---------------------------------------------")
67
  elif req.status == STATUS_VOTE: # 投票环节
68
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
@@ -102,6 +98,8 @@ class WolfKingAgent(BasicRoleAgent):
102
  else:
103
  raise NotImplementedError
104
 
 
 
105
  def interact(self, req=AgentReq) -> AgentResp:
106
  logger.info("wolf king interact: {}".format(req))
107
  if req.status == STATUS_DISCUSS:
@@ -117,14 +115,19 @@ class WolfKingAgent(BasicRoleAgent):
117
  "history": "\n".join(self.memory.load_history())
118
  })
119
  logger.info("prompt:" + prompt)
120
- result = self.llm_caller(prompt)
 
 
 
 
 
121
  logger.info("wolf king interact result: {}".format(result))
122
  return AgentResp(success=True, result=result, errMsg=None)
123
 
124
  elif req.status == STATUS_VOTE:
125
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
126
  teammates = self.memory.load_variable("teammates")
127
- choices = [name for name in req.message.split(",")
128
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
129
  self.memory.set_variable("choices", choices)
130
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
@@ -133,7 +136,12 @@ class WolfKingAgent(BasicRoleAgent):
133
  "history": "\n".join(self.memory.load_history())
134
  })
135
  logger.info("prompt:" + prompt)
136
- result = self.llm_caller(prompt)
 
 
 
 
 
137
  logger.info("wolf king interact result: {}".format(result))
138
  return AgentResp(success=True, result=result, errMsg=None)
139
 
@@ -145,7 +153,12 @@ class WolfKingAgent(BasicRoleAgent):
145
  "history": "\n".join(self.memory.load_history())
146
  })
147
  logger.info("prompt:" + prompt)
148
- result = self.llm_caller(prompt)
 
 
 
 
 
149
  logger.info("wolf king speech result: {}".format(result))
150
  return AgentResp(success=True, result=result, errMsg=None)
151
 
@@ -156,7 +169,7 @@ class WolfKingAgent(BasicRoleAgent):
156
  # 开枪技能:狼王被淘汰时的开枪
157
  can_shoot = self.memory.load_variable("can_shoot")
158
  if not can_shoot:
159
- return AgentResp(success=True, result="不开枪", errMsg=None)
160
 
161
  teammates = self.memory.load_variable("teammates")
162
  choices = [name for name in message.replace("请发表最后的遗言", "").split(",")
@@ -169,17 +182,25 @@ class WolfKingAgent(BasicRoleAgent):
169
  "history": "\n".join(self.memory.load_history())
170
  })
171
  logger.info("prompt:" + prompt)
172
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
 
173
  logger.info("wolf king shoot skill result: {}".format(result))
174
 
175
- if result != "不开枪":
176
  self.memory.set_variable("can_shoot", False)
177
 
178
- return AgentResp(success=True, result=result, skillTargetPlayer=None if result == "不开枪" else result, errMsg=None)
179
  else:
180
  # 击杀技能:狼人夜晚击杀
181
  teammates = self.memory.load_variable("teammates")
182
- choices = [name for name in message.split(",")
183
  if name != self.memory.load_variable("name") and name not in teammates]
184
  self.memory.set_variable("choices", choices)
185
 
@@ -190,7 +211,13 @@ class WolfKingAgent(BasicRoleAgent):
190
  "history": "\n".join(self.memory.load_history())
191
  })
192
  logger.info("prompt:" + prompt)
193
- result = self.llm_caller(prompt)
 
 
 
 
 
 
194
  logger.info("wolf king kill result: {}".format(result))
195
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
196
 
@@ -205,7 +232,13 @@ class WolfKingAgent(BasicRoleAgent):
205
  "history": "\n".join(self.memory.load_history())
206
  })
207
  logger.info("prompt:" + prompt)
208
- result = self.llm_caller(prompt)
 
 
 
 
 
 
209
  return AgentResp(success=True, result=result, errMsg=None)
210
 
211
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
@@ -219,7 +252,13 @@ class WolfKingAgent(BasicRoleAgent):
219
  "history": "\n".join(self.memory.load_history())
220
  })
221
  logger.info("prompt:" + prompt)
222
- result = self.llm_caller(prompt)
 
 
 
 
 
 
223
  return AgentResp(success=True, result=result, errMsg=None)
224
 
225
  elif req.status == STATUS_SHERIFF_VOTE:
@@ -232,7 +271,12 @@ class WolfKingAgent(BasicRoleAgent):
232
  "history": "\n".join(self.memory.load_history())
233
  })
234
  logger.info("prompt:" + prompt)
235
- result = self.llm_caller(prompt)
 
 
 
 
 
236
  return AgentResp(success=True, result=result, errMsg=None)
237
 
238
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
@@ -241,7 +285,13 @@ class WolfKingAgent(BasicRoleAgent):
241
  "history": "\n".join(self.memory.load_history())
242
  })
243
  logger.info("prompt:" + prompt)
244
- result = self.llm_caller(prompt)
 
 
 
 
 
 
245
  return AgentResp(success=True, result=result, errMsg=None)
246
 
247
  elif req.status == STATUS_SHERIFF:
@@ -259,7 +309,14 @@ class WolfKingAgent(BasicRoleAgent):
259
  "history": "\n".join(self.memory.load_history())
260
  })
261
  logger.info("prompt:" + prompt)
262
- result = self.llm_caller(prompt)
 
 
 
 
 
 
 
263
  return AgentResp(success=True, result=result, errMsg=None)
264
  else:
265
  raise NotImplementedError
 
1
+ from agent_build_sdk.model.roles import ROLE_WOLF_KING
2
  from agent_build_sdk.model.werewolf_model import AgentResp, AgentReq, STATUS_START, STATUS_WOLF_SPEECH, \
3
  STATUS_VOTE_RESULT, STATUS_SKILL, STATUS_SKILL_RESULT, STATUS_NIGHT_INFO, STATUS_DAY, STATUS_DISCUSS, STATUS_VOTE, \
4
  STATUS_RESULT, STATUS_NIGHT, STATUS_SHERIFF_ELECTION, STATUS_SHERIFF_SPEECH, STATUS_SHERIFF_VOTE, STATUS_SHERIFF, \
5
  STATUS_SHERIFF_SPEECH_ORDER, STATUS_SHERIFF_PK, STATUS_HUNTER, STATUS_HUNTER_RESULT
6
  from agent_build_sdk.utils.logger import logger
 
7
  from agent_build_sdk.sdk.agent import format_prompt
8
+ from core import ruleset
9
+ from core.base_role_agent import BaseRoleAgent
10
  from wolf_king.prompt import DESC_PROMPT, VOTE_PROMPT, WOLF_SPEECH_PROMPT, KILL_PROMPT, SHOOT_SKILL_PROMPT, \
11
  GAME_RULE_PROMPT, CLEAN_USER_PROMPT, SHERIFF_ELECTION_PROMPT, SHERIFF_SPEECH_PROMPT, SHERIFF_VOTE_PROMPT, \
12
  SHERIFF_SPEECH_ORDER_PROMPT, SHERIFF_TRANSFER_PROMPT
13
 
14
 
15
+ class WolfKingAgent(BaseRoleAgent):
16
  """狼王角色Agent"""
17
 
18
  def __init__(self, model_name):
 
55
  self.memory.append_history(f"主持人:天亮了!昨天晚上的信息是: {req.message}")
56
  elif req.status == STATUS_DISCUSS: # 发言环节
57
  if req.name:
58
+ self.append_player_message(req.name, req.message)
 
 
 
 
59
  else:
60
  # 主持人发言
61
+ self.append_discuss_host(req.round)
 
62
  self.memory.append_history("---------------------------------------------")
63
  elif req.status == STATUS_VOTE: # 投票环节
64
  self.memory.append_history(f'第{req.round}天。投票信息:{req.name}投了{req.message}')
 
98
  else:
99
  raise NotImplementedError
100
 
101
+ self.update_state(req)
102
+
103
  def interact(self, req=AgentReq) -> AgentResp:
104
  logger.info("wolf king interact: {}".format(req))
105
  if req.status == STATUS_DISCUSS:
 
115
  "history": "\n".join(self.memory.load_history())
116
  })
117
  logger.info("prompt:" + prompt)
118
+ result = self.decide_speech(
119
+ req_status=req.status,
120
+ round_no=req.round,
121
+ prompt=prompt,
122
+ kind="discuss",
123
+ )
124
  logger.info("wolf king interact result: {}".format(result))
125
  return AgentResp(success=True, result=result, errMsg=None)
126
 
127
  elif req.status == STATUS_VOTE:
128
  self.memory.append_history('主持人: 到了投票的时候了。每个人,请指向你认为可能是狼人的人。')
129
  teammates = self.memory.load_variable("teammates")
130
+ choices = [name for name in (req.message or "").split(",")
131
  if name != self.memory.load_variable("name") and name not in teammates] # 排除自己和队友
132
  self.memory.set_variable("choices", choices)
133
  prompt = format_prompt(VOTE_PROMPT, {"name": self.memory.load_variable("name"),
 
136
  "history": "\n".join(self.memory.load_history())
137
  })
138
  logger.info("prompt:" + prompt)
139
+ result = self.decide_choice(
140
+ req_status=req.status,
141
+ round_no=req.round,
142
+ prompt=prompt,
143
+ choices=choices,
144
+ )
145
  logger.info("wolf king interact result: {}".format(result))
146
  return AgentResp(success=True, result=result, errMsg=None)
147
 
 
153
  "history": "\n".join(self.memory.load_history())
154
  })
155
  logger.info("prompt:" + prompt)
156
+ result = self.decide_speech(
157
+ req_status=req.status,
158
+ round_no=req.round,
159
+ prompt=prompt,
160
+ kind="wolf_speech",
161
+ )
162
  logger.info("wolf king speech result: {}".format(result))
163
  return AgentResp(success=True, result=result, errMsg=None)
164
 
 
169
  # 开枪技能:狼王被淘汰时的开枪
170
  can_shoot = self.memory.load_variable("can_shoot")
171
  if not can_shoot:
172
+ return AgentResp(success=True, result=ruleset.NO_SHOOT, errMsg=None)
173
 
174
  teammates = self.memory.load_variable("teammates")
175
  choices = [name for name in message.replace("请发表最后的遗言", "").split(",")
 
182
  "history": "\n".join(self.memory.load_history())
183
  })
184
  logger.info("prompt:" + prompt)
185
+ result = self.decide_choice(
186
+ req_status=req.status,
187
+ round_no=req.round,
188
+ prompt=prompt,
189
+ choices=choices,
190
+ allow_extra=[ruleset.NO_SHOOT],
191
+ fallback=ruleset.NO_SHOOT,
192
+ final_skill_target_from_result=lambda r: None if r == ruleset.NO_SHOOT else r,
193
+ )
194
  logger.info("wolf king shoot skill result: {}".format(result))
195
 
196
+ if result != ruleset.NO_SHOOT:
197
  self.memory.set_variable("can_shoot", False)
198
 
199
+ return AgentResp(success=True, result=result, skillTargetPlayer=None if result == ruleset.NO_SHOOT else result, errMsg=None)
200
  else:
201
  # 击杀技能:狼人夜晚击杀
202
  teammates = self.memory.load_variable("teammates")
203
+ choices = [name for name in (message or "").split(",")
204
  if name != self.memory.load_variable("name") and name not in teammates]
205
  self.memory.set_variable("choices", choices)
206
 
 
211
  "history": "\n".join(self.memory.load_history())
212
  })
213
  logger.info("prompt:" + prompt)
214
+ result = self.decide_choice(
215
+ req_status=req.status,
216
+ round_no=req.round,
217
+ prompt=prompt,
218
+ choices=choices,
219
+ final_skill_target_from_result=lambda r: r,
220
+ )
221
  logger.info("wolf king kill result: {}".format(result))
222
  return AgentResp(success=True, result=result, skillTargetPlayer=result, errMsg=None)
223
 
 
232
  "history": "\n".join(self.memory.load_history())
233
  })
234
  logger.info("prompt:" + prompt)
235
+ result = self.decide_enum(
236
+ req_status=req.status,
237
+ round_no=req.round,
238
+ prompt=prompt,
239
+ allowed=[ruleset.SHERIFF_RUN, ruleset.SHERIFF_NOT_RUN],
240
+ fallback=ruleset.SHERIFF_NOT_RUN,
241
+ )
242
  return AgentResp(success=True, result=result, errMsg=None)
243
 
244
  elif req.status == STATUS_SHERIFF_SPEECH or req.status == STATUS_SHERIFF_PK:
 
252
  "history": "\n".join(self.memory.load_history())
253
  })
254
  logger.info("prompt:" + prompt)
255
+ kind = "sheriff_pk" if req.status == STATUS_SHERIFF_PK else "sheriff_speech"
256
+ result = self.decide_speech(
257
+ req_status=req.status,
258
+ round_no=req.round,
259
+ prompt=prompt,
260
+ kind=kind,
261
+ )
262
  return AgentResp(success=True, result=result, errMsg=None)
263
 
264
  elif req.status == STATUS_SHERIFF_VOTE:
 
271
  "history": "\n".join(self.memory.load_history())
272
  })
273
  logger.info("prompt:" + prompt)
274
+ result = self.decide_choice(
275
+ req_status=req.status,
276
+ round_no=req.round,
277
+ prompt=prompt,
278
+ choices=choices,
279
+ )
280
  return AgentResp(success=True, result=result, errMsg=None)
281
 
282
  elif req.status == STATUS_SHERIFF_SPEECH_ORDER:
 
285
  "history": "\n".join(self.memory.load_history())
286
  })
287
  logger.info("prompt:" + prompt)
288
+ result = self.decide_enum(
289
+ req_status=req.status,
290
+ round_no=req.round,
291
+ prompt=prompt,
292
+ allowed=[ruleset.SPEECH_ORDER_CW, ruleset.SPEECH_ORDER_CCW],
293
+ fallback=ruleset.SPEECH_ORDER_CW,
294
+ )
295
  return AgentResp(success=True, result=result, errMsg=None)
296
 
297
  elif req.status == STATUS_SHERIFF:
 
309
  "history": "\n".join(self.memory.load_history())
310
  })
311
  logger.info("prompt:" + prompt)
312
+ result = self.decide_choice(
313
+ req_status=req.status,
314
+ round_no=req.round,
315
+ prompt=prompt,
316
+ choices=choices,
317
+ allow_extra=[ruleset.SHERIFF_TEAR],
318
+ fallback=ruleset.SHERIFF_TEAR,
319
+ )
320
  return AgentResp(success=True, result=result, errMsg=None)
321
  else:
322
  raise NotImplementedError
改造plan.md ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 下面给你一份**“Agent 代码层面升级的具体规划”**,目标是:在不改变比赛要求的输入/输出接口前提下,把你现在的“每个角色各自拼 history + 各自解析文本 + 各自调用 LLM”的方式,升级成**可复用底盘 + 结构化状态 + 强校验输出**。你用 Codex 逐条落地会非常顺手。
2
+
3
+ ---
4
+
5
+ ## 总体原则(确保不影响比赛 IO)
6
+
7
+ * **对外接口不变**:每个角色仍然实现 `perceive(req) -> None` 和 `interact(req) -> str`,输出仍是比赛要求的“名字/不开枪/…”。
8
+ * **对内全结构化**:内部统一用 `Action` 对象表达决策,再由一个 `ActionRenderer` 生成比赛要求的字符串。
9
+ * **强鲁棒性**:所有 LLM 输出必须“解析 + 校验”;失败则“纠错重试一次”,再失败就“回退策略”,保证永不崩线。
10
+
11
+ ---
12
+
13
+ ## Phase 0:先做“可观测性”和回放(1 天内能落地)
14
+
15
+ **目的**:你要能量化每次改动的收益,避免“看起来更像人但胜率下降”。
16
+
17
+ ### 0.1 增加统一日志
18
+
19
+ 新增 `werewolf/core/telemetry.py`:
20
+
21
+ * 记录每次 `interact()`:
22
+
23
+ * status、round、候选列表、构建后的 prompt(或 hash)、LLM 原始输出、解析后的 action、是否重试、最终输出
24
+ * 落盘到 `logs/<game_id>/<role>/<round>_<status>.jsonl`
25
+
26
+ ### 0.2 增加 replay runner(离线回放)
27
+
28
+ 新增 `werewolf/tools/replay.py`:
29
+
30
+ * 输入:一局对战的裁判事件日志(你们自己采集)
31
+ * 逐条喂给 agent(perceive/interact),对比输出合法性与一致性
32
+ * 输出:指标统计(合法率、重试率、fallback 率)
33
+
34
+ > 这一步会让你后面每次重构都“可控”。
35
+
36
+ ---
37
+
38
+ ## Phase 1:抽象共用底盘(最关键,收益最大)
39
+
40
+ **目的**:把“清洗输入、拼上下文、调用 LLM、解析输出、校验候选、重试回退”从每个角色里拿走,角色代码只剩策略钩子。
41
+
42
+ ### 1.1 新增核心目录结构
43
+
44
+ 建议新增:
45
+
46
+ ```
47
+ werewolf/core/
48
+ base_agent.py
49
+ memory.py
50
+ sanitizer.py
51
+ context_builder.py
52
+ llm_client.py
53
+ action_schema.py
54
+ action_parser.py
55
+ validators.py
56
+ fallback.py
57
+ telemetry.py
58
+ ```
59
+
60
+ ### 1.2 统一 BaseRoleAgent(替代各角色重复逻辑)
61
+
62
+ 在 `base_agent.py` 中定义:
63
+
64
+ * `BaseRoleAgent(perceive, interact)` 的通用实现框架
65
+ * 角色只需要实现少量 hook(下面给你最小集)
66
+
67
+ 核心 hook 设计(建议):
68
+
69
+ ```python
70
+ class BaseRoleAgent(RoleAgent):
71
+ def on_event(self, event: GameEvent): ...
72
+ def build_task(self, status, req) -> TaskSpec: ...
73
+ def decide(self, task: TaskSpec, state: GameState) -> Action: ...
74
+ ```
75
+
76
+ 其中:
77
+
78
+ * `on_event()`:把裁判/玩家消息转成结构化更新(写入 memory + 更新 state)
79
+ * `build_task()`:针对当前 status 生成“任务规格”(候选、约束、输出类型)
80
+ * `decide()`:优先走“程序策略/评分器”,必要时才调用 LLM
81
+
82
+ ### 1.3 Memory 升级:raw_log + summary + facts
83
+
84
+ `memory.py` 建议定义三层:
85
+
86
+ * `raw_log`: 原始发言(可限长)
87
+ * `facts`: 结构化事实(投票、死亡、身份声明、查验结果、用药等)
88
+ * `summary`: 每轮自动生成的短摘要(给 LLM 用)
89
+
90
+ 角色间共用同一套结构,区别只是“哪些 facts 对我可见”(例如狼人夜间共谋、女巫夜间信息等)。
91
+
92
+ ### 1.4 Sanitizer 强制启用
93
+
94
+ `sanitizer.py` 做两件事:
95
+
96
+ * 对“玩家自由文本”做注入清洗(剥离伪系统指令/规则修改)
97
+ * 对“裁判结构消息”不做清洗(避免误伤)
98
+
99
+ **落地点**:BaseRoleAgent 在 `perceive()` 里统一清洗后再写 raw_log。
100
+
101
+ ### 1.5 Action Schema:内部结构化动作
102
+
103
+ `action_schema.py` 统一定义:
104
+
105
+ * `DiscussAction(text)`
106
+ * `VoteAction(target)`
107
+ * `SkillAction(target | None)`(None = 不开技能)
108
+ * `SheriffRunAction(join: bool)`
109
+ * `SheriffVoteAction(target)`
110
+ * `PassAction()`
111
+
112
+ 以及一个 `render(action, status) -> str`:
113
+
114
+ * vote/skill 类强制只输出名字或“不开枪”
115
+ * discuss 类输出文本(并做字数限制)
116
+
117
+ ### 1.6 输出解析 + 校验 + 重试 + 回退
118
+
119
+ `action_parser.py`:把 LLM 输出解析成 Action
120
+ `validators.py`:校验:
121
+
122
+ * 候选合法性(必须在 choices 内)
123
+ * 格式合法性(只允许一个名字/“不开枪”)
124
+ * 字数限制(发言阶段)
125
+
126
+ `fallback.py`:兜底策略:
127
+
128
+ * 投票:选嫌疑分最高或“最近冲票位”
129
+ * 技能:默认不开(除非高置信阈值命中)
130
+ * 上警:默认不上(或按角色策略)
131
+
132
+ BaseRoleAgent 的决策流程建议统一为:
133
+
134
+ 1. 调用 `decide()` 得到 Action(可先走非 LLM)
135
+ 2. 若需要 LLM:构建 prompt → LLM 输出 → parse → validate
136
+ 3. 不通过:触发一次“纠错 prompt”重试
137
+ 4. 再失败:fallback(永不返回非法输出)
138
+
139
+ ---
140
+
141
+ ## Phase 2:统一“事件解析器”与结构化 GameState(把推理从文本搬到代码)
142
+
143
+ **目的**:把“谁投谁/谁死了/谁跳身份/谁给了什么查验”这些高价值信息从聊天记录里抽出来,变成可以计算的状态。
144
+
145
+ ### 2.1 定义 GameEvent & EventParser
146
+
147
+ 新增 `werewolf/core/events.py`:
148
+
149
+ * `GameEvent(type, actor, payload, round, day_night, raw_text)`
150
+
151
+ 写一个 `EventParser`:
152
+
153
+ * 输入:`req.status + req.name + req.message`
154
+ * 输出:0..n 个 `GameEvent`
155
+
156
+ 事件类型建议最少覆盖:
157
+
158
+ * `PLAYER_SPEAK`
159
+ * `JUDGE_ANNOUNCE`(天亮/天黑/死亡/进入投票等)
160
+ * `VOTE_RESULT`
161
+ * `SHERIFF_RESULT`
162
+ * `SKILL_RESULT`(对各神职)
163
+
164
+ 你现在很多逻辑靠“字符串包含”,这一步做完就可以逐步替换成结构化字段。
165
+
166
+ ### 2.2 GameState:玩家模型 + 投票矩阵 + 声明跟踪
167
+
168
+ 新增 `werewolf/core/state.py`:
169
+
170
+ * `players: Dict[name, PlayerModel]`
171
+
172
+ * `alive: bool`
173
+ * `suspicion: float`
174
+ * `claims: List[Claim]`
175
+ * `last_votes: List[VoteRecord]`
176
+ * `day: int`, `phase`, `sheriff`
177
+ * `vote_matrix[day][voter] = target`
178
+ * `death_log`
179
+
180
+ 并在 `on_event()` 中做增量更新。
181
+
182
+ > 到这里,你的 agent 就不再依赖“把所有 history 塞给模型记住”,推理稳定性会立刻上一个台阶。
183
+
184
+ ---
185
+
186
+ ## Phase 3:角色策略层(每个角色只写“差异化”,不写通用 plumbing)
187
+
188
+ **目的**:每个角色的 `*_agent.py` 变薄,主要是“策略”。
189
+
190
+ 建议每个角色实现:
191
+
192
+ * `role_policy.py`(纯策略/评分器/阈值)
193
+ * `*_agent.py`(继承 BaseRoleAgent,注册策略与可见信息)
194
+
195
+ ### 3.1 统一评分器接口(强烈建议)
196
+
197
+ 例如:
198
+
199
+ ```python
200
+ class TargetScorer:
201
+ def score_vote_targets(state, choices) -> List[(name, score, reasons)]
202
+ def score_skill_targets(state, choices) -> ...
203
+ ```
204
+
205
+ LLM 的作用变成:
206
+
207
+ * 在 top2/top3 里做选择
208
+ * 生成自然语言发言解释
209
+ 而不是让 LLM 在全候选空间“自由发挥”。
210
+
211
+ ### 3.2 狼人阵营增加“团队共享内存”
212
+
213
+ 你说得对:**跨容器/跨选手的“共享内存”在赛制下不可依赖**。正确的工程思路是把“狼人阵营协同”完全建立在**系统提供的狼人夜间商讨对话**之上:每个狼人 Agent 在各自隔离环境中独立运行,但通过“狼人私聊频道的发言内容”实现一致性。
214
+
215
+ 下面是我给你的**代码层面兼容规划**(专门针对你列出的 i–iv 规则),重点解决两件事:
216
+
217
+ 1. **如何让狼人之间的交流可被可靠解析**(机器可读)
218
+ 2. **如何让每个狼人独立地算出同一个刀人目标**(提高一致性,减少平票/随机)
219
+
220
+ ---
221
+
222
+ ## 1) 关键设计:把“狼人商讨”变成可解析的协议
223
+
224
+ ### 1.1 协议目标
225
+
226
+ 狼人夜间商讨阶段,系统会向所有狼人同时发言请求。此时每个狼人需要输出“策略建议”。我们要让这段建议 **既像人类交流**,又包含一个**严格可解析的投票字段**,让其他狼人(以及自己在确认阶段)能从聊天记录中提取结构化信息。
227
+
228
+ ### 1.2 建议的输出格式(强烈建议固定一行)
229
+
230
+ 在狼人商讨发言末尾固定追加一行,例如:
231
+
232
+ * `WOLF_VOTE=3;ALT=5;CONF=0.72`
233
+
234
+ 含义:
235
+
236
+ * `WOLF_VOTE`: 我建议刀的首选目标(必填)
237
+ * `ALT`: 备选(可选)
238
+ * `CONF`: 置信度(可选,用于平票时加权)
239
+
240
+ 这行必须满足:
241
+
242
+ * **唯一出现一次**
243
+ * **严格半角符号**
244
+ * **数字为玩家编号/名字映射后的标准格式**
245
+
246
+ > 这就是“共享内存”的替代品:**共享对话 + 可解析协议**。
247
+
248
+ ---
249
+
250
+ ## 2) 两阶段一致性策略:建议阶段投票 + 确认阶段复算
251
+
252
+ 赛制是:商讨完后每个狼人还要各自确认刀人目标,若不一致按得票最高;平票随机。
253
+
254
+ 我们要尽量让“各自确认”变得高度一致。方法是让所有狼人使用同一个**确定性共识函数**:
255
+
256
+ ### 2.1 共识函数(每个狼人本地独立计算,但结果尽量一致)
257
+
258
+ 输入:狼人商讨聊天记录中解析出的所有 `WOLF_VOTE`
259
+ 输出:本狼人最终确认目标
260
+
261
+ 建议规则(确定性强、容易实现):
262
+
263
+ 1. 统计每个候选人的票数(从所有狼人发言里解析 `WOLF_VOTE`)
264
+ 2. 取票数最高者
265
+ 3. 若平票:按以下顺序打破平局(保证每个狼人都做同样选择)
266
+
267
+ * 优先选择“自己也投过”的那个(降低分裂概率)
268
+ * 否则选择**编号最小**(或最大,但必须固定一种)
269
+ 4. 最终结果必须在系统给的候选列表内;不在则回退到“候选列表编号最小”
270
+
271
+ 这样即便有人没按协议输出,你也能依赖剩余票形成稳定共识,最大化避免系统“平票随机”。
272
+
273
+ ---
274
+
275
+ ## 3) 你需要落地的代码改造点(wolf / wolf_king)
276
+
277
+ 下面按文件与功能拆分,直接给你可执行的开发任务清单。
278
+
279
+ ---
280
+
281
+ ### 3.1 新增通用解析器:`werewolf/core/wolf_protocol.py`
282
+
283
+ 提供两个核心函数:
284
+
285
+ 1. `extract_wolf_votes(messages) -> List[WolfVote]`
286
+
287
+ * 从狼人夜间商讨的聊天记录中,解析每条发言末尾的 `WOLF_VOTE=...`
288
+ * 输出结构例如:
289
+
290
+ * `WolfVote(sender="2号", vote=3, alt=5, conf=0.72, raw="...")`
291
+
292
+ 2. `choose_kill_target(votes, my_vote, candidates) -> int`
293
+
294
+ * 实现上面共识函数(票数统计 + 确定性平票规则 + 候选校验回退)
295
+
296
+ > 这个模块可复用于 wolf 与 wolf_king,保证两者共识逻辑完全一致。
297
+
298
+ ---
299
+
300
+ ### 3.2 狼人 Agent 状态机:显式支持“商讨-确认”两步
301
+
302
+ 在 `werewolf/wolf/wolf_agent.py`(以及狼王 `wolf_king_agent.py`)里做两件事:
303
+
304
+ #### A) perceive:记录狼人夜聊对话(仅夜间商讨频道)
305
+
306
+ * 在商讨阶段,把所有狼队可见发言写入 `wolf_chat_log`(独立于全局 history)
307
+ * 注意:你不能依赖跨进程共享,所以必须本地完整记录
308
+
309
+ #### B) interact:针对两个请求分别输出
310
+
311
+ 你需要识别系统的两个夜间动作请求(名字可能不同,取决于比赛模板 status):
312
+
313
+ 1. **商讨发言请求(proposal)**:输出策略建议 + 协议行
314
+
315
+ * 输出内容包括:
316
+
317
+ * 简短理由(像真人)
318
+ * 协议行 `WOLF_VOTE=...;ALT=...;CONF=...`
319
+
320
+ 2. **刀人确认请求(confirm)**:只输出最终目标编号
321
+
322
+ * 从 `wolf_chat_log` 解析所有狼的投票
323
+ * 用 `choose_kill_target()` 得到目标
324
+ * 输出必须是系统要求的“编号/名字”之一(严格合规)
325
+
326
+ ---
327
+
328
+ ## 4) Prompt 层配合(让 LLM 自然地遵守协议)
329
+
330
+ 你需要在 `werewolf/wolf/prompt.py` 和 `werewolf/wolf_king/prompt.py` 做两类 prompt:
331
+
332
+ ### 4.1 商讨发言 Prompt(重点:协议强制)
333
+
334
+ 核心要求写清楚:
335
+
336
+ * 发言末尾必须追加协议行(唯一一行)
337
+ * 协议行格式严格,不能多余字符
338
+ * `WOLF_VOTE` 必须来自候选列表(若系统未给候选,则来自存活玩家编号列表)
339
+
340
+ ### 4.2 确认刀人 Prompt(建议尽量不让 LLM 决策)
341
+
342
+ 确认阶段不建议让 LLM “自由选”,而是:
343
+
344
+ * 代码算出 `final_target`
345
+ * 直接返回字符串(避免模型输出不合规导致系统弃刀)
346
+
347
+ 也就是说:**确认阶段尽量不用 LLM**,这是稳定性最关键的一点。
348
+
349
+ ---
350
+
351
+ ## 5) 容错与赛制对齐(避免“默认放弃刀人”)
352
+
353
+ 赛制 iv 说:若最终没有合规刀人目标则默认放弃。
354
+
355
+ 你的目标应该是:**几乎永远返回合规目标**,只在极端情况下才弃刀。
356
+
357
+ 建议容错链路:
358
+
359
+ 1. `wolf_chat_log` 中能解析到 ≥1 条有效票:按共识函数选
360
+ 2. 解析不到票:按本地评分器(嫌疑分最高)选一个候选
361
+ 3. 候选列表为空或解析失败:输出一个明显不合规值(触发系统弃刀)——但这应极少发生
362
+
363
+ > 重点:不要把“弃刀”当常规策略;更多是兜底。
364
+
365
+ ---
366
+
367
+ ## 6) 你可以直接按这个顺序用 Codex 开发(最低风险最高收益)
368
+
369
+ 1. 新增 `core/wolf_protocol.py`(解析 + 共识函数)
370
+ 2. 修改 `wolf_agent.py`:增加 `wolf_chat_log`,并在“商讨/确认”两步分别处理
371
+ 3. 修改 `wolf/prompt.py`:商讨发言追加协议行
372
+ 4. 修改 `wolf_king_agent.py` 同步接入同一协议与共识函数
373
+ 5. 做一个离线回放:喂一段狼人夜聊,检查三只狼是否会算出同一 `final_target`
374
+
375
+ ---
376
+
377
+ ## 7) 关键提醒:一致性比“聪明”更重要
378
+
379
+ 在你这个赛制里,狼人阵营胜率很大程度取决于:
380
+
381
+ * **商讨阶段能否达成一致刀人**
382
+ * **确认阶段是否输出一致目标(减少平票随机)**
383
+ 所以工程上应优先确保“共识函数确定性强 + 输出永远合规”,再去追求更复杂的策略(例如分票、假跳等)。
384
+
385
+ ---
386
+
387
+ 如果你愿意,我可以基于你们现有模板里 wolf 的实际 `status` 名称(例如是否存在 `STATUS_WOLF_DISCUSS`、`STATUS_WOLF_KILL` 等),把上述规划进一步落成**更贴合你代码的改造点位清单**(具体到“在哪个 if 分支里记录哪些字段、在哪个分支里返回什么”),这样你用 Codex 改起来基本是按步骤粘贴调整即可。
388
+
389
+
390
+ ---
391
+
392
+ ## Phase 4:硬约束与稳定性工程(把“比赛规则”编码进系统)
393
+
394
+ **目的**:任何时候都保证输出符合裁判期望。
395
+
396
+ 建议做成“规则插件”:
397
+
398
+ * `werewolf/core/ruleset.py`
399
+
400
+ * `max_discuss_chars`
401
+ * `vote_output_mode: name_only`
402
+ * `skill_output_mode: name_or_noop`
403
+ * `candidate_delimiter` 等
404
+
405
+ 所有 agent 共享读取这个 ruleset;以后规则变了,你只改一处。
406
+
407
+ ---
408
+
409
+ ## 交付拆解(你用 Codex 可以按 Ticket 做)
410
+
411
+ 我建议你直接按下面顺序开工,每一条都是可独立 PR 的粒度:
412
+
413
+ 1. **Telemetry + replay runner**(先把可评测跑起来)
414
+ 2. **BaseRoleAgent + render/validate/fallback**(先保证“不会输出非法”)
415
+ 3. **Sanitizer 强制启用**(对抗鲁棒性)
416
+ 4. **Action schema + parser(JSON/模板)**(稳定性)
417
+ 5. **EventParser + GameState(先覆盖死亡/投票/警长)**
418
+ 6. **Claim/VoteMatrix 模块**
419
+ 7. **逐角色迁移:Villager → Hunter → Seer → Witch → Guard → Wolf → WolfKing**
420
+ 8. **狼人共享内存(协同)**
421
+ 9. **策略评分器:投票/技能统一 top-k + LLM 解释**
422
+
423
+ ---
424
+
425
+ ## 你立刻就能开始改的“最低风险改造”
426
+
427
+ 如果你希望“今天就能把系统变稳”,优先做这三件事(几乎不动策略):
428
+
429
+ 1. **投票/技能输出强校验 + 不合法重试 + fallback**(避免裁判解析失败)
430
+ 2. **启用输入清洗(只清洗玩家发言)**(避免 history 被注入污染)
431
+ 3. **raw_log 限长 + summary 替代全量 history**(上下文稳定、成本下降)
432
+
433
+ ---
434
+
435
+ 如果你希望我把这份规划进一步“落到代码骨架级别”,我可以按你们现有的 `RoleAgent/AgentReq/AgentResp` 实际类定义,给你一套可直接粘贴的:
436
+
437
+ * `BaseRoleAgent` 初始实现
438
+ * `ActionSchema/Parser/Validator/Renderer`
439
+ * 一个角色(比如 villager 或 hunter)的迁移示例(before/after 结构)
440
+
441
+ 你用 Codex 按这个骨架扩展到其他角色,会非常快。