Introduction
Responding to a Request for Proposal (RFP) is one of the most time-intensive processes in software consulting. It involves analyzing complex requirements, estimating effort across multiple roles, planning sprints, calculating costs, and drafting a professional proposal document — all before a single line of code is written.
What if AI could do the heavy lifting in seconds?
In this blog post, I walk through the complete technical implementation of RFP Price Calculator & Proposal Generator — a Streamlit application that uses Google Gemini (via LangChain) to intelligently analyze project requirements, estimate staffing and costs, plan 2-week Agile sprints, and auto-generate a client-ready technical proposal.
Architecture Overview
The application follows a clean, modular architecture:
┌─────────────────────────────────────────────────────────┐
│ Streamlit Frontend │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌────────┐ │
│ │ Cost & │ │ Technical │ │ Arch & │ │ Sprint │ │
│ │ Staffing │ │ Proposal │ │ Strategy │ │Roadmap │ │
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └───┬────┘ │
│ └───────────────┴─────────────┴────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Session State │ │
│ └──────────┬──────────┘ │
└─────────────────────────┼───────────────────────────────┘
│
┌─────────────────────────┼───────────────────────────────┐
│ LangChain Pipeline │
│ ┌──────────────┐ ┌────┴────┐ ┌────────────────────┐ │
│ │ PromptTemplate│→│ Gemini │→│ PydanticOutputParser│ │
│ └──────────────┘ │ LLM │ └────────────────────┘ │
│ └─────────┘ │
└─────────────────────────────────────────────────────────┘
Key components:
- Streamlit — The interactive web UI with sidebar configuration and tabbed results.
- LangChain — Orchestrates the prompt → LLM → structured output pipeline.
- Google Gemini — The large language model powering the analysis.
- Pydantic — Enforces structured, type-safe output from the LLM.
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Streamlit | Rapid, interactive web UI |
| LLM Provider | Google Gemini (via LangChain) | Requirement analysis and proposal generation |
| Orchestration | LangChain Core | Prompt management and output parsing |
| Data Validation | Pydantic | Structured output enforcement |
| Data Display | Pandas | Tabular data and cost breakdowns |
| Config | python-dotenv | Environment variable management |
Step 1: Data Models with Pydantic
The foundation of this application is structured AI output. Rather than asking the LLM for free-form text, we define strict Pydantic models that the AI must conform to. This is achieved using LangChain’s PydanticOutputParser.
Why This Matters
Without structured output, you’d receive a wall of text and would need complex regex or manual parsing to extract staffing hours, sprint plans, and costs. With Pydantic, the AI’s response is automatically validated and deserialized into Python objects.
from pydantic import BaseModel, Field
from typing import List
class StaffingRole(BaseModel):
role_name: str = Field(description="Name of the professional role")
hours: int = Field(description="Estimated hours for this role")
justification: str = Field(description="Why this role and these hours are needed")
class Sprint(BaseModel):
sprint_number: int = Field(description="Sprint number")
focus_area: str = Field(description="Main focus of this sprint")
key_deliverables: List[str] = Field(description="Key items delivered in this sprint")
class ProjectEstimation(BaseModel):
project_summary: str = Field(description="A brief summary of the project scope")
stack_recommendation: List[str] = Field(description="Recommended technical stack")
staffing_requirements: List[StaffingRole] = Field(description="List of required staff and their hours")
technical_architecture_outline: str = Field(description="High-level technical architecture description")
project_phases: List[str] = Field(description="Key phases of the project")
sprint_plan: List[Sprint] = Field(description="A detailed list of sprints for the project")
Design Decisions
StaffingRoleincludes ajustificationfield. This isn’t just for display — it forces the LLM to reason about why a role is needed, leading to more accurate hour estimates.Sprintis designed around 2-week Agile cycles, with each sprint having a clear focus area and a list of tangible deliverables. This maps directly to how real Scrum teams operate.ProjectEstimationis the root model that ties everything together. It captures the full picture: summary, tech stack, staffing, architecture, phases, and sprint roadmap.
Step 2: The LangChain Pipeline
Chain 1: Project Estimation
The estimation chain uses the powerful LCEL (LangChain Expression Language) pipe syntax:
chain = prompt | llm | parser
This creates a pipeline where:
- The PromptTemplate injects the user’s RFP text and configuration into a structured prompt.
- The LLM (Gemini) processes the prompt and generates a response.
- The PydanticOutputParser validates and converts the raw LLM output into a
ProjectEstimationobject.
The Estimation Prompt
Prompt engineering is the most critical part of this application. Here’s the prompt we use:
est_template = """
SYSTEM: You are a Senior Solutions Architect and Project Lead at a top-tier technology consultancy.
Your task is to decompose a complex RFP/Project requirement into a structured technical execution plan.
INPUT REQUIREMENTS:
{rfp_text}
CONSTRAINTS & CONTEXT:
- Available Roles: {available_roles}
- Deadline: The project MUST be delivered in exactly {duration} months.
- Methodology: Agile with 2-week sprints. Plan for exactly {total_sprints} sprints.
- Goal: Provide highly realistic hour estimates. Do not under-estimate.
Consider overhead like meetings, code reviews, testing, and CI/CD setup.
YOUR TASKS:
1. Summarize the core value proposition and technical challenge.
2. Recommend a modern, production-ready technical stack.
3. Estimate hours for each required role with strong justifications.
4. Outline the technical architecture.
5. Break down the sprints with clear focus areas and tangible deliverables.
{format_instructions}
"""
Key prompt engineering techniques used:
- Role assignment (“You are a Senior Solutions Architect”) — anchors the AI’s expertise level.
- Explicit constraints — prevents the AI from hallucinating unrealistic timelines.
- Overhead reminder — many AI estimators under-count. By explicitly mentioning meetings, code reviews, and CI/CD, we get significantly more realistic numbers.
{format_instructions}— automatically injected byPydanticOutputParser.get_format_instructions(), which tells the LLM exactly what JSON schema to produce.
Chain 2: Proposal Generation
The second chain takes the structured estimation data and generates a polished, C-level-ready proposal document:
prop_template = """
SYSTEM: You are a Principal Consultant and Business Development Director.
Your goal is to write a winning, high-stakes technical proposal.
ESTIMATION DATA:
- Project Summary: {summary}
- Recommended Stack: {stack}
- Resource Allocation: {staffing}
- Architecture Vision: {arch}
- Milestone Phases: {phases}
PROPOSAL STRUCTURE:
## 1. Executive Summary
## 2. Proposed Technical Solution & Architecture
## 3. Delivery Roadmap & Methodology
## 4. Team Composition & Resource Matrix
## 5. Cost Rationalization & Governance
## 6. Strategic Conclusion
TONE: Professional, authoritative, persuasive, and innovative.
FORMAT: Use high-quality Markdown with headers, bold text, tables, and lists.
"""
This prompt is deliberately structured to mirror a real consulting proposal. The AI fills in each section with data from the estimation, producing a document that can be directly shared with clients.
Step 3: Dynamic Cost Calculation & Fuzzy Role Matching
One of the trickiest problems we encountered was role name mismatch. The AI might return “Senior Software Developer” while our sidebar has “Senior Developer”. A naive dict.get() would return $0.
The Solution: Normalized Fuzzy Matching
import math
# Normalize all role names to lowercase for matching
normalized_rates = {k.lower(): (k, v) for k, v in rates.items()}
for role in estimation.staffing_requirements:
role_key = role.role_name.lower().strip()
display_name, rate = (role.role_name, 0)
# Check if either string contains the other
for k, (orig_name, r) in normalized_rates.items():
if k in role_key or role_key in k:
display_name, rate = orig_name, r
break
cost = rate * role.hours
headcount = math.ceil(role.hours / TOTAL_AVAILABLE_HOURS)
How it works:
"senior software developer"contains"senior developer"→ ✅ Match found."jr. developer"contains"junior developer"? No, but"junior developer"contains"junior"and"developer", so let’s check the reverse —"jr. developer"is contained in"junior developer"? No. In edge cases, you may want to add even more sophisticated matching (e.g., token overlap or Levenshtein distance). But for common role titles, substring matching handles 95%+ of cases.
FTE (Full-Time Equivalent) Calculation
We calculate headcount assuming 160 working hours per month per person:
HOURS_PER_MONTH = 160 TOTAL_AVAILABLE_HOURS = project_duration * HOURS_PER_MONTH headcount = math.ceil(role.hours / TOTAL_AVAILABLE_HOURS)
Using math.ceil ensures we always round up — if a role needs 1.1 people, you need 2 FTEs, because you can’t hire 0.1 of a person.
Step 4: Sprint Planning & Timeline
The sprint count is dynamically calculated from the project duration:
total_sprints = (project_duration * 4) // 2 # ~4 weeks/month, 2 weeks/sprint
For a 6-month project, this produces 12 sprints. This number is passed directly into the LLM prompt, ensuring the AI plans exactly the right number of iterations.
The sprint data is displayed using Streamlit’s st.expander widget, creating a collapsible roadmap:
for sprint in estimation.sprint_plan:
with st.expander(f"Sprint {sprint.sprint_number}: {sprint.focus_area}"):
for item in sprint.key_deliverables:
st.write(f"✅ {item}")
Step 5: The Streamlit UI
Sidebar Configuration
The sidebar serves as the control panel:
- API Key Input — Securely accepts the Gemini API key (with fallback to
.env). - Hourly Rates — Configurable per-role rates that instantly recalculate costs.
- Project Timeline — A slider from 1 to 24 months that controls sprint count and FTE calculations.
Tabbed Results
Results are organized into four tabs:
| Tab | Content |
|---|---|
| 📊 Cost & Staffing | DataFrame with roles, hours, FTE, rates, and total costs. Bar chart for cost distribution. |
| 📝 Technical Proposal | Full Markdown proposal with download button. |
| 🏗️ Architecture | Tech stack recommendations and milestone phases. |
| 📅 Sprint Roadmap | Expandable sprint-by-sprint delivery plan. |
Custom Styling
We inject custom CSS for a dark, premium aesthetic:
.stApp {
background: linear-gradient(135deg, #1e1e2f 0%, #121212 100%);
color: #e0e0e0;
}
.main-header {
background: -webkit-linear-gradient(#00c6ff, #0072ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stButton>button {
background: linear-gradient(90deg, #00c6ff 0%, #0072ff 100%);
border-radius: 30px;
transition: all 0.3s ease;
}
.stButton>button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 198, 255, 0.4);
}
Step 6: Session State Management
Streamlit re-runs the entire script on every interaction. Without session state, the AI results would disappear every time a user clicks a tab or adjusts a slider.
def initialize_session_state():
if 'estimation' not in st.session_state:
st.session_state.estimation = None
if 'proposal' not in st.session_state:
st.session_state.proposal = None
By storing estimation and proposal in session state, the results persist across re-renders until the user explicitly generates a new analysis.
Challenges & Learnings
1. LangChain Import Changes
LangChain recently restructured its package hierarchy. The old from langchain.prompts import PromptTemplate no longer works in v0.3+. You must use:
from langchain_core.prompts import PromptTemplate from langchain_core.output_parsers import PydanticOutputParser
2. Structured Output Parsing
Getting the LLM to consistently return valid JSON matching the Pydantic schema required careful prompt engineering. The PydanticOutputParser injects detailed format_instructions into the prompt, but the LLM occasionally wraps the JSON in markdown code fences. LangChain handles this gracefully in most cases.
3. Role Name Matching
The AI doesn’t always return role names exactly as defined. Our fuzzy matching approach (substring containment) solved this without adding heavy dependencies like fuzzywuzzy.
4. Streamlit API Deprecations
Streamlit’s API evolves rapidly. We encountered the deprecation of use_container_width=True in st.dataframe(), which was replaced with width='stretch'.
Project Structure
langchain-rfp-price-calculator/ ├── app.py # Main Streamlit application ├── requirements.txt # Python dependencies ├── .env # API key (gitignored) ├── .env.example # Template for API key ├── .gitignore # Git exclusions ├── README.md # Setup instructions └── BLOG.md # This blog post
How to Run
# Clone the repository git clone <repo-url> cd langchain-rfp-price-calculator # Create virtual environment python -m venv venv .\venv\Scripts\activate # Install dependencies pip install -r requirements.txt # Set your API key echo GOOGLE_API_KEY=your_key_here > .env # Launch the app streamlit run app.py
Future Enhancements
- PDF Export — Generate a polished PDF proposal instead of just Markdown.
- Multi-Model Support — Allow switching between Gemini, OpenAI GPT-4, and Claude.
- Historical Data — Store past estimations to improve accuracy over time.
- Risk Analysis — Add a risk register tied to each sprint.
- Client Portal — Allow clients to view and comment on proposals.
Conclusion
By combining LangChain’s orchestration, Gemini’s reasoning, and Pydantic’s type safety, we built a tool that transforms a wall of RFP text into a structured, costed, sprint-planned technical proposal — in under 30 seconds.
The key insight is that structured output parsing via Pydantic is what makes this production-ready. Without it, you’re just getting text. With it, you’re getting data — data you can calculate costs from, build charts with, and export to clients.
This pattern — PromptTemplate → LLM → PydanticOutputParser — is applicable far beyond RFPs. It’s the foundation of any AI application that needs reliable, structured intelligence.
Github link – https://github.com/sethlahaul/langchain-rfp-price-calculator
