rebase
This commit is contained in:
148
app/engine/world_editor.py
Normal file
148
app/engine/world_editor.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""World Editor — chat-based editing of an existing world."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.llm import LlmClient, MockLlmClient
|
||||
from app.core.logging import get_logger
|
||||
from app.core.time_utils import summarize_schemas
|
||||
from app.engine.sse import SseEmitter
|
||||
from app.engine.tools.base import ToolContext, get_registry
|
||||
from app.models import Entity, World
|
||||
from app.prompts.registry import get_prompt
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def run_world_editor(
|
||||
*,
|
||||
db: AsyncSession,
|
||||
world: World,
|
||||
instruction: str,
|
||||
llm: LlmClient | MockLlmClient,
|
||||
sse: SseEmitter,
|
||||
max_iterations: int = 8,
|
||||
) -> None:
|
||||
"""Run a world_editor iteration: instruction → propose_changes → done.
|
||||
|
||||
Simplified (vs §9.2): no `ask_user` blocking — the LLM gets one shot at
|
||||
producing a `propose_changes` (or applies tool calls directly if simple).
|
||||
"""
|
||||
try:
|
||||
registry = get_registry()
|
||||
ctx = ToolContext(db=db, world=world, stage="world_editor", sse_emitter=sse.emit)
|
||||
|
||||
# Snapshot current entities for the prompt
|
||||
entities = (
|
||||
await db.execute(
|
||||
select(Entity).where(
|
||||
Entity.world_id == world.id, Entity.deleted_at.is_(None)
|
||||
).limit(30)
|
||||
)
|
||||
).scalars().all()
|
||||
entities_summary = "\n".join(
|
||||
f"- {e.entity_type}: {e.name}" for e in entities
|
||||
)
|
||||
sys_prompt = get_prompt("world_editor", "en").format(
|
||||
world_name=world.name,
|
||||
world_description=world.description or "",
|
||||
language=world.language,
|
||||
schemas_summary=summarize_schemas(world.schemas or []),
|
||||
environment_json=json.dumps(world.environment, ensure_ascii=False, indent=2),
|
||||
entities_summary=entities_summary,
|
||||
instruction=instruction,
|
||||
)
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": instruction},
|
||||
]
|
||||
tools = registry.to_openai_format("world_editor")
|
||||
for _ in range(max_iterations):
|
||||
resp = await llm.complete(
|
||||
stage="world_editor",
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
temperature=0.5,
|
||||
max_tokens=2048,
|
||||
world_id=world.id,
|
||||
session=db,
|
||||
)
|
||||
msg = resp.get("message", {})
|
||||
tcs = msg.get("tool_calls") or []
|
||||
if not tcs:
|
||||
# Done
|
||||
await sse.emit("comment", {"text": msg.get("content", "")})
|
||||
break
|
||||
messages.append(msg)
|
||||
done = False
|
||||
for tc in tcs:
|
||||
fn = tc.get("function", {})
|
||||
tname = fn.get("name", "")
|
||||
try:
|
||||
targs = json.loads(fn.get("arguments") or "{}")
|
||||
except json.JSONDecodeError:
|
||||
targs = {}
|
||||
if tname == "ask_user":
|
||||
# Non-interactive: emit clarification and stop
|
||||
await sse.emit("clarification", {
|
||||
"question": targs.get("question"),
|
||||
"options": targs.get("options"),
|
||||
})
|
||||
await sse.done({"status": "needs_clarification"})
|
||||
return
|
||||
if tname == "propose_changes":
|
||||
await sse.emit("change_proposed", {
|
||||
"diff": targs.get("diff", []),
|
||||
"comment": targs.get("comment", ""),
|
||||
})
|
||||
# Apply changes directly (simplified: auto-accept)
|
||||
await _apply_diff(world, targs.get("diff", []))
|
||||
await db.commit()
|
||||
await sse.emit("apply_changes", {})
|
||||
done = True
|
||||
break
|
||||
# Execute tool
|
||||
result = await registry.execute(tname, targs, ctx)
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tc.get("id", ""),
|
||||
"name": tname,
|
||||
"content": json.dumps(result.to_dict(), ensure_ascii=False),
|
||||
})
|
||||
if done:
|
||||
break
|
||||
await sse.done({"status": "completed"})
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.exception("world_editor_failed", world_id=str(world.id), error=str(e))
|
||||
await sse.error("internal_error", str(e))
|
||||
|
||||
|
||||
async def _apply_diff(world: World, diff: list[dict[str, Any]]) -> None:
|
||||
"""Apply a propose_changes diff to the world.
|
||||
|
||||
Supports paths into environment and basic field operations.
|
||||
"""
|
||||
from app.core.state_validator import apply_patch
|
||||
|
||||
env_patch: dict[str, Any] = {}
|
||||
schemas_patch: dict[str, Any] = {}
|
||||
for d in diff:
|
||||
path = d.get("path", "")
|
||||
op = d.get("op", "replace")
|
||||
new = d.get("new")
|
||||
if path.startswith("environment."):
|
||||
field = path[len("environment."):]
|
||||
env_patch[field] = new
|
||||
elif path.startswith("schemas."):
|
||||
# For simplicity, replace entire schemas if any schema patch present
|
||||
schemas_patch[path] = new
|
||||
if env_patch:
|
||||
new_env, errors = apply_patch(dict(world.environment or {}), env_patch)
|
||||
if not errors:
|
||||
world.environment = new_env
|
||||
Reference in New Issue
Block a user