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