rebase
This commit is contained in:
180
app/engine/context.py
Normal file
180
app/engine/context.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Context manager — builds the LLM message list per stage.
|
||||
|
||||
Implements the compression strategy from §10.3 of the TDD:
|
||||
- If history > threshold, prepend the latest summary as a system message.
|
||||
- Truncate to last N guaranteed messages.
|
||||
- Optionally include RAG results.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.core.settings_service import get_setting
|
||||
from app.core.time_utils import summarize_schemas
|
||||
from app.models import StoryEntry, Step, World
|
||||
from app.prompts.registry import get_prompt
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _scene_text_truncate(text: str, max_tokens: int) -> str:
|
||||
"""Crude truncation: ~4 chars per token."""
|
||||
max_chars = max_tokens * 4
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[:max_chars] + "…"
|
||||
|
||||
|
||||
async def build_orchestrator_phase1_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
world: World,
|
||||
player_action: str,
|
||||
settings: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build messages list for orchestrator Phase 1."""
|
||||
guaranteed = int(settings.get("context.guaranteed_messages", 10))
|
||||
threshold = int(settings.get("context.compression_threshold_messages", 20))
|
||||
scene_trunc = int(settings.get("context.scene_text_truncate_tokens", 500))
|
||||
|
||||
# Fetch recent steps (most recent first)
|
||||
recent_steps = list(
|
||||
reversed(
|
||||
(
|
||||
await db.execute(
|
||||
select(Step)
|
||||
.where(Step.world_id == world.id, Step.deleted_at.is_(None))
|
||||
.order_by(Step.sequence_number.desc())
|
||||
.limit(max(threshold, guaranteed) + 1)
|
||||
)
|
||||
).scalars().all()
|
||||
)
|
||||
)
|
||||
|
||||
# Pull latest summary if available
|
||||
summary_text: str | None = None
|
||||
if len(recent_steps) > threshold:
|
||||
latest_summary = (
|
||||
await db.execute(
|
||||
select(StoryEntry)
|
||||
.where(
|
||||
StoryEntry.world_id == world.id,
|
||||
StoryEntry.entry_type == "event",
|
||||
StoryEntry.metadata_["type"].as_string() == "summary",
|
||||
)
|
||||
.order_by(StoryEntry.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if latest_summary:
|
||||
summary_text = latest_summary.content
|
||||
|
||||
# Build the message list
|
||||
sys_prompt = get_prompt("orchestrator_phase1", "en").format(
|
||||
world_name=world.name,
|
||||
rules="\n".join(f"- {r}" for r in (world.rules or [])),
|
||||
schemas_summary=summarize_schemas(world.schemas or []),
|
||||
environment_json=json.dumps(world.environment or {}, ensure_ascii=False, indent=2),
|
||||
plot_rails_json=json.dumps(world.plot_rails or {}, ensure_ascii=False, indent=2),
|
||||
current_time=world.current_time,
|
||||
recent_history=_format_recent_history(
|
||||
recent_steps[-guaranteed:], scene_trunc
|
||||
),
|
||||
max_substeps=settings.get("game.max_substeps_per_iteration", 8),
|
||||
language=world.language,
|
||||
player_action=player_action,
|
||||
)
|
||||
messages: list[dict[str, Any]] = [{"role": "system", "content": sys_prompt}]
|
||||
if summary_text:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": f"Summary of earlier events:\n{summary_text}",
|
||||
})
|
||||
# Recent steps as user/assistant pairs
|
||||
for s in recent_steps[-guaranteed:]:
|
||||
messages.append({"role": "user", "content": s.player_action})
|
||||
if s.scene_text:
|
||||
messages.append({"role": "assistant", "content": s.scene_text})
|
||||
# Current action
|
||||
messages.append({"role": "user", "content": player_action})
|
||||
return messages
|
||||
|
||||
|
||||
def _format_recent_history(steps: list[Step], scene_trunc: int) -> str:
|
||||
if not steps:
|
||||
return "(no recent history)"
|
||||
lines: list[str] = []
|
||||
for s in steps[-5:]: # only show last 5 in the prompt
|
||||
text = _scene_text_truncate(s.scene_text or "(no scene)", scene_trunc)
|
||||
lines.append(f"[step {s.sequence_number}] {s.player_action}\n → {text}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def build_orchestrator_phase2_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
world: World,
|
||||
player_action: str,
|
||||
plan: str,
|
||||
summary: list[dict[str, Any]],
|
||||
settings: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build messages list for orchestrator Phase 2 (writer)."""
|
||||
sys_prompt = get_prompt("orchestrator_phase2", "en").format(
|
||||
world_name=world.name,
|
||||
world_description=world.description or "",
|
||||
language=world.language,
|
||||
current_time=world.current_time,
|
||||
player_action=player_action,
|
||||
plan=plan,
|
||||
summary_json=json.dumps(summary, ensure_ascii=False, indent=2),
|
||||
environment_json=json.dumps(world.environment or {}, ensure_ascii=False, indent=2),
|
||||
)
|
||||
return [
|
||||
{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": "Write the scene and call submit_step."},
|
||||
]
|
||||
|
||||
|
||||
async def build_orchestrator_phase3_suggest_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
world: World,
|
||||
scene_text: str,
|
||||
settings: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
sys_prompt = get_prompt("orchestrator_phase3_suggest", "en").format(
|
||||
language=world.language,
|
||||
scene_text=scene_text[:2000],
|
||||
current_goals=", ".join((world.plot_rails or {}).get("current_goals", []) or ["(none)"]),
|
||||
)
|
||||
return [
|
||||
{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": "Suggest 1-3 next actions."},
|
||||
]
|
||||
|
||||
|
||||
async def build_summary_context(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
world: World,
|
||||
old_steps: list[Step],
|
||||
settings: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build messages list for the summary LLM call."""
|
||||
messages_json = json.dumps(
|
||||
[{"action": s.player_action, "scene": s.scene_text} for s in old_steps],
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
sys_prompt = get_prompt("summary", "en").format(messages_json=messages_json)
|
||||
return [
|
||||
{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": "Summarize."},
|
||||
]
|
||||
Reference in New Issue
Block a user