gmal-scope-builder/backend/app/api/projects.py
DJP bc778ce7af P2: Iterative prompting + RFP brief analysis engine
Iterative Prompting:
- Chat box on Match Review tab for natural language refinement
- "re-run under 70%" / "ignore zero volume" / "set all volumes to 1"
- Claude interprets instruction into structured actions
- Actions: rematch_below_threshold, rematch_specific, delete_assets, set_volume
- Re-matches affected assets automatically after refinement
- Chat log shows instruction history

RFP/Brief Analysis:
- New "Brief Analysis" tab between Upload and Match Review
- Extracts: summary, objectives, KPIs, channels, audiences, deliverable categories,
  constraints, timeline, budget, complexity assessment
- Generates prioritized discovery questions (Red/Amber/Green)
- Questions include category, rationale, and priority level
- Stored as JSON in project.brief_analysis field
- Uploaded files now saved to data dir for re-analysis
- Re-analyze button to refresh analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:15:31 -04:00

109 lines
3.7 KiB
Python

"""Project CRUD endpoints."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.project import Project, ClientAsset, ProjectStatus
from app.models.gmal import ModelType
from app.schemas.project import ProjectCreate, ProjectUpdate, ProjectOut
router = APIRouter()
@router.post("", response_model=ProjectOut)
async def create_project(data: ProjectCreate, db: AsyncSession = Depends(get_db)):
project = Project(
name=data.name,
client_name=data.client_name,
description=data.description,
model_type=ModelType(data.model_type),
)
db.add(project)
await db.commit()
await db.refresh(project)
return _project_out(project, 0)
@router.get("", response_model=list[ProjectOut])
async def list_projects(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Project).order_by(Project.created_at.desc()))
projects = result.scalars().all()
out = []
for p in projects:
count_result = await db.execute(
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == p.id)
)
count = count_result.scalar() or 0
out.append(_project_out(p, count))
return out
@router.get("/{project_id}", response_model=ProjectOut)
async def get_project(project_id: int, db: AsyncSession = Depends(get_db)):
project = await _get_project(project_id, db)
count_result = await db.execute(
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == project.id)
)
return _project_out(project, count_result.scalar() or 0)
@router.put("/{project_id}", response_model=ProjectOut)
async def update_project(project_id: int, data: ProjectUpdate, db: AsyncSession = Depends(get_db)):
project = await _get_project(project_id, db)
if data.name is not None:
project.name = data.name
if data.client_name is not None:
project.client_name = data.client_name
if data.description is not None:
project.description = data.description
if data.model_type is not None:
project.model_type = ModelType(data.model_type)
await db.commit()
await db.refresh(project)
count_result = await db.execute(
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == project.id)
)
return _project_out(project, count_result.scalar() or 0)
@router.delete("/{project_id}")
async def delete_project(project_id: int, db: AsyncSession = Depends(get_db)):
project = await _get_project(project_id, db)
await db.delete(project)
await db.commit()
return {"detail": "Project deleted"}
async def _get_project(project_id: int, db: AsyncSession) -> Project:
result = await db.execute(select(Project).where(Project.id == project_id))
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
def _project_out(project: Project, asset_count: int) -> ProjectOut:
return ProjectOut(
id=project.id,
name=project.name,
client_name=project.client_name,
description=project.description,
model_type=project.model_type.value,
status=project.status.value,
source_filename=project.source_filename,
parse_stage=project.parse_stage,
has_brief_analysis=bool(project.brief_analysis),
ai_input_tokens=project.ai_input_tokens or 0,
ai_output_tokens=project.ai_output_tokens or 0,
ai_cost_usd=float(project.ai_cost_usd or 0),
ai_call_count=project.ai_call_count or 0,
created_at=project.created_at,
updated_at=project.updated_at,
asset_count=asset_count,
)