feat: add Script Builder service and API endpoints
- Script Builder service with language-specific system prompts (PowerShell, Bash, Python) - AI-powered script generation with code block extraction and filename detection - Context window management (last 20 messages) and session message limits - REST API: CRUD sessions, send messages, save to Script Library - Rate limiting on message endpoint (10/min), max 5 concurrent sessions per user - Registered script_build action in AI model tier routing (standard tier) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
151
backend/app/api/endpoints/script_builder.py
Normal file
151
backend/app/api/endpoints/script_builder.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Script Builder API endpoints."""
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.rate_limit import limiter
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.models.user import User
|
||||
from app.schemas.script_builder import (
|
||||
ScriptBuilderCreateRequest,
|
||||
ScriptBuilderMessageRequest,
|
||||
ScriptBuilderMessageResponse,
|
||||
ScriptBuilderSessionDetail,
|
||||
ScriptBuilderSessionSummary,
|
||||
SaveToLibraryRequest,
|
||||
)
|
||||
from app.schemas.script_template import ScriptTemplateDetail
|
||||
from app.services import script_builder_service
|
||||
|
||||
router = APIRouter(prefix="/scripts/builder", tags=["script-builder"])
|
||||
|
||||
MAX_SESSIONS_PER_USER = 5
|
||||
|
||||
|
||||
@router.post("/sessions", response_model=ScriptBuilderSessionDetail, status_code=201)
|
||||
async def create_session(
|
||||
data: ScriptBuilderCreateRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> ScriptBuilderSessionDetail:
|
||||
"""Start a new Script Builder session."""
|
||||
# Enforce max concurrent sessions
|
||||
count = await script_builder_service.count_user_sessions(db, current_user.id)
|
||||
if count >= MAX_SESSIONS_PER_USER:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Maximum of {MAX_SESSIONS_PER_USER} builder sessions allowed. Delete an old session first.",
|
||||
)
|
||||
|
||||
session = await script_builder_service.create_session(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
team_id=current_user.team_id,
|
||||
language=data.language,
|
||||
)
|
||||
await db.commit()
|
||||
return ScriptBuilderSessionDetail.model_validate(session)
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[ScriptBuilderSessionSummary])
|
||||
async def list_sessions(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> list[ScriptBuilderSessionSummary]:
|
||||
"""List user's recent builder sessions (lightweight, no messages)."""
|
||||
sessions = await script_builder_service.list_sessions(
|
||||
db=db, user_id=current_user.id, limit=limit, offset=offset
|
||||
)
|
||||
return [ScriptBuilderSessionSummary.model_validate(s) for s in sessions]
|
||||
|
||||
|
||||
@router.get("/sessions/{session_id}", response_model=ScriptBuilderSessionDetail)
|
||||
async def get_session(
|
||||
session_id: UUID,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> ScriptBuilderSessionDetail:
|
||||
"""Get full session detail with message history."""
|
||||
session = await script_builder_service.get_session(db, session_id, current_user.id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return ScriptBuilderSessionDetail.model_validate(session)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/sessions/{session_id}/messages",
|
||||
response_model=ScriptBuilderMessageResponse,
|
||||
)
|
||||
@limiter.limit("10/minute")
|
||||
async def send_message(
|
||||
request: Request,
|
||||
session_id: UUID,
|
||||
data: ScriptBuilderMessageRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> ScriptBuilderMessageResponse:
|
||||
"""Send a message and get AI-generated script response."""
|
||||
session = await script_builder_service.get_session(db, session_id, current_user.id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
try:
|
||||
response = await script_builder_service.send_message(db, session, data.content)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
await db.commit()
|
||||
return response
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}", status_code=204)
|
||||
async def delete_session(
|
||||
session_id: UUID,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> None:
|
||||
"""Delete a builder session."""
|
||||
deleted = await script_builder_service.delete_session(db, session_id, current_user.id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/sessions/{session_id}/save",
|
||||
response_model=ScriptTemplateDetail,
|
||||
status_code=201,
|
||||
)
|
||||
async def save_to_library(
|
||||
session_id: UUID,
|
||||
data: SaveToLibraryRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> ScriptTemplateDetail:
|
||||
"""Save the latest generated script to the Script Library."""
|
||||
session = await script_builder_service.get_session(db, session_id, current_user.id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
try:
|
||||
template = await script_builder_service.save_to_library(
|
||||
db=db,
|
||||
session=session,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
category_id=data.category_id,
|
||||
share_with_team=data.share_with_team,
|
||||
user_id=current_user.id,
|
||||
team_id=current_user.team_id,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(template)
|
||||
return ScriptTemplateDetail.model_validate(template)
|
||||
Reference in New Issue
Block a user