appsmith-api / api /enhance.py
Kakashiix26's picture
ai-logs admin endpoint
d9e966d verified
Raw
History Blame Contribute Delete
5.84 kB
"""
VibeEngineer endpoint — POST /api/enhance.
Takes current files + a natural-language instruction and returns the complete
updated file set via a single LLM call (same single-pass parse pattern as
the coder node).
"""
import logging
import uuid
from datetime import datetime as _dt, timezone as _tz
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from agent.llm import CODER_CHAIN, CODER_CHAIN_PREMIUM, chat, strip_code_fences
from agent.premium_scaffold import is_premium_shaped, preserve_premium_scaffold
from agent.prompts import CODER_SYSTEM_PROMPT, vibe_prompt, vibe_prompt_premium
from agent.scaffold import (detect_framework, normalize_files,
parse_reply_and_files)
from auth.dependencies import get_current_user
from db.database import SessionLocal
from db.models import Project, User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/enhance", tags=["enhance"])
class EnhanceRequest(BaseModel):
project_id: str | None = None
files: dict[str, str]
instruction: str
session_id: str | None = None # ponytail: kept for backward compat, ignored for ownership
premium: bool = False # honored ONLY for SUPER_ADMIN (server-side gate below)
class EnhanceResponse(BaseModel):
project_id: str
files: dict[str, str]
reply: str # ponytail: short first-person message from the VibeEngineer (two-way chat)
@router.post("", response_model=EnhanceResponse)
def enhance(
req: EnhanceRequest,
current_user: User = Depends(get_current_user),
) -> EnhanceResponse:
if not req.instruction.strip():
raise HTTPException(status_code=400, detail="instruction must not be empty")
if not req.files:
raise HTTPException(status_code=400, detail="files must not be empty")
# ponytail: never trust the client flag — premium is honored only for SUPER_ADMIN
# (mirrors api.generate._resolve_premium); a non-admin asking for premium gets standard.
premium = bool(req.premium) and getattr(current_user, "role", "") == "SUPER_ADMIN"
chain = CODER_CHAIN_PREMIUM if premium else CODER_CHAIN
if is_premium_shaped(req.files):
# SCOPED edit: send only the editable surface (not the frozen /ui + /lib kit)
# + the primitive cheatsheet, and ask for ONLY the changed files. This keeps
# a premium edit fast + reliable instead of re-writing all ~22 files.
editable = {p: c for p, c in req.files.items()
if not p.startswith(("/ui/", "/lib/"))}
raw = chat(
vibe_prompt_premium(editable, req.instruction),
chain=chain, system=CODER_SYSTEM_PROMPT, max_tokens=16000, agent="enhance",
)
reply, changed = parse_reply_and_files(raw, strip_code_fences)
reply = reply or "Applied your changes."
# Merge the partial changes over the current app, then re-freeze the kit + deps.
merged = normalize_files({**req.files, **changed}, "react")
updated_files = preserve_premium_scaffold(req.files, merged)
else:
raw = chat(
vibe_prompt(req.files, req.instruction, premium=premium),
chain=chain, system=CODER_SYSTEM_PROMPT, max_tokens=20000 if premium else 8192, agent="enhance",
)
reply, parsed = parse_reply_and_files(raw, strip_code_fences)
reply = reply or "Applied your changes."
# ponytail: infer framework so static/next apps aren't React-normalized.
updated_files = normalize_files(parsed, detect_framework(req.files))
project_id = _upsert_project(req.project_id, current_user.id, req.instruction, updated_files, source="enhance")
return EnhanceResponse(project_id=project_id, files=updated_files, reply=reply)
_MAX_VERSIONS = 15
def snapshot_version(row: Project, source: str, label: str) -> None:
"""Append the row's CURRENT files as a recoverable version BEFORE it's
overwritten, capped at the most recent _MAX_VERSIONS. So every agent change
(enhance/UX/OnCall) or revert preserves the prior state for one-click revert."""
if not getattr(row, "files", None):
return
entry = {
"id": str(uuid.uuid4()),
"at": _dt.now(_tz.utc).isoformat(),
"source": source,
"label": (label or "").strip()[:80],
"files": row.files,
}
prior = list(row.versions or [])
prior.append(entry)
row.versions = prior[-_MAX_VERSIONS:]
def _upsert_project(project_id: str | None, user_id: uuid.UUID, instruction: str,
files: dict, source: str = "edit") -> str:
"""Update existing project (owned by user) or create a new one; returns the project UUID string.
Snapshots the PRIOR files as a version before overwriting an existing row, so
a bad change can be reverted."""
try:
with SessionLocal() as db:
if project_id:
row = db.get(Project, uuid.UUID(project_id))
if row and row.user_id == user_id:
snapshot_version(row, source, instruction)
row.files = files
db.commit()
db.refresh(row)
return str(row.id)
# ponytail: if not found / user mismatch, fall through to create
row = Project(
session_id="",
user_id=user_id,
title=f"Enhanced: {instruction[:60]}",
brief=instruction,
files=files,
)
db.add(row)
db.commit()
db.refresh(row)
return str(row.id)
except Exception as exc: # noqa: BLE001
logger.error("DB upsert failed: %s", exc)
# ponytail: return a stable placeholder so the FE still gets files
return project_id or str(uuid.uuid4())