fix: improve OpenAI-compatible LLM fallback and per-player round replies
This commit is contained in:
1 parent
e474e137ea
commit
22b90c4a6d
3 files changed
+196
-8
No files matched your search
+15
-2
@@ -40,6 +40,16 @@ def build_system_prompt() -> str:
|
||||
def build_user_prompt(context: dict[str, Any]) -> str:
|
||||
story_context = context.get("story_context", {})
|
||||
allowed_clues = story_context.get("allowed_clues", [])
|
||||
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 = "线索字段可新增任意短文本,但必须与当前游戏卡世界观一致"
|
||||
@@ -47,7 +57,9 @@ def build_user_prompt(context: dict[str, Any]) -> str:
|
||||
payload = {
|
||||
"story_context": story_context,
|
||||
"game_state": context.get("game_state", {}),
|
||||
"players": context.get("players", []),
|
||||
"players": players,
|
||||
"player_count": len(players),
|
||||
"player_usernames": player_names,
|
||||
"merged_actions": context.get("merged_actions", {}),
|
||||
"schema": SCHEMA_DESCRIPTION,
|
||||
"constraints": [
|
||||
@@ -60,7 +72,8 @@ def build_user_prompt(context: dict[str, Any]) -> str:
|
||||
"如果行动描述是“使用/消耗/投掷/服用/注射某道具”,且结果生效,请在 inventory_remove 删除该道具",
|
||||
"自定义卡会按 content_pack.end_conditions 由服务端判定结局",
|
||||
"choices 最多 4 条",
|
||||
"private_player_updates 可为空",
|
||||
"private_player_updates 需要覆盖所有玩家,每个 player_id 至少 1 条 message",
|
||||
"private_player_updates 中的 player_id 必须来自 players 列表",
|
||||
"如果故事未满足结局条件,应设置 should_end_game=false",
|
||||
],
|
||||
}
|
||||
|
||||
+56
-1
@@ -79,6 +79,48 @@ class RoomManager:
|
||||
return f"{text[:417]}..."
|
||||
return text
|
||||
|
||||
def _ensure_private_updates_for_all_players(
|
||||
self,
|
||||
ai_payload: dict[str, Any],
|
||||
players: list[dict[str, Any]],
|
||||
actions: list[PlayerAction],
|
||||
) -> None:
|
||||
updates_raw = ai_payload.get("private_player_updates", [])
|
||||
updates = updates_raw if isinstance(updates_raw, list) else []
|
||||
|
||||
by_player: dict[str, str] = {}
|
||||
for row in updates:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
player_id = str(row.get("player_id", "")).strip()
|
||||
message = " ".join(str(row.get("message", "")).strip().split())
|
||||
if not player_id or not message:
|
||||
continue
|
||||
if player_id not in by_player:
|
||||
by_player[player_id] = message[:220]
|
||||
|
||||
action_by_player: dict[str, PlayerAction] = {row.player_id: row for row in actions}
|
||||
for player in players:
|
||||
player_id = str(player.get("player_id", "")).strip()
|
||||
if not player_id:
|
||||
continue
|
||||
if player_id in by_player:
|
||||
continue
|
||||
action = action_by_player.get(player_id)
|
||||
player_name = str(player.get("name") or player_id).strip()
|
||||
if action:
|
||||
action_text = " ".join(str(action.content or "").strip().split())
|
||||
if len(action_text) > 56:
|
||||
action_text = f"{action_text[:56]}..."
|
||||
by_player[player_id] = f"{player_name},你的行动“{action_text}”已计入结算,请根据新线索继续推进。"
|
||||
else:
|
||||
by_player[player_id] = f"{player_name},本轮未记录到你的行动,下一轮请尽快提交你的选择。"
|
||||
|
||||
ai_payload["private_player_updates"] = [
|
||||
{"player_id": player_id, "message": message}
|
||||
for player_id, message in by_player.items()
|
||||
]
|
||||
|
||||
def _ensure_unique_name(self, name: str, existing_names: set[str]) -> str:
|
||||
if name not in existing_names:
|
||||
return name
|
||||
@@ -473,11 +515,23 @@ class RoomManager:
|
||||
|
||||
try:
|
||||
merge_result = self.action_resolver.merge_actions(actions, room_id, int(state.get("round_no", 0)) + 1)
|
||||
user_cache: dict[str, dict[str, Any]] = {}
|
||||
for player in players:
|
||||
user_id = str(player.get("user_id") or "")
|
||||
if not user_id or user_id in user_cache:
|
||||
continue
|
||||
user_cache[user_id] = self.persistence.get_user_by_id(user_id) or {}
|
||||
prompt_context = {
|
||||
"story_context": self.story_engine.story_context(state.get("scene_id", "lobby"), state),
|
||||
"game_state": state,
|
||||
"players": [
|
||||
{"player_id": player["player_id"], "name": player["name"], "is_online": player["is_online"]}
|
||||
{
|
||||
"player_id": player["player_id"],
|
||||
"name": player["name"],
|
||||
"username": user_cache.get(str(player.get("user_id") or ""), {}).get("username", ""),
|
||||
"display_name": user_cache.get(str(player.get("user_id") or ""), {}).get("display_name", ""),
|
||||
"is_online": player["is_online"],
|
||||
}
|
||||
for player in players
|
||||
],
|
||||
"merged_actions": merge_result,
|
||||
@@ -512,6 +566,7 @@ class RoomManager:
|
||||
if ai_payload is None:
|
||||
raise ValueError("ai_call_failed:empty_ai_payload")
|
||||
|
||||
self._ensure_private_updates_for_all_players(ai_payload, players, actions)
|
||||
next_state, round_result = self.story_engine.apply_turn(state, ai_payload, merge_result, players)
|
||||
next_status = "ended" if next_state.get("game_phase") == "ended" else "running"
|
||||
self.state_manager.set_room_state(room_id, next_status, next_state)
|
||||
|
||||
+125
-5
@@ -63,9 +63,33 @@ class DeepSeekClient:
|
||||
payload: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
payload["temperature"] = temperature
|
||||
payload["max_tokens"] = max_tokens
|
||||
if use_json_mode:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
return payload
|
||||
|
||||
def _build_payload(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
*,
|
||||
use_json_mode: bool,
|
||||
token_field: str,
|
||||
include_temperature: bool,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
}
|
||||
if include_temperature:
|
||||
payload["temperature"] = temperature
|
||||
if token_field == "max_tokens":
|
||||
payload["max_tokens"] = max_tokens
|
||||
elif token_field == "max_completion_tokens":
|
||||
payload["max_completion_tokens"] = max_tokens
|
||||
if use_json_mode:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
return payload
|
||||
@@ -80,6 +104,50 @@ class DeepSeekClient:
|
||||
return True
|
||||
return "json schema" in lower and "support" in lower
|
||||
|
||||
def _extract_error_message(self, body: Any, raw_text: str) -> str:
|
||||
if isinstance(body, dict):
|
||||
error = body.get("error")
|
||||
if isinstance(error, dict):
|
||||
msg = error.get("message")
|
||||
if isinstance(msg, str) and msg.strip():
|
||||
return msg.strip()
|
||||
if isinstance(error, str) and error.strip():
|
||||
return error.strip()
|
||||
msg = body.get("message")
|
||||
if isinstance(msg, str) and msg.strip():
|
||||
return msg.strip()
|
||||
text = str(raw_text or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if len(text) > 1000:
|
||||
return text[:1000]
|
||||
return text
|
||||
|
||||
def _detect_token_field_switch(self, status_code: int, error_message: str, current_field: str) -> str | None:
|
||||
if status_code not in {400, 404, 415, 422}:
|
||||
return None
|
||||
lower = str(error_message or "").lower()
|
||||
if current_field == "max_tokens":
|
||||
if "max_tokens" in lower and any(word in lower for word in ("unsupported", "unknown", "invalid", "not allowed", "unrecognized")):
|
||||
return "max_completion_tokens"
|
||||
if current_field == "max_completion_tokens":
|
||||
if "max_completion_tokens" in lower and any(word in lower for word in ("unsupported", "unknown", "invalid", "not allowed", "unrecognized")):
|
||||
return "max_tokens"
|
||||
if current_field != "none":
|
||||
if "maximum context length" in lower and "max_tokens" in lower:
|
||||
return "none"
|
||||
if "unexpected parameter" in lower and ("max_tokens" in lower or "max_completion_tokens" in lower):
|
||||
return "none"
|
||||
return None
|
||||
|
||||
def _is_temperature_unsupported(self, status_code: int, error_message: str) -> bool:
|
||||
if status_code not in {400, 404, 415, 422}:
|
||||
return False
|
||||
lower = str(error_message or "").lower()
|
||||
if "temperature" not in lower:
|
||||
return False
|
||||
return any(word in lower for word in ("unsupported", "unknown", "invalid", "not allowed", "unrecognized"))
|
||||
|
||||
def _content_to_text(self, value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
@@ -119,14 +187,43 @@ class DeepSeekClient:
|
||||
content = ""
|
||||
if isinstance(message, dict):
|
||||
content = self._content_to_text(message.get("content"))
|
||||
if not content:
|
||||
tool_calls = message.get("tool_calls", [])
|
||||
if isinstance(tool_calls, list) and tool_calls:
|
||||
call = tool_calls[0] if isinstance(tool_calls[0], dict) else {}
|
||||
func = call.get("function", {}) if isinstance(call, dict) else {}
|
||||
if isinstance(func, dict):
|
||||
content = self._content_to_text(func.get("arguments"))
|
||||
if not content:
|
||||
content = self._content_to_text(message.get("reasoning"))
|
||||
if not content:
|
||||
content = self._content_to_text(first.get("text"))
|
||||
if not content and isinstance(first.get("delta"), dict):
|
||||
content = self._content_to_text(first.get("delta", {}).get("content"))
|
||||
if content:
|
||||
return content
|
||||
|
||||
output_text = self._content_to_text(body.get("output_text"))
|
||||
if output_text:
|
||||
return output_text
|
||||
|
||||
output = body.get("output", [])
|
||||
if isinstance(output, list):
|
||||
blocks: list[str] = []
|
||||
for item in output:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
content_rows = item.get("content", [])
|
||||
if not isinstance(content_rows, list):
|
||||
continue
|
||||
for row in content_rows:
|
||||
if isinstance(row, dict):
|
||||
text = self._content_to_text(row.get("text"))
|
||||
if text:
|
||||
blocks.append(text)
|
||||
merged = "\n".join(blocks).strip()
|
||||
if merged:
|
||||
return merged
|
||||
return ""
|
||||
|
||||
def chat_json(
|
||||
@@ -146,12 +243,22 @@ class DeepSeekClient:
|
||||
last_detail = ""
|
||||
attempt = 0
|
||||
use_json_mode = self.force_json_mode
|
||||
token_field = "max_tokens"
|
||||
include_temperature = True
|
||||
while attempt <= self.retries:
|
||||
try:
|
||||
payload = self._build_payload(
|
||||
messages,
|
||||
temperature,
|
||||
max_tokens,
|
||||
use_json_mode=use_json_mode,
|
||||
token_field=token_field,
|
||||
include_temperature=include_temperature,
|
||||
)
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=self._payload(messages, temperature, max_tokens, use_json_mode),
|
||||
json=payload,
|
||||
timeout=(self.connect_timeout, self.read_timeout),
|
||||
)
|
||||
raw_text = response.text or ""
|
||||
@@ -160,15 +267,28 @@ class DeepSeekClient:
|
||||
raise requests.HTTPError(last_error)
|
||||
|
||||
if response.status_code >= 400:
|
||||
body_error: Any = None
|
||||
try:
|
||||
body_error = response.json()
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
body_error = None
|
||||
error_message = self._extract_error_message(body_error, raw_text)
|
||||
if use_json_mode and self._is_response_format_unsupported(response.status_code, raw_text):
|
||||
use_json_mode = False
|
||||
continue
|
||||
next_token_field = self._detect_token_field_switch(response.status_code, error_message, token_field)
|
||||
if next_token_field is not None and next_token_field != token_field:
|
||||
token_field = next_token_field
|
||||
continue
|
||||
if include_temperature and self._is_temperature_unsupported(response.status_code, error_message):
|
||||
include_temperature = False
|
||||
continue
|
||||
return {
|
||||
"ok": False,
|
||||
"error": f"http_{response.status_code}",
|
||||
"content": None,
|
||||
"detail": "",
|
||||
"raw": raw_text,
|
||||
"detail": error_message,
|
||||
"raw": body_error if isinstance(body_error, dict) else raw_text,
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user