- 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>
152 lines
5.3 KiB
Python
152 lines
5.3 KiB
Python
"""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)
|