From 86be290633a81ee81ff62b43c114f3e728812f59 Mon Sep 17 00:00:00 2001 From: bridge Date: Fri, 24 Oct 2025 23:50:05 +0800 Subject: [PATCH] refactor event handler --- src/classes/avatar.py | 20 +++--- src/classes/event_manager.py | 71 ++++++++++++++++++++++ src/classes/mutual_action/mutual_action.py | 8 +++ src/classes/story_teller.py | 33 ++++++++-- src/classes/world.py | 3 + src/sim/simulator.py | 4 ++ static/config.yml | 3 +- static/templates/mutual_action.txt | 3 + static/templates/story.txt | 3 + 9 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 src/classes/event_manager.py diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 2ff581a..0702f11 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -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 diff --git a/src/classes/event_manager.py b/src/classes/event_manager.py new file mode 100644 index 0000000..e5fed99 --- /dev/null +++ b/src/classes/event_manager.py @@ -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:] + + diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index e99ea5e..5c3933a 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -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: diff --git a/src/classes/story_teller.py b/src/classes/story_teller.py index 52e6b26..390247a 100644 --- a/src/classes/story_teller.py +++ b/src/classes/story_teller.py @@ -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"] diff --git a/src/classes/world.py b/src/classes/world.py index 8088c93..ecaa77d 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -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: """ diff --git a/src/sim/simulator.py b/src/sim/simulator.py index ef35579..1111970 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -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. 时间推进 diff --git a/static/config.yml b/static/config.yml index e543ce0..366babd 100644 --- a/static/config.yml +++ b/static/config.yml @@ -27,4 +27,5 @@ avatar: persona_num: 3 social: - talk_into_relation_probability: 0.1 \ No newline at end of file + talk_into_relation_probability: 0.1 + event_context_num: 6 \ No newline at end of file diff --git a/static/templates/mutual_action.txt b/static/templates/mutual_action.txt index 2899aa4..6daf5fc 100644 --- a/static/templates/mutual_action.txt +++ b/static/templates/mutual_action.txt @@ -6,6 +6,9 @@ {avatar_name_2}可以进行的选择为: {feedback_actions} +最近事件: +{recent_events} + 注意,只返回json格式的结果。 只返回{avatar_name_2}的行动,格式为: {{ diff --git a/static/templates/story.txt b/static/templates/story.txt index 452b2c9..bcf2033 100644 --- a/static/templates/story.txt +++ b/static/templates/story.txt @@ -9,6 +9,9 @@ 结果为: {res} +最近事件: +{recent_events} + 注意,只返回json格式的结果,格式为: {{ "story": "", // 第三人称的故事正文,仙侠语言风格