gmal-scope-builder/backend/app/api/efficiency.py
DJP 82046c784c P1: Role-specific efficiency profiles + BTG tool efficiencies
Backend:
- New models: MatchFeedback, EfficiencyProfile, EfficiencyRate, ToolEfficiency, ToolEfficiencyRate
- 3 preset profiles seeded: Conservative, Moderate, Aggressive with per-discipline rates
- 6 BTG tools seeded: Pencil, OMG, Creative X, Cortex, Semblance, Share of Model
- Efficiency API: CRUD for profiles and tools at /api/efficiency/
- team_shape.py: accepts profile_rates + tool_rates (per-discipline, additive, capped at 90%)
- team-shape endpoint: accepts profile_id and tool_ids query params
- Programme roles always exempt regardless of method

Example: Moderate profile + Creative X + Pencil → Account Mgmt 10%, Creative 70%, Production 65%

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

176 lines
6.6 KiB
Python

"""Efficiency profiles and tool efficiency endpoints."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.feedback import EfficiencyProfile, EfficiencyRate, ToolEfficiency, ToolEfficiencyRate
router = APIRouter()
# Preset profiles seeded on first access
PRESET_PROFILES = {
"Conservative": {
"Account Management": 5, "Delivery": 10, "Strategy": 5,
"Creative": 15, "Editorial": 20, "Production": 25,
"Web Design": 20, "Data": 10, "UX": 10,
"Tech and Web Dev": 15, "QA": 20,
"Social & Community Management": 15, "Third Party Fees": 0,
},
"Moderate": {
"Account Management": 10, "Delivery": 20, "Strategy": 15,
"Creative": 35, "Editorial": 40, "Production": 50,
"Web Design": 40, "Data": 25, "UX": 25,
"Tech and Web Dev": 35, "QA": 45,
"Social & Community Management": 30, "Third Party Fees": 0,
},
"Aggressive": {
"Account Management": 15, "Delivery": 30, "Strategy": 25,
"Creative": 60, "Editorial": 70, "Production": 80,
"Web Design": 65, "Data": 40, "UX": 45,
"Tech and Web Dev": 60, "QA": 75,
"Social & Community Management": 50, "Third Party Fees": 0,
},
}
PRESET_TOOLS = {
"Pencil": {
"description": "AI-powered ad creative generation and concepting",
"rates": {"Creative": 15, "Production": 10, "Delivery": 5},
},
"OMG": {
"description": "Media optimization and campaign management platform",
"rates": {"Delivery": 10, "Data": 15, "Strategy": 5},
},
"Creative X": {
"description": "Scalable asset generation and adaptation platform",
"rates": {"Creative": 20, "Production": 15, "Editorial": 10},
},
"Cortex": {
"description": "AI model orchestration and experimentation",
"rates": {"Data": 20, "Strategy": 10, "Creative": 5, "Production": 5},
},
"Semblance": {
"description": "Audience analysis and model insights",
"rates": {"Data": 15, "Strategy": 10},
},
"Share of Model": {
"description": "Media modeling and optimization engine",
"rates": {"Data": 15, "Strategy": 10, "Delivery": 5},
},
}
async def _ensure_presets(db: AsyncSession):
"""Seed preset profiles and tools if they don't exist."""
existing = await db.execute(select(EfficiencyProfile))
if existing.scalars().first():
return
for name, rates in PRESET_PROFILES.items():
profile = EfficiencyProfile(name=name, is_default=(name == "Moderate"))
db.add(profile)
await db.flush()
for discipline, pct in rates.items():
db.add(EfficiencyRate(profile_id=profile.id, discipline=discipline, efficiency_pct=pct))
for tool_name, tool_data in PRESET_TOOLS.items():
tool = ToolEfficiency(tool_name=tool_name, tool_description=tool_data["description"])
db.add(tool)
await db.flush()
for discipline, pct in tool_data["rates"].items():
db.add(ToolEfficiencyRate(tool_id=tool.id, discipline=discipline, additional_efficiency_pct=pct))
await db.commit()
@router.get("/profiles")
async def list_profiles(db: AsyncSession = Depends(get_db)):
await _ensure_presets(db)
result = await db.execute(select(EfficiencyProfile).order_by(EfficiencyProfile.id))
profiles = result.scalars().all()
out = []
for p in profiles:
rates_result = await db.execute(
select(EfficiencyRate).where(EfficiencyRate.profile_id == p.id)
)
rates = {r.discipline: float(r.efficiency_pct) for r in rates_result.scalars().all()}
out.append({
"id": p.id,
"name": p.name,
"is_default": p.is_default,
"rates": rates,
})
return out
@router.get("/profiles/{profile_id}")
async def get_profile(profile_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(EfficiencyProfile).where(EfficiencyProfile.id == profile_id))
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
rates_result = await db.execute(
select(EfficiencyRate).where(EfficiencyRate.profile_id == profile.id)
)
rates = {r.discipline: float(r.efficiency_pct) for r in rates_result.scalars().all()}
return {"id": profile.id, "name": profile.name, "is_default": profile.is_default, "rates": rates}
@router.put("/profiles/{profile_id}")
async def update_profile(profile_id: int, data: dict, db: AsyncSession = Depends(get_db)):
"""Update rates for a profile. Body: {"rates": {"Creative": 40, "Production": 55, ...}}"""
result = await db.execute(select(EfficiencyProfile).where(EfficiencyProfile.id == profile_id))
profile = result.scalar_one_or_none()
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
if "rates" in data:
# Delete existing rates and re-create
existing = await db.execute(select(EfficiencyRate).where(EfficiencyRate.profile_id == profile.id))
for r in existing.scalars().all():
await db.delete(r)
for discipline, pct in data["rates"].items():
db.add(EfficiencyRate(profile_id=profile.id, discipline=discipline, efficiency_pct=pct))
if "name" in data:
profile.name = data["name"]
await db.commit()
return await get_profile(profile_id, db)
@router.post("/profiles")
async def create_profile(data: dict, db: AsyncSession = Depends(get_db)):
"""Create a custom profile. Body: {"name": "My Profile", "rates": {...}}"""
profile = EfficiencyProfile(name=data["name"])
db.add(profile)
await db.flush()
for discipline, pct in data.get("rates", {}).items():
db.add(EfficiencyRate(profile_id=profile.id, discipline=discipline, efficiency_pct=pct))
await db.commit()
return await get_profile(profile.id, db)
@router.get("/tools")
async def list_tools(db: AsyncSession = Depends(get_db)):
await _ensure_presets(db)
result = await db.execute(select(ToolEfficiency).order_by(ToolEfficiency.tool_name))
tools = result.scalars().all()
out = []
for t in tools:
rates_result = await db.execute(
select(ToolEfficiencyRate).where(ToolEfficiencyRate.tool_id == t.id)
)
rates = {r.discipline: float(r.additional_efficiency_pct) for r in rates_result.scalars().all()}
out.append({
"id": t.id,
"tool_name": t.tool_name,
"tool_description": t.tool_description,
"rates": rates,
})
return out