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>
135 lines
5.4 KiB
Python
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
|