Files
openswarm/backend/apps/outputs/versions_routes.py
T

80 lines
3.1 KiB
Python

"""HTTP surface for app version history. Thin: each route validates then
delegates to versions.py. Its own SubApp so the already-large outputs.py doesn't
grow, and so the agent-running restore guard lives next to the route, not in the
pure store. Prefix: /api/output_versions."""
import logging
import os
from contextlib import asynccontextmanager
from typing import Literal, Optional
from fastapi import HTTPException
from pydantic import BaseModel
from backend.apps.outputs import versions
from backend.apps.outputs.workspace_io import load_output
from backend.config.Apps import SubApp
from backend.config.paths import OUTPUTS_VERSIONS_DIR
logger = logging.getLogger(__name__)
@asynccontextmanager
async def output_versions_lifespan():
os.makedirs(OUTPUTS_VERSIONS_DIR, exist_ok=True)
yield
output_versions = SubApp("output_versions", output_versions_lifespan)
class CaptureRequest(BaseModel):
# Clients can only ask for auto/manual; pre_restore is set internally by restore.
source: Literal["auto", "manual"] = "manual"
label: str = ""
thumbnail: Optional[str] = None
@output_versions.router.get("/{output_id}")
async def list_output_versions(output_id: str):
return {"versions": [v.model_dump() for v in versions.list_versions(output_id)]}
@output_versions.router.post("/{output_id}")
async def capture_output_version(output_id: str, body: CaptureRequest):
v = versions.capture(
output_id, source=body.source, label=body.label, thumbnail=body.thumbnail
)
if v is None:
raise HTTPException(status_code=404, detail="Output not found")
return {"ok": True, "version": v.model_dump()}
@output_versions.router.post("/{output_id}/{version_id}/restore")
async def restore_output_version(output_id: str, version_id: str):
output = load_output(output_id)
if output is None:
raise HTTPException(status_code=404, detail="Output not found")
# Don't restore out from under a live builder run. The frontend disables the button while the agent is active; this is the backend half of that guard.
if output.session_id:
from backend.apps.agents.agent_manager import agent_manager
session = agent_manager.sessions.get(output.session_id)
if session and getattr(session, "status", None) in ("running", "waiting_approval"):
raise HTTPException(
status_code=409,
detail="This app is still being edited. Wait for the current change to finish, then try again.",
)
restored = versions.restore(output_id, version_id)
if restored is None:
raise HTTPException(status_code=404, detail="Version not found")
return {"ok": True, "output": restored.model_dump()}
@output_versions.router.post("/{output_id}/{version_id}/branch")
async def branch_output_version(output_id: str, version_id: str):
if load_output(output_id) is None:
raise HTTPException(status_code=404, detail="Output not found")
new_id = versions.branch(output_id, version_id)
if new_id is None:
raise HTTPException(status_code=404, detail="Version not found")
return {"ok": True, "new_output_id": new_id}