Spaces:
Runtime error
Runtime error
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 |