103 lines
5.0 KiB
Python
103 lines
5.0 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
SCHEMA_DESCRIPTION = {
|
||
"narration": "string",
|
||
"npc_dialogues": [{"npc": "string", "line": "string", "tone": "string"}],
|
||
"state_updates": {
|
||
"danger_delta": "integer",
|
||
"add_clues": ["string"],
|
||
"set_flags": {"flag_name": "bool|string|number"},
|
||
"set_flags.player_metric_set": {"player_id": {"metric_key": "number"}},
|
||
"set_flags.player_metric_delta": {"player_id": {"metric_key": "number"}},
|
||
"npc_changes": {"npc_id": {"mood": "string", "trust": "number", "location": "string"}},
|
||
"inventory_add": {"player_id": ["string"]},
|
||
"inventory_remove": {"player_id": ["string"]},
|
||
},
|
||
"private_player_updates": [{"player_id": "string", "message": "string"}],
|
||
"choices": ["string"],
|
||
"should_end_scene": "boolean",
|
||
"should_end_game": "boolean",
|
||
"ending_summary": "string|null",
|
||
}
|
||
|
||
|
||
def build_system_prompt() -> str:
|
||
return (
|
||
"你是《Echo Tavern》联机文字冒险的主持 AI。"
|
||
"你只能在给定剧本骨架内叙述,不得凭空改写核心真相。"
|
||
"你必须输出严格 JSON,不要输出 markdown 或额外说明。"
|
||
"你需要综合多名玩家本轮行动,并明确谁触发了结果。"
|
||
"如果行动冲突,按提供的冲突解析结果进行叙述。"
|
||
"state_updates 仅可做增量更新,不可删除已有关键线索。"
|
||
"若本轮发生道具消耗,必须在 inventory_remove 填写被消耗的道具。"
|
||
"不要写过长文本,每段叙述控制在 120 中文字以内。"
|
||
)
|
||
|
||
|
||
def build_user_prompt(context: dict[str, Any]) -> str:
|
||
story_context = context.get("story_context", {})
|
||
allowed_clues = story_context.get("allowed_clues", [])
|
||
runtime_options_raw = story_context.get("runtime_options", {})
|
||
runtime_options = runtime_options_raw if isinstance(runtime_options_raw, dict) else {}
|
||
interaction_mode = str(runtime_options.get("interaction_mode", "hybrid")).strip().lower()
|
||
if interaction_mode not in {"cooperative", "split", "hybrid"}:
|
||
interaction_mode = "hybrid"
|
||
players = context.get("players", [])
|
||
if not isinstance(players, list):
|
||
players = []
|
||
player_names = []
|
||
for row in players:
|
||
if not isinstance(row, dict):
|
||
continue
|
||
name = str(row.get("username") or row.get("name") or row.get("player_id") or "").strip()
|
||
if name:
|
||
player_names.append(name)
|
||
clue_rule = "关键线索只可从允许列表中新增"
|
||
if allowed_clues == "ANY_STRING":
|
||
clue_rule = "线索字段可新增任意短文本,但必须与当前游戏卡世界观一致"
|
||
|
||
payload = {
|
||
"story_context": story_context,
|
||
"game_state": context.get("game_state", {}),
|
||
"players": players,
|
||
"player_count": len(players),
|
||
"player_usernames": player_names,
|
||
"runtime_options": runtime_options,
|
||
"merged_actions": context.get("merged_actions", {}),
|
||
"schema": SCHEMA_DESCRIPTION,
|
||
"constraints": [
|
||
clue_rule,
|
||
"danger_delta 取值范围 -2 到 3",
|
||
"可在 set_flags 中写入任意指标字段(如 hp=90)或 *_delta 增量(如 hp_delta=-12)",
|
||
"当数据作用域为 per_player 时,优先在 set_flags.player_metric_set / player_metric_delta 中按玩家更新",
|
||
"当背包作用域为 shared 时,inventory_add 写任意玩家键都会合并到共享背包",
|
||
"当背包作用域为 shared 时,inventory_remove 写任意玩家键都会从共享背包移除",
|
||
"如果行动描述是“使用/消耗/投掷/服用/注射某道具”,且结果生效,请在 inventory_remove 删除该道具",
|
||
"自定义卡会按 content_pack.end_conditions 由服务端判定结局",
|
||
"choices 最多 4 条",
|
||
"private_player_updates 需要覆盖所有玩家,每个 player_id 至少 1 条 message",
|
||
"private_player_updates 中的 player_id 必须来自 players 列表",
|
||
f"互动模式 interaction_mode={interaction_mode}:cooperative 强调协同,split 允许分头行动,hybrid 两者可混合",
|
||
"当 require_all_players_response=true 时,默认按全员回合制叙述,避免忽略任一玩家行动",
|
||
"可在 set_flags.player_status 中设置玩家状态(alive/down/dead/failed/revived)",
|
||
"可在 set_flags.player_dead/player_death/player_revive/player_survival 中按玩家控制生存状态",
|
||
"如果故事未满足结局条件,应设置 should_end_game=false",
|
||
],
|
||
}
|
||
return (
|
||
"以下是当前回合上下文,请根据上下文推进剧情并返回 JSON。"
|
||
"\n\n上下文:\n"
|
||
+ json.dumps(payload, ensure_ascii=False)
|
||
+ "\n\n仅返回合法 JSON。"
|
||
)
|
||
|
||
|
||
def build_messages(context: dict[str, Any]) -> list[dict[str, str]]:
|
||
return [
|
||
{"role": "system", "content": build_system_prompt()},
|
||
{"role": "user", "content": build_user_prompt(context)},
|
||
]
|