Files
ai-agent/backend/forms/service.py
Gabriel Vidal 0ff9e40242 refactor(backend): split main.py into domain packages with app.state injection
main.py (5146 lines, 106 routes) becomes an assembly only: one package per
domain — core, files, settings, dashboards, conversations, diff,
notifications, forms, runs, accounts, workers, models, cron, agents,
projects, services, goals, memories, plans, templates — each exposing an
APIRouter; the flat domain modules move into their package behind a barrel
that keeps the old `import conversations` / `import projects` spellings.

The shared singletons (store, meta_store, hub, indexer, …) are built once by
core.state.build_state() and attached to app.state.ai; routes take them as
the `deps: State` dependency and helpers as an explicit `deps: AppState`.
conversations/pricing.py carries the per-model rates out of the parser.

Verified: route table and OpenAPI byte-identical; 90 read endpoints
golden-diffed against the monolith on a copy of the live data (identical);
write routes smoke-tested; 66 backend tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 23:55:47 +02:00

80 lines
3.3 KiB
Python

"""Creating a form (the ask-form skill's entry point) and its inlined assets."""
import base64
import binascii
import pathlib
import uuid as uuidlib
from fastapi import HTTPException
import forms as forms_mod
from core.config import MAX_UPLOAD_BYTES, UPLOADS_DIR
from core.state import AppState
from files.assets import _safe_upload_name
from notifications.service import _client_id, _conversation_url, _create_notify
def _create_form(deps: AppState, d: dict) -> dict:
"""Create a form (FormCreateBody dump in) — the route and the outbox
mirror both come through here. With ``notify`` the hub also sends the
push that deep-links to it; the result carries ``focusUrl``."""
if not (d.get("title") or "").strip():
raise HTTPException(400, "need a title")
raw_fields = d.get("fields") or []
description = (d.get("description") or "").strip()
if d.get("assets"):
urls = _store_form_assets(d["assets"])
raw_fields = forms_mod.apply_assets(raw_fields, urls)
description = forms_mod.apply_assets(description, urls)
try:
fields = forms_mod.valid_fields(raw_fields)
except ValueError as e:
raise HTTPException(400, str(e))
fid = _client_id(d.get("id"), "frm")
sid = (d.get("sessionId") or "").strip()
url = (d.get("url") or "").strip() or _conversation_url(deps, sid)
form = deps.form_store.create(
d["title"].strip(), fields, description=description,
type_=d.get("type") or "", url=url, session_id=sid, fid=fid)
deps.hub.publish({"type": "form", "id": form["id"],
"sessionId": form["sessionId"]})
form["focusUrl"] = f"{url}?form={form['id']}"
if d.get("notify"):
res = _create_notify(deps, {
"title": "📋 " + form["title"],
"message": form["description"] or "Tap to fill in the form",
"type": form["type"] or "ai", "url": form["focusUrl"],
"sessionId": sid})
form["delivered"] = res.get("delivered") or []
return form
def _store_form_assets(assets: list) -> dict[str, str]:
"""Decode a form's inlined assets into one uploads batch; name → serve URL."""
if not isinstance(assets, list) or len(assets) > 20:
raise HTTPException(400, "assets: at most 20 files")
batch = uuidlib.uuid4().hex[:12]
dest = UPLOADS_DIR / batch
urls: dict[str, str] = {}
for a in assets:
if not isinstance(a, dict):
raise HTTPException(400, "assets: each entry must be an object")
name = _safe_upload_name(str(a.get("name") or "asset"))
try:
data = base64.b64decode(str(a.get("data") or ""), validate=True)
except (ValueError, binascii.Error):
raise HTTPException(400, f"assets: {name}: data is not base64")
if not data:
raise HTTPException(400, f"assets: {name}: empty")
if len(data) > MAX_UPLOAD_BYTES:
raise HTTPException(413, f"assets: {name} exceeds {MAX_UPLOAD_BYTES // (1 << 20)} MB")
dest.mkdir(parents=True, exist_ok=True)
target = dest / name
i = 1
while target.exists():
target = dest / f"{target.stem}-{i}{pathlib.Path(name).suffix}"
i += 1
target.write_bytes(data)
urls[str(a.get("name") or name)] = f"/api/upload-file?path={batch}/{target.name}"
return urls