refactor event handler
This commit is contained in:
@@ -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
|
||||
|
||||
71
src/classes/event_manager.py
Normal file
71
src/classes/event_manager.py
Normal 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:]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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. 时间推进
|
||||
|
||||
@@ -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
|
||||
@@ -6,6 +6,9 @@
|
||||
{avatar_name_2}可以进行的选择为:
|
||||
{feedback_actions}
|
||||
|
||||
最近事件:
|
||||
{recent_events}
|
||||
|
||||
注意,只返回json格式的结果。
|
||||
只返回{avatar_name_2}的行动,格式为:
|
||||
{{
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
结果为:
|
||||
{res}
|
||||
|
||||
最近事件:
|
||||
{recent_events}
|
||||
|
||||
注意,只返回json格式的结果,格式为:
|
||||
{{
|
||||
"story": "", // 第三人称的故事正文,仙侠语言风格
|
||||
|
||||
Reference in New Issue
Block a user