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 |