rebase
This commit is contained in:
@@ -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
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user