When a potential client asks for a proposal, most consultants spend 2-3 hours crafting one from scratch. I built a system that generates polished, personalized proposals in under 60 seconds using Python and the Claude API. Here's the complete technical breakdown.
Architecture Overview
The system has three components:
- Client intake parser — extracts requirements from emails/messages
- Proposal engine — generates customized proposals via Claude API
- PDF renderer — outputs professional documents with ReportLab
Setting Up the Environment
pip install anthropic reportlab jinja2 pydantic
The Data Model
Every proposal starts with structured client data:
from pydantic import BaseModel
from typing import Optional
from enum import Enum
class ProjectType(str, Enum):
AUTOMATION = "automation"
CHATBOT = "chatbot"
DATA_PIPELINE = "data_pipeline"
CONSULTING = "consulting"
CUSTOM_AI = "custom_ai"
class ClientIntake(BaseModel):
company_name: str
contact_name: str
industry: str
project_type: ProjectType
budget_range: Optional[str] = None
timeline: Optional[str] = None
pain_points: list[str]
current_tools: list[str] = []
requirements: str
class ProposalSection(BaseModel):
title: str
content: str
class Proposal(BaseModel):
client: ClientIntake
executive_summary: str
solution_overview: str
technical_approach: list[ProposalSection]
deliverables: list[str]
timeline_weeks: int
investment: str
roi_projection: str
The Proposal Generator
Here's the core engine that calls Claude to generate each section:
import anthropic
import json
from datetime import datetime
class ProposalGenerator:
def __init__(self, api_key: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = "claude-sonnet-4-20250514"
def _call_claude(self, system_prompt: str, user_prompt: str) -> str:
message = self.client.messages.create(
model=self.model,
max_tokens=2000,
system=system_prompt,
messages=[{"role": "user", "content": user_prompt}]
)
return message.content[0].text
def generate(self, intake: ClientIntake) -> Proposal:
system = (
"You are a senior AI consultant writing a project proposal. "
"Be specific, quantify benefits, and use the client's industry "
"terminology. Output valid JSON matching the requested schema."
)
prompt = f"""Generate a complete project proposal for:
Company: {intake.company_name}
Industry: {intake.industry}
Project Type: {intake.project_type.value}
Pain Points: {', '.join(intake.pain_points)}
Current Tools: {', '.join(intake.current_tools)}
Requirements: {intake.requirements}
Budget Range: {intake.budget_range or 'Not specified'}
Return JSON with these keys:
- executive_summary (2-3 paragraphs)
- solution_overview (technical approach summary)
- technical_approach (array of {{title, content}} sections)
- deliverables (array of strings)
- timeline_weeks (integer)
- investment (dollar amount with justification)
- roi_projection (expected ROI with calculations)"""
response = self._call_claude(system, prompt)
data = json.loads(response)
return Proposal(
client=intake,
executive_summary=data["executive_summary"],
solution_overview=data["solution_overview"],
technical_approach=[
ProposalSection(**s) for s in data["technical_approach"]
],
deliverables=data["deliverables"],
timeline_weeks=data["timeline_weeks"],
investment=data["investment"],
roi_projection=data["roi_projection"],
)
PDF Generation with ReportLab
The final step converts the proposal object into a polished PDF:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
)
from reportlab.lib.colors import HexColor
from reportlab.lib.units import inch
class ProposalPDF:
def __init__(self, proposal: Proposal):
self.proposal = proposal
self.styles = getSampleStyleSheet()
self._setup_styles()
def _setup_styles(self):
self.styles.add(ParagraphStyle(
name="ProposalTitle",
fontSize=24,
spaceAfter=20,
textColor=HexColor("#1a1a2e"),
fontName="Helvetica-Bold",
))
self.styles.add(ParagraphStyle(
name="SectionHeader",
fontSize=16,
spaceBefore=15,
spaceAfter=10,
textColor=HexColor("#16213e"),
fontName="Helvetica-Bold",
))
def render(self, output_path: str):
doc = SimpleDocTemplate(output_path, pagesize=letter)
story = []
p = self.proposal
# Title
story.append(Paragraph(
f"AI Implementation Proposal<br/>"
f"<font size=14>{p.client.company_name}</font>",
self.styles["ProposalTitle"]
))
story.append(Spacer(1, 0.3 * inch))
# Executive Summary
story.append(Paragraph("Executive Summary", self.styles["SectionHeader"]))
story.append(Paragraph(p.executive_summary, self.styles["BodyText"]))
story.append(Spacer(1, 0.2 * inch))
# Technical Approach
story.append(Paragraph("Technical Approach", self.styles["SectionHeader"]))
for section in p.technical_approach:
story.append(Paragraph(section.title, self.styles["Heading3"]))
story.append(Paragraph(section.content, self.styles["BodyText"]))
# Deliverables table
story.append(Paragraph("Deliverables", self.styles["SectionHeader"]))
table_data = [["#", "Deliverable"]]
for i, d in enumerate(p.deliverables, 1):
table_data.append([str(i), d])
table = Table(table_data, colWidths=[0.5 * inch, 5.5 * inch])
table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), HexColor("#1a1a2e")),
("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#ffffff")),
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#cccccc")),
("PADDING", (0, 0), (-1, -1), 8),
]))
story.append(table)
# Investment
story.append(Spacer(1, 0.2 * inch))
story.append(Paragraph("Investment & ROI", self.styles["SectionHeader"]))
story.append(Paragraph(f"<b>Investment:</b> {p.investment}", self.styles["BodyText"]))
story.append(Paragraph(f"<b>ROI Projection:</b> {p.roi_projection}", self.styles["BodyText"]))
doc.build(story)
return output_path
Putting It All Together
import os
def main():
generator = ProposalGenerator(api_key=os.environ["ANTHROPIC_API_KEY"])
intake = ClientIntake(
company_name="TechFlow Solutions",
contact_name="Sarah Chen",
industry="SaaS / B2B",
project_type=ProjectType.AUTOMATION,
budget_range="$15,000 - $30,000",
timeline="6-8 weeks",
pain_points=[
"Manual customer onboarding takes 3 hours per client",
"Support tickets are triaged manually",
"No automated reporting for stakeholders",
],
current_tools=["Salesforce", "Zendesk", "Slack"],
requirements=(
"Automate customer onboarding flow, build AI-powered "
"ticket triage system, and create weekly executive reports."
),
)
proposal = generator.generate(intake)
pdf = ProposalPDF(proposal)
output_path = pdf.render(f"proposal_{intake.company_name.replace(' ', '_')}.pdf")
print(f"Proposal generated: {output_path}")
if __name__ == "__main__":
main()
Results
This system now generates proposals in under 60 seconds that used to take me 2-3 hours. The quality is consistent, the formatting is professional, and every proposal is customized to the client's specific pain points and industry.
A few things I learned building this:
- Pydantic validation catches malformed Claude responses before they hit the PDF renderer. Always validate AI output.
- Structured JSON output from Claude is more reliable than asking for freeform text. Give it a schema.
-
ReportLab is excellent for programmatic PDF generation. For simpler needs,
weasyprintwith HTML/CSS templates works too.
I packaged the complete version of this system — with 15 proposal templates across different industries, email follow-up sequences, and a client intake web form — into my AI Agency Launch Kit. It's what I use to run proposals for my own consulting practice.
What automation have you built for your consulting workflow? Drop a comment — I'm always looking for ideas to steal.
Top comments (0)