This commit is contained in:
Mikan
2026-06-21 04:11:38 +03:00
parent bd85e186dc
commit 4dee4fb0a8
19 changed files with 1423 additions and 115 deletions

View File

@@ -260,6 +260,171 @@ async def iterate_stream(
)
# --------------------------------------------------------------------------- #
# World editor: accept/reject proposed changes, answer clarifications
# --------------------------------------------------------------------------- #
@router.post("/worlds/{world_id}/apply")
async def apply_proposed_changes(
world_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
) -> dict:
"""Accept proposed changes from the world_editor stream."""
from app.engine.world_editor import submit_propose_changes_answer
world = await _load_world(db, world_id, user)
ok = submit_propose_changes_answer(world.id, True)
if not ok:
raise HTTPException(409, "no_pending_changes")
return {"ok": True}
@router.post("/worlds/{world_id}/discard")
async def discard_proposed_changes(
world_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
) -> dict:
"""Reject proposed changes from the world_editor stream."""
from app.engine.world_editor import submit_propose_changes_answer
world = await _load_world(db, world_id, user)
ok = submit_propose_changes_answer(world.id, False)
if not ok:
raise HTTPException(409, "no_pending_changes")
return {"ok": True}
@router.post("/worlds/{world_id}/answer")
async def answer_clarification(
world_id: uuid.UUID,
body: AnswerRequest,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
) -> dict:
"""Answer an ask_user clarification from the world_editor stream."""
from app.engine.world_editor import submit_clarification_answer
world = await _load_world(db, world_id, user)
ok = submit_clarification_answer(world.id, body.text)
if not ok:
raise HTTPException(409, "no_pending_clarification")
return {"ok": True}
# --------------------------------------------------------------------------- #
# Generate / re-generate intro scene (for worlds stuck in draft)
# --------------------------------------------------------------------------- #
@router.post("/worlds/{world_id}/generate-intro", response_model=dict, status_code=status.HTTP_202_ACCEPTED)
async def generate_intro(
world_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
settings: dict = Depends(get_settings_dict),
) -> dict:
"""Re-generate the intro scene for a draft world.
This runs the intro_scene stage of the world_builder flow without
regenerating schemas/entities. Useful when the world was created from
a preset (which provides schemas + entities) but the intro scene
generation failed.
"""
world = await _load_world(db, world_id, user)
if world.status == "archived":
raise HTTPException(422, "world_archived")
return {
"stream_url": f"/api/sessions/worlds/{world.id}/intro/stream",
}
@router.get("/worlds/{world_id}/intro/stream")
async def intro_stream(
world_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
settings: dict = Depends(get_settings_dict),
) -> StreamingResponse:
world = await _load_world(db, world_id, user)
emitter = SseEmitter()
llm = _llm_factory(settings)
async def run_bg():
async with _session_scope() as bg_db:
from sqlalchemy import select as sa_select
from app.engine.world_builder import _run_tool_loop, _strip_code_fence
from app.engine.tools.base import ToolContext, get_registry
from app.models import Entity
from app.prompts.registry import get_prompt
from app.core.time_utils import advance_time, summarize_schemas
import json as _json
bg_world = (
await bg_db.execute(sa_select(World).where(World.id == world.id))
).scalar_one()
try:
await emitter.emit("step", {"step": "generating_intro", "message": "Generating intro scene..."})
entities = (
await bg_db.execute(
sa_select(Entity).where(
Entity.world_id == bg_world.id, Entity.deleted_at.is_(None)
)
)
).scalars().all()
entities_summary = "\n".join(
f"- {e.entity_type}: {e.name}" for e in entities[:20]
) or "(no entities)"
intro_prompt = get_prompt("intro_scene", "en").format(
world_name=bg_world.name,
world_description=bg_world.description or "",
language=bg_world.language,
current_time=bg_world.current_time,
environment_json=_json.dumps(bg_world.environment or {}, ensure_ascii=False, indent=2),
plot_rails_json=_json.dumps(bg_world.plot_rails or {}, ensure_ascii=False, indent=2),
entities_summary=entities_summary,
)
scene_result = await _run_tool_loop(
db=bg_db, world=bg_world, llm=llm, sse=emitter,
stage="intro_scene",
system_prompt=intro_prompt,
terminal_tool="submit_step",
max_substeps=3,
settings={},
)
scene_text = ""
delta_time = "hours_1"
if scene_result and scene_result.get("ok"):
scene_text = scene_result.get("data", {}).get("scene_text", "")
delta_time = scene_result.get("data", {}).get("delta_time", "hours_1")
bg_world.intro_scene = scene_text
bg_world.current_time = advance_time(bg_world.current_time, delta_time, bg_world.time_schema)
bg_world.status = "ready"
await bg_db.commit()
await emitter.emit("intro_scene_complete", {
"text": scene_text, "delta_time": delta_time,
"current_time": bg_world.current_time,
})
await emitter.done({"world_id": str(bg_world.id), "status": "ready"})
except Exception as e:
await emitter.error("internal_error", str(e))
import asyncio
task = asyncio.create_task(run_bg())
async def gen():
try:
async for evt in emitter.stream():
yield _format_sse(evt)
finally:
await task
return StreamingResponse(
gen(), media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# --------------------------------------------------------------------------- #
# Retry / rollback
# --------------------------------------------------------------------------- #