AjaykumarPilla commited on
Commit
d9e9b1b
·
verified ·
1 Parent(s): 3dad0cf

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +16 -0
  2. README.md +42 -0
  3. app.py +493 -0
  4. model.py +303 -0
  5. requirements.txt +12 -0
  6. utils.py +57 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+ COPY model.py .
10
+ COPY utils.py .
11
+ COPY models/delay_model.pth models/delay_model.pth
12
+ COPY models/scaler.pkl models/scaler.pkl
13
+
14
+ EXPOSE 8501
15
+
16
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
README.md ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Delay Predictor
3
+ emoji: 🚀
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
+ - distilbart
11
+ - project-delay
12
+ pinned: false
13
+ short_description: Streamlit app for project delay prediction using DistilBART
14
+ ---
15
+
16
+ # Project Delay Predictor
17
+
18
+ This Streamlit app predicts project delays based on task data, workforce, and weather conditions, using DistilBART (`sshleifer/distilbart-cnn-6-6`) for AI-generated insights. It runs on the free CPU tier of Hugging Face Spaces, generating delay probabilities, insights, and a downloadable PDF report, with integration to Salesforce.
19
+
20
+ ## Features
21
+ - Input project details via a Streamlit interface.
22
+ - Predict delay probability and generate AI insights.
23
+ - Visualize delay risk with an interactive Chart.js heatmap.
24
+ - Save results and PDF to Salesforce.
25
+ - Download a PDF report.
26
+
27
+ ## Setup
28
+ 1. Ensure Salesforce credentials are set as environment variables (`SF_USERNAME`, `SF_PASSWORD`, `SF_SECURITY_TOKEN`, `SF_INSTANCE_URL`).
29
+ 2. Deploy on a Hugging Face Space with the free CPU tier.
30
+ 3. Access the app at the Space's URL.
31
+
32
+ ## Notes
33
+ - Uses DistilBART for CPU-friendly inference (~5-10 seconds per prediction).
34
+ - Secure model loading with `safetensors` and `trust_remote_code=False`.
35
+ - Includes logging for debugging and rule-based fallback insights if the model fails.
36
+
37
+ ## Troubleshooting
38
+ - **AI Insights Unavailable**: Check Space logs for errors (e.g., memory issues, network failures). Restart the Space or reduce `max_new_tokens` in `model.py`.
39
+ - **Slow Inference**: CPU inference may take ~5-10 seconds. Consider switching to `t5-small` for faster performance.
40
+ - **Dependency Errors**: Ensure all dependencies in `requirements.txt` are installed correctly.
41
+
42
+ For questions, refer to [Streamlit documentation](https://docs.streamlit.io) or [Hugging Face forums](https://discuss.huggingface.co).
app.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit.components.v1 as components
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ import plotly.figure_factory as ff
6
+ import os
7
+ from datetime import datetime, timedelta
8
+ import json
9
+ import requests
10
+ import base64
11
+ import logging
12
+ from model import predict_delay, get_weather_condition
13
+ from utils import validate_inputs, generate_heatmap
14
+ from reportlab.lib.pagesizes import letter
15
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
16
+ from reportlab.lib.styles import getSampleStyleSheet
17
+ from reportlab.lib.units import inch
18
+ from io import BytesIO
19
+ from simple_salesforce import Salesforce
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Streamlit app configuration
26
+ st.set_page_config(page_title="Delay 🚀", layout="wide")
27
+
28
+ # Salesforce connection (using environment variables)
29
+ try:
30
+ sf_instance_url = os.environ.get("SF_INSTANCE_URL")
31
+ if not sf_instance_url:
32
+ raise ValueError("SF_INSTANCE_URL environment variable not set")
33
+ if "lightning.force.com" in sf_instance_url:
34
+ logger.warning("SF_INSTANCE_URL contains lightning.force.com; consider using my.salesforce.com for reliable PDF downloads")
35
+ sf = Salesforce(
36
+ username=os.environ.get("SF_USERNAME"),
37
+ password=os.environ.get("SF_PASSWORD"),
38
+ security_token=os.environ.get("SF_SECURITY_TOKEN"),
39
+ instance_url=sf_instance_url
40
+ )
41
+ except Exception as e:
42
+ st.error(f"Failed to connect to Salesforce: {str(e)}")
43
+ logger.error(f"Salesforce connection failed: {str(e)}")
44
+ sf = None
45
+
46
+ # Weather API configuration
47
+ WEATHER_API_KEY = os.environ.get("WEATHER_API_KEY")
48
+ WEATHER_API_URL = "http://api.openweathermap.org/data/2.5/forecast"
49
+
50
+ # Title
51
+ st.title("Project Delay Predictor 🚀")
52
+
53
+ # Task options per phase
54
+ task_options = {
55
+ "Planning": ["Define Scope", "Resource Allocation", "Permit Acquisition"],
56
+ "Design": ["Architectural Drafting", "Engineering Analysis", "Design Review"],
57
+ "Construction": ["Foundation Work", "Structural Build", "Utility Installation"]
58
+ }
59
+
60
+ # Initialize session state
61
+ if 'phase' not in st.session_state:
62
+ st.session_state.phase = ""
63
+ if 'task' not in st.session_state:
64
+ st.session_state.task = ""
65
+ if 'weather_data' not in st.session_state:
66
+ st.session_state.weather_data = None
67
+
68
+ # Function to fetch weather data
69
+ def fetch_weather_data(project_location, date):
70
+ if not WEATHER_API_KEY:
71
+ logger.error("WEATHER_API_KEY not set")
72
+ return None, {"error": "Weather API key not set. Please provide a valid API key."}
73
+ try:
74
+ params = {
75
+ "q": project_location,
76
+ "appid": WEATHER_API_KEY,
77
+ "units": "metric"
78
+ }
79
+ response = requests.get(WEATHER_API_URL, params=params)
80
+ response.raise_for_status()
81
+ data = response.json()
82
+
83
+ # Find the closest forecast to the target date
84
+ target_date = datetime.strptime(date, "%Y-%m-%d")
85
+ closest_forecast = None
86
+ min_time_diff = float('inf')
87
+
88
+ for forecast in data['list']:
89
+ forecast_time = datetime.fromtimestamp(forecast['dt'])
90
+ time_diff = abs((forecast_time - target_date).total_seconds())
91
+ if time_diff < min_time_diff:
92
+ min_time_diff = time_diff
93
+ closest_forecast = forecast
94
+
95
+ if not closest_forecast:
96
+ return None, {"error": "No forecast available for the specified date."}
97
+
98
+ # Map weather conditions to impact score
99
+ weather_main = forecast['weather'][0]['main'].lower()
100
+ impact_score = 50 # Default
101
+ if 'clear' in weather_main:
102
+ impact_score = 10
103
+ elif 'clouds' in weather_main:
104
+ impact_score = 30 if forecast['clouds']['all'] < 50 else 50
105
+ elif 'rain' in weather_main:
106
+ impact_score = 70 if forecast['rain'].get('3h', 0) < 2.5 else 85
107
+ elif 'storm' in weather_main or 'thunderstorm' in weather_main:
108
+ impact_score = 90
109
+
110
+ weather_condition = get_weather_condition(impact_score)
111
+ return {
112
+ "weather_impact_score": impact_score,
113
+ "weather_condition": weather_condition,
114
+ "temperature": forecast['main']['temp'],
115
+ "humidity": forecast['main']['humidity']
116
+ }, None
117
+ except Exception as e:
118
+ logger.error(f"Failed to fetch weather data: {str(e)}")
119
+ return None, {"error": f"Failed to fetch weather data for {project_location}: {str(e)}"}
120
+
121
+ # Function to format high_risk_phases with flag and alert
122
+ def format_high_risk_phases(high_risk_phases):
123
+ formatted = []
124
+ for phase in high_risk_phases:
125
+ flag = "🚩" if phase['risk'] > 75 else ""
126
+ alert = "[Alert]" if phase['risk'] > 75 else ""
127
+ formatted.append(f"{flag} {phase['phase']}: {phase['task']} (Risk: {phase['risk']:.1f}%) {alert}")
128
+ return formatted
129
+
130
+ # Function to generate Gantt chart
131
+ def generate_gantt_chart(input_data, prediction):
132
+ try:
133
+ phase = input_data["phase"]
134
+ task = input_data["task"]
135
+ expected_duration = input_data["task_expected_duration"]
136
+ actual_duration = input_data["task_actual_duration"]
137
+ forecast_date = datetime.strptime(input_data["weather_forecast_date"], "%Y-%m-%d")
138
+ delay_risk = prediction["delay_probability"]
139
+
140
+ # Calculate start and end dates
141
+ start_date = forecast_date - timedelta(days=max(expected_duration, actual_duration))
142
+ expected_end = start_date + timedelta(days=expected_duration)
143
+ actual_end = start_date + timedelta(days=actual_duration) if actual_duration > 0 else expected_end
144
+
145
+ # Prepare Gantt chart data
146
+ df = [
147
+ dict(Task=f"{phase}: {task} (Expected)", Start=start_date.strftime("%Y-%m-%d"), Finish=expected_end.strftime("%Y-%m-%d"), Resource="Expected", Risk=delay_risk),
148
+ dict(Task=f"{phase}: {task} (Actual)", Start=start_date.strftime("%Y-%m-%d"), Finish=actual_end.strftime("%Y-%m-%d"), Resource="Actual", Risk=delay_risk)
149
+ ]
150
+
151
+ # Color based on delay risk
152
+ colors = {
153
+ "Expected": "rgb(0, 255, 0)" if delay_risk <= 50 else "rgb(255, 255, 0)" if delay_risk <= 75 else "rgb(255, 0, 0)",
154
+ "Actual": "rgb(0, 200, 0)" if delay_risk <= 50 else "rgb(200, 200, 0)" if delay_risk <= 75 else "rgb(200, 0, 0)"
155
+ }
156
+
157
+ # Create Gantt chart
158
+ fig = ff.create_gantt(
159
+ df,
160
+ colors=colors,
161
+ index_col="Resource",
162
+ title=f"Gantt Chart for {phase}: {task}",
163
+ show_colorbar=True,
164
+ bar_width=0.4,
165
+ showgrid_x=True,
166
+ showgrid_y=True
167
+ )
168
+ fig.update_layout(
169
+ xaxis_title="Timeline",
170
+ yaxis_title="Task",
171
+ height=300,
172
+ margin=dict(l=150)
173
+ )
174
+ return fig
175
+ except Exception as e:
176
+ logger.error(f"Failed to generate Gantt chart: {str(e)}")
177
+ return None
178
+
179
+ # Function to generate PDF
180
+ def generate_pdf(input_data, prediction, heatmap_fig, gantt_fig):
181
+ buffer = BytesIO()
182
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
183
+ styles = getSampleStyleSheet()
184
+ story = []
185
+
186
+ # Title
187
+ story.append(Paragraph("Project Delay Prediction Report", styles['Title']))
188
+ story.append(Spacer(1, 12))
189
+
190
+ # Input Data
191
+ story.append(Paragraph("Input Data", styles['Heading2']))
192
+ input_fields = [
193
+ f"Project Name: {input_data['project_name']}",
194
+ f"Phase: {input_data['phase']}",
195
+ f"Task: {input_data['task']}",
196
+ f"Current Progress: {input_data['current_progress']}%",
197
+ f"Task Expected Duration: {input_data['task_expected_duration']} days",
198
+ f"Task Actual Duration: {input_data['task_actual_duration']} days",
199
+ f"Workforce Gap: {input_data['workforce_gap']}%",
200
+ f"Workforce Skill Level: {input_data['workforce_skill_level']}",
201
+ f"Workforce Shift Hours: {input_data['workforce_shift_hours']}",
202
+ f"Weather Impact Score: {input_data['weather_impact_score']}",
203
+ f"Weather Condition: {input_data['weather_condition']}",
204
+ f"Weather Forecast Date: {input_data['weather_forecast_date']}",
205
+ f"Project Location: {input_data['project_location']}"
206
+ ]
207
+ for field in input_fields:
208
+ story.append(Paragraph(field, styles['Normal']))
209
+ story.append(Spacer(1, 12))
210
+
211
+ # Prediction Results
212
+ story.append(Paragraph("Prediction Results", styles['Heading2']))
213
+ high_risk_text = "<br/>".join(format_high_risk_phases(prediction['high_risk_phases']))
214
+
215
+ # Check for 2-week risk alert in AI insights
216
+ two_week_alert = next((insight for insight in prediction['ai_insights'].split("; ") if "2-Week Risk Alert" in insight), None)
217
+ if two_week_alert:
218
+ story.append(Paragraph("2-Week Risk Alert", styles['Heading3']))
219
+ story.append(Paragraph(two_week_alert, styles['Normal']))
220
+ story.append(Spacer(1, 12))
221
+
222
+ prediction_fields = [
223
+ f"Delay Probability: {prediction['delay_probability']:.2f}%",
224
+ f"High Risk Phases:<br/>{high_risk_text}",
225
+ f"AI Insights: {prediction['ai_insights']}",
226
+ f"Weather Condition: {prediction['weather_condition']}"
227
+ ]
228
+ for field in prediction_fields:
229
+ story.append(Paragraph(field, styles['Normal']))
230
+ story.append(Spacer(1, 12))
231
+
232
+ # Heatmap
233
+ story.append(Paragraph("Delay Risk Heatmap", styles['Heading2']))
234
+ img_buffer = BytesIO()
235
+ heatmap_fig.savefig(img_buffer, format='png', bbox_inches='tight')
236
+ img_buffer.seek(0)
237
+ story.append(Image(img_buffer, width=6*inch, height=2*inch))
238
+ story.append(Spacer(1, 12))
239
+
240
+ # Gantt Chart
241
+ if gantt_fig:
242
+ story.append(Paragraph("Gantt Chart", styles['Heading2']))
243
+ gantt_buffer = BytesIO()
244
+ try:
245
+ gantt_fig.write_image(gantt_buffer, format='PNG')
246
+ gantt_buffer.seek(0)
247
+ story.append(Image(gantt_buffer, width=6*inch, height=3*inch))
248
+ except Exception as e:
249
+ logger.error(f"Failed to include Gantt chart in PDF: {str(e)}")
250
+ story.append(Paragraph("Gantt Chart unavailable due to rendering issues.", styles['Normal']))
251
+ story.append(Spacer(1, 12))
252
+
253
+ doc.build(story)
254
+ buffer.seek(0)
255
+ return buffer
256
+
257
+ # Function to save data to Salesforce, including PDF and Status__c
258
+ def save_to_salesforce(input_data, prediction, pdf_buffer):
259
+ if sf is None:
260
+ return "Salesforce connection not established."
261
+ try:
262
+ # Determine Status__c based on delay probability
263
+ status = "Flagged" if prediction["delay_probability"] > 75 else "Running"
264
+
265
+ # Prepare data for Delay_Predictor__c object
266
+ sf_data = {
267
+ "Project_Name__c": input_data["project_name"],
268
+ "Phase__c": input_data["phase"],
269
+ "Task__c": input_data["task"],
270
+ "Current_Progress__c": input_data["current_progress"],
271
+ "Task_Expected_Duration__c": input_data["task_expected_duration"],
272
+ "Task_Actual_Duration__c": input_data["task_actual_duration"],
273
+ "Workforce_Gap__c": input_data["workforce_gap"],
274
+ "Workforce_Skill_Level__c": input_data["workforce_skill_level"],
275
+ "Workforce_Shift_Hours__c": input_data["workforce_shift_hours"],
276
+ "Weather_Impact_Score__c": input_data["weather_impact_score"],
277
+ "Weather_Condition__c": input_data["weather_condition"],
278
+ "Weather_Forecast_Date__c": input_data["weather_forecast_date"],
279
+ "Project_Location__c": input_data["project_location"],
280
+ "Delay_Probability__c": prediction["delay_probability"],
281
+ "AI_Insights__c": prediction["ai_insights"],
282
+ "High_Risk_Phases__c": "; ".join(format_high_risk_phases(prediction["high_risk_phases"])),
283
+ "Status__c": status
284
+ }
285
+ logger.info(f"Attempting to save to Salesforce Delay_Predictor__c: {sf_data}")
286
+
287
+ # Create a new record in Delay_Predictor__c
288
+ result = sf.Delay_Predictor__c.create(sf_data)
289
+ if not result["success"]:
290
+ logger.error(f"Salesforce save failed: {result['errors']}")
291
+ return f"Salesforce save failed: {result['errors']}"
292
+
293
+ # Get the record ID
294
+ record_id = result["id"]
295
+ logger.info(f"Created Salesforce record ID: {record_id}")
296
+
297
+ # Upload PDF as ContentVersion
298
+ pdf_data = pdf_buffer.getvalue()
299
+ pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
300
+ content_version = {
301
+ "Title": f"Delay_Prediction_Report_{input_data['project_name']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
302
+ "PathOnClient": "project_delay_report.pdf",
303
+ "VersionData": pdf_base64,
304
+ "FirstPublishLocationId": record_id
305
+ }
306
+ cv_result = sf.ContentVersion.create(content_version)
307
+ if not cv_result["success"]:
308
+ logger.error(f"Failed to upload PDF to Salesforce: {cv_result['errors']}")
309
+ return f"Failed to upload PDF to Salesforce: {cv_result['errors']}"
310
+
311
+ # Get the ContentVersion ID
312
+ content_version_id = cv_result["id"]
313
+
314
+ # Query the ContentDocumentId from the ContentVersion
315
+ query = f"SELECT ContentDocumentId FROM ContentVersion WHERE Id = '{content_version_id}'"
316
+ query_result = sf.query(query)
317
+ if query_result["totalSize"] == 0:
318
+ logger.error(f"Failed to retrieve ContentDocumentId for ContentVersion {content_version_id}")
319
+ return "Failed to retrieve ContentDocumentId for the ContentVersion"
320
+ content_document_id = query_result["records"][0]["ContentDocumentId"]
321
+
322
+ # Construct the Salesforce URL for the ContentDocument
323
+ pdf_url = f"{sf_instance_url}/sfc/servlet.shepherd/document/download/{content_document_id}"
324
+ logger.info(f"Generated PDF URL: {pdf_url}")
325
+
326
+ # Update the Delay_Predictor__c record with the PDF URL
327
+ update_result = sf.Delay_Predictor__c.update(record_id, {"PDF_Report__c": pdf_url})
328
+ if update_result != 204:
329
+ logger.error(f"Failed to update PDF_Report__c with URL: {pdf_url}")
330
+ return f"Failed to update PDF_Report__c field: {update_result}"
331
+
332
+ return None
333
+ except Exception as e:
334
+ logger.error(f"Error saving to Salesforce: {str(e)}")
335
+ return f"Error saving to Salesforce: {str(e)}"
336
+
337
+ # Input form
338
+ with st.form("project_form"):
339
+ col1, col2 = st.columns(2)
340
+
341
+ with col1:
342
+ project_name = st.text_input("Project Name")
343
+ phase = st.selectbox("Phase", [""] + ["Planning", "Design", "Construction"], index=0, key="phase_select")
344
+
345
+ if phase != st.session_state.phase:
346
+ st.session_state.phase = phase
347
+ st.session_state.task = ""
348
+ task_options_list = [""] + task_options.get(phase, []) if phase else [""]
349
+ task = st.selectbox("Task", task_options_list, index=0, key="task_select")
350
+ current_progress = st.number_input("Current Progress (%)", min_value=0.0, max_value=100.0, step=1.0, value=0.0)
351
+ task_expected_duration = st.number_input("Task Expected Duration (days)", min_value=0, step=1, value=0)
352
+ task_actual_duration = st.number_input("Task Actual Duration (days)", min_value=0, step=1, value=0)
353
+
354
+ with col2:
355
+ workforce_gap = st.number_input("Workforce Gap (%)", min_value=0.0, max_value=100.0, step=1.0, value=0.0)
356
+ workforce_skill_level = st.selectbox("Workforce Skill Level", ["", "Low", "Medium", "High"], index=0)
357
+ workforce_shift_hours = st.number_input("Workforce Shift Hours", min_value=0, step=1, value=0)
358
+ st.write(f"**Selected Shift Hours**: {workforce_shift_hours}")
359
+ project_location = st.text_input("Project Location (City)", placeholder="e.g., New York")
360
+ weather_forecast_date = st.date_input("Weather Forecast Date", min_value=datetime(2025, 1, 1), value=None)
361
+
362
+ submit_button = st.form_submit_button("Fetch Weather and Predict Delay")
363
+
364
+ # Process form submission
365
+ if submit_button:
366
+ logger.info("Processing form submission")
367
+ input_data = {
368
+ "project_name": project_name,
369
+ "phase": phase,
370
+ "task": task,
371
+ "current_progress": current_progress,
372
+ "task_expected_duration": task_expected_duration,
373
+ "task_actual_duration": task_actual_duration,
374
+ "workforce_gap": workforce_gap,
375
+ "workforce_skill_level": workforce_skill_level,
376
+ "workforce_shift_hours": workforce_shift_hours,
377
+ "weather_impact_score": 0, # Placeholder, to be updated
378
+ "weather_condition": "", # Placeholder, to be updated
379
+ "weather_forecast_date": weather_forecast_date.strftime("%Y-%m-%d") if weather_forecast_date else "",
380
+ "project_location": project_location
381
+ }
382
+
383
+ # Validate inputs (excluding weather fields initially)
384
+ error = validate_inputs(input_data)
385
+ if error and not error.startswith("Please select or fill in weather"):
386
+ st.error(error)
387
+ logger.error(f"Validation error: {error}")
388
+ else:
389
+ # Fetch weather data
390
+ if project_location and weather_forecast_date:
391
+ weather_data, weather_error = fetch_weather_data(project_location, input_data["weather_forecast_date"])
392
+ if weather_error:
393
+ st.error(weather_error.get("error", "Unknown weather error"))
394
+ logger.error(weather_error.get("error", "Unknown weather error"))
395
+ input_data["weather_impact_score"] = 50 # Fallback value
396
+ input_data["weather_condition"] = "Unknown"
397
+ else:
398
+ input_data["weather_impact_score"] = weather_data["weather_impact_score"]
399
+ input_data["weather_condition"] = weather_data["weather_condition"]
400
+ st.write(f"**Weather Data for {project_location} on {input_data['weather_forecast_date']}**:")
401
+ st.write(f"- Condition: {weather_data['weather_condition']}")
402
+ st.write(f"- Impact Score: {weather_data['weather_impact_score']}")
403
+ st.write(f"- Temperature: {weather_data['temperature']}°C")
404
+ st.write(f"- Humidity: {weather_data['humidity']}%")
405
+ st.session_state.weather_data = weather_data
406
+ else:
407
+ st.error("Please provide a project location and weather forecast date.")
408
+ logger.error("Project location or weather forecast date missing")
409
+ input_data["weather_impact_score"] = 50 # Fallback value
410
+ input_data["weather_condition"] = "Unknown"
411
+
412
+ # Re-validate with weather data
413
+ error = validate_inputs(input_data)
414
+ if error:
415
+ st.error(error)
416
+ logger.error(f"Validation error: {error}")
417
+ else:
418
+ with st.spinner("Generating predictions and AI insights..."):
419
+ try:
420
+ prediction = predict_delay(input_data)
421
+ except Exception as e:
422
+ st.error(f"Prediction failed: {str(e)}")
423
+ logger.error(f"Prediction failed: {str(e)}")
424
+ prediction = {"error": str(e)}
425
+
426
+ if "error" in prediction:
427
+ st.error(prediction["error"])
428
+ else:
429
+ st.subheader("Prediction Results")
430
+ st.write(f"**Delay Probability**: {prediction['delay_probability']:.2f}%")
431
+ st.write("**High Risk Phases**:")
432
+ for line in format_high_risk_phases(prediction['high_risk_phases']):
433
+ st.write(line)
434
+ st.write(f"**AI Insights**: {prediction['ai_insights']}")
435
+ st.write(f"**Weather Condition**: {prediction['weather_condition']}")
436
+
437
+ # Generate Chart.js heatmap
438
+ chart_config = generate_heatmap(prediction['delay_probability'], f"{phase}: {task}")
439
+ chart_id = f"chart-{hash(str(chart_config))}"
440
+ chart_html = f"""
441
+ <canvas id="{chart_id}" style="max-height: 200px; max-width: 600px;"></canvas>
442
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
443
+ <script>
444
+ try {{
445
+ const ctx = document.getElementById('{chart_id}').getContext('2d');
446
+ new Chart(ctx, {json.dumps(chart_config)});
447
+ }} catch (e) {{
448
+ console.error('Chart.js failed: ' + e);
449
+ }}
450
+ </script>
451
+ """
452
+ try:
453
+ components.html(chart_html, height=250)
454
+ logger.info("Chart.js heatmap rendered")
455
+ except Exception as e:
456
+ logger.error(f"Chart.js rendering failed: {str(e)}")
457
+ st.error("Failed to render heatmap; please check your browser settings.")
458
+
459
+ # Generate matplotlib figure for PDF
460
+ fig, ax = plt.subplots(figsize=(8, 2))
461
+ color = 'red' if prediction['delay_probability'] > 75 else 'yellow' if prediction['delay_probability'] > 50 else 'green'
462
+ ax.barh([f"{phase}: {task}"], [prediction['delay_probability']], color=color, edgecolor='black')
463
+ ax.set_xlim(0, 100)
464
+ ax.set_xlabel("Delay Probability (%)")
465
+ ax.set_title("Delay Risk Heatmap")
466
+ plt.tight_layout()
467
+
468
+ # Generate Gantt chart
469
+ gantt_fig = generate_gantt_chart(input_data, prediction)
470
+ if gantt_fig:
471
+ st.plotly_chart(gantt_fig, use_container_width=True)
472
+ logger.info("Gantt chart rendered")
473
+
474
+ pdf_buffer = generate_pdf(input_data, prediction, fig, gantt_fig)
475
+ plt.close(fig)
476
+ st.download_button(
477
+ label="Download Prediction Report (PDF)",
478
+ data=pdf_buffer,
479
+ file_name="project_delay_report.pdf",
480
+ mime="application/pdf"
481
+ )
482
+
483
+ # Save to Salesforce, including PDF
484
+ sf_error = save_to_salesforce(input_data, prediction, pdf_buffer)
485
+ if sf_error:
486
+ st.error(sf_error)
487
+ logger.error(f"Salesforce error: {sf_error}")
488
+ else:
489
+ st.success("Prediction data and PDF successfully saved to Salesforce!")
490
+ logger.info("Data and PDF saved to Salesforce")
491
+
492
+ st.session_state.prediction = prediction
493
+ st.session_state.input_data = input_data
model.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Dict, List
3
+ import torch
4
+ import torch.nn as nn
5
+ import numpy as np
6
+ import pickle
7
+ from sklearn.preprocessing import StandardScaler
8
+ from datetime import datetime, timedelta
9
+
10
+ # Configure logging
11
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # LSTM Model Definition (must match training script)
15
+ class DelayPredictor(nn.Module):
16
+ def __init__(self, input_size, hidden_size, num_layers):
17
+ super(DelayPredictor, self).__init__()
18
+ self.hidden_size = hidden_size
19
+ self.num_layers = num_layers
20
+ self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
21
+ self.attention = nn.Linear(hidden_size, 1)
22
+ self.fc = nn.Linear(hidden_size, 1)
23
+ self.sigmoid = nn.Sigmoid()
24
+
25
+ def forward(self, x):
26
+ lstm_out, _ = self.lstm(x)
27
+ attn_weights = torch.softmax(self.attention(lstm_out).squeeze(-1), dim=1)
28
+ context = torch.bmm(attn_weights.unsqueeze(1), lstm_out).squeeze(1)
29
+ out = self.fc(context)
30
+ return self.sigmoid(out) * 100
31
+
32
+ # Load model and scaler
33
+ try:
34
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
35
+ model = DelayPredictor(input_size=7, hidden_size=64, num_layers=2).to(device)
36
+ model.load_state_dict(torch.load("models/delay_model.pth", map_location=device))
37
+ model.eval()
38
+ with open("models/scaler.pkl", "rb") as f:
39
+ scaler = pickle.load(f)
40
+ logger.info("LSTM model and scaler loaded successfully")
41
+ except Exception as e:
42
+ logger.error(f"Failed to load model or scaler: {str(e)}")
43
+ model = None
44
+ scaler = None
45
+
46
+ def get_weather_condition(score: int) -> str:
47
+ """Map weather impact score (0-100) to descriptive weather condition."""
48
+ if score <= 10:
49
+ return "Sunny"
50
+ elif score <= 30:
51
+ return "Partly Cloudy"
52
+ elif score <= 50:
53
+ return "Cloudy"
54
+ elif score <= 70:
55
+ return "Light Rain"
56
+ elif score <= 85:
57
+ return "Heavy Rain"
58
+ else:
59
+ return "Severe Storm"
60
+
61
+ def call_ai_model_for_insights(input_data: Dict, delay_risk: float) -> List[str]:
62
+ """
63
+ Generate detailed hardcoded insights based on input data and delay risk.
64
+ Includes a 2-week risk alert if weather_forecast_date is within 14 days.
65
+ Returns 3-5 prioritized, phase/task-specific insights.
66
+ """
67
+ logger.info("Generating detailed hardcoded AI insights")
68
+ phase = input_data.get("phase", "")
69
+ task = input_data.get("task", "")
70
+ current_progress = input_data.get("current_progress", 0)
71
+ expected_duration = input_data.get("task_expected_duration", 0)
72
+ actual_duration = input_data.get("task_actual_duration", 0)
73
+ workforce_gap = input_data.get("workforce_gap", 0)
74
+ skill_level = input_data.get("workforce_skill_level", "").lower()
75
+ shift_hours = input_data.get("workforce_shift_hours", 0)
76
+ weather_score = input_data.get("weather_impact_score", 0)
77
+ weather_condition = input_data.get("weather_condition", get_weather_condition(weather_score))
78
+ project_location = input_data.get("project_location", "Unknown")
79
+ weather_forecast_date = input_data.get("weather_forecast_date", "")
80
+
81
+ # Initialize insights with scores for prioritization
82
+ insights = []
83
+
84
+ # Helper function to add insight with priority score
85
+ def add_insight(message: str, priority: float):
86
+ insights.append((message, priority))
87
+
88
+ # 2-week risk alert
89
+ try:
90
+ forecast_date = datetime.strptime(weather_forecast_date, "%Y-%m-%d")
91
+ current_date = datetime(2025, 5, 26) # Fixed date as per system
92
+ two_weeks_later = current_date + timedelta(days=14)
93
+ if current_date <= forecast_date <= two_weeks_later:
94
+ if delay_risk > 75 or weather_score > 75:
95
+ add_insight(
96
+ f"⚠️ Critical 2-Week Risk Alert: High risk of delay for {phase}: {task} in {project_location} by {weather_forecast_date} due to {'severe weather' if weather_score > 75 else 'high delay probability'}. Implement contingency plans immediately.",
97
+ 1.2
98
+ )
99
+ elif delay_risk > 50 or weather_score > 50:
100
+ add_insight(
101
+ f"⚠️ 2-Week Risk Alert: Moderate risk of delay for {phase}: {task} in {project_location} by {weather_forecast_date}. Monitor closely and prepare mitigation measures.",
102
+ 1.1
103
+ )
104
+ except ValueError:
105
+ logger.warning("Invalid weather_forecast_date format; skipping 2-week risk alert")
106
+
107
+ # Delay risk-based insights
108
+ if delay_risk > 75:
109
+ add_insight(f"Urgent: High delay risk detected for {phase}: {task} in {project_location}. Take immediate action.", 1.0)
110
+ elif delay_risk > 50:
111
+ add_insight(f"Monitor {phase}: {task} closely in {project_location} to prevent delays.", 0.9)
112
+ elif delay_risk > 25:
113
+ add_insight(f"Maintain steady progress for {phase}: {task} in {project_location}.", 0.7)
114
+ else:
115
+ add_insight(f"Optimize resources for {phase}: {task} in {project_location} to maintain schedule.", 0.6)
116
+
117
+ # Weather-specific insights
118
+ if weather_score > 85:
119
+ add_insight(f"Critical: Severe storm forecast in {project_location} for {phase}: {task}. Consider halting outdoor activities.", 1.1)
120
+ elif weather_score > 70:
121
+ add_insight(f"Reschedule outdoor tasks for {phase}: {task} in {project_location} due to heavy rain forecast.", 1.0)
122
+ elif weather_score > 50:
123
+ add_insight(f"Shift to indoor or weather-resistant tasks for {phase}: {task} in {project_location} due to light rain.", 0.9)
124
+ elif weather_score > 30:
125
+ add_insight(f"Monitor cloudy conditions in {project_location} for {phase}: {task} to avoid unexpected delays.", 0.7)
126
+ else:
127
+ add_insight(f"Proceed with {phase}: {task} in {project_location} under favorable weather conditions.", 0.6)
128
+
129
+ # Phase/task-specific insights
130
+ task_specific = {
131
+ "Planning": {
132
+ "Define Scope": f"Ensure stakeholder alignment for Planning: Define Scope in {project_location}, considering weather impacts.",
133
+ "Resource Allocation": f"Secure budget and resources early for Planning: Resource Allocation in {project_location}.",
134
+ "Permit Acquisition": f"Expedite permits for Planning: Permit Acquisition in {project_location} to avoid weather-related delays."
135
+ },
136
+ "Design": {
137
+ "Architectural Drafting": f"Engage architects early for Design: Architectural Drafting in {project_location}, accounting for weather.",
138
+ "Engineering Analysis": f"Hire additional engineers for Design: Engineering Analysis in {project_location} to meet deadlines.",
139
+ "Design Review": f"Schedule thorough reviews for Design: Design Review in {project_location}, considering forecast."
140
+ },
141
+ "Construction": {
142
+ "Foundation Work": f"Optimize material delivery for Construction: Foundation Work in {project_location}, avoiding {weather_condition.lower()}.",
143
+ "Structural Build": f"Ensure equipment availability for Construction: Structural Build in {project_location} under {weather_condition.lower()}.",
144
+ "Utility Installation": f"Coordinate subcontractors for Construction: Utility Installation in {project_location}, monitoring weather."
145
+ }
146
+ }
147
+ if phase in task_specific and task in task_specific[phase]:
148
+ add_insight(task_specific[phase][task], 0.8)
149
+
150
+ # Workforce-based insights
151
+ if workforce_gap > 30:
152
+ add_insight(f"Urgently hire subcontractors in {project_location} to address {workforce_gap}% workforce shortage.", 1.0)
153
+ elif workforce_gap > 15:
154
+ add_insight(f"Recruit additional workers in {project_location} to reduce {workforce_gap}% workforce gap.", 0.9)
155
+ elif workforce_gap > 5:
156
+ add_insight(f"Consider temporary staff in {project_location} to address minor workforce gap.", 0.7)
157
+
158
+ if skill_level == "low":
159
+ add_insight(f"Provide training in {project_location} to improve low skill levels for {phase}: {task}.", 0.9)
160
+ elif skill_level == "medium" and delay_risk > 50:
161
+ add_insight(f"Upskill workforce in {project_location} for efficiency in {phase}: {task}.", 0.8)
162
+ elif skill_level == "high" and delay_risk < 25:
163
+ add_insight(f"Leverage high skill levels in {project_location} to maintain {phase}: {task} progress.", 0.6)
164
+
165
+ if shift_hours < 6:
166
+ add_insight(f"Extend shift hours beyond {shift_hours} in {project_location} to meet {phase}: {task} deadlines.", 0.9)
167
+ elif shift_hours < 8 and delay_risk > 50:
168
+ add_insight(f"Increase shift hours to 8 in {project_location} for {phase}: {task}.", 0.8)
169
+ elif shift_hours > 10:
170
+ add_insight(f"Balance shifts in {project_location} to prevent burnout during {phase}: {task}.", 0.7)
171
+
172
+ # Progress and duration-based insights
173
+ if expected_duration > 0 and actual_duration > expected_duration:
174
+ overrun_pct = ((actual_duration - expected_duration) / expected_duration) * 100
175
+ if overrun_pct > 20:
176
+ add_insight(f"Address significant duration overrun ({overrun_pct:.1f}%) for {phase}: {task} in {project_location}.", 1.0)
177
+ elif overrun_pct > 10:
178
+ add_insight(f"Review scheduling to address {overrun_pct:.1f}% overrun in {phase}: {task} in {project_location}.", 0.8)
179
+
180
+ if expected_duration > 0:
181
+ expected_progress = min((actual_duration / expected_duration) * 100, 100)
182
+ if current_progress < expected_progress * 0.8:
183
+ add_insight(f"Accelerate task progress for {phase}: {task} in {project_location} to align with schedule.", 0.9)
184
+ elif current_progress < 50 and delay_risk > 50:
185
+ add_insight(f"Increase resources to boost {current_progress}% progress in {phase}: {task} in {project_location}.", 0.8)
186
+
187
+ # Edge cases
188
+ if workforce_gap >= 90:
189
+ add_insight(f"Critical: Halt non-essential tasks in {project_location} until workforce gap for {phase}: {task} is resolved.", 1.1)
190
+ if current_progress == 0 and delay_risk > 50:
191
+ add_insight(f"Initiate {phase}: {task} in {project_location} immediately to avoid further delays.", 1.0)
192
+ if expected_duration == 0 or actual_duration == 0:
193
+ add_insight(f"Provide accurate duration estimates for {phase}: {task} in {project_location} for reliable predictions.", 0.7)
194
+ if weather_score > 50 and phase == "Construction":
195
+ add_insight(f"Prepare contingency plans for {phase}: {task} in {project_location} due to adverse weather forecast.", 0.95)
196
+
197
+ # Sort insights by priority and select top 3-5
198
+ insights.sort(key=lambda x: x[1], reverse=True)
199
+ selected_insights = [insight[0] for insight in insights[:5]]
200
+
201
+ logger.info(f"Generated insights: {selected_insights}")
202
+ return selected_insights or [f"No significant delay factors detected for {phase}: {task} in {project_location}."]
203
+
204
+ def predict_delay(input_data: Dict) -> Dict:
205
+ """
206
+ Predict delay probability using LSTM model.
207
+ Inputs: Project task data (phase, progress, duration, workforce, weather).
208
+ Outputs: Delay probability, AI insights, high-risk phases, weather condition.
209
+ """
210
+ logger.info("Starting LSTM delay prediction")
211
+ if model is None or scaler is None:
212
+ logger.error("Model or scaler not loaded; falling back to default")
213
+ return {
214
+ "project": input_data.get("project_name", "Unnamed Project"),
215
+ "phase": input_data.get("phase", ""),
216
+ "task": input_data.get("task", ""),
217
+ "delay_probability": 50.0,
218
+ "ai_insights": "Model unavailable; please check deployment.",
219
+ "high_risk_phases": [],
220
+ "weather_condition": "Unknown"
221
+ }
222
+
223
+ phase = input_data.get("phase", "")
224
+ task = input_data.get("task", "")
225
+ weather_condition = input_data.get("weather_condition", get_weather_condition(input_data.get("weather_impact_score", 0)))
226
+
227
+ # Prepare input features
228
+ phase_mapping = {"Planning": 0, "Design": 1, "Construction": 2}
229
+ skill_mapping = {"Low": 0, "Medium": 1, "High": 2}
230
+ try:
231
+ features = np.array([[
232
+ input_data.get("current_progress", 0),
233
+ input_data.get("task_expected_duration", 0),
234
+ input_data.get("task_actual_duration", 0),
235
+ input_data.get("workforce_gap", 0),
236
+ input_data.get("weather_impact_score", 0),
237
+ skill_mapping.get(input_data.get("workforce_skill_level", "Medium"), 1),
238
+ phase_mapping.get(phase, 0)
239
+ ]])
240
+ except KeyError as e:
241
+ logger.error(f"Invalid input data: {str(e)}")
242
+ return {
243
+ "project": input_data.get("project_name", "Unnamed Project"),
244
+ "phase": phase,
245
+ "task": task,
246
+ "delay_probability": 50.0,
247
+ "ai_insights": f"Invalid input: {str(e)}",
248
+ "high_risk_phases": [],
249
+ "weather_condition": weather_condition
250
+ }
251
+
252
+ # Standardize and reshape
253
+ try:
254
+ features_scaled = scaler.transform(features)
255
+ features_tensor = torch.tensor(features_scaled.reshape(1, 1, -1), dtype=torch.float32).to(device)
256
+ except Exception as e:
257
+ logger.error(f"Feature preprocessing failed: {str(e)}")
258
+ return {
259
+ "project": input_data.get("project_name", "Unnamed Project"),
260
+ "phase": phase,
261
+ "task": task,
262
+ "delay_probability": 50.0,
263
+ "ai_insights": f"Preprocessing error: {str(e)}",
264
+ "high_risk_phases": [],
265
+ "weather_condition": weather_condition
266
+ }
267
+
268
+ # Predict
269
+ with torch.no_grad():
270
+ delay_risk = model(features_tensor).cpu().numpy().item()
271
+ delay_risk = round(max(0, min(delay_risk, 100)), 1)
272
+
273
+ # Generate high_risk_phases
274
+ task_options = {
275
+ "Planning": ["Define Scope", "Resource Allocation", "Permit Acquisition"],
276
+ "Design": ["Architectural Drafting", "Engineering Analysis", "Design Review"],
277
+ "Construction": ["Foundation Work", "Structural Build", "Utility Installation"]
278
+ }
279
+ high_risk_phases = []
280
+ if phase in task_options:
281
+ for t in task_options[phase]:
282
+ task_risk = delay_risk
283
+ if t != task:
284
+ task_risk = min(max(task_risk + (hash(t) % 10 - 5), 0), 100)
285
+ high_risk_phases.append({
286
+ "phase": phase,
287
+ "task": t,
288
+ "risk": round(task_risk, 1)
289
+ })
290
+
291
+ # Generate insights
292
+ insights = call_ai_model_for_insights(input_data, delay_risk)
293
+
294
+ logger.info(f"Prediction completed: Delay risk = {delay_risk:.1f}%")
295
+ return {
296
+ "project": input_data.get("project_name", "Unnamed Project"),
297
+ "phase": phase,
298
+ "task": task,
299
+ "delay_probability": delay_risk,
300
+ "ai_insights": "; ".join(insights) if insights else "No significant delay factors detected.",
301
+ "high_risk_phases": high_risk_phases,
302
+ "weather_condition": weather_condition
303
+ }
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.39.0
2
+ pandas==2.2.3
3
+ numpy==1.26.4
4
+ matplotlib==3.9.2
5
+ requests==2.32.3
6
+ simple-salesforce==1.12.6
7
+ reportlab==4.2.5
8
+ python-dateutil==2.9.0
9
+ scikit-learn==1.5.2
10
+ torch==2.4.1
11
+ plotly==5.24.1
12
+ kaleido==0.2.1
utils.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+
3
+ def validate_inputs(input_data):
4
+ """
5
+ Validate input data for required fields and ranges.
6
+ """
7
+ required_fields = [
8
+ "project_name", "phase", "task", "current_progress",
9
+ "task_expected_duration", "task_actual_duration", "workforce_gap",
10
+ "workforce_skill_level", "workforce_shift_hours", "weather_impact_score",
11
+ "weather_condition", "weather_forecast_date", "project_location"
12
+ ]
13
+ for field in required_fields:
14
+ if not input_data[field]:
15
+ return f"Please select or fill in {field.replace('_', ' ').lower()}"
16
+ if not (0 <= input_data["current_progress"] <= 100):
17
+ return "Current progress must be between 0 and 100"
18
+ if not (0 <= input_data["workforce_gap"] <= 100):
19
+ return "Workforce gap must be between 0 and 100"
20
+ if not (0 <= input_data["weather_impact_score"] <= 100):
21
+ return "Weather impact score must be between 0 and 100"
22
+ return None
23
+
24
+ def generate_heatmap(delay_probability, label):
25
+ """
26
+ Generate a Chart.js bar chart configuration to visualize delay probability.
27
+ Returns a Chart.js configuration dictionary.
28
+ """
29
+ color = '#FF0000' if delay_probability > 75 else '#FFFF00' if delay_probability > 50 else '#00FF00'
30
+ chart_config = {
31
+ "type": "bar",
32
+ "data": {
33
+ "labels": [label],
34
+ "datasets": [{
35
+ "label": "Delay Probability (%)",
36
+ "data": [delay_probability],
37
+ "backgroundColor": [color],
38
+ "borderColor": ["#000000"],
39
+ "borderWidth": 1
40
+ }]
41
+ },
42
+ "options": {
43
+ "indexAxis": "y",
44
+ "scales": {
45
+ "x": {
46
+ "beginAtZero": True,
47
+ "max": 100,
48
+ "title": {"display": True, "text": "Delay Probability (%)"}
49
+ },
50
+ "y": {"title": {"display": True, "text": "Task"}}
51
+ },
52
+ "plugins": {
53
+ "title": {"display": True, "text": "Delay Risk Heatmap"}
54
+ }
55
+ }
56
+ }
57
+ return chart_config