refactor talk & conversation
This commit is contained in:
@@ -29,7 +29,6 @@ from .sold import SellItems
|
|||||||
from .battle import Battle
|
from .battle import Battle
|
||||||
from .plunder_mortals import PlunderMortals
|
from .plunder_mortals import PlunderMortals
|
||||||
from .help_mortals import HelpMortals
|
from .help_mortals import HelpMortals
|
||||||
from .talk import Talk
|
|
||||||
from .devour_mortals import DevourMortals
|
from .devour_mortals import DevourMortals
|
||||||
from .self_heal import SelfHeal
|
from .self_heal import SelfHeal
|
||||||
from .catch import Catch
|
from .catch import Catch
|
||||||
@@ -58,10 +57,10 @@ register_action(actual=True)(SellItems)
|
|||||||
register_action(actual=False)(Battle)
|
register_action(actual=False)(Battle)
|
||||||
register_action(actual=True)(PlunderMortals)
|
register_action(actual=True)(PlunderMortals)
|
||||||
register_action(actual=True)(HelpMortals)
|
register_action(actual=True)(HelpMortals)
|
||||||
register_action(actual=True)(Talk)
|
|
||||||
register_action(actual=True)(DevourMortals)
|
register_action(actual=True)(DevourMortals)
|
||||||
register_action(actual=True)(SelfHeal)
|
register_action(actual=True)(SelfHeal)
|
||||||
register_action(actual=True)(Catch)
|
register_action(actual=True)(Catch)
|
||||||
|
# Talk 已移动到 mutual_action 模块,在那里注册
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# 基类
|
# 基类
|
||||||
@@ -89,10 +88,10 @@ __all__ = [
|
|||||||
"Battle",
|
"Battle",
|
||||||
"PlunderMortals",
|
"PlunderMortals",
|
||||||
"HelpMortals",
|
"HelpMortals",
|
||||||
"Talk",
|
|
||||||
"DevourMortals",
|
"DevourMortals",
|
||||||
"SelfHeal",
|
"SelfHeal",
|
||||||
"Catch",
|
"Catch",
|
||||||
|
# Talk 已移动到 mutual_action 模块
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from src.classes.action import InstantAction
|
|
||||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
|
||||||
from src.utils.config import CONFIG
|
|
||||||
from src.classes.event import Event
|
|
||||||
|
|
||||||
|
|
||||||
class Talk(InstantAction):
|
|
||||||
"""
|
|
||||||
攀谈:尝试与交互范围内的某个NPC进行交谈。
|
|
||||||
- can_start:交互范围内存在其他NPC
|
|
||||||
- 发起后:从交互范围内随机选择一个目标,进入 MutualAction: Conversation(允许建立关系)
|
|
||||||
"""
|
|
||||||
|
|
||||||
COMMENT = "与交互范围内的NPC发起攀谈"
|
|
||||||
DOABLES_REQUIREMENTS = "交互范围内存在其他NPC"
|
|
||||||
PARAMS = {}
|
|
||||||
|
|
||||||
def _get_observed_others(self) -> list["Avatar"]:
|
|
||||||
return self.world.avatar_manager.get_observable_avatars(self.avatar)
|
|
||||||
|
|
||||||
# 不再限定必须同一 tile,由交互范围统一约束
|
|
||||||
|
|
||||||
def _execute(self) -> None:
|
|
||||||
# Talk 本身不做长期效果,主要在 step 中驱动 Conversation
|
|
||||||
return
|
|
||||||
|
|
||||||
def can_start(self, **kwargs) -> tuple[bool, str]:
|
|
||||||
# 交互范围内是否存在其他NPC(用于展示在动作空间)
|
|
||||||
ok = len(self._get_observed_others()) > 0
|
|
||||||
return (ok, "" if ok else "交互范围内没有可交谈对象")
|
|
||||||
|
|
||||||
def start(self) -> Event:
|
|
||||||
self.observed_others = self._get_observed_others()
|
|
||||||
# 记录开始事件
|
|
||||||
return Event(self.world.month_stamp, f"{self.avatar.name} 尝试与交互范围内的他人攀谈", related_avatars=[self.avatar.id])
|
|
||||||
|
|
||||||
def step(self) -> ActionResult:
|
|
||||||
import random
|
|
||||||
|
|
||||||
target = random.choice(self.observed_others)
|
|
||||||
|
|
||||||
# 进入交谈:由概率决定本次是否允许建立关系
|
|
||||||
from src.classes.mutual_action import Conversation
|
|
||||||
# 由配置决定本次是否有“有机会进入关系”标记
|
|
||||||
prob = CONFIG.social.talk_into_relation_probability
|
|
||||||
can_into_relation = random.random() < prob
|
|
||||||
|
|
||||||
conv = Conversation(self.avatar, self.world)
|
|
||||||
# 启动事件写入历史,不入侧边栏
|
|
||||||
conv.start(target_avatar=target)
|
|
||||||
conv.step(target_avatar=target, can_into_relation=can_into_relation)
|
|
||||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
|
||||||
|
|
||||||
def finish(self) -> list[Event]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
@@ -590,23 +590,26 @@ class Avatar:
|
|||||||
def set_relation(self, other: "Avatar", relation: Relation) -> None:
|
def set_relation(self, other: "Avatar", relation: Relation) -> None:
|
||||||
"""
|
"""
|
||||||
设置与另一个角色的关系。
|
设置与另一个角色的关系。
|
||||||
- 对称关系(如 FRIEND/ENEMY/LOVERS/SIBLING/KIN)会在对方处写入相同的关系。
|
委托给 relations.py 中的函数。
|
||||||
- 有向关系(如 MASTER、APPRENTICE、PARENT、CHILD)会在对方处写入对偶关系。
|
|
||||||
"""
|
"""
|
||||||
if other is self:
|
from src.classes.relations import set_relation
|
||||||
return
|
set_relation(self, other, relation)
|
||||||
self.relations[other] = relation
|
|
||||||
# 写入对方的对偶关系(对称关系会得到同一枚举值)
|
|
||||||
if getattr(other, "relations", None) is not None:
|
|
||||||
other.relations[self] = get_reciprocal(relation)
|
|
||||||
|
|
||||||
def get_relation(self, other: "Avatar") -> Optional[Relation]:
|
def get_relation(self, other: "Avatar") -> Optional[Relation]:
|
||||||
return self.relations.get(other)
|
"""
|
||||||
|
获取与另一个角色的关系。
|
||||||
|
委托给 relations.py 中的函数。
|
||||||
|
"""
|
||||||
|
from src.classes.relations import get_relation
|
||||||
|
return get_relation(self, other)
|
||||||
|
|
||||||
def clear_relation(self, other: "Avatar") -> None:
|
def clear_relation(self, other: "Avatar") -> None:
|
||||||
self.relations.pop(other, None)
|
"""
|
||||||
if getattr(other, "relations", None) is not None:
|
清除与另一个角色的关系。
|
||||||
other.relations.pop(self, None)
|
委托给 relations.py 中的函数。
|
||||||
|
"""
|
||||||
|
from src.classes.relations import clear_relation
|
||||||
|
clear_relation(self, other)
|
||||||
|
|
||||||
def _get_relations_summary_str(self, max_count: int = 8) -> str:
|
def _get_relations_summary_str(self, max_count: int = 8) -> str:
|
||||||
entries: list[str] = []
|
entries: list[str] = []
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .drive_away import DriveAway
|
|||||||
from .attack import Attack
|
from .attack import Attack
|
||||||
from .conversation import Conversation
|
from .conversation import Conversation
|
||||||
from .dual_cultivation import DualCultivation
|
from .dual_cultivation import DualCultivation
|
||||||
|
from .talk import Talk
|
||||||
from src.classes.action.registry import register_action
|
from src.classes.action.registry import register_action
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -13,6 +14,7 @@ __all__ = [
|
|||||||
"Attack",
|
"Attack",
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"DualCultivation",
|
"DualCultivation",
|
||||||
|
"Talk",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 注册 mutual actions(均为实际动作)
|
# 注册 mutual actions(均为实际动作)
|
||||||
@@ -20,5 +22,6 @@ register_action(actual=True)(DriveAway)
|
|||||||
register_action(actual=True)(Attack)
|
register_action(actual=True)(Attack)
|
||||||
register_action(actual=True)(Conversation)
|
register_action(actual=True)(Conversation)
|
||||||
register_action(actual=True)(DualCultivation)
|
register_action(actual=True)(DualCultivation)
|
||||||
|
register_action(actual=True)(Talk)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .mutual_action import MutualAction
|
from .mutual_action import MutualAction
|
||||||
from src.classes.relation import relation_display_names, Relation, get_possible_post_relations
|
from src.classes.relation import relation_display_names, Relation
|
||||||
|
from src.classes.relations import (
|
||||||
|
get_possible_new_relations,
|
||||||
|
get_possible_cancel_relations,
|
||||||
|
set_relation,
|
||||||
|
cancel_relation,
|
||||||
|
)
|
||||||
from src.classes.event import Event
|
from src.classes.event import Event
|
||||||
from src.utils.config import CONFIG
|
from src.utils.config import CONFIG
|
||||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||||
@@ -17,9 +23,9 @@ if TYPE_CHECKING:
|
|||||||
class Conversation(MutualAction):
|
class Conversation(MutualAction):
|
||||||
"""交谈:两名角色在同一区域进行交流。
|
"""交谈:两名角色在同一区域进行交流。
|
||||||
|
|
||||||
- 可由“攀谈”触发,或直接发起
|
- 可由"攀谈"触发,或直接发起
|
||||||
- 仅当双方处于同一 Region 时可启动
|
- 仅当双方处于同一 Region 时可启动
|
||||||
- 当 can_into_relation=True 且 LLM 决策返回 into_relation 时,根据返回建立关系
|
- LLM 可决策是否进入新关系或取消旧关系
|
||||||
- 会将对话内容写入事件系统
|
- 会将对话内容写入事件系统
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -27,14 +33,14 @@ class Conversation(MutualAction):
|
|||||||
COMMENT = "与对方进行一段交流对话"
|
COMMENT = "与对方进行一段交流对话"
|
||||||
DOABLES_REQUIREMENTS = "目标在交互范围内"
|
DOABLES_REQUIREMENTS = "目标在交互范围内"
|
||||||
PARAMS = {"target_avatar": "AvatarName"}
|
PARAMS = {"target_avatar": "AvatarName"}
|
||||||
FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"]
|
FEEDBACK_ACTIONS: list[str] = [] # Conversation 自动触发,不需要对方决策
|
||||||
STORY_PROMPT: str = ""
|
STORY_PROMPT: str = ""
|
||||||
|
|
||||||
def _get_template_path(self) -> Path:
|
def _get_template_path(self) -> Path:
|
||||||
# 使用 talk.txt 模板,以获取是否接受与对话内容
|
# 使用专门的 conversation.txt 模板
|
||||||
return CONFIG.paths.templates / "talk.txt"
|
return CONFIG.paths.templates / "conversation.txt"
|
||||||
|
|
||||||
def _build_prompt_infos(self, target_avatar: "Avatar", *, can_into_relation: bool) -> dict:
|
def _build_prompt_infos(self, target_avatar: "Avatar") -> dict:
|
||||||
avatar_name_1 = self.avatar.name
|
avatar_name_1 = self.avatar.name
|
||||||
avatar_name_2 = target_avatar.name
|
avatar_name_2 = target_avatar.name
|
||||||
# 交谈:使用详细信息,便于生成更丰富对话
|
# 交谈:使用详细信息,便于生成更丰富对话
|
||||||
@@ -43,8 +49,12 @@ class Conversation(MutualAction):
|
|||||||
avatar_name_2: target_avatar.get_info(detailed=True),
|
avatar_name_2: target_avatar.get_info(detailed=True),
|
||||||
}
|
}
|
||||||
# 可能的后天关系(转中文名,给模板阅读)
|
# 可能的后天关系(转中文名,给模板阅读)
|
||||||
possible_relations = [relation_display_names[r] for r in get_possible_post_relations(self.avatar, target_avatar)]
|
# 注意:这里计算的是 target 相对于 avatar 的可能关系
|
||||||
# 历史上下文:仅双方共同经历的最近事件(与 MutualAction 对齐)
|
possible_new_relations = [relation_display_names[r] for r in get_possible_new_relations(self.avatar, target_avatar)]
|
||||||
|
# 可能取消的关系
|
||||||
|
possible_cancel_relations = [relation_display_names[r] for r in get_possible_cancel_relations(target_avatar, self.avatar)]
|
||||||
|
|
||||||
|
# 历史上下文:仅双方共同经历的最近事件
|
||||||
n = CONFIG.social.event_context_num
|
n = CONFIG.social.event_context_num
|
||||||
em = self.world.event_manager
|
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)]
|
pair_recent_events = [str(e) for e in em.get_events_between(self.avatar.id, target_avatar.id, limit=n)]
|
||||||
@@ -52,11 +62,12 @@ class Conversation(MutualAction):
|
|||||||
"avatar_infos": avatar_infos,
|
"avatar_infos": avatar_infos,
|
||||||
"avatar_name_1": avatar_name_1,
|
"avatar_name_1": avatar_name_1,
|
||||||
"avatar_name_2": avatar_name_2,
|
"avatar_name_2": avatar_name_2,
|
||||||
"can_into_relation": bool(can_into_relation),
|
"possible_new_relations": possible_new_relations,
|
||||||
"possible_relations": possible_relations,
|
"possible_cancal_relations": possible_cancel_relations, # 保持模板中的拼写
|
||||||
"recent_events": pair_recent_events,
|
"recent_events": pair_recent_events,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 覆盖 can_start:Conversation 不需要检查观察范围,只需要在有效区域即可
|
||||||
def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> tuple[bool, str]:
|
def can_start(self, target_avatar: "Avatar|str|None" = None, **kwargs) -> tuple[bool, str]:
|
||||||
if target_avatar is None:
|
if target_avatar is None:
|
||||||
return False, "缺少参数 target_avatar"
|
return False, "缺少参数 target_avatar"
|
||||||
@@ -65,11 +76,9 @@ class Conversation(MutualAction):
|
|||||||
return False, "目标不存在"
|
return False, "目标不存在"
|
||||||
if target.tile is None or self.avatar.tile is None:
|
if target.tile is None or self.avatar.tile is None:
|
||||||
return False, "目标未处于有效区域"
|
return False, "目标未处于有效区域"
|
||||||
# 先不限定同一区域,之后再限制
|
|
||||||
# if target.tile.region != self.avatar.tile.region:
|
|
||||||
# return False, "目标不在同一区域"
|
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
# 覆盖 start:自定义事件消息
|
||||||
def start(self, target_avatar: "Avatar|str", **kwargs) -> Event:
|
def start(self, target_avatar: "Avatar|str", **kwargs) -> Event:
|
||||||
target = self._get_target_avatar(target_avatar)
|
target = self._get_target_avatar(target_avatar)
|
||||||
target_name = target.name if target is not None else str(target_avatar)
|
target_name = target.name if target is not None else str(target_avatar)
|
||||||
@@ -77,49 +86,84 @@ class Conversation(MutualAction):
|
|||||||
if target is not None:
|
if target is not None:
|
||||||
rel_ids.append(target.id)
|
rel_ids.append(target.id)
|
||||||
event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target_name} 开始交谈", related_avatars=rel_ids)
|
event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target_name} 开始交谈", related_avatars=rel_ids)
|
||||||
# 写入历史即可,内容事件稍后生成
|
|
||||||
self.avatar.add_event(event, to_sidebar=False)
|
self.avatar.add_event(event, to_sidebar=False)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
target.add_event(event, to_sidebar=False)
|
target.add_event(event, to_sidebar=False)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def step(self, target_avatar: "Avatar|str", can_into_relation: bool = False) -> ActionResult:
|
def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult:
|
||||||
|
"""
|
||||||
|
处理 LLM 返回的对话结果,包括对话内容和关系变化。
|
||||||
|
Conversation 不需要反馈(FEEDBACK_ACTIONS 为空),直接生成内容。
|
||||||
|
"""
|
||||||
|
conversation_content = str(result.get("conversation_content", "")).strip()
|
||||||
|
new_relation_str = str(result.get("new_relation", "")).strip()
|
||||||
|
cancel_relation_str = str(result.get("cancal_relation", "")).strip() # 保持模板中的拼写
|
||||||
|
|
||||||
|
# 记录对话内容
|
||||||
|
if conversation_content:
|
||||||
|
content_event = Event(
|
||||||
|
self.world.month_stamp,
|
||||||
|
f"{self.avatar.name} 与 {target.name} 的交谈:{conversation_content}",
|
||||||
|
related_avatars=[self.avatar.id, target.id]
|
||||||
|
)
|
||||||
|
EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||||
|
|
||||||
|
# 处理进入新关系
|
||||||
|
if new_relation_str:
|
||||||
|
rel = Relation.from_chinese(new_relation_str)
|
||||||
|
if rel is not None:
|
||||||
|
set_relation(target, self.avatar, rel)
|
||||||
|
set_event = Event(
|
||||||
|
self.world.month_stamp,
|
||||||
|
f"{target.name} 与 {self.avatar.name} 的关系变为:{relation_display_names.get(rel, str(rel))}",
|
||||||
|
related_avatars=[self.avatar.id, target.id]
|
||||||
|
)
|
||||||
|
EventHelper.push_pair(set_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||||
|
|
||||||
|
# 处理取消关系
|
||||||
|
if cancel_relation_str:
|
||||||
|
rel = Relation.from_chinese(cancel_relation_str)
|
||||||
|
if rel is not None:
|
||||||
|
success = cancel_relation(target, self.avatar, rel)
|
||||||
|
if success:
|
||||||
|
cancel_event = Event(
|
||||||
|
self.world.month_stamp,
|
||||||
|
f"{target.name} 与 {self.avatar.name} 取消了关系:{relation_display_names.get(rel, str(rel))}",
|
||||||
|
related_avatars=[self.avatar.id, target.id]
|
||||||
|
)
|
||||||
|
EventHelper.push_pair(cancel_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||||
|
|
||||||
|
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
||||||
|
|
||||||
|
def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult:
|
||||||
|
"""调用通用异步 step 逻辑"""
|
||||||
target = self._get_target_avatar(target_avatar)
|
target = self._get_target_avatar(target_avatar)
|
||||||
if target is None:
|
if target is None:
|
||||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
return ActionResult(status=ActionStatus.FAILED, events=[])
|
||||||
|
|
||||||
infos = self._build_prompt_infos(target, can_into_relation=can_into_relation)
|
# 若无任务,创建异步任务
|
||||||
res = self._call_llm_feedback(infos)
|
if self._feedback_task is None and self._feedback_cached is None:
|
||||||
r = res.get(infos["avatar_name_2"], {})
|
infos = self._build_prompt_infos(target)
|
||||||
thinking = r.get("thinking", "")
|
import asyncio
|
||||||
feedback = str(r.get("feedback", "")).strip()
|
try:
|
||||||
talk_content = str(r.get("talk_content", "")).strip()
|
loop = asyncio.get_running_loop()
|
||||||
into_relation_str = str(r.get("into_relation", "")).strip()
|
self._feedback_task = loop.create_task(self._call_llm_feedback_async(infos))
|
||||||
|
except RuntimeError:
|
||||||
|
self._feedback_cached = self._call_llm_feedback(infos)
|
||||||
|
|
||||||
target.thinking = thinking
|
# 若任务已完成,消费结果
|
||||||
|
if self._feedback_task is not None and self._feedback_task.done():
|
||||||
fb = feedback.strip()
|
self._feedback_cached = self._feedback_task.result()
|
||||||
# 仅当明确接受时才记录对话与关系;其余一律视为拒绝
|
self._feedback_task = None
|
||||||
if fb == "Talk":
|
|
||||||
if talk_content:
|
|
||||||
content_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的交谈:{talk_content}", related_avatars=[self.avatar.id, target.id])
|
|
||||||
# 进入侧栏一次,并写入双方历史
|
|
||||||
EventHelper.push_pair(content_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
|
||||||
|
|
||||||
if can_into_relation and into_relation_str:
|
|
||||||
rel = Relation.from_chinese(into_relation_str)
|
|
||||||
if rel is not None:
|
|
||||||
self.avatar.set_relation(target, rel)
|
|
||||||
set_event = Event(self.world.month_stamp, f"{self.avatar.name} 与 {target.name} 的关系变为:{relation_display_names.get(rel, str(rel))}", related_avatars=[self.avatar.id, target.id])
|
|
||||||
EventHelper.push_pair(set_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
|
||||||
|
|
||||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
|
||||||
else:
|
|
||||||
feedback_event = Event(self.world.month_stamp, f"{target.name} 拒绝与 {self.avatar.name} 交谈", related_avatars=[self.avatar.id, target.id])
|
|
||||||
EventHelper.push_pair(feedback_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
|
||||||
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
|
||||||
|
|
||||||
def finish(self, target_avatar: "Avatar|str", **kwargs) -> list[Event]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
if self._feedback_cached is not None:
|
||||||
|
res = self._feedback_cached
|
||||||
|
self._feedback_cached = None
|
||||||
|
r = res.get(target.name, {})
|
||||||
|
thinking = r.get("thinking", "")
|
||||||
|
target.thinking = thinking
|
||||||
|
|
||||||
|
return self._handle_feedback_result(target, r)
|
||||||
|
|
||||||
|
return ActionResult(status=ActionStatus.RUNNING, events=[])
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from src.classes.action import DefineAction, ActualActionMixin, LLMAction
|
from src.classes.action.action import DefineAction, ActualActionMixin, LLMAction
|
||||||
from src.classes.tile import get_avatar_distance
|
from src.classes.tile import get_avatar_distance
|
||||||
from src.classes.event import Event
|
from src.classes.event import Event
|
||||||
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
|
from src.utils.llm import get_prompt_and_call_llm, get_prompt_and_call_llm_async
|
||||||
from src.utils.config import CONFIG
|
from src.utils.config import CONFIG
|
||||||
from src.classes.relation import relation_display_names, Relation, get_possible_post_relations
|
from src.classes.relation import relation_display_names, Relation
|
||||||
|
from src.classes.relations import get_possible_new_relations
|
||||||
from src.classes.action_runtime import ActionResult, ActionStatus
|
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||||
from src.classes.action.event_helper import EventHelper
|
from src.classes.action.event_helper import EventHelper
|
||||||
from src.classes.action.targeting_mixin import TargetingMixin
|
from src.classes.action.targeting_mixin import TargetingMixin
|
||||||
|
|||||||
98
src/classes/mutual_action/talk.py
Normal file
98
src/classes/mutual_action/talk.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src.classes.action_runtime import ActionResult, ActionStatus
|
||||||
|
from src.classes.event import Event
|
||||||
|
from src.classes.action.event_helper import EventHelper
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.classes.avatar import Avatar
|
||||||
|
|
||||||
|
from .mutual_action import MutualAction
|
||||||
|
|
||||||
|
|
||||||
|
class Talk(MutualAction):
|
||||||
|
"""
|
||||||
|
攀谈:向交互范围内的某个NPC发起攀谈。
|
||||||
|
- 接受后自动进入 Conversation
|
||||||
|
"""
|
||||||
|
|
||||||
|
ACTION_NAME = "攀谈"
|
||||||
|
COMMENT = "向对方发起攀谈"
|
||||||
|
DOABLES_REQUIREMENTS = "目标在交互范围内"
|
||||||
|
PARAMS = {"target_avatar": "AvatarName"}
|
||||||
|
FEEDBACK_ACTIONS: list[str] = ["Talk", "Reject"]
|
||||||
|
|
||||||
|
# 复用父类的所有方法:
|
||||||
|
# - _get_template_path() -> mutual_action.txt
|
||||||
|
# - _build_prompt_infos() -> 标准的双方信息和历史事件
|
||||||
|
# - can_start() -> 检查目标在交互范围内
|
||||||
|
# - start() -> 生成开始事件
|
||||||
|
# - finish() -> 返回空列表(已在父类实现)
|
||||||
|
|
||||||
|
def _handle_feedback_result(self, target: "Avatar", result: dict) -> ActionResult:
|
||||||
|
"""
|
||||||
|
处理 LLM 返回的反馈结果。
|
||||||
|
子类可覆盖此方法来定义自己的反馈处理逻辑。
|
||||||
|
"""
|
||||||
|
feedback = str(result.get("feedback", "")).strip()
|
||||||
|
|
||||||
|
# 处理反馈
|
||||||
|
if feedback == "Talk":
|
||||||
|
# 接受攀谈,自动进入 Conversation
|
||||||
|
accept_event = Event(
|
||||||
|
self.world.month_stamp,
|
||||||
|
f"{target.name} 接受了 {self.avatar.name} 的攀谈",
|
||||||
|
related_avatars=[self.avatar.id, target.id]
|
||||||
|
)
|
||||||
|
EventHelper.push_pair(accept_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||||
|
|
||||||
|
# 立即启动 Conversation
|
||||||
|
from .conversation import Conversation
|
||||||
|
conv = Conversation(self.avatar, self.world)
|
||||||
|
conv.start(target_avatar=target)
|
||||||
|
# 直接执行一次 step,启动异步调用
|
||||||
|
conv.step(target_avatar=target)
|
||||||
|
else:
|
||||||
|
# 拒绝攀谈
|
||||||
|
reject_event = Event(
|
||||||
|
self.world.month_stamp,
|
||||||
|
f"{target.name} 拒绝了 {self.avatar.name} 的攀谈",
|
||||||
|
related_avatars=[self.avatar.id, target.id]
|
||||||
|
)
|
||||||
|
EventHelper.push_pair(reject_event, initiator=self.avatar, target=target, to_sidebar_once=True)
|
||||||
|
|
||||||
|
return ActionResult(status=ActionStatus.COMPLETED, events=[])
|
||||||
|
|
||||||
|
def step(self, target_avatar: "Avatar|str", **kwargs) -> ActionResult:
|
||||||
|
"""调用父类的通用异步 step 逻辑"""
|
||||||
|
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)
|
||||||
|
import asyncio
|
||||||
|
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", "")
|
||||||
|
target.thinking = thinking
|
||||||
|
|
||||||
|
return self._handle_feedback_result(target, r)
|
||||||
|
|
||||||
|
return ActionResult(status=ActionStatus.RUNNING, events=[])
|
||||||
@@ -91,50 +91,6 @@ if TYPE_CHECKING:
|
|||||||
from src.classes.avatar import Avatar
|
from src.classes.avatar import Avatar
|
||||||
|
|
||||||
|
|
||||||
def get_possible_post_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]:
|
|
||||||
"""
|
|
||||||
评估“to_avatar 相对于 from_avatar”可能新增的后天关系集合(方向性明确)。
|
|
||||||
|
|
||||||
清晰规则:
|
|
||||||
- LOVERS(道侣):要求男女异性;若已存在 to->from 的相同关系则不重复
|
|
||||||
- MASTER(师傅):要求 to.level >= from.level + 20
|
|
||||||
- APPRENTICE(徒弟):要求 to.level <= from.level - 20
|
|
||||||
- FRIEND(朋友):始终可能(若未已存在)
|
|
||||||
- ENEMY(仇人):始终可能(若未已存在)
|
|
||||||
|
|
||||||
说明:本函数只判断“是否可能”,不做概率与人格相关控制;概率留给上层逻辑。
|
|
||||||
返回的是 Relation 列表,均为 to_avatar 相对于 from_avatar 的候选。
|
|
||||||
"""
|
|
||||||
# 方向相关:检查 to->from 已有关系,避免重复推荐
|
|
||||||
existing_to_from = to_avatar.get_relation(from_avatar)
|
|
||||||
|
|
||||||
candidates: list[Relation] = []
|
|
||||||
|
|
||||||
# 基础信息(Avatar 定义确保存在)
|
|
||||||
level_from = from_avatar.cultivation_progress.level
|
|
||||||
level_to = to_avatar.cultivation_progress.level
|
|
||||||
|
|
||||||
# - FRIEND
|
|
||||||
if existing_to_from != Relation.FRIEND:
|
|
||||||
candidates.append(Relation.FRIEND)
|
|
||||||
|
|
||||||
# - ENEMY
|
|
||||||
if existing_to_from != Relation.ENEMY:
|
|
||||||
candidates.append(Relation.ENEMY)
|
|
||||||
|
|
||||||
# - LOVERS:异性(Avatar 定义确保性别存在)
|
|
||||||
if from_avatar.gender != to_avatar.gender and existing_to_from != Relation.LOVERS:
|
|
||||||
candidates.append(Relation.LOVERS)
|
|
||||||
|
|
||||||
# - 师徒(方向性):
|
|
||||||
# MASTER:to 是 from 的师傅 → to.level >= from.level + 20
|
|
||||||
# APPRENTICE:to 是 from 的徒弟 → to.level <= from.level - 20
|
|
||||||
if level_to >= level_from + 20 and existing_to_from != Relation.MASTER:
|
|
||||||
candidates.append(Relation.MASTER)
|
|
||||||
if level_to <= level_from - 20 and existing_to_from != Relation.APPRENTICE:
|
|
||||||
candidates.append(Relation.APPRENTICE)
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
|
|
||||||
|
|
||||||
# ——— 显示层:性别化称谓映射与标签工具 ———
|
# ——— 显示层:性别化称谓映射与标签工具 ———
|
||||||
@@ -189,4 +145,3 @@ def get_relations_strs(avatar: "Avatar", max_lines: int = 6) -> list[str]:
|
|||||||
def relations_to_str(avatar: "Avatar", sep: str = ";", max_lines: int = 6) -> str:
|
def relations_to_str(avatar: "Avatar", sep: str = ";", max_lines: int = 6) -> str:
|
||||||
lines = get_relations_strs(avatar, max_lines=max_lines)
|
lines = get_relations_strs(avatar, max_lines=max_lines)
|
||||||
return sep.join(lines) if lines else "无"
|
return sep.join(lines) if lines else "无"
|
||||||
|
|
||||||
|
|||||||
126
src/classes/relations.py
Normal file
126
src/classes/relations.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
两个角色之间的关系操作函数
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from src.classes.relation import Relation, INNATE_RELATIONS, get_reciprocal, is_innate
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.classes.avatar import Avatar
|
||||||
|
|
||||||
|
|
||||||
|
def get_possible_new_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]:
|
||||||
|
"""
|
||||||
|
评估"to_avatar 相对于 from_avatar"可能新增的后天关系集合(方向性明确)。
|
||||||
|
|
||||||
|
清晰规则:
|
||||||
|
- LOVERS(道侣):要求男女异性;若已存在 to->from 的相同关系则不重复
|
||||||
|
- MASTER(师傅):要求 to.level >= from.level + 20
|
||||||
|
- APPRENTICE(徒弟):要求 to.level <= from.level - 20
|
||||||
|
- FRIEND(朋友):始终可能(若未已存在)
|
||||||
|
- ENEMY(仇人):始终可能(若未已存在)
|
||||||
|
|
||||||
|
说明:本函数只判断"是否可能",不做概率与人格相关控制;概率留给上层逻辑。
|
||||||
|
返回的是 Relation 列表,均为 to_avatar 相对于 from_avatar 的候选。
|
||||||
|
"""
|
||||||
|
# 方向相关:检查 to->from 已有关系,避免重复推荐
|
||||||
|
existing_to_from = to_avatar.get_relation(from_avatar)
|
||||||
|
|
||||||
|
candidates: list[Relation] = []
|
||||||
|
|
||||||
|
# 基础信息(Avatar 定义确保存在)
|
||||||
|
level_from = from_avatar.cultivation_progress.level
|
||||||
|
level_to = to_avatar.cultivation_progress.level
|
||||||
|
|
||||||
|
# - FRIEND
|
||||||
|
if existing_to_from != Relation.FRIEND:
|
||||||
|
candidates.append(Relation.FRIEND)
|
||||||
|
|
||||||
|
# - ENEMY
|
||||||
|
if existing_to_from != Relation.ENEMY:
|
||||||
|
candidates.append(Relation.ENEMY)
|
||||||
|
|
||||||
|
# - LOVERS:异性(Avatar 定义确保性别存在)
|
||||||
|
if from_avatar.gender != to_avatar.gender and existing_to_from != Relation.LOVERS:
|
||||||
|
candidates.append(Relation.LOVERS)
|
||||||
|
|
||||||
|
# - 师徒(方向性):
|
||||||
|
# MASTER:to 是 from 的师傅 → to.level >= from.level + 20
|
||||||
|
# APPRENTICE:to 是 from 的徒弟 → to.level <= from.level - 20
|
||||||
|
if level_to >= level_from + 20 and existing_to_from != Relation.MASTER:
|
||||||
|
candidates.append(Relation.MASTER)
|
||||||
|
if level_to <= level_from - 20 and existing_to_from != Relation.APPRENTICE:
|
||||||
|
candidates.append(Relation.APPRENTICE)
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def set_relation(from_avatar: "Avatar", to_avatar: "Avatar", relation: Relation) -> None:
|
||||||
|
"""
|
||||||
|
设置 from_avatar 对 to_avatar 的关系。
|
||||||
|
- 对称关系(如 FRIEND/ENEMY/LOVERS/SIBLING/KIN)会在对方处写入相同的关系。
|
||||||
|
- 有向关系(如 MASTER、APPRENTICE、PARENT、CHILD)会在对方处写入对偶关系。
|
||||||
|
"""
|
||||||
|
if to_avatar is from_avatar:
|
||||||
|
return
|
||||||
|
from_avatar.relations[to_avatar] = relation
|
||||||
|
# 写入对方的对偶关系(对称关系会得到同一枚举值)
|
||||||
|
to_avatar.relations[from_avatar] = get_reciprocal(relation)
|
||||||
|
|
||||||
|
|
||||||
|
def get_relation(from_avatar: "Avatar", to_avatar: "Avatar") -> Relation | None:
|
||||||
|
"""
|
||||||
|
获取 from_avatar 对 to_avatar 的关系。
|
||||||
|
"""
|
||||||
|
return from_avatar.relations.get(to_avatar)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_relation(from_avatar: "Avatar", to_avatar: "Avatar") -> None:
|
||||||
|
"""
|
||||||
|
清除 from_avatar 和 to_avatar 之间的关系(双向清除)。
|
||||||
|
"""
|
||||||
|
from_avatar.relations.pop(to_avatar, None)
|
||||||
|
to_avatar.relations.pop(from_avatar, None)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_relation(from_avatar: "Avatar", to_avatar: "Avatar", relation: Relation) -> bool:
|
||||||
|
"""
|
||||||
|
取消指定的后天关系。
|
||||||
|
- 只能取消后天关系(INNATE_RELATIONS 不可取消)
|
||||||
|
- 检查该关系是否存在且匹配
|
||||||
|
- 双向清除
|
||||||
|
|
||||||
|
返回:是否成功取消
|
||||||
|
"""
|
||||||
|
# 先天关系不可取消
|
||||||
|
if is_innate(relation):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查关系是否存在且匹配
|
||||||
|
existing = get_relation(from_avatar, to_avatar)
|
||||||
|
if existing != relation:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 清除关系
|
||||||
|
clear_relation(from_avatar, to_avatar)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_possible_cancel_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]:
|
||||||
|
"""
|
||||||
|
获取可能取消的关系列表(仅后天关系)。
|
||||||
|
|
||||||
|
返回:from_avatar 对 to_avatar 的可取消关系列表
|
||||||
|
"""
|
||||||
|
existing = get_relation(from_avatar, to_avatar)
|
||||||
|
if existing is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 只有后天关系可以取消
|
||||||
|
if is_innate(existing):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [existing]
|
||||||
|
|
||||||
@@ -26,8 +26,7 @@ avatar:
|
|||||||
persona_num: 3
|
persona_num: 3
|
||||||
|
|
||||||
social:
|
social:
|
||||||
talk_into_relation_probability: 0.1
|
event_context_num: 8
|
||||||
event_context_num: 6
|
|
||||||
|
|
||||||
# defined_avatar:
|
# defined_avatar:
|
||||||
# surname: 丰川
|
# surname: 丰川
|
||||||
|
|||||||
23
static/templates/conversation.txt
Normal file
23
static/templates/conversation.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
你是一个决策者,这是一个仙侠世界,你负责来生成两个NPC间的对话内容。
|
||||||
|
|
||||||
|
你需要进行决策的NPC的dict[AvatarName, info]为
|
||||||
|
{avatar_infos}
|
||||||
|
正在进行的动作为:{avatar_name_1}和{avatar_name_2}正在对话。这个对话可能是善意的,也可能是恶意的,也可能是闲聊。内容和性质取决于NPC性格、正邪、关系等因素。
|
||||||
|
|
||||||
|
两者可能进入的关系:{possible_new_relations}
|
||||||
|
两者可能取消的关系:{possible_cancal_relations}
|
||||||
|
注意:进入/取消关系不是必须的,完全由你根据对话情况、双方性格、历史事件等判断决定。
|
||||||
|
|
||||||
|
最近事件:
|
||||||
|
{recent_events}
|
||||||
|
|
||||||
|
注意,只返回json格式的结果。
|
||||||
|
格式为:
|
||||||
|
{{
|
||||||
|
"{avatar_name_2}": {{
|
||||||
|
"thinking": ..., // 简单思考对话的情况
|
||||||
|
"conversation_content": ... // 对话双方均为第三人称视角的,对话的主题和情况概括,约100字。注意不是对话的口语内容,仙侠语言风格。
|
||||||
|
"new_relation": ... // 可选,如果你认为可以让两者产生某种身份关系,则返回关系的中文名。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
|
||||||
|
"cancal_relation": ... // 可选,如果你认为可以让两者取消某种身份关系,则返回关系的中文名。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
|
||||||
|
}}
|
||||||
|
}}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
你是一个决策者,这是一个仙侠世界,你负责来决定一个NPC对另一个NPC的攀谈行为。
|
|
||||||
|
|
||||||
你需要进行决策的NPC的dict[AvatarName, info]为
|
|
||||||
{avatar_infos}
|
|
||||||
正在进行的动作为:{avatar_name_1}向{avatar_name_2}发起了攀谈。这代表{avatar_name_1}希望与{avatar_name_2}进行对话(他们目前是陌生人)。这个对话可能是善意的,也可能是恶意的,也可能是闲聊。取决于NPC性格、正邪等因素。
|
|
||||||
{avatar_name_2}可以进行的选择为:
|
|
||||||
["Talk", "Reject"]
|
|
||||||
两者是否可能进入某种关系:{can_into_relation}。注意,如果为True,也不代表一定要进入某种关系。这都由你来判断。
|
|
||||||
{avatar_name_2}可能相对于{avatar_name_1}的身份为: {possible_relations}
|
|
||||||
|
|
||||||
最近事件:
|
|
||||||
{recent_events}
|
|
||||||
|
|
||||||
注意,只返回json格式的结果。
|
|
||||||
只返回{avatar_name_2}的行动,格式为:
|
|
||||||
{{
|
|
||||||
{avatar_name_2}: {{
|
|
||||||
"thinking": ..., // 简单思考应该怎么决策
|
|
||||||
"feedback": ... // 面对{avatar_name_1}的行为的合法feedback action name
|
|
||||||
"talk_content": ... // 对话双方均为第三人称视角的,对话的主题和情况概括,约100字。注意不是对话的口语内容,仙侠语言风格。
|
|
||||||
"into_relation": ... // 如果你认为可以让两者产生某种身份关系,则返回。注意这是{avatar_name_2}相对于{avatar_name_1}的身份。
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
Reference in New Issue
Block a user