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>
109 lines
3.7 KiB
Python
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,
|
|
)
|