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>
176 lines
6.6 KiB
Python
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
|