Files
claude-skills-reference/business-growth/sales-engineer/scripts/rfp_response_analyzer.py
Alireza Rezvani eef020c9e0 feat(skills): add 5 new skills via factory methodology (#176)
Build campaign-analytics, financial-analyst, customer-success-manager,
sales-engineer, and revenue-operations skills using the Claude Skills
Factory workflow. Each skill includes SKILL.md, Python CLI tools,
reference guides, and asset templates. All 16 Python scripts use
standard library only with --format json/text support.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 23:51:58 +01:00

558 lines
19 KiB
Python

#!/usr/bin/env python3
"""RFP/RFI Response Analyzer - Score coverage, identify gaps, and recommend bid/no-bid.
Parses RFP/RFI requirements and scores coverage using Full/Partial/Planned/Gap
categories. Generates weighted coverage scores, gap analysis with mitigation
strategies, effort estimation, and bid/no-bid recommendations.
Usage:
python rfp_response_analyzer.py rfp_data.json
python rfp_response_analyzer.py rfp_data.json --format json
python rfp_response_analyzer.py rfp_data.json --format text
"""
import argparse
import json
import sys
from typing import Any
# Coverage status to score mapping
COVERAGE_SCORES: dict[str, float] = {
"full": 1.0,
"partial": 0.5,
"planned": 0.25,
"gap": 0.0,
}
# Priority to weight mapping
PRIORITY_WEIGHTS: dict[str, float] = {
"must-have": 3.0,
"should-have": 2.0,
"nice-to-have": 1.0,
}
# Bid thresholds
BID_THRESHOLD = 0.70
CONDITIONAL_THRESHOLD = 0.50
MAX_MUST_HAVE_GAPS_FOR_BID = 3
def safe_divide(numerator: float, denominator: float, default: float = 0.0) -> float:
"""Safely divide two numbers, returning default if denominator is zero."""
if denominator == 0:
return default
return numerator / denominator
def load_rfp_data(filepath: str) -> dict[str, Any]:
"""Load and validate RFP data from a JSON file.
Args:
filepath: Path to the JSON file containing RFP data.
Returns:
Parsed RFP data dictionary.
Raises:
SystemExit: If the file cannot be read or parsed.
"""
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {filepath}: {e}", file=sys.stderr)
sys.exit(1)
if "requirements" not in data:
print("Error: JSON must contain a 'requirements' array.", file=sys.stderr)
sys.exit(1)
return data
def analyze_requirement(req: dict[str, Any]) -> dict[str, Any]:
"""Analyze a single requirement and compute its score.
Args:
req: Requirement dictionary with category, priority, coverage_status, etc.
Returns:
Enriched requirement with computed score and weight.
"""
coverage_status = req.get("coverage_status", "gap").lower()
priority = req.get("priority", "nice-to-have").lower()
coverage_score = COVERAGE_SCORES.get(coverage_status, 0.0)
weight = PRIORITY_WEIGHTS.get(priority, 1.0)
weighted_score = coverage_score * weight
max_weighted = weight
effort_hours = req.get("effort_hours", 0)
result = {
"id": req.get("id", "unknown"),
"requirement": req.get("requirement", "Unnamed requirement"),
"category": req.get("category", "Uncategorized"),
"priority": priority,
"coverage_status": coverage_status,
"coverage_score": coverage_score,
"weight": weight,
"weighted_score": weighted_score,
"max_weighted": max_weighted,
"effort_hours": effort_hours,
"notes": req.get("notes", ""),
"mitigation": req.get("mitigation", ""),
}
return result
def generate_gap_analysis(analyzed_reqs: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Generate gap analysis for requirements not fully covered.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
Returns:
List of gap entries with mitigation strategies.
"""
gaps = []
for req in analyzed_reqs:
if req["coverage_status"] in ("gap", "partial", "planned"):
severity = "critical" if req["priority"] == "must-have" else (
"high" if req["priority"] == "should-have" else "low"
)
mitigation = req["mitigation"]
if not mitigation:
if req["coverage_status"] == "partial":
mitigation = "Enhance existing capability to achieve full coverage"
elif req["coverage_status"] == "planned":
mitigation = "Communicate roadmap timeline and interim workaround"
else:
mitigation = "Evaluate build vs. partner vs. no-bid for this requirement"
gaps.append({
"id": req["id"],
"requirement": req["requirement"],
"category": req["category"],
"priority": req["priority"],
"coverage_status": req["coverage_status"],
"severity": severity,
"effort_hours": req["effort_hours"],
"mitigation": mitigation,
})
# Sort by severity: critical > high > low
severity_order = {"critical": 0, "high": 1, "low": 2}
gaps.sort(key=lambda g: severity_order.get(g["severity"], 3))
return gaps
def compute_category_scores(analyzed_reqs: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Compute coverage scores grouped by requirement category.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
Returns:
Dictionary of category names to score summaries.
"""
categories: dict[str, dict[str, float]] = {}
for req in analyzed_reqs:
cat = req["category"]
if cat not in categories:
categories[cat] = {
"weighted_score": 0.0,
"max_weighted": 0.0,
"count": 0,
"full_count": 0,
"partial_count": 0,
"planned_count": 0,
"gap_count": 0,
"effort_hours": 0,
}
categories[cat]["weighted_score"] += req["weighted_score"]
categories[cat]["max_weighted"] += req["max_weighted"]
categories[cat]["count"] += 1
categories[cat]["effort_hours"] += req["effort_hours"]
status_key = f"{req['coverage_status']}_count"
if status_key in categories[cat]:
categories[cat][status_key] += 1
result = {}
for cat, scores in categories.items():
coverage_pct = safe_divide(scores["weighted_score"], scores["max_weighted"]) * 100
result[cat] = {
"coverage_percentage": round(coverage_pct, 1),
"requirements_count": int(scores["count"]),
"full": int(scores["full_count"]),
"partial": int(scores["partial_count"]),
"planned": int(scores["planned_count"]),
"gap": int(scores["gap_count"]),
"effort_hours": int(scores["effort_hours"]),
}
return result
def determine_bid_recommendation(
overall_coverage: float,
must_have_gaps: int,
strategic_value: str,
) -> dict[str, Any]:
"""Determine bid/no-bid recommendation based on coverage and gaps.
Args:
overall_coverage: Overall weighted coverage percentage (0-100).
must_have_gaps: Number of must-have requirements with gap status.
strategic_value: Strategic value assessment (high, medium, low).
Returns:
Recommendation dictionary with decision and rationale.
"""
coverage_ratio = overall_coverage / 100.0
reasons = []
# Primary decision logic
if coverage_ratio >= BID_THRESHOLD and must_have_gaps <= MAX_MUST_HAVE_GAPS_FOR_BID:
decision = "BID"
reasons.append(f"Coverage score {overall_coverage:.1f}% exceeds {BID_THRESHOLD*100:.0f}% threshold")
if must_have_gaps > 0:
reasons.append(f"{must_have_gaps} must-have gap(s) within acceptable range (max {MAX_MUST_HAVE_GAPS_FOR_BID})")
elif coverage_ratio >= CONDITIONAL_THRESHOLD or (
must_have_gaps <= MAX_MUST_HAVE_GAPS_FOR_BID and coverage_ratio >= 0.4
):
decision = "CONDITIONAL BID"
reasons.append(f"Coverage score {overall_coverage:.1f}% in conditional range ({CONDITIONAL_THRESHOLD*100:.0f}%-{BID_THRESHOLD*100:.0f}%)")
if must_have_gaps > 0:
reasons.append(f"{must_have_gaps} must-have gap(s) require mitigation plan")
else:
decision = "NO-BID"
if coverage_ratio < CONDITIONAL_THRESHOLD:
reasons.append(f"Coverage score {overall_coverage:.1f}% below {CONDITIONAL_THRESHOLD*100:.0f}% minimum")
if must_have_gaps > MAX_MUST_HAVE_GAPS_FOR_BID:
reasons.append(f"{must_have_gaps} must-have gaps exceed maximum of {MAX_MUST_HAVE_GAPS_FOR_BID}")
# Strategic value adjustment
if strategic_value.lower() == "high" and decision == "CONDITIONAL BID":
reasons.append("High strategic value supports pursuing despite coverage gaps")
elif strategic_value.lower() == "low" and decision == "CONDITIONAL BID":
decision = "NO-BID"
reasons.append("Low strategic value does not justify investment for conditional coverage")
confidence = "high" if coverage_ratio >= 0.80 else (
"medium" if coverage_ratio >= 0.60 else "low"
)
return {
"decision": decision,
"confidence": confidence,
"overall_coverage_percentage": round(overall_coverage, 1),
"must_have_gaps": must_have_gaps,
"strategic_value": strategic_value,
"reasons": reasons,
}
def generate_risk_assessment(
analyzed_reqs: list[dict[str, Any]],
gaps: list[dict[str, Any]],
) -> list[dict[str, str]]:
"""Generate risk assessment based on gaps and coverage patterns.
Args:
analyzed_reqs: List of analyzed requirement dictionaries.
gaps: List of gap analysis entries.
Returns:
List of risk entries with impact and mitigation.
"""
risks = []
critical_gaps = [g for g in gaps if g["severity"] == "critical"]
if critical_gaps:
risks.append({
"risk": "Critical requirement gaps",
"impact": "high",
"description": f"{len(critical_gaps)} must-have requirements not fully met",
"mitigation": "Prioritize engineering effort or partner integration for gap closure",
})
total_effort = sum(r["effort_hours"] for r in analyzed_reqs if r["coverage_status"] != "full")
if total_effort > 200:
risks.append({
"risk": "High customization effort",
"impact": "high",
"description": f"{total_effort} hours estimated for non-full requirements",
"mitigation": "Evaluate resource availability and timeline feasibility before committing",
})
elif total_effort > 80:
risks.append({
"risk": "Moderate customization effort",
"impact": "medium",
"description": f"{total_effort} hours estimated for non-full requirements",
"mitigation": "Phase implementation and set clear expectations on delivery timeline",
})
planned_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "planned")
if planned_count > 3:
risks.append({
"risk": "Roadmap dependency",
"impact": "medium",
"description": f"{planned_count} requirements depend on planned product features",
"mitigation": "Confirm roadmap timelines with product team; include contractual commitments if needed",
})
partial_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "partial")
if partial_count > 5:
risks.append({
"risk": "Workaround complexity",
"impact": "medium",
"description": f"{partial_count} requirements need workarounds or configuration",
"mitigation": "Document workarounds clearly; plan for native support in future releases",
})
if not risks:
risks.append({
"risk": "No significant risks identified",
"impact": "low",
"description": "Strong coverage across all requirement categories",
"mitigation": "Maintain standard engagement process",
})
return risks
def analyze_rfp(data: dict[str, Any]) -> dict[str, Any]:
"""Run the complete RFP analysis pipeline.
Args:
data: Parsed RFP data with requirements array.
Returns:
Complete analysis results dictionary.
"""
rfp_info = {
"rfp_name": data.get("rfp_name", "Unnamed RFP"),
"customer": data.get("customer", "Unknown Customer"),
"due_date": data.get("due_date", "Not specified"),
"strategic_value": data.get("strategic_value", "medium"),
"deal_value": data.get("deal_value", "Not specified"),
}
# Analyze each requirement
analyzed_reqs = [analyze_requirement(req) for req in data["requirements"]]
# Compute overall scores
total_weighted = sum(r["weighted_score"] for r in analyzed_reqs)
total_max = sum(r["max_weighted"] for r in analyzed_reqs)
overall_coverage = safe_divide(total_weighted, total_max) * 100
# Coverage summary
total_count = len(analyzed_reqs)
full_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "full")
partial_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "partial")
planned_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "planned")
gap_count = sum(1 for r in analyzed_reqs if r["coverage_status"] == "gap")
# Must-have gap count
must_have_gaps = sum(
1 for r in analyzed_reqs
if r["priority"] == "must-have" and r["coverage_status"] == "gap"
)
# Category breakdown
category_scores = compute_category_scores(analyzed_reqs)
# Gap analysis
gaps = generate_gap_analysis(analyzed_reqs)
# Bid recommendation
bid_recommendation = determine_bid_recommendation(
overall_coverage,
must_have_gaps,
rfp_info["strategic_value"],
)
# Risk assessment
risks = generate_risk_assessment(analyzed_reqs, gaps)
# Effort summary
total_effort = sum(r["effort_hours"] for r in analyzed_reqs)
gap_effort = sum(r["effort_hours"] for r in analyzed_reqs if r["coverage_status"] != "full")
return {
"rfp_info": rfp_info,
"coverage_summary": {
"overall_coverage_percentage": round(overall_coverage, 1),
"total_requirements": total_count,
"full": full_count,
"partial": partial_count,
"planned": planned_count,
"gap": gap_count,
"must_have_gaps": must_have_gaps,
},
"category_scores": category_scores,
"bid_recommendation": bid_recommendation,
"gap_analysis": gaps,
"risk_assessment": risks,
"effort_estimate": {
"total_hours": total_effort,
"gap_closure_hours": gap_effort,
"full_coverage_hours": total_effort - gap_effort,
},
"requirements_detail": analyzed_reqs,
}
def format_text(result: dict[str, Any]) -> str:
"""Format analysis results as human-readable text.
Args:
result: Complete analysis results dictionary.
Returns:
Formatted text string.
"""
lines = []
info = result["rfp_info"]
lines.append("=" * 70)
lines.append("RFP RESPONSE ANALYSIS")
lines.append("=" * 70)
lines.append(f"RFP: {info['rfp_name']}")
lines.append(f"Customer: {info['customer']}")
lines.append(f"Due Date: {info['due_date']}")
lines.append(f"Deal Value: {info['deal_value']}")
lines.append(f"Strategic Value: {info['strategic_value'].upper()}")
lines.append("")
# Coverage summary
cs = result["coverage_summary"]
lines.append("-" * 70)
lines.append("COVERAGE SUMMARY")
lines.append("-" * 70)
lines.append(f"Overall Coverage: {cs['overall_coverage_percentage']}%")
lines.append(f"Total Requirements: {cs['total_requirements']}")
lines.append(f" Full: {cs['full']} | Partial: {cs['partial']} | Planned: {cs['planned']} | Gap: {cs['gap']}")
lines.append(f"Must-Have Gaps: {cs['must_have_gaps']}")
lines.append("")
# Bid recommendation
bid = result["bid_recommendation"]
lines.append("-" * 70)
lines.append(f"BID RECOMMENDATION: {bid['decision']}")
lines.append(f"Confidence: {bid['confidence'].upper()}")
lines.append("-" * 70)
for reason in bid["reasons"]:
lines.append(f" - {reason}")
lines.append("")
# Category scores
lines.append("-" * 70)
lines.append("CATEGORY BREAKDOWN")
lines.append("-" * 70)
lines.append(f"{'Category':<25} {'Coverage':>8} {'Full':>5} {'Part':>5} {'Plan':>5} {'Gap':>5} {'Effort':>7}")
lines.append("-" * 70)
for cat, scores in result["category_scores"].items():
lines.append(
f"{cat:<25} {scores['coverage_percentage']:>7.1f}% "
f"{scores['full']:>5} {scores['partial']:>5} "
f"{scores['planned']:>5} {scores['gap']:>5} "
f"{scores['effort_hours']:>6}h"
)
lines.append("")
# Gap analysis
gaps = result["gap_analysis"]
if gaps:
lines.append("-" * 70)
lines.append("GAP ANALYSIS")
lines.append("-" * 70)
for gap in gaps:
severity_marker = "!!!" if gap["severity"] == "critical" else (
"!!" if gap["severity"] == "high" else "!"
)
lines.append(f" [{severity_marker}] {gap['id']}: {gap['requirement']}")
lines.append(f" Category: {gap['category']} | Priority: {gap['priority']} | Status: {gap['coverage_status']}")
lines.append(f" Effort: {gap['effort_hours']}h | Mitigation: {gap['mitigation']}")
lines.append("")
# Risk assessment
risks = result["risk_assessment"]
lines.append("-" * 70)
lines.append("RISK ASSESSMENT")
lines.append("-" * 70)
for risk in risks:
lines.append(f" [{risk['impact'].upper()}] {risk['risk']}")
lines.append(f" {risk['description']}")
lines.append(f" Mitigation: {risk['mitigation']}")
lines.append("")
# Effort estimate
effort = result["effort_estimate"]
lines.append("-" * 70)
lines.append("EFFORT ESTIMATE")
lines.append("-" * 70)
lines.append(f" Total Effort: {effort['total_hours']} hours")
lines.append(f" Gap Closure Effort: {effort['gap_closure_hours']} hours")
lines.append(f" Supported Effort: {effort['full_coverage_hours']} hours")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
def main() -> None:
"""Main entry point for the RFP Response Analyzer."""
parser = argparse.ArgumentParser(
description="Analyze RFP/RFI requirements for coverage, gaps, and bid recommendation.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Coverage Categories:\n"
" Full (100%) - Requirement fully met\n"
" Partial (50%) - Partially met, workaround needed\n"
" Planned (25%) - On roadmap, not yet available\n"
" Gap (0%) - Not supported\n"
"\n"
"Priority Weights:\n"
" Must-Have (3x) | Should-Have (2x) | Nice-to-Have (1x)\n"
"\n"
"Example:\n"
" python rfp_response_analyzer.py rfp_data.json --format json\n"
),
)
parser.add_argument(
"input_file",
help="Path to JSON file containing RFP requirements data",
)
parser.add_argument(
"--format",
choices=["json", "text"],
default="text",
dest="output_format",
help="Output format: json or text (default: text)",
)
args = parser.parse_args()
data = load_rfp_data(args.input_file)
result = analyze_rfp(data)
if args.output_format == "json":
print(json.dumps(result, indent=2))
else:
print(format_text(result))
if __name__ == "__main__":
main()