Files
cultivation-world-simulator/src/classes/ai.py
2025-12-29 22:02:44 +08:00

107 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
NPC AI 的类。
这里指的是 NPC 的决策机制。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
import asyncio
from src.classes.world import World
from src.classes.event import Event, NULL_EVENT
from src.utils.llm import call_llm_with_task_name
from src.classes.typings import ACTION_NAME_PARAMS_PAIRS
from src.classes.actions import ACTION_INFOS_STR
from src.utils.config import CONFIG
if TYPE_CHECKING:
from src.classes.avatar import Avatar
class AI(ABC):
"""
抽象AI统一采用批量接口。
"""
@abstractmethod
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple]:
pass
async def decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str, Event]]:
"""
决定做什么,同时生成对应的事件。
由于底层 LLM 调用已接入全局任务池,此处直接并发执行所有任务即可。
"""
# 调用具体的决策逻辑
results = await self._decide(world, avatars_to_decide)
# 补全 Event 字段
for avatar in list(results.keys()):
action_name_params_pairs, avatar_thinking, short_term_objective = results[avatar] # type: ignore
# 不在决策阶段生成开始事件,提交阶段统一触发
results[avatar] = (action_name_params_pairs, avatar_thinking, short_term_objective, NULL_EVENT)
return results
class LLMAI(AI):
"""
LLM AI
"""
async def _decide(self, world: World, avatars_to_decide: list[Avatar]) -> dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]]:
"""
异步决策逻辑通过LLM决定执行什么动作和参数
"""
general_action_infos = ACTION_INFOS_STR
async def decide_one(avatar: Avatar):
# 获取基于该角色已知区域的世界信息(包含距离计算)
world_info = world.get_info(avatar=avatar, detailed=True)
# 在提示中包含处于角色观测范围内的其他角色
observed = world.get_observable_avatars(avatar)
avatar_info = avatar.get_expanded_info(co_region_avatars=observed)
info = {
"avatar_name": avatar.name,
"avatar_info": avatar_info,
"world_info": world_info,
"general_action_infos": general_action_infos,
}
template_path = CONFIG.paths.templates / "ai.txt"
res = await call_llm_with_task_name("action_decision", template_path, info)
return avatar, res
# 直接并发所有任务
tasks = [decide_one(avatar) for avatar in avatars_to_decide]
results_list = await asyncio.gather(*tasks)
results: dict[Avatar, tuple[ACTION_NAME_PARAMS_PAIRS, str, str]] = {}
for avatar, res in results_list:
if not res or avatar.name not in res:
continue
r = res[avatar.name]
# 仅接受 action_name_params_pairs不再支持单个 action_name/action_params
raw_pairs = r.get("action_name_params_pairs", [])
pairs: ACTION_NAME_PARAMS_PAIRS = []
for p in raw_pairs:
if isinstance(p, list) and len(p) == 2:
pairs.append((p[0], p[1]))
elif isinstance(p, dict) and "action_name" in p and "action_params" in p:
pairs.append((p["action_name"], p["action_params"]))
else:
continue
# 至少有一个
if not pairs:
continue # Skip if no valid actions found
avatar_thinking = r.get("avatar_thinking", r.get("thinking", ""))
short_term_objective = r.get("short_term_objective", "")
results[avatar] = (pairs, avatar_thinking, short_term_objective)
return results
llm_ai = LLMAI()