This commit is contained in:
Mikan
2026-06-19 16:31:45 +03:00
parent d0d1f003ae
commit 5a78def096
21 changed files with 1250 additions and 548 deletions

View File

@@ -58,7 +58,10 @@ async def build_orchestrator_messages(
recent = visible_msgs[-recent_n:] if visible_msgs else []
# Build orchestrator system prompt with current state
# Build orchestrator system prompt with current state.
# NOTE: prompts are always English (system content convention). The LLM
# produces player-facing text in world.language when relevant (the
# step-writer prompt interpolates world_language explicitly).
defn = world.definition or {}
system_prompt_template = get_prompt("orchestrator", world.language)
player_state = world.state.get("player", {}) if world.state else {}
@@ -69,14 +72,14 @@ async def build_orchestrator_messages(
current_time=world.current_time or "",
player_state=json.dumps(player_state, ensure_ascii=False)[:600],
plot_rails=json.dumps(defn.get("plot_rails", {}), ensure_ascii=False)[:400],
summary=summary_text or "(нет сводки)",
summary=summary_text or "(no summary yet)",
)
messages: List[Dict[str, Any]] = [{"role": "system", "content": system_prompt}]
# Add summary as a system note if present
if summary_text:
messages.append({"role": "system", "content": f"Сводка прошлого:\n{summary_text}"})
messages.append({"role": "system", "content": f"Past summary:\n{summary_text}"})
# Add recent visible messages
for m in recent:
@@ -86,7 +89,7 @@ async def build_orchestrator_messages(
messages.append({"role": "assistant", "content": m.content})
# The current action
messages.append({"role": "user", "content": f'Действие игрока: "{action_text}"'})
messages.append({"role": "user", "content": f'Player action: "{action_text}"'})
return messages, settings_map
@@ -100,7 +103,11 @@ async def _maybe_compress(
world: World,
settings_map: Dict[str, Any],
) -> None:
"""If history exceeds threshold, summarize older messages into a single summary message."""
"""If history exceeds threshold, summarize older messages into a single summary message.
Uses the `submit_summary` tool (tool-calling-first design) to get structured
output from the summarizer LLM.
"""
visible = [m for m in all_msgs if not m.hidden]
if len(visible) <= recent_n + summary_n:
return
@@ -110,45 +117,61 @@ async def _maybe_compress(
if not to_summarize:
return
# Build summarization input
# Build summarization input (English labels — system content convention)
summary_input_lines = []
for m in to_summarize:
prefix = {
"player_action": "Игрок",
"narrative_step": "Сцена",
"summary": "Сводка",
"player_action": "Player",
"narrative_step": "Scene",
"summary": "Summary",
"orchestrator_plan": "GM",
"technical_offscreen": "За кадром",
"technical_offscreen": "Offscreen",
}.get(m.kind, m.kind)
summary_input_lines.append(f"{prefix}: {m.content[:300]}")
summary_input = "\n\n".join(summary_input_lines)
llm = LlmClient(settings_map)
system_prompt = get_prompt("summarizer", world.language)
from app.engine.tools.tools import SUMMARIZER_TOOL_SCHEMAS
response = await llm.chat(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": summary_input[:4000]},
],
tools=SUMMARIZER_TOOL_SCHEMAS,
temperature=float(cast_setting("llm.summary_temperature", settings_map.get("llm.summary_temperature", 0.3))),
max_tokens=300,
max_tokens=400,
purpose="summary",
session_id=session_id,
db=db,
)
# Parse summary response
summary_text = response.text
# Extract from submit_summary tool call; fall back to text parse.
summary_text = response.text or ""
facts: List[Dict[str, Any]] = []
import re as _re
json_match = _re.search(r"\{[\s\S]*\}", response.text)
if json_match:
try:
data = json.loads(json_match.group(0))
summary_text = data.get("summary", response.text)
facts = data.get("facts", [])
except json.JSONDecodeError:
pass
extracted = False
for tc in (response.tool_calls or []):
if tc.get("function", {}).get("name") == "submit_summary":
args_str = tc.get("function", {}).get("arguments", "{}")
try:
data = json.loads(args_str) if args_str else {}
summary_text = data.get("summary", response.text or "")
facts = data.get("facts", []) or []
extracted = True
except json.JSONDecodeError:
pass
break
if not extracted:
# Fallback: extract JSON from text response (older models).
import re as _re
json_match = _re.search(r"\{[\s\S]*\}", response.text or "")
if json_match:
try:
data = json.loads(json_match.group(0))
summary_text = data.get("summary", response.text or "")
facts = data.get("facts", []) or []
except json.JSONDecodeError:
pass
# Create summary message
next_seq = (max((m.seq for m in all_msgs), default=0)) + 1
@@ -207,7 +230,11 @@ async def build_step_writer_messages(
outcome: str,
narrative_prompt: str,
) -> List[Dict[str, Any]]:
"""Build messages for the step writer LLM call."""
"""Build messages for the step writer LLM call.
The step-writer prompt is in English (system content convention) but
instructs the LLM to produce the narrative in world.language.
"""
defn = world.definition or {}
player_state = world.state.get("player", {}) if world.state else {}
system_prompt = get_prompt("step_writer", world.language).format(
@@ -216,6 +243,7 @@ async def build_step_writer_messages(
player_state=json.dumps(player_state, ensure_ascii=False)[:400],
outcome=outcome,
narrative_prompt=narrative_prompt[:600],
world_language=world.language or "en",
)
return [{"role": "system", "content": system_prompt}]