fix action bugs
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from src.classes.action import InstantAction
|
||||
from src.classes.event import Event
|
||||
from src.classes.battle import decide_battle
|
||||
from src.classes.story_teller import StoryTeller
|
||||
|
||||
|
||||
class Battle(InstantAction):
|
||||
@@ -22,7 +23,7 @@ class Battle(InstantAction):
|
||||
return
|
||||
winner, loser, damage = decide_battle(self.avatar, target)
|
||||
loser.hp.reduce(damage)
|
||||
self._last_result = (winner.name, loser.name)
|
||||
self._last_result = (winner.name, loser.name, damage)
|
||||
|
||||
def can_start(self, avatar_name: str | None = None) -> bool:
|
||||
if avatar_name is None:
|
||||
@@ -32,15 +33,31 @@ class Battle(InstantAction):
|
||||
def start(self, avatar_name: str) -> Event:
|
||||
target = self._get_target(avatar_name)
|
||||
target_name = target.name if target is not None else avatar_name
|
||||
return Event(self.world.month_stamp, f"{self.avatar.name} 对 {target_name} 发起战斗")
|
||||
event = Event(self.world.month_stamp, f"{self.avatar.name} 对 {target_name} 发起战斗")
|
||||
# 记录开始事件内容,供故事生成使用
|
||||
self._start_event_content = event.content
|
||||
return event
|
||||
|
||||
# InstantAction 已实现 step 完成
|
||||
|
||||
def finish(self, avatar_name: str) -> list[Event]:
|
||||
res = self._last_result
|
||||
if isinstance(res, tuple) and len(res) == 2:
|
||||
winner, loser = res
|
||||
return [Event(self.world.month_stamp, f"{winner} 战胜了 {loser}")]
|
||||
if isinstance(res, tuple) and len(res) in (2, 3):
|
||||
winner, loser = res[0], res[1]
|
||||
damage = res[2] if len(res) == 3 else None
|
||||
if damage is not None:
|
||||
result_text = f"{winner} 战胜了 {loser},造成{damage}点伤害"
|
||||
else:
|
||||
result_text = f"{winner} 战胜了 {loser}"
|
||||
result_event = Event(self.world.month_stamp, result_text)
|
||||
|
||||
# 生成战斗小故事:直接复用已生成的事件文本
|
||||
target = self._get_target(avatar_name)
|
||||
avatar_infos = StoryTeller.build_avatar_infos(self.avatar, target)
|
||||
start_text = getattr(self, "_start_event_content", "") or result_event.content
|
||||
story = StoryTeller.tell_story(avatar_infos, start_text, result_event.content)
|
||||
story_event = Event(self.world.month_stamp, story)
|
||||
return [result_event, story_event]
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -38,9 +38,6 @@ class MoveToAvatar(DefineAction, ActualActionMixin):
|
||||
Move(self.avatar, self.world).execute(delta_x, delta_y)
|
||||
|
||||
def can_start(self, avatar_name: str | None = None) -> bool:
|
||||
target = self._get_target(avatar_name)
|
||||
if target is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def start(self, avatar_name: str) -> Event:
|
||||
|
||||
@@ -18,10 +18,8 @@ class ActionRegistry:
|
||||
def register(cls, action_cls: type, *, actual: bool) -> None:
|
||||
name = action_cls.__name__
|
||||
cls._name_to_cls[name] = action_cls
|
||||
cls._name_to_cls[name.lower()] = action_cls # 大小写别名
|
||||
if actual:
|
||||
cls._actual_name_to_cls[name] = action_cls
|
||||
cls._actual_name_to_cls[name.lower()] = action_cls # 大小写别名
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> type:
|
||||
@@ -29,11 +27,25 @@ class ActionRegistry:
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> Iterable[type]:
|
||||
return cls._name_to_cls.values()
|
||||
# 去重保持稳定顺序
|
||||
seen = set()
|
||||
ordered: list[type] = []
|
||||
for t in cls._name_to_cls.values():
|
||||
if t not in seen:
|
||||
seen.add(t)
|
||||
ordered.append(t)
|
||||
return ordered
|
||||
|
||||
@classmethod
|
||||
def all_actual(cls) -> Iterable[type]:
|
||||
return cls._actual_name_to_cls.values()
|
||||
# 去重保持稳定顺序
|
||||
seen = set()
|
||||
ordered: list[type] = []
|
||||
for t in cls._actual_name_to_cls.values():
|
||||
if t not in seen:
|
||||
seen.add(t)
|
||||
ordered.append(t)
|
||||
return ordered
|
||||
|
||||
|
||||
def register_action(*, actual: bool = True) -> Callable[[type], type]:
|
||||
|
||||
@@ -59,7 +59,8 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
|
||||
def _call_llm_feedback(self, infos: dict) -> dict:
|
||||
template_path = self._get_template_path()
|
||||
res = get_prompt_and_call_llm(template_path, infos)
|
||||
# mutual用快速llm,不需要复杂决策
|
||||
res = get_prompt_and_call_llm(template_path, infos, mode="fast")
|
||||
return res
|
||||
|
||||
def _set_target_immediate_action(self, target_avatar: "Avatar", action_name: str, action_params: dict) -> None:
|
||||
|
||||
53
src/classes/story_teller.py
Normal file
53
src/classes/story_teller.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from src.utils.config import CONFIG
|
||||
from src.utils.llm import get_prompt_and_call_llm
|
||||
|
||||
|
||||
class StoryTeller:
|
||||
"""
|
||||
故事生成器:基于模板与 LLM,将给定事件扩展为简短的小故事。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_avatar_infos(*avatars: "Avatar") -> Dict[str, str]:
|
||||
"""
|
||||
将若干角色信息组织为 {name: info} 映射,供故事模板使用。
|
||||
优先使用 `get_prompt_info([])`,失败时退化为 `get_info()`。
|
||||
"""
|
||||
infos: Dict[str, str] = {}
|
||||
for av in avatars:
|
||||
try:
|
||||
infos[av.name] = av.get_prompt_info([])
|
||||
except Exception:
|
||||
infos[av.name] = getattr(av, "name", "未知角色")
|
||||
return infos
|
||||
|
||||
@staticmethod
|
||||
def tell_story(avatar_infos: Dict[str, str], event: str, res: str) -> str:
|
||||
"""
|
||||
基于 `static/templates/story.txt` 模板生成小故事。
|
||||
始终使用 fast 模式以提升速度。
|
||||
失败时返回降级版文案,避免中断流程。
|
||||
"""
|
||||
template_path = CONFIG.paths.templates / "story.txt"
|
||||
infos = {
|
||||
"avatar_infos": avatar_infos,
|
||||
"event": event,
|
||||
"res": res,
|
||||
}
|
||||
try:
|
||||
data = get_prompt_and_call_llm(template_path, infos, mode="fast")
|
||||
story = str(data.get("story", "")).strip()
|
||||
if story:
|
||||
return story
|
||||
except Exception:
|
||||
return (res or event or "")
|
||||
return (res or event or "")
|
||||
|
||||
|
||||
__all__ = ["StoryTeller"]
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def get_prompt(template: str, infos: dict) -> str:
|
||||
return prompt_template.format(**infos)
|
||||
|
||||
|
||||
def call_llm(prompt: str) -> str:
|
||||
def call_llm(prompt: str, mode="normal") -> str:
|
||||
"""
|
||||
调用LLM
|
||||
|
||||
@@ -27,7 +27,12 @@ def call_llm(prompt: str) -> str:
|
||||
str: LLM返回的结果
|
||||
"""
|
||||
# 从配置中获取模型信息
|
||||
model_name = CONFIG.llm.model_name
|
||||
if mode == "normal":
|
||||
model_name = CONFIG.llm.model_name
|
||||
elif mode == "fast":
|
||||
model_name = CONFIG.llm.fast_model_name
|
||||
else:
|
||||
raise ValueError(f"Invalid mode: {mode}")
|
||||
api_key = CONFIG.llm.key
|
||||
base_url = CONFIG.llm.base_url
|
||||
# 调用litellm的completion函数
|
||||
@@ -43,7 +48,7 @@ def call_llm(prompt: str) -> str:
|
||||
log_llm_call(model_name, prompt, result) # 记录日志
|
||||
return result
|
||||
|
||||
async def call_llm_async(prompt: str) -> str:
|
||||
async def call_llm_async(prompt: str, mode="normal") -> str:
|
||||
"""
|
||||
异步调用LLM
|
||||
|
||||
@@ -53,7 +58,7 @@ async def call_llm_async(prompt: str) -> str:
|
||||
str: LLM返回的结果
|
||||
"""
|
||||
# 使用asyncio.to_thread包装同步调用
|
||||
result = await asyncio.to_thread(call_llm, prompt)
|
||||
result = await asyncio.to_thread(call_llm, prompt, mode)
|
||||
return result
|
||||
|
||||
def parse_llm_response(res: str) -> dict:
|
||||
@@ -69,38 +74,36 @@ def parse_llm_response(res: str) -> dict:
|
||||
|
||||
return json5.loads(res)
|
||||
|
||||
def get_prompt_and_call_llm(template_path: Path, infos: dict) -> str:
|
||||
def get_prompt_and_call_llm(template_path: Path, infos: dict, mode="normal") -> str:
|
||||
"""
|
||||
根据模板,获取提示词,并调用LLM
|
||||
"""
|
||||
template = read_txt(template_path)
|
||||
prompt = get_prompt(template, infos)
|
||||
res = call_llm(prompt)
|
||||
res = call_llm(prompt, mode)
|
||||
json_res = parse_llm_response(res)
|
||||
return json_res
|
||||
|
||||
async def get_prompt_and_call_llm_async(template_path: Path, infos: dict) -> str:
|
||||
async def get_prompt_and_call_llm_async(template_path: Path, infos: dict, mode="normal") -> str:
|
||||
"""
|
||||
异步版本:根据模板,获取提示词,并调用LLM
|
||||
"""
|
||||
template = read_txt(template_path)
|
||||
prompt = get_prompt(template, infos)
|
||||
res = await call_llm_async(prompt)
|
||||
res = await call_llm_async(prompt, mode)
|
||||
json_res = parse_llm_response(res)
|
||||
# print(f"prompt = {prompt}")
|
||||
# print(f"json_res = {json_res}")
|
||||
return json_res
|
||||
|
||||
def get_ai_prompt_and_call_llm(infos: dict) -> dict:
|
||||
def get_ai_prompt_and_call_llm(infos: dict, mode="normal") -> dict:
|
||||
"""
|
||||
根据模板,获取提示词,并调用LLM
|
||||
"""
|
||||
template_path = CONFIG.paths.templates / "ai.txt"
|
||||
return get_prompt_and_call_llm(template_path, infos)
|
||||
return get_prompt_and_call_llm(template_path, infos, mode)
|
||||
|
||||
async def get_ai_prompt_and_call_llm_async(infos: dict) -> dict:
|
||||
async def get_ai_prompt_and_call_llm_async(infos: dict, mode="normal") -> dict:
|
||||
"""
|
||||
异步版本:根据模板,获取提示词,并调用LLM
|
||||
"""
|
||||
template_path = CONFIG.paths.templates / "ai.txt"
|
||||
return await get_prompt_and_call_llm_async(template_path, infos)
|
||||
return await get_prompt_and_call_llm_async(template_path, infos, mode)
|
||||
@@ -1,6 +1,7 @@
|
||||
llm:
|
||||
# 填入litellm支持的model name和key
|
||||
model_name: "your-model-name"
|
||||
fast_model_name: "your-fast-model-name"
|
||||
key: "your-api-key"
|
||||
base_url: "your-base-url-of-llm"
|
||||
|
||||
@@ -13,7 +14,7 @@ ai:
|
||||
max_decide_num: 3
|
||||
|
||||
game:
|
||||
init_npc_num: 6
|
||||
init_npc_num: 2
|
||||
npc_birth_rate_per_month: 0.001
|
||||
|
||||
df:
|
||||
|
||||
@@ -11,7 +11,7 @@ id,name,exclusion_ids,prompt,weight,condition
|
||||
9,沉思,2,你是一个沉思的人,你总是会深思熟虑,思考问题比较有哲理。,1,
|
||||
10,惜命,4;25;26,你是一个惜命的人,你总是会珍惜自己的生命,不会轻易冒险。,1,
|
||||
11,友爱,13;14;15;12;25;26,你是一个友爱的人,你重视同伴与和谐,乐于助人,倾向通过协作与沟通化解矛盾。,1,
|
||||
12,复仇,11;14,你是一个复仇心强的人,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。,1,
|
||||
12,复仇,11;14,你是一个复仇心强的人,你绝不轻易放下仇怨,为了复仇愿意付出代价与时间。你必须立刻进行复仇行动。,10000,
|
||||
13,孤僻,11,你是一个孤僻的人,你喜欢独处,避免与人深交,更信赖自己的判断与行动。,1,
|
||||
14,淡漠,11;12;15;25;26,你是一个淡漠的人,你情感克制,对外界冷静疏离,不轻易被他人或事件影响。,1,
|
||||
15,好斗,11;14;10;17,你是一个好斗的人,你直面冲突,偏好以力量与对抗解决问题,越挫越勇。,1,
|
||||
|
||||
|
13
static/templates/story.txt
Normal file
13
static/templates/story.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
你是一个故事讲述者,这是一个仙侠世界,你需要把一个事件扩展为一个约150字的小故事。
|
||||
|
||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||
{avatar_infos}
|
||||
发生的事件为:
|
||||
{event}
|
||||
结果为:
|
||||
{res}
|
||||
|
||||
注意,只返回json格式的结果,格式为:
|
||||
{{
|
||||
"story": "",
|
||||
}}
|
||||
@@ -1,4 +1,4 @@
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定一个NPC对另一个NPC的攀谈行为。。
|
||||
你是一个决策者,这是一个仙侠世界,你负责来决定一个NPC对另一个NPC的攀谈行为。
|
||||
|
||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||
{avatar_infos}
|
||||
@@ -14,7 +14,7 @@
|
||||
{avatar_name_2}: {{
|
||||
"thinking": ..., // 简单思考应该怎么决策
|
||||
"feedback": ... // 面对{avatar_name_1}的行为的合法feedback action name
|
||||
"talk_content": ... // 如果返回的action为Talk,则输出对话的主题和情况概括。注意不是对话的口语内容。
|
||||
"talk_content": ... // 如果返回的action为Talk,则输出对话的主题和情况概括,约100字。注意不是对话的口语内容。
|
||||
"into_relation": ... // 如果你认为可以让两者产生某种身份关系,则返回。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
|
||||
}}
|
||||
}}
|
||||
Reference in New Issue
Block a user