refactor event handler

This commit is contained in:
bridge
2025-10-24 23:50:05 +08:00
parent 30d5372608
commit 86be290633
9 changed files with 133 additions and 15 deletions

View File

@@ -451,15 +451,14 @@ class Avatar:
def add_event(self, event: Event, *, to_sidebar: bool = True, to_history: bool = True) -> None:
"""
添加事件:
- to_sidebar: 是否进入全局侧边栏(通过 Simulator 收集
- to_history: 是否进入本角色的历史事件(最多保留 MAX_HISTORY_EVENTS 条
- to_sidebar: 是否进入全局侧边栏(通过 Avatar._pending_events 暂存
- to_history: 兼容参数,已废弃(统一改为通过 World.event_manager 查询历史
"""
if to_sidebar:
self._pending_events.append(event)
if to_history:
self.history_events.append(event)
if len(self.history_events) > MAX_HISTORY_EVENTS:
self.history_events = self.history_events[-MAX_HISTORY_EVENTS:]
# 侧边栏类事件通常不在 Simulator 的 events 列表里,直接记入全局事件管理器
em = self.world.event_manager
em.add_event(event)
def get_action_space_str(self) -> str:
action_space = self.get_action_space()
@@ -492,10 +491,11 @@ class Avatar:
for other in co_region_avatars[:8]:
observed.append(f"{other.name}(境界:{other.cultivation_progress.get_info()})")
if self.history_events:
history_list = [str(e) for e in self.history_events[-8:]]
else:
history_list = []
# 历史事件改为从全局事件管理器查询
n = CONFIG.social.event_context_num
em = self.world.event_manager
events = em.get_events_by_avatar(self.id, limit=n)
history_list = [str(e) for e in events]
info["观察到的角色"] = observed
info["历史事件"] = history_list

View File

@@ -0,0 +1,71 @@
from typing import Dict, List
from collections import deque, defaultdict
from src.classes.event import Event
class EventManager:
"""
全局事件管理器:统一保存事件,并提供按角色、按角色对、按时间的查询。
- 限长清理,避免内存无限增长。
- 幂等写入(基于 event_id
- 仅对恰为两人参与的事件建立“按人对”索引。
"""
def __init__(self, *, max_global_events: int = 5000, max_index_events: int = 200) -> None:
self.max_global_events = max_global_events
self.max_index_events = max_index_events
self._events: deque[Event] = deque()
self._by_id: Dict[str, Event] = {}
self._by_avatar: Dict[str, deque[Event]] = defaultdict(deque)
self._by_pair: Dict[frozenset[str], deque[Event]] = defaultdict(deque)
def _append_with_limit(self, dq: deque, item: Event) -> None:
dq.append(item)
if len(dq) > self.max_index_events:
dq.popleft()
def add_event(self, event: Event) -> None:
# 幂等:若已存在同 event_id跳过
if getattr(event, "event_id", None) and event.event_id in self._by_id:
return
if getattr(event, "event_id", None):
self._by_id[event.event_id] = event
# 全局
self._events.append(event)
if len(self._events) > self.max_global_events:
self._events.popleft()
# 分索引:按人/人对
rel = getattr(event, "related_avatars", None) or []
rel_unique = list(dict.fromkeys(rel)) # 去重但保持顺序
for aid in rel_unique:
self._append_with_limit(self._by_avatar[aid], event)
# 仅当且仅当“恰有两位参与者”时建立按人对索引
if len(rel_unique) == 2:
a, b = rel_unique[0], rel_unique[1]
pair_key = frozenset([a, b])
self._append_with_limit(self._by_pair[pair_key], event)
# —— 查询接口 ——
def get_recent_events(self, limit: int = 100) -> List[Event]:
if limit <= 0:
return []
return list(self._events)[-limit:]
def get_events_by_avatar(self, avatar_id: str, *, limit: int = 50) -> List[Event]:
dq = self._by_avatar.get(avatar_id)
if not dq:
return []
return list(dq)[-limit:]
def get_events_between(self, avatar_id1: str, avatar_id2: str, *, limit: int = 50) -> List[Event]:
key = frozenset([avatar_id1, avatar_id2])
dq = self._by_pair.get(key)
if not dq:
return []
return list(dq)[-limit:]

View File

@@ -16,6 +16,7 @@ from src.classes.action.targeting_mixin import TargetingMixin
if TYPE_CHECKING:
from src.classes.avatar import Avatar
from src.classes.world import World
class MutualAction(DefineAction, LLMAction, TargetingMixin):
@@ -64,6 +65,12 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
avatar_name_1: self.avatar.get_info(detailed=False),
avatar_name_2: target_avatar.get_info(detailed=False),
}
# 历史上下文:仅双方共同经历的最近事件
n = CONFIG.social.event_context_num
pair_recent_events: list[str] = []
em = self.world.event_manager
pair_recent_events = [str(e) for e in em.get_events_between(self.avatar.id, target_avatar.id, limit=n)]
feedback_actions = self.FEEDBACK_ACTIONS
comment = self.COMMENT
action_name = self.ACTION_NAME
@@ -74,6 +81,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
"action_name": action_name,
"action_info": comment,
"feedback_actions": feedback_actions,
"recent_events": pair_recent_events,
}
def _call_llm_feedback(self, infos: dict) -> dict:

View File

@@ -45,7 +45,28 @@ class StoryTeller:
return infos
@staticmethod
def tell_story(avatar_infos: Dict[str, dict], event: str, res: str, STORY_PROMPT: str = "") -> str:
def _collect_recent_events(*actors: "Avatar") -> list[str]:
from src.utils.config import CONFIG as _CONFIG
n = _CONFIG.social.event_context_num
world = None
for av in actors:
if av is not None:
world = av.world
break
if world is None:
return []
em = world.event_manager
non_null = [a for a in actors if a is not None]
if len(non_null) >= 2:
a1, a2 = non_null[0], non_null[1]
return [str(e) for e in em.get_events_between(a1.id, a2.id, limit=n)]
if non_null:
a = non_null[0]
return [str(e) for e in em.get_events_by_avatar(a.id, limit=n)]
return []
@staticmethod
def tell_story(avatar_infos: Dict[str, dict], event: str, res: str, STORY_PROMPT: str = "", *, recent_events: list[str] | None = None) -> str:
"""
基于 `static/templates/story.txt` 模板生成小故事。
始终使用 fast 模式以提升速度。
@@ -58,6 +79,7 @@ class StoryTeller:
"res": res,
"style": random.choice(story_styles),
"story_prompt": STORY_PROMPT or "",
"recent_events": (recent_events or []),
}
try:
data = get_prompt_and_call_llm(template_path, infos, mode="fast")
@@ -71,7 +93,7 @@ class StoryTeller:
return f"{event}{res}{style}"
@staticmethod
async def tell_story_async(avatar_infos: Dict[str, dict], event: str, res: str, STORY_PROMPT: str = "") -> str:
async def tell_story_async(avatar_infos: Dict[str, dict], event: str, res: str, STORY_PROMPT: str = "", *, recent_events: list[str] | None = None) -> str:
"""
异步版本:生成小故事,失败时返回降级文案。
"""
@@ -82,6 +104,7 @@ class StoryTeller:
"res": res,
"style": random.choice(story_styles),
"story_prompt": STORY_PROMPT or "",
"recent_events": (recent_events or []),
}
try:
data = await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
@@ -99,12 +122,14 @@ class StoryTeller:
便捷方法:直接从参与者对象生成 avatar_infos 并讲述故事。
"""
avatar_infos = StoryTeller.build_avatar_infos(*actors)
return StoryTeller.tell_story(avatar_infos, event, res, prompt or "")
recent_events = StoryTeller._collect_recent_events(*actors)
return StoryTeller.tell_story(avatar_infos, event, res, prompt or "", recent_events=recent_events)
@staticmethod
async def tell_from_actors_async(event: str, res: str, *actors: "Avatar", prompt: str | None = None) -> str:
avatar_infos = StoryTeller.build_avatar_infos(*actors)
return await StoryTeller.tell_story_async(avatar_infos, event, res, prompt or "")
recent_events = StoryTeller._collect_recent_events(*actors)
return await StoryTeller.tell_story_async(avatar_infos, event, res, prompt or "", recent_events=recent_events)
__all__ = ["StoryTeller"]

View File

@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
from src.classes.map import Map
from src.classes.calendar import Year, Month, MonthStamp
from src.classes.avatar_manager import AvatarManager
from src.classes.event_manager import EventManager
if TYPE_CHECKING:
from src.classes.avatar import Avatar
@@ -14,6 +15,8 @@ class World():
map: Map
month_stamp: MonthStamp
avatar_manager: AvatarManager = field(default_factory=AvatarManager)
# 全局事件管理器
event_manager: EventManager = field(default_factory=EventManager)
def get_info(self, detailed: bool = False) -> dict:
"""

View File

@@ -161,6 +161,10 @@ class Simulator:
events.extend(self._phase_passive_effects())
# 7. 日志
# 统一写入事件管理器
if hasattr(self.world, "event_manager") and self.world.event_manager is not None:
for e in events:
self.world.event_manager.add_event(e)
self._phase_log_events(events)
# 8. 时间推进

View File

@@ -27,4 +27,5 @@ avatar:
persona_num: 3
social:
talk_into_relation_probability: 0.1
talk_into_relation_probability: 0.1
event_context_num: 6

View File

@@ -6,6 +6,9 @@
{avatar_name_2}可以进行的选择为:
{feedback_actions}
最近事件:
{recent_events}
注意只返回json格式的结果。
只返回{avatar_name_2}的行动,格式为:
{{

View File

@@ -9,6 +9,9 @@
结果为:
{res}
最近事件:
{recent_events}
注意只返回json格式的结果格式为
{{
"story": "", // 第三人称的故事正文,仙侠语言风格