feat: 添加角色事件评论系统
- 新增 Opinion 模型,记录角色对事件的观点 - 新增 opinions.py,基于规则生成观点(支持5种事件×3种情绪) - 同一事件生命周期内每个 agent 只生成一次观点 - 观点同时记录到 agent.memory - 新增 3 个测试用例 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from .models import (
|
|||||||
GlobalEventResult, GlobalEventInfo
|
GlobalEventResult, GlobalEventInfo
|
||||||
)
|
)
|
||||||
from .global_events import GLOBAL_EVENT_POOL, GlobalEvent
|
from .global_events import GLOBAL_EVENT_POOL, GlobalEvent
|
||||||
|
from .opinions import generate_opinions
|
||||||
|
|
||||||
MAX_EVENTS = 20
|
MAX_EVENTS = 20
|
||||||
MAX_MEMORY_PER_AGENT = 3
|
MAX_MEMORY_PER_AGENT = 3
|
||||||
@@ -82,6 +83,9 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState:
|
|||||||
# 更新所有 agent 的 emotion
|
# 更新所有 agent 的 emotion
|
||||||
update_emotions(state)
|
update_emotions(state)
|
||||||
|
|
||||||
|
# 生成角色对事件的观点
|
||||||
|
generate_opinions(state)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
@@ -112,6 +116,9 @@ def check_and_trigger_global_event(
|
|||||||
# 更新 emotion
|
# 更新 emotion
|
||||||
update_emotions(state)
|
update_emotions(state)
|
||||||
|
|
||||||
|
# 生成角色对新事件的观点
|
||||||
|
generate_opinions(state)
|
||||||
|
|
||||||
result = GlobalEventResult(
|
result = GlobalEventResult(
|
||||||
triggered=True,
|
triggered=True,
|
||||||
event=global_event.to_info()
|
event=global_event.to_info()
|
||||||
|
|||||||
@@ -31,10 +31,20 @@ class WorldEffect(BaseModel):
|
|||||||
mood_modifier: int = 0
|
mood_modifier: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Opinion(BaseModel):
|
||||||
|
"""角色对事件的观点"""
|
||||||
|
about: str # 事件类型 (effect_type)
|
||||||
|
text: str # 观点内容
|
||||||
|
tick: int # 生成时的 tick
|
||||||
|
|
||||||
|
|
||||||
class AgentState(BaseModel):
|
class AgentState(BaseModel):
|
||||||
emotion: Emotion = Emotion.CALM
|
emotion: Emotion = Emotion.CALM
|
||||||
goal: str = ""
|
goal: str = ""
|
||||||
memory: List[str] = Field(default_factory=list)
|
memory: List[str] = Field(default_factory=list)
|
||||||
|
opinion: Optional[Opinion] = None
|
||||||
|
# 记录已评论过的事件,防止重复生成
|
||||||
|
commented_effects: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class WorldState(BaseModel):
|
class WorldState(BaseModel):
|
||||||
|
|||||||
163
engine-python/app/opinions.py
Normal file
163
engine-python/app/opinions.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"""角色事件评论系统 - 基于规则生成观点"""
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from .models import WorldState, AgentState, Opinion, Emotion, WorldEffect
|
||||||
|
|
||||||
|
|
||||||
|
# 观点模板:effect_type -> emotion -> 观点列表
|
||||||
|
OPINION_TEMPLATES: Dict[str, Dict[str, List[str]]] = {
|
||||||
|
"festival": {
|
||||||
|
"happy": [
|
||||||
|
"这真是个让人开心的日子!",
|
||||||
|
"节日的气氛太棒了!",
|
||||||
|
"大家都在庆祝,真好!",
|
||||||
|
],
|
||||||
|
"calm": [
|
||||||
|
"节日挺热闹的。",
|
||||||
|
"看来大家都很高兴。",
|
||||||
|
"难得的庆典活动。",
|
||||||
|
],
|
||||||
|
"anxious": [
|
||||||
|
"我不太确定这是不是好事...",
|
||||||
|
"人太多了,有点不安。",
|
||||||
|
"希望一切顺利吧。",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"storm": {
|
||||||
|
"happy": [
|
||||||
|
"暴风雨也有它的美!",
|
||||||
|
"待在室内也挺好的。",
|
||||||
|
"雨声其实很治愈。",
|
||||||
|
],
|
||||||
|
"calm": [
|
||||||
|
"暴风雨来了,注意安全。",
|
||||||
|
"等雨停了再出门吧。",
|
||||||
|
"天气变化无常啊。",
|
||||||
|
],
|
||||||
|
"anxious": [
|
||||||
|
"这暴风雨让我很担心...",
|
||||||
|
"情况看起来不太妙。",
|
||||||
|
"希望大家都平安。",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"scandal": {
|
||||||
|
"happy": [
|
||||||
|
"不管怎样,生活还要继续!",
|
||||||
|
"真相总会大白的。",
|
||||||
|
"保持乐观吧。",
|
||||||
|
],
|
||||||
|
"calm": [
|
||||||
|
"这件事确实令人意外。",
|
||||||
|
"让我们静观其变。",
|
||||||
|
"事情总会解决的。",
|
||||||
|
],
|
||||||
|
"anxious": [
|
||||||
|
"这消息太令人震惊了...",
|
||||||
|
"不知道接下来会怎样。",
|
||||||
|
"小镇的未来让人担忧。",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"miracle": {
|
||||||
|
"happy": [
|
||||||
|
"太神奇了!这是奇迹!",
|
||||||
|
"彩虹真的太美了!",
|
||||||
|
"今天是幸运的一天!",
|
||||||
|
],
|
||||||
|
"calm": [
|
||||||
|
"难得一见的景象。",
|
||||||
|
"大自然真是神奇。",
|
||||||
|
"值得记住的时刻。",
|
||||||
|
],
|
||||||
|
"anxious": [
|
||||||
|
"希望这是好兆头...",
|
||||||
|
"美丽的景象,但愿能持续。",
|
||||||
|
"也许事情会好转吧。",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"fire": {
|
||||||
|
"happy": [
|
||||||
|
"消防队很快就会控制住的!",
|
||||||
|
"大家齐心协力,一定没问题!",
|
||||||
|
"相信我们能度过难关!",
|
||||||
|
],
|
||||||
|
"calm": [
|
||||||
|
"火灾很危险,大家要小心。",
|
||||||
|
"希望没有人受伤。",
|
||||||
|
"消防队正在处理。",
|
||||||
|
],
|
||||||
|
"anxious": [
|
||||||
|
"情况变得不妙,我得小心...",
|
||||||
|
"火灾太可怕了...",
|
||||||
|
"希望大家都能平安。",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 默认观点(未知事件类型)
|
||||||
|
DEFAULT_OPINIONS: Dict[str, List[str]] = {
|
||||||
|
"happy": ["不管发生什么,保持乐观!"],
|
||||||
|
"calm": ["让我们看看会怎样。"],
|
||||||
|
"anxious": ["不知道这意味着什么..."],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_opinion_text(
|
||||||
|
effect_type: str,
|
||||||
|
emotion: Emotion,
|
||||||
|
agent_id: str,
|
||||||
|
tick: int
|
||||||
|
) -> str:
|
||||||
|
"""根据事件类型和情绪生成观点文本"""
|
||||||
|
emotion_key = emotion.value
|
||||||
|
|
||||||
|
# 获取对应模板
|
||||||
|
if effect_type in OPINION_TEMPLATES:
|
||||||
|
templates = OPINION_TEMPLATES[effect_type].get(
|
||||||
|
emotion_key,
|
||||||
|
DEFAULT_OPINIONS[emotion_key]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
templates = DEFAULT_OPINIONS[emotion_key]
|
||||||
|
|
||||||
|
# 基于 agent_id 和 tick 选择模板(确保可复现)
|
||||||
|
index = (hash(agent_id) + tick) % len(templates)
|
||||||
|
return templates[index]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_opinions(state: WorldState) -> None:
|
||||||
|
"""为所有 agent 生成对当前活跃事件的观点"""
|
||||||
|
# 无活跃效果时,清空 opinion
|
||||||
|
if not state.world_effects:
|
||||||
|
for agent in state.agents.values():
|
||||||
|
agent.opinion = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# 取第一个活跃效果(优先级最高)
|
||||||
|
active_effect = state.world_effects[0]
|
||||||
|
effect_key = f"{active_effect.type}_{active_effect.name}"
|
||||||
|
|
||||||
|
for agent_id, agent in state.agents.items():
|
||||||
|
# 检查是否已对此事件发表过观点
|
||||||
|
if effect_key in agent.commented_effects:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 生成观点
|
||||||
|
text = get_opinion_text(
|
||||||
|
active_effect.type,
|
||||||
|
agent.emotion,
|
||||||
|
agent_id,
|
||||||
|
state.tick
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新 agent.opinion
|
||||||
|
agent.opinion = Opinion(
|
||||||
|
about=active_effect.type,
|
||||||
|
text=text,
|
||||||
|
tick=state.tick
|
||||||
|
)
|
||||||
|
|
||||||
|
# 记录到 memory
|
||||||
|
memory_entry = f"[{agent_id}对{active_effect.name}的看法] {text}"
|
||||||
|
agent.memory.append(memory_entry)
|
||||||
|
|
||||||
|
# 标记已评论
|
||||||
|
agent.commented_effects.append(effect_key)
|
||||||
@@ -171,3 +171,61 @@ def test_global_event_cooldown(client):
|
|||||||
assert data["global_event"]["triggered"] is False
|
assert data["global_event"]["triggered"] is False
|
||||||
# 冷却递减
|
# 冷却递减
|
||||||
assert data["world_state"]["global_meter"]["cooldown"] == 4
|
assert data["world_state"]["global_meter"]["cooldown"] == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_opinion_generated_on_world_effect(client):
|
||||||
|
"""测试世界事件触发时生成观点"""
|
||||||
|
# 触发世界事件
|
||||||
|
events = [
|
||||||
|
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
|
||||||
|
for i in range(20)
|
||||||
|
]
|
||||||
|
resp = client.post("/step", json={"events": events})
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# 验证事件触发
|
||||||
|
assert data["global_event"]["triggered"] is True
|
||||||
|
assert len(data["world_state"]["world_effects"]) > 0
|
||||||
|
|
||||||
|
# 验证每个 agent 都有 opinion
|
||||||
|
for agent_id, agent in data["world_state"]["agents"].items():
|
||||||
|
assert agent["opinion"] is not None
|
||||||
|
assert "about" in agent["opinion"]
|
||||||
|
assert "text" in agent["opinion"]
|
||||||
|
assert "tick" in agent["opinion"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_opinion_only_once_per_effect(client):
|
||||||
|
"""测试同一事件只生成一次观点"""
|
||||||
|
# 触发世界事件
|
||||||
|
events = [
|
||||||
|
{"type": "chaos", "text": f"chaos{i}", "user": "test", "ts": i}
|
||||||
|
for i in range(20)
|
||||||
|
]
|
||||||
|
resp = client.post("/step", json={"events": events})
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# 记录第一次的观点
|
||||||
|
first_opinions = {
|
||||||
|
aid: agent["opinion"]["text"]
|
||||||
|
for aid, agent in data["world_state"]["agents"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 再次调用 step(事件仍在持续)
|
||||||
|
resp2 = client.post("/step", json={"events": []})
|
||||||
|
data2 = resp2.json()
|
||||||
|
|
||||||
|
# 观点应该保持不变(不重新生成)
|
||||||
|
for aid, agent in data2["world_state"]["agents"].items():
|
||||||
|
if agent["opinion"]:
|
||||||
|
assert agent["opinion"]["text"] == first_opinions[aid]
|
||||||
|
|
||||||
|
|
||||||
|
def test_opinion_cleared_when_no_effect(client):
|
||||||
|
"""测试无活跃事件时观点为空"""
|
||||||
|
resp = client.post("/step", json={"events": []})
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# 无世界事件时,opinion 应为 None
|
||||||
|
for agent_id, agent in data["world_state"]["agents"].items():
|
||||||
|
assert agent["opinion"] is None
|
||||||
|
|||||||
Reference in New Issue
Block a user