gmal-scope-builder/backend/app/api/projects.py
DJP 26d3435be0 Improve matching, upload UX, collapse fix, full catalog approach
- Upload now shows live stage progress (uploading -> extracting -> AI parsing -> done)
- Fix match group collapse: proper React state instead of DOM manipulation
- Replace pre-filter with full GMAL catalog sent to Claude (~3k tokens, <$0.01)
  - FTS and keyword matching missed too many semantic matches
  - Claude now sees all 243 assets and uses semantic understanding
- Improved system prompt with terminology bridges for better scoring
- Per-project AI cost tracking persisted to DB
- Parallel matching with cancel support
- Auto-select matches >= 80%, YOLO button for rest
- Debug panel for AI call inspection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:22:08 -04:00

108 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,
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,
)