Files
openswarm/backend/OLDapps/outputs/outputs.py

235 lines
8.9 KiB
Python

"""Outputs SubApp — CRUD, workspace management, and file serving.
AI-generation endpoints live in ``ai_generation.py``; pure helpers in ``helpers.py``.
"""
from __future__ import annotations
import json
import mimetypes
import os
from datetime import datetime
from contextlib import asynccontextmanager
from fastapi import HTTPException
from fastapi.responses import Response
from backend.config.Apps import SubApp
from backend.apps.common.json_store import JsonStore
from backend.apps.outputs.models import (
Output, OutputCreate, OutputUpdate, OutputExecute, OutputExecuteResult,
AutoRunConfig, WorkspaceSeedRequest,
)
from backend.apps.outputs.executor import execute_backend_code
from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL, VIEW_TEMPLATE_FILES
from backend.apps.outputs.helpers import (
_validate_against_schema, _inject_data_into_html, _decode_data_param, _walk_directory,
)
from backend.apps.outputs import ai_generation
from backend.config.paths import OUTPUTS_DIR as DATA_DIR, OUTPUTS_WORKSPACE_DIR as WORKSPACE_DIR
from backend.apps.analytics.collector import record as _analytics
@asynccontextmanager
async def outputs_lifespan():
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(WORKSPACE_DIR, exist_ok=True)
yield
outputs = SubApp("outputs", outputs_lifespan)
_store = JsonStore(Output, DATA_DIR, not_found_detail="Output not found")
_load_all = _store.load_all
_save = _store.save
_load = _store.load
# -- File serving --
@outputs.router.get("/workspace/{workspace_id}/serve/{filepath:path}")
async def serve_workspace_file(workspace_id: str, filepath: str, _d: str = ""):
folder = os.path.join(WORKSPACE_DIR, workspace_id)
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
if not os.path.isfile(full_path):
raise HTTPException(status_code=404, detail="File not found")
with open(full_path) as f:
content = f.read()
if filepath == "index.html":
input_json, result_json = _decode_data_param(_d) if _d else ("{}", "null")
content = _inject_data_into_html(content, input_json, result_json)
mime, _ = mimetypes.guess_type(filepath)
return Response(content=content, media_type=mime or "text/plain")
@outputs.router.get("/{output_id}/serve/{filepath:path}")
async def serve_output_file(output_id: str, filepath: str, _d: str = ""):
output = _load(output_id)
content = output.files.get(filepath)
if content is None:
raise HTTPException(status_code=404, detail="File not found in output")
if filepath == "index.html":
input_json, result_json = _decode_data_param(_d) if _d else ("{}", "null")
content = _inject_data_into_html(content, input_json, result_json)
mime, _ = mimetypes.guess_type(filepath)
return Response(content=content, media_type=mime or "text/plain")
# -- CRUD --
@outputs.router.get("/list")
async def list_outputs():
return {"outputs": [o.model_dump() for o in _load_all()]}
@outputs.router.get("/workspace/{workspace_id}")
async def read_workspace(workspace_id: str):
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
files = _walk_directory(folder)
meta = None
if "meta.json" in files:
try:
meta = json.loads(files["meta.json"])
except (json.JSONDecodeError, ValueError):
pass
return {"files": files, "meta": meta}
@outputs.router.post("/workspace/seed")
async def seed_workspace(body: WorkspaceSeedRequest):
folder = os.path.join(WORKSPACE_DIR, body.workspace_id)
os.makedirs(folder, exist_ok=True)
if body.files:
for rel_path, content in body.files.items():
full_path = os.path.normpath(os.path.join(folder, rel_path))
if not full_path.startswith(os.path.normpath(folder)):
continue
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(content)
else:
for rel_path, content in VIEW_TEMPLATE_FILES.items():
full_path = os.path.join(folder, rel_path)
with open(full_path, "w") as f:
f.write(content)
with open(os.path.join(folder, "SKILL.md"), "w") as f:
f.write(VIEW_BUILDER_SKILL)
if body.meta:
with open(os.path.join(folder, "meta.json"), "w") as f:
json.dump(body.meta, f, indent=2)
return {"path": os.path.abspath(folder)}
@outputs.router.put("/workspace/{workspace_id}/file/{filepath:path}")
async def write_workspace_file(workspace_id: str, filepath: str, body: dict):
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "w") as f:
f.write(body.get("content", ""))
return {"ok": True}
@outputs.router.delete("/workspace/{workspace_id}/file/{filepath:path}")
async def delete_workspace_file(workspace_id: str, filepath: str):
folder = os.path.join(WORKSPACE_DIR, workspace_id)
if not os.path.isdir(folder):
raise HTTPException(status_code=404, detail="Workspace not found")
full_path = os.path.normpath(os.path.join(folder, filepath))
if not full_path.startswith(os.path.normpath(folder)):
raise HTTPException(status_code=403, detail="Path traversal not allowed")
if os.path.isfile(full_path):
os.remove(full_path)
parent = os.path.dirname(full_path)
while parent != os.path.normpath(folder):
if os.path.isdir(parent) and not os.listdir(parent):
os.rmdir(parent)
parent = os.path.dirname(parent)
else:
break
return {"ok": True}
@outputs.router.get("/{output_id}")
async def get_output(output_id: str):
return _load(output_id).model_dump()
@outputs.router.post("/create")
async def create_output(body: OutputCreate):
now = datetime.now().isoformat()
output = Output(
name=body.name, description=body.description, icon=body.icon,
input_schema=body.input_schema, files=body.files,
auto_run_config=body.auto_run_config, thumbnail=body.thumbnail,
created_at=now, updated_at=now,
)
_save(output)
_analytics("feature.used", {"feature": "view.created"})
return {"ok": True, "output": output.model_dump()}
@outputs.router.put("/{output_id}")
async def update_output(output_id: str, body: OutputUpdate):
output = _load(output_id)
for k, v in body.model_dump(exclude_none=True).items():
if k == "auto_run_config" and isinstance(v, dict):
v = AutoRunConfig(**v)
setattr(output, k, v)
output.updated_at = datetime.now().isoformat()
_save(output)
return {"ok": True, "output": output.model_dump()}
@outputs.router.delete("/{output_id}")
async def delete_output(output_id: str):
_load(output_id)
path = os.path.join(DATA_DIR, f"{output_id}.json")
if os.path.exists(path):
os.remove(path)
return {"ok": True}
@outputs.router.post("/execute")
async def execute_output(body: OutputExecute):
output = _load(body.output_id)
validation_err = _validate_against_schema(body.input_data, output.input_schema)
if validation_err:
return OutputExecuteResult(
output_id=output.id, output_name=output.name,
frontend_code=output.frontend_code, input_data=body.input_data,
backend_result=None, error=validation_err,
).model_dump()
backend_result = None
stdout_text = None
stderr_text = None
error = None
if output.backend_code:
try:
exec_result = await execute_backend_code(output.backend_code, body.input_data)
backend_result = exec_result.result
stdout_text = exec_result.stdout
stderr_text = exec_result.stderr
except Exception as e:
error = str(e)
return OutputExecuteResult(
output_id=output.id, output_name=output.name,
frontend_code=output.frontend_code, input_data=body.input_data,
backend_result=backend_result, stdout=stdout_text,
stderr=stderr_text, error=error,
).model_dump()
# -- AI generation routes --
outputs.router.add_api_route("/auto-run", ai_generation.auto_run_output, methods=["POST"])
outputs.router.add_api_route("/auto-run-agent", ai_generation.auto_run_agent, methods=["POST"])
outputs.router.add_api_route("/auto-run-agent/{session_id}", ai_generation.cleanup_auto_run_agent, methods=["DELETE"])