"""Read-only views of the context an adventure would send, or did send. The dry run assembles a prompt without calling the model. The per-action endpoint returns the prompt a turn was actually generated from. Neither writes anything. """ from fastapi import Depends, HTTPException from sqlalchemy.orm import Session from ... import derived, memorybank, models, summaries from ...context import ContextOverflow, build_context from ...database import get_db from ...knowledge import retrieval as knowledge_retrieval from ..settings import get_settings from .deps import CurrentUser, current_adventure, router @router.get("/{adventure_id}/context") async def dry_run_context( db: Session = Depends(get_db), user: models.User = CurrentUser, adventure: models.Adventure = Depends(current_adventure), ): """Returns what the app would send to the AI if the player continued now.""" settings = get_settings(db, user) memories = await memorybank.retrieve_memories(adventure, settings, update_stats=False) # M7: retrieved here too, and by the same call the turn makes. A dry run # that skipped the library would show a prompt the next turn will not send, # which is the one thing this panel must never do. knowledge = await knowledge_retrieval.retrieve(adventure, settings) try: _, _, report = build_context( adventure, settings, memories, knowledge=knowledge ) except ContextOverflow as exc: # M6: a dry run of a prompt that cannot be built is still an answer, and # a more useful one than a 500. The reader opened this panel to find out # what would be sent; "nothing, because the protected context does not # fit, and here is by how much" is exactly that. raise HTTPException(422, str(exc)) from exc return report @router.get("/{adventure_id}/derived") def derived_status( db: Session = Depends(get_db), adventure: models.Adventure = Depends(current_adventure), ): """M6: whether background memory, summary and embedding work is healthy. The surface that makes a dead memory bank findable. M2 shipped with the whole bank failing inside a fire-and-forget task and nothing anywhere said so — not the UI, not a log a player would read, not a failing test (`BUILD-MILESTONES.md`, note from M2). This endpoint is where that now shows. """ # Resolved once, not once per row: which summary the current head is # entitled to. Asking inside the comprehension would be one query per # summary, which is the shape M5 spent a finding removing. eligible = summaries.current(db, adventure) eligible_id = eligible.id if eligible is not None else None status = derived.report(db, adventure.id) return { "status": status, "failing": [row["kind"] for row in status if row["status"] == "failed"], "summaries": [ { "id": row.id, "branch_id": row.branch_id, "depth": row.depth, "trigger": row.trigger, "model": row.model_name, "eligible": row.id == eligible_id, "created_at": row.created_at.isoformat() if row.created_at else None, "preview": row.text[:200], } for row in summaries.all_for(db, adventure) ], } @router.get("/{adventure_id}/actions/{action_id}/context") def action_context( adventure_id: int, action_id: int, db: Session = Depends(get_db), adventure: models.Adventure = Depends(current_adventure), ): action = db.get(models.Action, action_id) if action is None or action.adventure_id != adventure_id: raise HTTPException(404, "Action not found") if action.context_snapshot is None: raise HTTPException(404, "No context snapshot for this action") return action.context_snapshot