gmal-scope-builder/backend/app/services/rfp_analysis.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

135 lines
5.4 KiB
Python

"""RFP/Brief analysis engine - extract structured requirements from client documents."""
import logging
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.project import Project
from app.utils.claude_client import call_claude, extract_tool_result
logger = logging.getLogger(__name__)
ANALYSIS_TOOL = {
"name": "save_analysis",
"description": "Save the structured RFP/brief analysis.",
"input_schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "2-3 sentence executive summary of the brief"
},
"objectives": {
"type": "array",
"items": {"type": "string"},
"description": "Key business objectives and goals"
},
"kpis": {
"type": "array",
"items": {"type": "string"},
"description": "KPIs and success metrics mentioned or implied"
},
"channels": {
"type": "array",
"items": {"type": "string"},
"description": "Marketing/media channels in scope (social, web, print, video, OOH, etc.)"
},
"audiences": {
"type": "array",
"items": {"type": "string"},
"description": "Target audiences or customer segments"
},
"deliverable_categories": {
"type": "array",
"items": {"type": "string"},
"description": "High-level categories of deliverables (e.g. 'Toolbox assets', 'Paid media', 'eCommerce')"
},
"constraints": {
"type": "array",
"items": {"type": "string"},
"description": "Constraints, requirements, or restrictions (timeline, budget, legal, brand, tech)"
},
"timeline": {
"type": "string",
"description": "Timeline or deadline information if mentioned"
},
"budget_band": {
"type": "string",
"description": "Budget range or band if mentioned"
},
"missing_info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"priority": {"type": "string", "enum": ["red", "amber", "green"]},
"category": {"type": "string"},
"question": {"type": "string"},
"rationale": {"type": "string"},
},
"required": ["priority", "category", "question", "rationale"],
},
"description": "Discovery questions for missing information, prioritized Red/Amber/Green"
},
"complexity_assessment": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Overall scope complexity"
},
"notes": {
"type": "string",
"description": "Any other observations or flags for the scoping team"
},
},
"required": ["summary", "objectives", "channels", "deliverable_categories", "missing_info", "complexity_assessment"],
},
}
SYSTEM_PROMPT = """You are an expert creative agency strategist analyzing a client RFP or brief document.
Your job is to extract structured requirements that will help the production team scope the work accurately.
Be thorough but practical. Focus on what the scoping team needs to know:
- What are they actually asking for?
- What channels and formats are involved?
- What's the volume and complexity?
- What information is MISSING that we'd need to scope accurately?
For missing_info, prioritize questions:
- RED (must-have): Without this, we cannot scope accurately (e.g. primary KPIs, data access, volumes)
- AMBER (important): Needed for accurate sizing (e.g. budget, timeline, audience segments)
- GREEN (nice-to-have): Would improve the scope but not a blocker (e.g. brand guidelines, vendor preferences)
Be specific in your questions - reference the actual document content."""
async def analyze_brief(db: AsyncSession, project: Project, document_text: str) -> dict:
"""Analyze a client brief/RFP and extract structured requirements.
Returns the analysis dict and saves it to the project.
"""
response = call_claude(
system=SYSTEM_PROMPT,
user_message=f"Analyze this client brief/RFP and extract structured requirements:\n\n{document_text}",
tools=[ANALYSIS_TOOL],
tool_choice={"type": "tool", "name": "save_analysis"},
max_tokens=8192,
)
usage = getattr(response, '_usage_info', {})
project.ai_input_tokens = (project.ai_input_tokens or 0) + usage.get("input_tokens", 0)
project.ai_output_tokens = (project.ai_output_tokens or 0) + usage.get("output_tokens", 0)
project.ai_cost_usd = float(project.ai_cost_usd or 0) + usage.get("cost_usd", 0)
project.ai_call_count = (project.ai_call_count or 0) + 1
result = extract_tool_result(response)
if not result:
return {"summary": "Analysis failed - no structured data returned", "missing_info": []}
# Store analysis as JSON in project description (or a new field)
import json
project.brief_analysis = json.dumps(result)
await db.commit()
return result