fix async bug
This commit is contained in:
@@ -4,6 +4,8 @@ from src.classes.action import InstantAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.battle import decide_battle, get_effective_strength_pair
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.action.event_helper import EventHelper
|
||||
from src.utils.asyncio_utils import schedule_background
|
||||
|
||||
|
||||
class Battle(InstantAction):
|
||||
@@ -71,11 +73,23 @@ class Battle(InstantAction):
|
||||
pass
|
||||
result_event = Event(self.world.month_stamp, result_text, related_avatars=rel_ids)
|
||||
|
||||
# 生成战斗小故事:使用便捷方法从参与者直接生成
|
||||
# 异步生成战斗小故事并在完成后推送事件,避免阻塞事件循环
|
||||
target = self._get_target(avatar_name)
|
||||
start_text = getattr(self, "_start_event_content", "") or result_event.content
|
||||
story = StoryTeller.tell_from_actors(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
|
||||
story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids)
|
||||
return [result_event, story_event]
|
||||
month_at_finish = self.world.month_stamp
|
||||
|
||||
async def _gen_and_push_story():
|
||||
story = await StoryTeller.tell_from_actors_async(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
|
||||
story_event = Event(month_at_finish, story, related_avatars=rel_ids)
|
||||
EventHelper.push_pair(story_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||
|
||||
def _fallback_sync():
|
||||
story = StoryTeller.tell_from_actors(start_text, result_event.content, self.avatar, target, prompt=self.STORY_PROMPT)
|
||||
story_event = Event(month_at_finish, story, related_avatars=rel_ids)
|
||||
EventHelper.push_pair(story_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||
|
||||
schedule_background(_gen_and_push_story(), fallback=_fallback_sync)
|
||||
|
||||
return [result_event]
|
||||
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
from src.utils.config import CONFIG
|
||||
from src.classes.avatar import Avatar
|
||||
from src.classes.event import Event
|
||||
from src.classes.story_teller import StoryTeller
|
||||
from src.classes.action.event_helper import EventHelper
|
||||
from src.utils.asyncio_utils import schedule_background
|
||||
from src.classes.technique import TechniqueGrade, get_random_upper_technique_for_avatar
|
||||
from src.classes.treasure import Treasure, treasures_by_id
|
||||
|
||||
@@ -107,18 +110,28 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
avatar.technique = tech
|
||||
res_text = f"{avatar.name} 得到上品功法『{tech.name}』"
|
||||
|
||||
# 生成故事
|
||||
# 生成故事(异步避免阻塞)
|
||||
event_text = f"遭遇奇遇({theme}),{res_text}"
|
||||
story_prompt = (
|
||||
f"请据此写100~150字小故事。"
|
||||
)
|
||||
story = StoryTeller.tell_from_actors(event_text, res_text, avatar, prompt=story_prompt)
|
||||
|
||||
events: list[Event] = [
|
||||
Event(avatar.world.month_stamp, event_text, related_avatars=[avatar.id]),
|
||||
Event(avatar.world.month_stamp, story, related_avatars=[avatar.id]),
|
||||
]
|
||||
return events
|
||||
month_at_finish = avatar.world.month_stamp
|
||||
base_event = Event(month_at_finish, event_text, related_avatars=[avatar.id])
|
||||
|
||||
async def _gen_and_push_story():
|
||||
story = await StoryTeller.tell_from_actors_async(event_text, res_text, avatar, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=[avatar.id])
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
|
||||
def _fallback_sync():
|
||||
story = StoryTeller.tell_from_actors(event_text, res_text, avatar, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=[avatar.id])
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
|
||||
schedule_background(_gen_and_push_story(), fallback=_fallback_sync)
|
||||
|
||||
return [base_event]
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -2,11 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
import asyncio
|
||||
|
||||
from src.classes.action import DefineAction, ActualActionMixin, LLMAction
|
||||
from src.classes.tile import get_avatar_distance
|
||||
from src.classes.event import Event
|
||||
from src.utils.llm import get_prompt_and_call_llm
|
||||
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
|
||||
from src.utils.config import CONFIG
|
||||
from src.classes.relation import relation_display_names, Relation, get_possible_post_relations
|
||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||
@@ -45,6 +46,12 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
# 若该互动动作可能生成小故事,可在子类中覆盖该提示词
|
||||
STORY_PROMPT: str | None = None
|
||||
|
||||
def __init__(self, avatar: "Avatar", world: "World"):
|
||||
super().__init__(avatar, world)
|
||||
# 异步反馈任务句柄与缓存结果
|
||||
self._feedback_task: asyncio.Task | None = None
|
||||
self._feedback_cached: dict | None = None
|
||||
|
||||
def _get_template_path(self) -> Path:
|
||||
return CONFIG.paths.templates / "mutual_action.txt"
|
||||
|
||||
@@ -70,11 +77,17 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
}
|
||||
|
||||
def _call_llm_feedback(self, infos: dict) -> dict:
|
||||
"""
|
||||
兼容保留:同步调用(不在事件循环内使用)。
|
||||
"""
|
||||
template_path = self._get_template_path()
|
||||
# mutual用快速llm,不需要复杂决策
|
||||
res = get_prompt_and_call_llm(template_path, infos, mode="fast")
|
||||
return res
|
||||
|
||||
async def _call_llm_feedback_async(self, infos: dict) -> dict:
|
||||
template_path = self._get_template_path()
|
||||
return await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
|
||||
|
||||
def _set_target_immediate_action(self, target_avatar: "Avatar", action_name: str, action_params: dict) -> None:
|
||||
"""
|
||||
将反馈决定落地为目标角色的立即动作(清空后加载单步动作链)。
|
||||
@@ -115,29 +128,25 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
return target_avatar
|
||||
|
||||
def _execute(self, target_avatar: "Avatar|str") -> None:
|
||||
"""
|
||||
保留同步实现(不在事件循环内使用)。
|
||||
"""
|
||||
target_avatar = self._get_target_avatar(target_avatar)
|
||||
if target_avatar is None:
|
||||
return
|
||||
|
||||
infos = self._build_prompt_infos(target_avatar)
|
||||
res = self._call_llm_feedback(infos)
|
||||
# LLM 只返回 {avatar_name_2: {thinking, feedback}}
|
||||
r = res.get(infos["avatar_name_2"], {})
|
||||
thinking = r.get("thinking", "")
|
||||
feedback = r.get("feedback", "")
|
||||
|
||||
# 挂到目标的thinking上(面向UI/日志),并执行反馈落地
|
||||
target_avatar.thinking = thinking
|
||||
# 1) 先清空目标后续计划(仅清空队列,不动当前动作)
|
||||
target_avatar.clear_plans()
|
||||
# 2) 再结算反馈映射为对应动作
|
||||
self._settle_feedback(target_avatar, feedback)
|
||||
# 3) 反馈事件(进入侧边栏与双方历史,中文化文案)
|
||||
fb_label = self.FEEDBACK_LABELS.get(str(feedback).strip(), str(feedback))
|
||||
feedback_event = Event(self.world.month_stamp, f"{target_avatar.name} 对 {self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target_avatar.id])
|
||||
# 侧边栏仅推送一次,另一侧仅写入历史,避免重复
|
||||
EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target_avatar, to_sidebar_once=True)
|
||||
# 4) 记录历史(文本记录)
|
||||
self._apply_feedback(target_avatar, feedback)
|
||||
|
||||
# 实现 ActualActionMixin 接口
|
||||
@@ -173,10 +182,44 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
|
||||
def step(self, target_avatar: "Avatar|str") -> ActionResult:
|
||||
"""
|
||||
执行互动动作,互动动作是即时完成的
|
||||
异步化:首帧发起LLM任务并返回RUNNING;任务完成后在后续帧落地反馈并完成。
|
||||
"""
|
||||
self.execute(target_avatar=target_avatar)
|
||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
||||
target = self._get_target_avatar(target_avatar)
|
||||
if target is None:
|
||||
return ActionResult(status=ActionStatus.FAILED, events=[])
|
||||
|
||||
# 若无任务,创建异步任务
|
||||
if self._feedback_task is None and self._feedback_cached is None:
|
||||
infos = self._build_prompt_infos(target)
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
self._feedback_task = loop.create_task(self._call_llm_feedback_async(infos))
|
||||
except RuntimeError:
|
||||
# 无运行中的事件循环时,退化为同步调用(如离线批处理)
|
||||
self._feedback_cached = self._call_llm_feedback(infos)
|
||||
|
||||
# 若任务已完成,消费结果
|
||||
if self._feedback_task is not None and self._feedback_task.done():
|
||||
self._feedback_cached = self._feedback_task.result()
|
||||
self._feedback_task = None
|
||||
|
||||
if self._feedback_cached is not None:
|
||||
res = self._feedback_cached
|
||||
self._feedback_cached = None
|
||||
r = res.get(target.name, {})
|
||||
thinking = r.get("thinking", "")
|
||||
feedback = r.get("feedback", "")
|
||||
|
||||
target.thinking = thinking
|
||||
target.clear_plans()
|
||||
self._settle_feedback(target, feedback)
|
||||
fb_label = self.FEEDBACK_LABELS.get(str(feedback).strip(), str(feedback))
|
||||
feedback_event = Event(self.world.month_stamp, f"{target.name} 对 {self.avatar.name} 的反馈:{fb_label}", related_avatars=[self.avatar.id, target.id])
|
||||
EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||
self._apply_feedback(target, feedback)
|
||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
||||
|
||||
return ActionResult(status=ActionStatus.RUNNING, events=[])
|
||||
|
||||
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from src.utils.config import CONFIG
|
||||
from src.utils.llm import get_prompt_and_call_llm
|
||||
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
|
||||
|
||||
story_styles = [
|
||||
"平淡叙述:语句克制、少修饰、像旁观者记录。",
|
||||
@@ -64,12 +65,34 @@ class StoryTeller:
|
||||
if story:
|
||||
return story
|
||||
except Exception:
|
||||
# 避免过度 try/catch,仅在外部依赖失败时提供降级
|
||||
pass
|
||||
# 降级文案(不中断主流程)
|
||||
style = infos.get("style", "")
|
||||
return f"{event}。{res}。{style}"
|
||||
|
||||
@staticmethod
|
||||
async def tell_story_async(avatar_infos: Dict[str, dict], event: str, res: str, STORY_PROMPT: str = "") -> str:
|
||||
"""
|
||||
异步版本:生成小故事,失败时返回降级文案。
|
||||
"""
|
||||
template_path = CONFIG.paths.templates / "story.txt"
|
||||
infos = {
|
||||
"avatar_infos": avatar_infos,
|
||||
"event": event,
|
||||
"res": res,
|
||||
"style": random.choice(story_styles),
|
||||
"story_prompt": STORY_PROMPT or "",
|
||||
}
|
||||
try:
|
||||
data = await get_prompt_and_call_llm_async(template_path, infos, mode="fast")
|
||||
story = str(data.get("story", "")).strip()
|
||||
if story:
|
||||
return story
|
||||
except Exception:
|
||||
pass
|
||||
style = infos.get("style", "")
|
||||
return f"{event}。{res}。{style}"
|
||||
|
||||
@staticmethod
|
||||
def tell_from_actors(event: str, res: str, *actors: "Avatar", prompt: str | None = None) -> str:
|
||||
"""
|
||||
@@ -78,6 +101,11 @@ class StoryTeller:
|
||||
avatar_infos = StoryTeller.build_avatar_infos(*actors)
|
||||
return StoryTeller.tell_story(avatar_infos, event, res, prompt or "")
|
||||
|
||||
@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 "")
|
||||
|
||||
|
||||
__all__ = ["StoryTeller"]
|
||||
|
||||
|
||||
24
src/utils/asyncio_utils.py
Normal file
24
src/utils/asyncio_utils.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Awaitable, Callable, Optional
|
||||
|
||||
|
||||
def schedule_background(coro: Awaitable, *, fallback: Optional[Callable[[], None]] = None) -> None:
|
||||
"""
|
||||
在有事件循环时将协程投递为后台任务;否则执行同步回退。
|
||||
|
||||
- coro: 需要异步执行的协程对象
|
||||
- fallback: 无事件循环时的回退执行函数(可为空)
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(coro)
|
||||
except RuntimeError:
|
||||
if fallback is not None:
|
||||
fallback()
|
||||
else:
|
||||
# 无回退则静默返回,调用方自行决定后续行为
|
||||
return
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ ai:
|
||||
max_decide_num: 4
|
||||
|
||||
game:
|
||||
init_npc_num: 3
|
||||
init_npc_num: 6
|
||||
sect_num: 2 # init_npc_num大于sect_num时,会随机选择sect_num个宗门
|
||||
npc_birth_rate_per_month: 0.001
|
||||
fortune_probability: 0.001
|
||||
|
||||
@@ -29,4 +29,4 @@ id,name,exclusion_ids,desc,weight,condition
|
||||
27,腼腆,26,你对待和他人结为道侣或者双修比较谨慎,1,
|
||||
28,舔狗,13;14;22;27,你对异性中外貌出众者格外友善,倾向主动接近、帮助与合作。,1,
|
||||
29,嫉妒,11;23,你对在修为、外貌或财富等方面远超于你的人容易产生敌意,更倾向对其冷淡、挑衅或打压。,1,
|
||||
30,穿越者,,你来自现代社会,怀念现代社会的一切,希望调查清楚你来的原因,早日回到现代,你的思考方式都是现代化的,100,
|
||||
30,穿越者,,你来自现代社会,怀念现代社会的一切,希望调查清楚你来的原因,早日回到现代,你的思考方式都是现代化的,1,
|
||||
|
Can't render this file because it contains an unexpected character in line 26 and column 121.
|
Reference in New Issue
Block a user