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 +4 -1
- mission.md +171 -0
- werewolf/core/__init__.py +2 -0
- werewolf/core/action_parser.py +51 -0
- werewolf/core/action_renderer.py +53 -0
- werewolf/core/actions.py +43 -0
- werewolf/core/base_role_agent.py +220 -0
- werewolf/core/event_parser.py +117 -0
- werewolf/core/memory_store.py +75 -0
- werewolf/core/output_guard.py +302 -0
- werewolf/core/ruleset.py +14 -0
- werewolf/core/sanitizer.py +34 -0
- werewolf/core/state.py +11 -0
- werewolf/core/telemetry.py +180 -0
- werewolf/guard/guard_agent.py +70 -20
- werewolf/hunter/hunter_agent.py +75 -23
- werewolf/seer/seer_agent.py +73 -22
- werewolf/tools/__init__.py +2 -0
- werewolf/tools/replay.py +156 -0
- werewolf/villager/villager_agent.py +66 -20
- werewolf/witch/witch_agent.py +118 -22
- werewolf/wolf/wolf_agent.py +79 -19
- werewolf/wolf_king/wolf_king_agent.py +82 -25
- 改造plan.md +441 -0
.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(
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ==
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
logger.info("witch skill result: {}".format(result))
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
skill_target_person = None
|
| 137 |
-
if result.startswith("救")
|
| 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("毒")
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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.
|
| 56 |
else:
|
| 57 |
# 主持人发言
|
| 58 |
-
self.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ==
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 按这个骨架扩展到其他角色,会非常快。
|