Home_Design_Agent / detailed_budget_agent.py
wangzerui's picture
init
10b617b
from typing import Dict, Any
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
from state import ConversationState
from agents import BaseAgent
class DetailedBudgetAgent(BaseAgent):
"""Creates comprehensive Montreal construction cost estimates based on detailed floorplan"""
def process(self, state: ConversationState) -> ConversationState:
"""Generate detailed budget breakdown based on floorplan and user requirements"""
# Get shared information from other agents
user_reqs = state["user_requirements"]
floorplan_details = state["detailed_floorplan"]
detailed_rooms = floorplan_details["detailed_rooms"]
# Build context for budget analysis
context = self._build_budget_context(state)
system_prompt = f"""You are a senior construction estimator with 15+ years of experience in Montreal residential construction.
You have access to detailed floorplan information from the architectural team.
Create a comprehensive cost breakdown for this custom home construction project.
MONTREAL CONSTRUCTION COSTS (2024):
- Site preparation: $8-15 per sq ft
- Foundation: $15-25 per sq ft
- Framing: $25-40 per sq ft
- Roofing: $12-20 per sq ft
- Exterior finishes: $20-35 per sq ft
- Interior finishes: $30-60 per sq ft (varies by quality)
- Kitchen: $25,000-60,000 (depending on size/finishes)
- Bathrooms: $15,000-35,000 each
- Mechanical (HVAC): $8-15 per sq ft
- Electrical: $6-12 per sq ft
- Plumbing: $8-15 per sq ft
- Permits & fees: $8,000-15,000
- Professional services (architect/engineer): 8-12% of construction
- Contingency: 10-15% of total
QUEBEC-SPECIFIC FACTORS:
- GST: 5%
- QST: 9.975%
- Cold climate considerations (insulation, heating)
- Local building codes and requirements
Analyze each room from the floorplan and provide detailed cost estimates.
Consider the user's budget and provide realistic recommendations.
Return detailed JSON format cost breakdown."""
# Safe formatting for budget
budget = user_reqs.get('budget', 0)
budget_str = f"${budget:,.0f}" if budget else "Not specified"
user_message = f"""
CLIENT REQUIREMENTS:
Budget: {budget_str} CAD
Family size: {user_reqs.get('family_size', 'Not specified')}
Location: {user_reqs.get('location', 'Montreal')}
DETAILED FLOORPLAN INFORMATION:
{context}
Please provide a comprehensive construction cost estimate for this project.
Break down costs by:
1. Major construction categories
2. Room-by-room estimates where relevant
3. Quebec taxes and permits
4. Professional services
5. Contingency recommendations
Respond with detailed JSON:
{{
"site_preparation": cost_in_cad,
"foundation": cost_in_cad,
"framing": cost_in_cad,
"roofing": cost_in_cad,
"exterior_finishes": cost_in_cad,
"interior_finishes": cost_in_cad,
"kitchen_costs": cost_in_cad,
"bathroom_costs": cost_in_cad,
"mechanical_systems": cost_in_cad,
"electrical_systems": cost_in_cad,
"plumbing_systems": cost_in_cad,
"permits_fees": cost_in_cad,
"professional_services": cost_in_cad,
"subtotal": cost_in_cad,
"taxes_gst_qst": cost_in_cad,
"contingency": cost_in_cad,
"total_construction_cost": cost_in_cad,
"cost_per_sqft": cost_per_sqft,
"budget_analysis": "analysis of fit with client budget",
"recommendations": ["recommendation1", "recommendation2"],
"room_breakdown": {{"room_name": cost_estimate}},
"timeline_estimate": "construction timeline estimate"
}}
"""
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=user_message)
]
response = self.model.invoke(messages)
try:
import json
budget_data = json.loads(response.content)
# Update state with detailed budget breakdown
state["budget_breakdown"].update({
"site_preparation": budget_data.get("site_preparation"),
"foundation": budget_data.get("foundation"),
"framing": budget_data.get("framing"),
"roofing": budget_data.get("roofing"),
"exterior_finishes": budget_data.get("exterior_finishes"),
"interior_finishes": budget_data.get("interior_finishes"),
"mechanical_systems": budget_data.get("mechanical_systems"),
"electrical_systems": budget_data.get("electrical_systems"),
"plumbing_systems": budget_data.get("plumbing_systems"),
"permits_fees": budget_data.get("permits_fees"),
"professional_services": budget_data.get("professional_services"),
"contingency": budget_data.get("contingency"),
"total_construction_cost": budget_data.get("total_construction_cost"),
"cost_per_sqft": budget_data.get("cost_per_sqft"),
"budget_analysis": budget_data.get("budget_analysis")
})
# Store additional details in agent memory for future reference
state["agent_memory"]["detailed_budget"] = budget_data
state["budget_ready"] = True
# Create comprehensive response
formatted_response = self._format_budget_response(budget_data, user_reqs)
except json.JSONDecodeError:
# Fallback if JSON parsing fails
formatted_response = f"""πŸ’° **DETAILED CONSTRUCTION COST ESTIMATE** πŸ’°
Based on the architectural floorplan and Montreal market conditions, here's a comprehensive cost breakdown:
{response.content}
Note: This is an estimated breakdown. Final costs may vary based on specific material choices, contractor selection, and market conditions."""
# Set basic budget ready flag even with fallback
state["budget_ready"] = True
# Add response to conversation
state["messages"].append({
"role": "assistant",
"content": formatted_response,
"agent": "detailed_budget"
})
return state
def _build_budget_context(self, state: ConversationState) -> str:
"""Build context from floorplan details for budget estimation"""
floorplan_details = state["detailed_floorplan"]
floorplan_reqs = state["floorplan_requirements"]
context_parts = []
# Basic project info
if floorplan_reqs["total_sqft"]:
context_parts.append(f"Total Area: {floorplan_reqs['total_sqft']:,} sq ft")
if floorplan_reqs["num_floors"]:
context_parts.append(f"Number of Floors: {floorplan_reqs['num_floors']}")
if floorplan_reqs["lot_dimensions"]:
context_parts.append(f"Lot Size: {floorplan_reqs['lot_dimensions']}")
# Detailed rooms information
detailed_rooms = floorplan_details["detailed_rooms"]
if detailed_rooms:
context_parts.append("\nROOM BREAKDOWN:")
for room in detailed_rooms:
room_info = f"- {room.get('label', room.get('type', 'Unknown'))}: {room.get('width', '?')}' Γ— {room.get('height', '?')}' = {room.get('width', 0) * room.get('height', 0)} sq ft"
if room.get('features'):
room_info += f" (Features: {', '.join(room['features'])})"
context_parts.append(room_info)
# Architectural features
arch_features = floorplan_details["architectural_features"]
if arch_features:
context_parts.append(f"\nSpecial Architectural Features: {', '.join(arch_features)}")
# Structural elements
structural = floorplan_details["structural_elements"]
if structural:
context_parts.append(f"\nStructural Elements: {len(structural)} custom elements")
return "\n".join(context_parts) if context_parts else "Basic floorplan information available"
def _format_budget_response(self, budget_data: Dict[str, Any], user_reqs: Dict[str, Any]) -> str:
"""Format the budget response in a clear, professional manner"""
total_cost = budget_data.get("total_construction_cost", 0)
user_budget = user_reqs.get("budget", 0)
cost_per_sqft = budget_data.get("cost_per_sqft", 0)
# Budget fit analysis
if user_budget and total_cost:
if total_cost <= user_budget * 0.95:
budget_status = "βœ… WITHIN BUDGET"
budget_message = f"Excellent! The estimated cost is within your ${user_budget:,.0f} budget with room for upgrades."
elif total_cost <= user_budget * 1.1:
budget_status = "⚠️ CLOSE TO BUDGET"
budget_message = f"The estimate is close to your ${user_budget:,.0f} budget. Minor adjustments may be needed."
else:
budget_status = "❌ OVER BUDGET"
budget_message = f"The estimate exceeds your ${user_budget:,.0f} budget by ${total_cost - user_budget:,.0f}. Consider modifications."
else:
budget_status = "πŸ“Š COST ESTIMATE"
budget_message = "Comprehensive cost breakdown based on architectural specifications."
response = f"""πŸ’° **DETAILED MONTREAL CONSTRUCTION ESTIMATE** πŸ’°
{budget_status}
{budget_message}
**CONSTRUCTION COST BREAKDOWN:**
πŸ—οΈ Site Preparation: ${budget_data.get('site_preparation', 0):,.0f} CAD
🏠 Foundation: ${budget_data.get('foundation', 0):,.0f} CAD
πŸ”¨ Framing: ${budget_data.get('framing', 0):,.0f} CAD
🏠 Roofing: ${budget_data.get('roofing', 0):,.0f} CAD
🎨 Exterior Finishes: ${budget_data.get('exterior_finishes', 0):,.0f} CAD
🏑 Interior Finishes: ${budget_data.get('interior_finishes', 0):,.0f} CAD
🍳 Kitchen: ${budget_data.get('kitchen_costs', 0):,.0f} CAD
πŸ› Bathrooms: ${budget_data.get('bathroom_costs', 0):,.0f} CAD
**SYSTEMS & SERVICES:**
🌑️ Mechanical (HVAC): ${budget_data.get('mechanical_systems', 0):,.0f} CAD
⚑ Electrical: ${budget_data.get('electrical_systems', 0):,.0f} CAD
🚿 Plumbing: ${budget_data.get('plumbing_systems', 0):,.0f} CAD
πŸ“‹ Permits & Fees: ${budget_data.get('permits_fees', 0):,.0f} CAD
πŸ‘· Professional Services: ${budget_data.get('professional_services', 0):,.0f} CAD
**FINAL TOTALS:**
Subtotal: ${budget_data.get('subtotal', total_cost * 0.85):,.0f} CAD
Quebec Taxes (GST+QST): ${budget_data.get('taxes_gst_qst', total_cost * 0.15):,.0f} CAD
Contingency (12%): ${budget_data.get('contingency', 0):,.0f} CAD
**πŸ’° TOTAL PROJECT COST: ${total_cost:,.0f} CAD**
**πŸ“ Cost per sq ft: ${cost_per_sqft:.0f} CAD/sq ft**
**BUDGET ANALYSIS:**
{budget_data.get('budget_analysis', 'Cost analysis based on Montreal market rates and architectural specifications.')}
**RECOMMENDATIONS:**
"""
# Add recommendations
recommendations = budget_data.get('recommendations', [])
for rec in recommendations:
response += f"β€’ {rec}\n"
# Add room breakdown if available
room_breakdown = budget_data.get('room_breakdown', {})
if room_breakdown:
response += "\n**ROOM-BY-ROOM COSTS:**\n"
for room, cost in room_breakdown.items():
response += f"β€’ {room}: ${cost:,.0f} CAD\n"
# Add timeline
timeline = budget_data.get('timeline_estimate', 'Construction timeline to be determined')
response += f"\n**CONSTRUCTION TIMELINE:**\n{timeline}"
response += "\n\n*Note: This estimate is based on current Montreal market conditions and the provided architectural specifications. Final costs may vary based on material selections, contractor choice, and market fluctuations.*"
return response