fix
This commit is contained in:
@@ -72,21 +72,39 @@ async def run_iteration(
|
||||
settings_map = await get_all_settings(db)
|
||||
llm = LlmClient(settings_map)
|
||||
|
||||
# Save the player's action as a message
|
||||
next_seq = await _next_seq(db, session_id)
|
||||
player_msg = Message(
|
||||
session_id=session_id,
|
||||
seq=next_seq,
|
||||
role="user",
|
||||
kind="player_action",
|
||||
content=action_text,
|
||||
payload={},
|
||||
is_pinned=True,
|
||||
hidden=False,
|
||||
# Save the player's action as a message — UNLESS this is a retry of the
|
||||
# previous action (frontend re-sent the same action_text after an error).
|
||||
# In that case we reuse the existing player_action row so the chat
|
||||
# history doesn't fill up with duplicates.
|
||||
last_msg_result = await db.execute(
|
||||
select(Message)
|
||||
.where(Message.session_id == session_id)
|
||||
.order_by(Message.seq.desc())
|
||||
.limit(1)
|
||||
)
|
||||
db.add(player_msg)
|
||||
await db.commit()
|
||||
await db.refresh(player_msg)
|
||||
last_msg = last_msg_result.scalars().first()
|
||||
is_retry = (
|
||||
last_msg is not None
|
||||
and last_msg.kind == "player_action"
|
||||
and last_msg.content == action_text
|
||||
)
|
||||
if is_retry:
|
||||
player_msg = last_msg
|
||||
else:
|
||||
next_seq = await _next_seq(db, session_id)
|
||||
player_msg = Message(
|
||||
session_id=session_id,
|
||||
seq=next_seq,
|
||||
role="user",
|
||||
kind="player_action",
|
||||
content=action_text,
|
||||
payload={},
|
||||
is_pinned=True,
|
||||
hidden=False,
|
||||
)
|
||||
db.add(player_msg)
|
||||
await db.commit()
|
||||
await db.refresh(player_msg)
|
||||
|
||||
yield {"type": "status", "data": {"message": "planning"}}
|
||||
|
||||
@@ -476,3 +494,122 @@ def _safe_parse_json(s: str) -> Any:
|
||||
return json.loads(s) if s else {}
|
||||
except Exception:
|
||||
return s
|
||||
|
||||
|
||||
|
||||
async def generate_intro_scene(
|
||||
db: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
session_id: uuid.UUID,
|
||||
) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""Generate the opening cinematic scene for a freshly-created session.
|
||||
|
||||
Yields the same SSE event stream shape as `run_iteration` so the
|
||||
frontend can consume it identically. Saves a `narrative_step` message
|
||||
of kind `intro_scene` (still kind=narrative_step for compatibility,
|
||||
but with payload.kind=intro so the UI can style it differently if
|
||||
desired).
|
||||
"""
|
||||
result = await db.execute(select(Session).where(Session.id == session_id))
|
||||
session = result.scalars().first()
|
||||
if not session:
|
||||
yield {"type": "error", "data": {"message": "session_not_found"}}
|
||||
return
|
||||
result = await db.execute(select(World).where(World.id == session.world_id))
|
||||
world = result.scalars().first()
|
||||
if not world:
|
||||
yield {"type": "error", "data": {"message": "world_not_found"}}
|
||||
return
|
||||
|
||||
settings_map = await get_all_settings(db)
|
||||
llm = LlmClient(settings_map)
|
||||
|
||||
yield {"type": "status", "data": {"message": "writing_scene"}}
|
||||
|
||||
import json as _json
|
||||
defn = world.definition or {}
|
||||
player_state = world.state.get("player", {}) if world.state else {}
|
||||
system_prompt = get_prompt("intro_scene", world.language).format(
|
||||
setting_description=defn.get("setting_description", "")[:1200],
|
||||
current_time=world.current_time or "",
|
||||
player_state=_json.dumps(player_state, ensure_ascii=False)[:800],
|
||||
plot_rails=_json.dumps(defn.get("plot_rails", {}), ensure_ascii=False)[:600],
|
||||
world_language=world.language or "en",
|
||||
)
|
||||
|
||||
step_resp = await llm.chat(
|
||||
messages=[{"role": "system", "content": system_prompt}],
|
||||
tools=STEP_WRITER_TOOL_SCHEMAS,
|
||||
temperature=float(cast_setting("llm.step_temperature", settings_map.get("llm.step_temperature", 0.85))),
|
||||
max_tokens=1500,
|
||||
purpose="intro_scene",
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
db=db,
|
||||
)
|
||||
|
||||
step_text = step_resp.text or ""
|
||||
step_options: List[str] = []
|
||||
for tc in (step_resp.tool_calls or []):
|
||||
if tc.get("function", {}).get("name") == "submit_scene":
|
||||
args_str = tc.get("function", {}).get("arguments", "{}")
|
||||
try:
|
||||
scene_data = _json.loads(args_str) if args_str else {}
|
||||
if scene_data.get("narrative"):
|
||||
step_text = scene_data["narrative"]
|
||||
if scene_data.get("options") and isinstance(scene_data["options"], list):
|
||||
step_options = [str(o) for o in scene_data["options"]][:5]
|
||||
except _json.JSONDecodeError:
|
||||
log.warning("intro_scene_invalid_json", args=args_str[:200])
|
||||
break
|
||||
else:
|
||||
# Fallback: extract JSON from text.
|
||||
import re as _re
|
||||
json_match = _re.search(r"\{[\s\S]*\}", step_resp.text or "")
|
||||
if json_match:
|
||||
try:
|
||||
step_data = _json.loads(json_match.group(0))
|
||||
if "narrative" in step_data:
|
||||
step_text = step_data["narrative"]
|
||||
if "options" in step_data and isinstance(step_data["options"], list):
|
||||
step_options = [str(o) for o in step_data["options"]][:5]
|
||||
except _json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Save as a narrative_step message flagged as intro in payload.
|
||||
step_seq = await _next_seq(db, session_id)
|
||||
step_msg = Message(
|
||||
session_id=session_id,
|
||||
seq=step_seq,
|
||||
role="assistant",
|
||||
kind="narrative_step",
|
||||
content=step_text,
|
||||
payload={
|
||||
"kind": "intro",
|
||||
"options": step_options,
|
||||
"world_time": world.current_time,
|
||||
"player_state": world.state.get("player", {}),
|
||||
},
|
||||
is_pinned=True,
|
||||
hidden=False,
|
||||
)
|
||||
db.add(step_msg)
|
||||
session.last_played_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
await db.refresh(step_msg)
|
||||
|
||||
yield {
|
||||
"type": "step_complete",
|
||||
"data": {
|
||||
"message_id": str(step_msg.id),
|
||||
"seq": step_msg.seq,
|
||||
"narrative": step_text,
|
||||
"options": step_options,
|
||||
"state": world.state,
|
||||
"world_time": world.current_time,
|
||||
"player_state": world.state.get("player", {}),
|
||||
"fired_triggers": [],
|
||||
"is_intro": True,
|
||||
},
|
||||
}
|
||||
yield {"type": "done", "data": {}}
|
||||
|
||||
Reference in New Issue
Block a user