gmal-scope-builder/backend/app/models/project.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

108 lines
4.8 KiB
Python

import enum
from datetime import datetime
from sqlalchemy import String, Text, Integer, Numeric, Boolean, DateTime, Enum, ForeignKey, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
from app.models.gmal import ModelType
class ProjectStatus(str, enum.Enum):
DRAFT = "draft"
PARSING = "parsing"
MATCHING = "matching"
REVIEW = "review"
BUILDING = "building"
FINALIZED = "finalized"
class MatchConfidence(str, enum.Enum):
EXACT = "exact"
CLOSE = "close"
MULTIPLE = "multiple"
NONE = "none"
class Project(Base):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
client_name: Mapped[str | None] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(Text)
model_type: Mapped[ModelType] = mapped_column(Enum(ModelType), default=ModelType.CURRENT_OPLUS)
status: Mapped[ProjectStatus] = mapped_column(Enum(ProjectStatus), default=ProjectStatus.DRAFT)
source_filename: Mapped[str | None] = mapped_column(String(255))
parse_stage: Mapped[str | None] = mapped_column(String(255))
brief_analysis: Mapped[str | None] = mapped_column(Text)
ai_input_tokens: Mapped[int] = mapped_column(Integer, default=0)
ai_output_tokens: Mapped[int] = mapped_column(Integer, default=0)
ai_cost_usd: Mapped[float] = mapped_column(Numeric(10, 6), default=0)
ai_call_count: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
client_assets: Mapped[list["ClientAsset"]] = relationship(back_populates="project", cascade="all, delete-orphan")
ratecard_lines: Mapped[list["RatecardLine"]] = relationship(back_populates="project", cascade="all, delete-orphan")
class ClientAsset(Base):
__tablename__ = "client_assets"
id: Mapped[int] = mapped_column(primary_key=True)
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
raw_name: Mapped[str | None] = mapped_column(String(500))
raw_description: Mapped[str | None] = mapped_column(Text)
volume: Mapped[int] = mapped_column(Integer, default=1)
sort_order: Mapped[int | None] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
project: Mapped["Project"] = relationship(back_populates="client_assets")
matches: Mapped[list["Match"]] = relationship(back_populates="client_asset", cascade="all, delete-orphan")
class Match(Base):
__tablename__ = "matches"
id: Mapped[int] = mapped_column(primary_key=True)
client_asset_id: Mapped[int] = mapped_column(ForeignKey("client_assets.id", ondelete="CASCADE"), nullable=False)
gmal_asset_id: Mapped[int] = mapped_column(ForeignKey("gmal_assets.id"), nullable=False)
confidence: Mapped[MatchConfidence] = mapped_column(Enum(MatchConfidence), nullable=False)
confidence_score: Mapped[float | None] = mapped_column(Numeric(3, 2))
ai_reasoning: Mapped[str | None] = mapped_column(Text)
caveat_text: Mapped[str | None] = mapped_column(Text)
is_selected: Mapped[bool] = mapped_column(Boolean, default=False)
rank: Mapped[int] = mapped_column(Integer, default=1)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
client_asset: Mapped["ClientAsset"] = relationship(back_populates="matches")
gmal_asset: Mapped["GmalAsset"] = relationship()
__table_args__ = (
Index("idx_matches_client_asset", "client_asset_id"),
)
class RatecardLine(Base):
__tablename__ = "ratecard_lines"
id: Mapped[int] = mapped_column(primary_key=True)
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False)
client_asset_id: Mapped[int] = mapped_column(ForeignKey("client_assets.id"), nullable=False)
gmal_asset_id: Mapped[int] = mapped_column(ForeignKey("gmal_assets.id"), nullable=False)
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False)
base_hours: Mapped[float | None] = mapped_column(Numeric(10, 2))
volume: Mapped[int] = mapped_column(Integer, default=1)
total_hours: Mapped[float | None] = mapped_column(Numeric(10, 2))
manual_override: Mapped[float | None] = mapped_column(Numeric(10, 2))
notes: Mapped[str | None] = mapped_column(Text)
project: Mapped["Project"] = relationship(back_populates="ratecard_lines")
client_asset: Mapped["ClientAsset"] = relationship()
gmal_asset: Mapped["GmalAsset"] = relationship()
role: Mapped["Role"] = relationship()
__table_args__ = (
Index("idx_ratecard_project", "project_id"),
)