AjaykumarPilla commited on
Commit
c47a773
·
verified ·
1 Parent(s): 2be1a85

Update model.py

Browse files
Files changed (1) hide show
  1. model.py +141 -97
model.py CHANGED
@@ -1,10 +1,47 @@
1
  import logging
2
  from typing import Dict, List
 
 
 
 
 
3
 
4
  # Configure logging
5
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
6
  logger = logging.getLogger(__name__)
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  def get_weather_condition(score: int) -> str:
9
  """Map weather impact score (0-100) to descriptive weather condition."""
10
  if score <= 10:
@@ -36,7 +73,7 @@ def call_ai_model_for_insights(input_data: Dict, delay_risk: float) -> List[str]
36
  shift_hours = input_data.get("workforce_shift_hours", 0)
37
  weather_score = input_data.get("weather_impact_score", 0)
38
  weather_condition = input_data.get("weather_condition", get_weather_condition(weather_score))
39
- city = input_data.get("city", "Unknown")
40
 
41
  # Initialize insights with scores for prioritization
42
  insights = []
@@ -47,42 +84,42 @@ def call_ai_model_for_insights(input_data: Dict, delay_risk: float) -> List[str]
47
 
48
  # Delay risk-based insights
49
  if delay_risk > 75:
50
- add_insight(f"Urgent: High delay risk detected for {phase}: {task} in {city}. Take immediate action.", 1.0)
51
  elif delay_risk > 50:
52
- add_insight(f"Monitor {phase}: {task} closely in {city} to prevent delays.", 0.9)
53
  elif delay_risk > 25:
54
- add_insight(f"Maintain steady progress for {phase}: {task} in {city}.", 0.7)
55
  else:
56
- add_insight(f"Optimize resources for {phase}: {task} in {city} to maintain schedule.", 0.6)
57
 
58
- # Weather-specific insights (enhanced)
59
  if weather_score > 85:
60
- add_insight(f"Critical: Severe storm forecast in {city} for {phase}: {task}. Consider halting outdoor activities.", 1.1)
61
  elif weather_score > 70:
62
- add_insight(f"Reschedule outdoor tasks for {phase}: {task} in {city} due to heavy rain forecast.", 1.0)
63
  elif weather_score > 50:
64
- add_insight(f"Shift to indoor or weather-resistant tasks for {phase}: {task} in {city} due to light rain.", 0.9)
65
  elif weather_score > 30:
66
- add_insight(f"Monitor cloudy conditions in {city} for {phase}: {task} to avoid unexpected delays.", 0.7)
67
  else:
68
- add_insight(f"Proceed with {phase}: {task} in {city} under favorable weather conditions.", 0.6)
69
 
70
  # Phase/task-specific insights
71
  task_specific = {
72
  "Planning": {
73
- "Define Scope": f"Ensure stakeholder alignment for Planning: Define Scope in {city}, considering weather impacts.",
74
- "Resource Allocation": f"Secure budget and resources early for Planning: Resource Allocation in {city}.",
75
- "Permit Acquisition": f"Expedite permits for Planning: Permit Acquisition in {city} to avoid weather-related delays."
76
  },
77
  "Design": {
78
- "Architectural Drafting": f"Engage architects early for Design: Architectural Drafting in {city}, accounting for weather.",
79
- "Engineering Analysis": f"Hire additional engineers for Design: Engineering Analysis in {city} to meet deadlines.",
80
- "Design Review": f"Schedule thorough reviews for Design: Design Review in {city}, considering forecast."
81
  },
82
  "Construction": {
83
- "Foundation Work": f"Optimize material delivery for Construction: Foundation Work in {city}, avoiding {weather_condition.lower()}.",
84
- "Structural Build": f"Ensure equipment availability for Construction: Structural Build in {city} under {weather_condition.lower()}.",
85
- "Utility Installation": f"Coordinate subcontractors for Construction: Utility Installation in {city}, monitoring weather."
86
  }
87
  }
88
  if phase in task_specific and task in task_specific[phase]:
@@ -90,139 +127,146 @@ def call_ai_model_for_insights(input_data: Dict, delay_risk: float) -> List[str]
90
 
91
  # Workforce-based insights
92
  if workforce_gap > 30:
93
- add_insight(f"Urgently hire subcontractors in {city} to address {workforce_gap}% workforce shortage.", 1.0)
94
  elif workforce_gap > 15:
95
- add_insight(f"Recruit additional workers in {city} to reduce {workforce_gap}% workforce gap.", 0.9)
96
  elif workforce_gap > 5:
97
- add_insight(f"Consider temporary staff in {city} to address minor workforce gap.", 0.7)
98
 
99
  if skill_level == "low":
100
- add_insight(f"Provide training in {city} to improve low skill levels for {phase}: {task}.", 0.9)
101
  elif skill_level == "medium" and delay_risk > 50:
102
- add_insight(f"Upskill workforce in {city} for efficiency in {phase}: {task}.", 0.8)
103
  elif skill_level == "high" and delay_risk < 25:
104
- add_insight(f"Leverage high skill levels in {city} to maintain {phase}: {task} progress.", 0.6)
105
 
106
  if shift_hours < 6:
107
- add_insight(f"Extend shift hours beyond {shift_hours} in {city} to meet {phase}: {task} deadlines.", 0.9)
108
  elif shift_hours < 8 and delay_risk > 50:
109
- add_insight(f"Increase shift hours to 8 in {city} for {phase}: {task}.", 0.8)
110
  elif shift_hours > 10:
111
- add_insight(f"Balance shifts in {city} to prevent burnout during {phase}: {task}.", 0.7)
112
 
113
  # Progress and duration-based insights
114
  if expected_duration > 0 and actual_duration > expected_duration:
115
  overrun_pct = ((actual_duration - expected_duration) / expected_duration) * 100
116
  if overrun_pct > 20:
117
- add_insight(f"Address significant duration overrun ({overrun_pct:.1f}%) for {phase}: {task} in {city}.", 1.0)
118
  elif overrun_pct > 10:
119
- add_insight(f"Review scheduling to address {overrun_pct:.1f}% overrun in {phase}: {task} in {city}.", 0.8)
120
 
121
  if expected_duration > 0:
122
  expected_progress = min((actual_duration / expected_duration) * 100, 100)
123
  if current_progress < expected_progress * 0.8:
124
- add_insight(f"Accelerate task progress for {phase}: {task} in {city} to align with schedule.", 0.9)
125
  elif current_progress < 50 and delay_risk > 50:
126
- add_insight(f"Increase resources to boost {current_progress}% progress in {phase}: {task} in {city}.", 0.8)
127
 
128
  # Edge cases
129
  if workforce_gap >= 90:
130
- add_insight(f"Critical: Halt non-essential tasks in {city} until workforce gap for {phase}: {task} is resolved.", 1.1)
131
  if current_progress == 0 and delay_risk > 50:
132
- add_insight(f"Initiate {phase}: {task} in {city} immediately to avoid further delays.", 1.0)
133
  if expected_duration == 0 or actual_duration == 0:
134
- add_insight(f"Provide accurate duration estimates for {phase}: {task} in {city} for reliable predictions.", 0.7)
135
  if weather_score > 50 and phase == "Construction":
136
- add_insight(f"Prepare contingency plans for {phase}: {task} in {city} due to adverse weather forecast.", 0.95)
137
 
138
  # Sort insights by priority and select top 3-5
139
  insights.sort(key=lambda x: x[1], reverse=True)
140
  selected_insights = [insight[0] for insight in insights[:5]]
141
 
142
  logger.info(f"Generated insights: {selected_insights}")
143
- return selected_insights or [f"No significant delay factors detected for {phase}: {task} in {city}."]
144
 
145
  def predict_delay(input_data: Dict) -> Dict:
146
  """
147
- Predict delay probability based on project task data.
148
- Uses task duration, progress, workforce info, and weather impact.
149
- Insights are generated using detailed hardcoded rules.
150
  """
151
- logger.info("Starting delay prediction")
 
 
 
 
 
 
 
 
 
 
 
 
152
  phase = input_data.get("phase", "")
153
  task = input_data.get("task", "")
154
- expected_duration = input_data.get("task_expected_duration", 0)
155
- actual_duration = input_data.get("task_actual_duration", 0)
156
-
157
- current_progress = input_data.get("current_progress", 0) # in %
158
- workforce_gap_pct = input_data.get("workforce_gap", 0) # in %
159
- skill_level = input_data.get("workforce_skill_level", "").lower()
160
- shift_hours = input_data.get("workforce_shift_hours", 0)
161
- weather_score = input_data.get("weather_impact_score", 0) # 0-100 scale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
- # Auto-set weather condition if missing or inconsistent
164
- weather_condition = input_data.get("weather_condition", "")
165
- if not weather_condition:
166
- weather_condition = get_weather_condition(weather_score)
167
 
168
- # Task options for phase (hardcoded to match app.py)
169
  task_options = {
170
  "Planning": ["Define Scope", "Resource Allocation", "Permit Acquisition"],
171
  "Design": ["Architectural Drafting", "Engineering Analysis", "Design Review"],
172
  "Construction": ["Foundation Work", "Structural Build", "Utility Installation"]
173
  }
174
-
175
- delay_risk = 0
176
-
177
- # 1. Duration overrun risk
178
- if expected_duration > 0 and actual_duration > expected_duration:
179
- overrun_pct = ((actual_duration - expected_duration) / expected_duration) * 100
180
- delay_risk += min(overrun_pct, 30)
181
-
182
- # 2. Progress lag risk
183
- if expected_duration > 0 and current_progress >= 0:
184
- expected_progress = (actual_duration / expected_duration) * 100
185
- if current_progress < expected_progress:
186
- progress_gap = expected_progress - current_progress
187
- delay_risk += min(progress_gap, 25)
188
-
189
- # 3. Workforce gap impact
190
- if workforce_gap_pct > 0:
191
- delay_risk += min(workforce_gap_pct * 0.5, 20)
192
-
193
- # 4. Skill level effect
194
- if skill_level == "low":
195
- delay_risk += 15
196
- elif skill_level == "medium":
197
- delay_risk += 7
198
-
199
- # 5. Shift hours effect
200
- if shift_hours < 8:
201
- delay_risk += (8 - shift_hours) * 3
202
- elif shift_hours > 8:
203
- delay_risk -= min((shift_hours - 8) * 2, 10)
204
-
205
- # 6. Weather impact effect
206
- if weather_score > 50:
207
- delay_risk += min(weather_score / 2, 20)
208
-
209
- # Ensure delay_risk is between 0 and 100
210
- delay_risk = max(0, min(delay_risk, 100))
211
-
212
- # Generate high_risk_phases for all tasks in the phase
213
  high_risk_phases = []
214
  if phase in task_options:
215
  for t in task_options[phase]:
216
  task_risk = delay_risk
217
  if t != task:
218
- task_risk = min(max(task_risk + (hash(t) % 10 - 5), 0), 100) # ±5% variation
219
  high_risk_phases.append({
220
  "phase": phase,
221
  "task": t,
222
  "risk": round(task_risk, 1)
223
  })
224
 
225
- # Generate hardcoded insights
226
  insights = call_ai_model_for_insights(input_data, delay_risk)
227
 
228
  logger.info(f"Prediction completed: Delay risk = {delay_risk:.1f}%")
@@ -230,7 +274,7 @@ def predict_delay(input_data: Dict) -> Dict:
230
  "project": input_data.get("project_name", "Unnamed Project"),
231
  "phase": phase,
232
  "task": task,
233
- "delay_probability": round(delay_risk, 1),
234
  "ai_insights": "; ".join(insights) if insights else "No significant delay factors detected.",
235
  "high_risk_phases": high_risk_phases,
236
  "weather_condition": weather_condition
 
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
 
9
  # Configure logging
10
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
11
  logger = logging.getLogger(__name__)
12
 
13
+ # LSTM Model Definition (must match training script)
14
+ class DelayPredictor(nn.Module):
15
+ def __init__(self, input_size, hidden_size, num_layers):
16
+ super(DelayPredictor, self).__init__()
17
+ self.hidden_size = hidden_size
18
+ self.num_layers = num_layers
19
+ self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
20
+ self.attention = nn.Linear(hidden_size, 1)
21
+ self.fc = nn.Linear(hidden_size, 1)
22
+ self.sigmoid = nn.Sigmoid()
23
+
24
+ def forward(self, x):
25
+ lstm_out, _ = self.lstm(x)
26
+ attn_weights = torch.softmax(self.attention(lstm_out).squeeze(-1), dim=1)
27
+ context = torch.bmm(attn_weights.unsqueeze(1), lstm_out).squeeze(1)
28
+ out = self.fc(context)
29
+ return self.sigmoid(out) * 100
30
+
31
+ # Load model and scaler
32
+ try:
33
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
+ model = DelayPredictor(input_size=7, hidden_size=64, num_layers=2).to(device)
35
+ model.load_state_dict(torch.load("models/delay_model.pth", map_location=device))
36
+ model.eval()
37
+ with open("models/scaler.pkl", "rb") as f:
38
+ scaler = pickle.load(f)
39
+ logger.info("LSTM model and scaler loaded successfully")
40
+ except Exception as e:
41
+ logger.error(f"Failed to load model or scaler: {str(e)}")
42
+ model = None
43
+ scaler = None
44
+
45
  def get_weather_condition(score: int) -> str:
46
  """Map weather impact score (0-100) to descriptive weather condition."""
47
  if score <= 10:
 
73
  shift_hours = input_data.get("workforce_shift_hours", 0)
74
  weather_score = input_data.get("weather_impact_score", 0)
75
  weather_condition = input_data.get("weather_condition", get_weather_condition(weather_score))
76
+ project_location = input_data.get("project_location", "Unknown")
77
 
78
  # Initialize insights with scores for prioritization
79
  insights = []
 
84
 
85
  # Delay risk-based insights
86
  if delay_risk > 75:
87
+ add_insight(f"Urgent: High delay risk detected for {phase}: {task} in {project_location}. Take immediate action.", 1.0)
88
  elif delay_risk > 50:
89
+ add_insight(f"Monitor {phase}: {task} closely in {project_location} to prevent delays.", 0.9)
90
  elif delay_risk > 25:
91
+ add_insight(f"Maintain steady progress for {phase}: {task} in {project_location}.", 0.7)
92
  else:
93
+ add_insight(f"Optimize resources for {phase}: {task} in {project_location} to maintain schedule.", 0.6)
94
 
95
+ # Weather-specific insights
96
  if weather_score > 85:
97
+ add_insight(f"Critical: Severe storm forecast in {project_location} for {phase}: {task}. Consider halting outdoor activities.", 1.1)
98
  elif weather_score > 70:
99
+ add_insight(f"Reschedule outdoor tasks for {phase}: {task} in {project_location} due to heavy rain forecast.", 1.0)
100
  elif weather_score > 50:
101
+ add_insight(f"Shift to indoor or weather-resistant tasks for {phase}: {task} in {project_location} due to light rain.", 0.9)
102
  elif weather_score > 30:
103
+ add_insight(f"Monitor cloudy conditions in {project_location} for {phase}: {task} to avoid unexpected delays.", 0.7)
104
  else:
105
+ add_insight(f"Proceed with {phase}: {task} in {project_location} under favorable weather conditions.", 0.6)
106
 
107
  # Phase/task-specific insights
108
  task_specific = {
109
  "Planning": {
110
+ "Define Scope": f"Ensure stakeholder alignment for Planning: Define Scope in {project_location}, considering weather impacts.",
111
+ "Resource Allocation": f"Secure budget and resources early for Planning: Resource Allocation in {project_location}.",
112
+ "Permit Acquisition": f"Expedite permits for Planning: Permit Acquisition in {project_location} to avoid weather-related delays."
113
  },
114
  "Design": {
115
+ "Architectural Drafting": f"Engage architects early for Design: Architectural Drafting in {project_location}, accounting for weather.",
116
+ "Engineering Analysis": f"Hire additional engineers for Design: Engineering Analysis in {project_location} to meet deadlines.",
117
+ "Design Review": f"Schedule thorough reviews for Design: Design Review in {project_location}, considering forecast."
118
  },
119
  "Construction": {
120
+ "Foundation Work": f"Optimize material delivery for Construction: Foundation Work in {project_location}, avoiding {weather_condition.lower()}.",
121
+ "Structural Build": f"Ensure equipment availability for Construction: Structural Build in {project_location} under {weather_condition.lower()}.",
122
+ "Utility Installation": f"Coordinate subcontractors for Construction: Utility Installation in {project_location}, monitoring weather."
123
  }
124
  }
125
  if phase in task_specific and task in task_specific[phase]:
 
127
 
128
  # Workforce-based insights
129
  if workforce_gap > 30:
130
+ add_insight(f"Urgently hire subcontractors in {project_location} to address {workforce_gap}% workforce shortage.", 1.0)
131
  elif workforce_gap > 15:
132
+ add_insight(f"Recruit additional workers in {project_location} to reduce {workforce_gap}% workforce gap.", 0.9)
133
  elif workforce_gap > 5:
134
+ add_insight(f"Consider temporary staff in {project_location} to address minor workforce gap.", 0.7)
135
 
136
  if skill_level == "low":
137
+ add_insight(f"Provide training in {project_location} to improve low skill levels for {phase}: {task}.", 0.9)
138
  elif skill_level == "medium" and delay_risk > 50:
139
+ add_insight(f"Upskill workforce in {project_location} for efficiency in {phase}: {task}.", 0.8)
140
  elif skill_level == "high" and delay_risk < 25:
141
+ add_insight(f"Leverage high skill levels in {project_location} to maintain {phase}: {task} progress.", 0.6)
142
 
143
  if shift_hours < 6:
144
+ add_insight(f"Extend shift hours beyond {shift_hours} in {project_location} to meet {phase}: {task} deadlines.", 0.9)
145
  elif shift_hours < 8 and delay_risk > 50:
146
+ add_insight(f"Increase shift hours to 8 in {project_location} for {phase}: {task}.", 0.8)
147
  elif shift_hours > 10:
148
+ add_insight(f"Balance shifts in {project_location} to prevent burnout during {phase}: {task}.", 0.7)
149
 
150
  # Progress and duration-based insights
151
  if expected_duration > 0 and actual_duration > expected_duration:
152
  overrun_pct = ((actual_duration - expected_duration) / expected_duration) * 100
153
  if overrun_pct > 20:
154
+ add_insight(f"Address significant duration overrun ({overrun_pct:.1f}%) for {phase}: {task} in {project_location}.", 1.0)
155
  elif overrun_pct > 10:
156
+ add_insight(f"Review scheduling to address {overrun_pct:.1f}% overrun in {phase}: {task} in {project_location}.", 0.8)
157
 
158
  if expected_duration > 0:
159
  expected_progress = min((actual_duration / expected_duration) * 100, 100)
160
  if current_progress < expected_progress * 0.8:
161
+ add_insight(f"Accelerate task progress for {phase}: {task} in {project_location} to align with schedule.", 0.9)
162
  elif current_progress < 50 and delay_risk > 50:
163
+ add_insight(f"Increase resources to boost {current_progress}% progress in {phase}: {task} in {project_location}.", 0.8)
164
 
165
  # Edge cases
166
  if workforce_gap >= 90:
167
+ add_insight(f"Critical: Halt non-essential tasks in {project_location} until workforce gap for {phase}: {task} is resolved.", 1.1)
168
  if current_progress == 0 and delay_risk > 50:
169
+ add_insight(f"Initiate {phase}: {task} in {project_location} immediately to avoid further delays.", 1.0)
170
  if expected_duration == 0 or actual_duration == 0:
171
+ add_insight(f"Provide accurate duration estimates for {phase}: {task} in {project_location} for reliable predictions.", 0.7)
172
  if weather_score > 50 and phase == "Construction":
173
+ add_insight(f"Prepare contingency plans for {phase}: {task} in {project_location} due to adverse weather forecast.", 0.95)
174
 
175
  # Sort insights by priority and select top 3-5
176
  insights.sort(key=lambda x: x[1], reverse=True)
177
  selected_insights = [insight[0] for insight in insights[:5]]
178
 
179
  logger.info(f"Generated insights: {selected_insights}")
180
+ return selected_insights or [f"No significant delay factors detected for {phase}: {task} in {project_location}."]
181
 
182
  def predict_delay(input_data: Dict) -> Dict:
183
  """
184
+ Predict delay probability using LSTM model.
185
+ Inputs: Project task data (phase, progress, duration, workforce, weather).
186
+ Outputs: Delay probability, AI insights, high-risk phases, weather condition.
187
  """
188
+ logger.info("Starting LSTM delay prediction")
189
+ if model is None or scaler is None:
190
+ logger.error("Model or scaler not loaded; falling back to default")
191
+ return {
192
+ "project": input_data.get("project_name", "Unnamed Project"),
193
+ "phase": input_data.get("phase", ""),
194
+ "task": input_data.get("task", ""),
195
+ "delay_probability": 50.0,
196
+ "ai_insights": "Model unavailable; please check deployment.",
197
+ "high_risk_phases": [],
198
+ "weather_condition": "Unknown"
199
+ }
200
+
201
  phase = input_data.get("phase", "")
202
  task = input_data.get("task", "")
203
+ weather_condition = input_data.get("weather_condition", get_weather_condition(input_data.get("weather_impact_score", 0)))
204
+
205
+ # Prepare input features
206
+ phase_mapping = {"Planning": 0, "Design": 1, "Construction": 2}
207
+ skill_mapping = {"Low": 0, "Medium": 1, "High": 2}
208
+ try:
209
+ features = np.array([[
210
+ input_data.get("current_progress", 0),
211
+ input_data.get("task_expected_duration", 0),
212
+ input_data.get("task_actual_duration", 0),
213
+ input_data.get("workforce_gap", 0),
214
+ input_data.get("weather_impact_score", 0),
215
+ skill_mapping.get(input_data.get("workforce_skill_level", "Medium"), 1),
216
+ phase_mapping.get(phase, 0)
217
+ ]])
218
+ except KeyError as e:
219
+ logger.error(f"Invalid input data: {str(e)}")
220
+ return {
221
+ "project": input_data.get("project_name", "Unnamed Project"),
222
+ "phase": phase,
223
+ "task": task,
224
+ "delay_probability": 50.0,
225
+ "ai_insights": f"Invalid input: {str(e)}",
226
+ "high_risk_phases": [],
227
+ "weather_condition": weather_condition
228
+ }
229
+
230
+ # Standardize and reshape
231
+ try:
232
+ features_scaled = scaler.transform(features)
233
+ features_tensor = torch.tensor(features_scaled.reshape(1, 1, -1), dtype=torch.float32).to(device)
234
+ except Exception as e:
235
+ logger.error(f"Feature preprocessing failed: {str(e)}")
236
+ return {
237
+ "project": input_data.get("project_name", "Unnamed Project"),
238
+ "phase": phase,
239
+ "task": task,
240
+ "delay_probability": 50.0,
241
+ "ai_insights": f"Preprocessing error: {str(e)}",
242
+ "high_risk_phases": [],
243
+ "weather_condition": weather_condition
244
+ }
245
 
246
+ # Predict
247
+ with torch.no_grad():
248
+ delay_risk = model(features_tensor).cpu().numpy().item()
249
+ delay_risk = round(max(0, min(delay_risk, 100)), 1)
250
 
251
+ # Generate high_risk_phases
252
  task_options = {
253
  "Planning": ["Define Scope", "Resource Allocation", "Permit Acquisition"],
254
  "Design": ["Architectural Drafting", "Engineering Analysis", "Design Review"],
255
  "Construction": ["Foundation Work", "Structural Build", "Utility Installation"]
256
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  high_risk_phases = []
258
  if phase in task_options:
259
  for t in task_options[phase]:
260
  task_risk = delay_risk
261
  if t != task:
262
+ task_risk = min(max(task_risk + (hash(t) % 10 - 5), 0), 100)
263
  high_risk_phases.append({
264
  "phase": phase,
265
  "task": t,
266
  "risk": round(task_risk, 1)
267
  })
268
 
269
+ # Generate insights
270
  insights = call_ai_model_for_insights(input_data, delay_risk)
271
 
272
  logger.info(f"Prediction completed: Delay risk = {delay_risk:.1f}%")
 
274
  "project": input_data.get("project_name", "Unnamed Project"),
275
  "phase": phase,
276
  "task": task,
277
+ "delay_probability": delay_risk,
278
  "ai_insights": "; ".join(insights) if insights else "No significant delay factors detected.",
279
  "high_risk_phases": high_risk_phases,
280
  "weather_condition": weather_condition