SmartHeal commited on
Commit
da9c442
·
verified ·
1 Parent(s): ad8f4a6

Update src/ai_processor.py

Browse files
Files changed (1) hide show
  1. src/ai_processor.py +125 -101
src/ai_processor.py CHANGED
@@ -17,7 +17,6 @@ import spaces
17
  from .config import Config
18
  import os
19
 
20
- # Inline system prompt for MedGemma GPU pipeline
21
  default_system_prompt = (
22
  "You are a world-class medical AI assistant specializing in wound care "
23
  "with expertise in wound assessment and treatment. Provide concise, "
@@ -40,21 +39,31 @@ class AIProcessor:
40
 
41
  def _initialize_models(self):
42
  """Initialize CPU-only AI models; MedGemma is loaded on demand within GPU context."""
43
- # Set HuggingFace token early
44
  if self.config.HF_TOKEN:
45
  HfFolder.save_token(self.config.HF_TOKEN)
46
  logging.info("HuggingFace token set successfully")
47
 
48
- # YOLO detection on CPU (force CPU to avoid CUDA init)
49
  try:
50
- self.models_cache['det'] = YOLO(self.config.YOLO_MODEL_PATH, device='cpu')
 
 
 
51
  logging.info("✅ YOLO detection model loaded on CPU")
52
  except Exception as e:
53
- logging.warning(f"YOLO model not available: {e}")
 
 
 
 
54
 
55
  # Segmentation model on CPU
56
  try:
57
- self.models_cache['seg'] = load_model(self.config.SEG_MODEL_PATH, compile=False)
 
 
 
58
  logging.info("✅ Segmentation model loaded on CPU")
59
  except Exception as e:
60
  logging.warning(f"Segmentation model not available: {e}")
@@ -71,7 +80,7 @@ class AIProcessor:
71
  except Exception as e:
72
  logging.warning(f"Wound classification model not available: {e}")
73
 
74
- # Embedding for knowledge base on CPU
75
  try:
76
  self.models_cache['embedding_model'] = HuggingFaceEmbeddings(
77
  model_name='sentence-transformers/all-MiniLM-L6-v2',
@@ -81,11 +90,10 @@ class AIProcessor:
81
  except Exception as e:
82
  logging.warning(f"Embedding model not available: {e}")
83
 
84
- # Load knowledge base
85
  self._load_knowledge_base()
86
 
87
  def _load_knowledge_base(self):
88
- """Load PDF guidelines into a FAISS vector store."""
89
  docs = []
90
  for pdf in self.config.GUIDELINE_PDFS:
91
  if os.path.exists(pdf):
@@ -94,92 +102,132 @@ class AIProcessor:
94
  logging.info(f"Loaded PDF: {pdf}")
95
 
96
  if docs and 'embedding_model' in self.models_cache:
97
- splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
 
 
98
  chunks = splitter.split_documents(docs)
99
- vs = FAISS.from_documents(chunks, self.models_cache['embedding_model'])
 
 
100
  self.knowledge_base_cache['vectorstore'] = vs
101
  logging.info(f"✅ Knowledge base loaded ({len(chunks)} chunks)")
102
  else:
103
  self.knowledge_base_cache['vectorstore'] = None
104
  logging.warning("Knowledge base unavailable")
105
 
106
-
107
  def perform_visual_analysis(self, image_pil):
108
  """Detect & segment on CPU; return only paths + metrics."""
 
 
 
 
 
109
  try:
110
- img_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
111
- # YOLO detect
112
- res = self.models_cache['det'].predict(img_cv, verbose=False)[0]
 
 
 
113
  if not res.boxes:
114
  raise ValueError("No wound detected")
 
115
  # Bounding box
116
  x1, y1, x2, y2 = res.boxes.xyxy[0].cpu().numpy().astype(int)
117
  region = img_cv[y1:y2, x1:x2]
 
118
  # Save detection overlay
119
  det_vis = img_cv.copy()
120
- cv2.rectangle(det_vis, (x1, y1), (x2, y2), (0,255,0), 2)
121
  os.makedirs(f"{self.config.UPLOADS_DIR}/analysis", exist_ok=True)
122
  ts = datetime.now().strftime('%Y%m%d_%H%M%S')
123
- det_path = f"{self.config.UPLOADS_DIR}/analysis/detection_{ts}.png"
 
 
124
  cv2.imwrite(det_path, det_vis)
 
125
  # Initialize metrics & seg
126
  length = breadth = area = 0
127
  seg_path = None
128
- # Segmentation
 
129
  if 'seg' in self.models_cache:
130
  h, w = self.models_cache['seg'].input_shape[1:3]
131
- inp = cv2.resize(region, (w,h)) / 255.0
132
- mask = (self.models_cache['seg'].predict(np.expand_dims(inp,0))[0,:,:,0] > 0.5).astype(np.uint8)
133
- mask_rs = cv2.resize(mask, (region.shape[1], region.shape[0]), interpolation=cv2.INTER_NEAREST)
 
 
 
 
 
 
 
134
  ov = region.copy()
135
- ov[mask_rs==1] = [0,0,255]
136
- seg_vis = cv2.addWeighted(region,0.7,ov,0.3,0)
137
- seg_path = f"{self.config.UPLOADS_DIR}/analysis/segmentation_{ts}.png"
 
 
138
  cv2.imwrite(seg_path, seg_vis)
139
- # measure
140
- cnts, _ = cv2.findContours(mask_rs, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
 
 
 
 
141
  if cnts:
142
  cnt = max(cnts, key=cv2.contourArea)
143
- _,_,w0,h0 = cv2.boundingRect(cnt)
144
- length = round(h0/self.px_per_cm,2)
145
- breadth= round(w0/self.px_per_cm,2)
146
- area = round(cv2.contourArea(cnt)/(self.px_per_cm**2),2)
 
 
 
147
  # Classification
148
  wound_type = 'Unknown'
149
  if 'cls' in self.models_cache:
150
  try:
151
- label = self.models_cache['cls'](Image.fromarray(cv2.cvtColor(region, cv2.COLOR_BGR2RGB)))
152
- wound_type = max(label, key=lambda x: x['score'])['label']
 
 
153
  except Exception:
154
  pass
 
155
  return {
156
  'wound_type': wound_type,
157
  'length_cm': length,
158
  'breadth_cm': breadth,
159
  'surface_area_cm2': area,
160
- 'detection_confidence': float(res.boxes.conf[0].cpu().item()),
 
 
161
  'detection_image_path': det_path,
162
  'segmentation_image_path': seg_path
163
  }
 
164
  except Exception as e:
165
  logging.error(f"Visual analysis error: {e}")
166
  raise
167
 
168
  def query_guidelines(self, query: str):
169
- """Retrieve clinical guidelines from vectorstore."""
170
  vs = self.knowledge_base_cache.get('vectorstore')
171
  if not vs:
172
  return "Clinical guidelines unavailable"
173
  docs = vs.as_retriever(search_kwargs={'k':10}).invoke(query)
174
  return '\n\n'.join(
175
- f"Source: {d.metadata.get('source','?')}, Page: {d.metadata.get('page','?')}\n{d.page_content}"
 
176
  for d in docs
177
  )
178
 
179
  @spaces.GPU(enable_queue=True, duration=120)
180
- def generate_final_report(self, patient_info, visual_results, guideline_context, image_pil, max_new_tokens=None):
 
 
181
  """Run MedGemma on GPU; return markdown report."""
182
- # lazy-load MedGemma pipeline here to avoid CUDA init in main process
183
  if 'medgemma_pipe' not in self.models_cache:
184
  try:
185
  self.models_cache['medgemma_pipe'] = pipeline(
@@ -193,22 +241,26 @@ class AIProcessor:
193
  logging.info("✅ MedGemma pipeline loaded on GPU")
194
  except Exception as e:
195
  logging.warning(f"MedGemma pipeline not available: {e}")
196
- return self._generate_fallback_report(patient_info, visual_results, guideline_context)
 
 
197
 
198
- # build messages
199
  msgs = [
200
  {'role':'system','content':[{'type':'text','text':default_system_prompt}]},
201
  {'role':'user','content':[]}
202
  ]
203
- # images
204
  if image_pil:
205
  msgs[1]['content'].append({'type':'image','image':image_pil})
206
  for key in ('detection_image_path','segmentation_image_path'):
207
  p = visual_results.get(key)
208
  if p and os.path.exists(p):
209
- msgs[1]['content'].append({'type':'image','image':Image.open(p)})
210
- # text prompt
211
- prompt = f"## Patient\n{patient_info}\n## Wound Type: {visual_results['wound_type']}"
 
 
 
 
212
  msgs[1]['content'].append({'type':'text','text':prompt})
213
 
214
  out = self.models_cache['medgemma_pipe'](
@@ -222,7 +274,6 @@ class AIProcessor:
222
  )
223
 
224
  def _generate_fallback_report(self, patient_info, visual_results, guideline_context):
225
- """Produce text-only fallback."""
226
  dp = visual_results.get('detection_image_path','N/A')
227
  sp = visual_results.get('segmentation_image_path','N/A')
228
  return (
@@ -234,12 +285,11 @@ class AIProcessor:
234
  )
235
 
236
  def save_and_commit_image(self, image_pil):
237
- """Save locally and optionally to HuggingFace."""
238
  os.makedirs(self.config.UPLOADS_DIR, exist_ok=True)
239
  fn = f"{datetime.now():%Y%m%d_%H%M%S}.png"
240
  path = os.path.join(self.config.UPLOADS_DIR, fn)
241
  image_pil.convert('RGB').save(path)
242
- if self.config.HF_TOKEN and hasattr(self.config,'DATASET_ID') and self.config.DATASET_ID:
243
  try:
244
  api = HfApi()
245
  api.upload_file(
@@ -253,77 +303,51 @@ class AIProcessor:
253
  return path
254
 
255
  def full_analysis_pipeline(self, image_pil, questionnaire_data):
256
- """Orchestrate CPU steps + GPU report."""
257
  try:
258
  saved = self.save_and_commit_image(image_pil)
259
  vis = self.perform_visual_analysis(image_pil)
260
- info = ", ".join(f"{k}:{v}" for k,v in questionnaire_data.items() if v)
 
 
261
  gc = self.query_guidelines(info)
262
  report = self.generate_final_report(info, vis, gc, image_pil)
263
- return {'success':True,'visual_analysis':vis,'report':report,'saved_image_path':saved}
 
 
 
 
 
264
  except Exception as e:
265
  logging.error(f"Pipeline error: {e}")
266
- return {'success':False,'error':str(e)}
267
 
268
  def analyze_wound(self, image, questionnaire_data):
269
- """Legacy wrapper."""
270
- if isinstance(image,str):
271
  image = Image.open(image)
272
  return self.full_analysis_pipeline(image, questionnaire_data)
273
 
274
  def _assess_risk_legacy(self, questionnaire_data):
275
- """Legacy risk assessment for backward compatibility"""
276
- risk_factors = []
277
- risk_score = 0
278
-
279
  try:
280
- # Age factor
281
  age = questionnaire_data.get('patient_age', 0)
282
  if age > 65:
283
- risk_factors.append("Advanced age (>65)")
284
- risk_score += 2
285
  elif age > 50:
286
- risk_factors.append("Older adult (50-65)")
287
- risk_score += 1
288
-
289
- # Duration factor
290
- duration = questionnaire_data.get('wound_duration', '').lower()
291
- if any(term in duration for term in ['month', 'months', 'year']):
292
- risk_factors.append("Chronic wound (>4 weeks)")
293
- risk_score += 3
294
-
295
- # Pain level
296
- pain_level = questionnaire_data.get('pain_level', 0)
297
- if pain_level >= 7:
298
- risk_factors.append("High pain level")
299
- risk_score += 2
300
-
301
- # Medical history risk factors
302
- medical_history = questionnaire_data.get('medical_history', '').lower()
303
- if 'diabetes' in medical_history:
304
- risk_factors.append("Diabetes mellitus")
305
- risk_score += 3
306
- if 'circulation' in medical_history or 'vascular' in medical_history:
307
- risk_factors.append("Vascular/circulation issues")
308
- risk_score += 2
309
- if 'immune' in medical_history:
310
- risk_factors.append("Immune system compromise")
311
- risk_score += 2
312
-
313
- # Determine risk level
314
- if risk_score >= 7:
315
- risk_level = "High"
316
- elif risk_score >= 4:
317
- risk_level = "Moderate"
318
- else:
319
- risk_level = "Low"
320
-
321
- return {
322
- 'risk_score': risk_score,
323
- 'risk_level': risk_level,
324
- 'risk_factors': risk_factors
325
- }
326
-
327
  except Exception as e:
328
  logging.error(f"Risk assessment error: {e}")
329
- return {'risk_score': 0, 'risk_level': 'Unknown', 'risk_factors': []}
 
17
  from .config import Config
18
  import os
19
 
 
20
  default_system_prompt = (
21
  "You are a world-class medical AI assistant specializing in wound care "
22
  "with expertise in wound assessment and treatment. Provide concise, "
 
39
 
40
  def _initialize_models(self):
41
  """Initialize CPU-only AI models; MedGemma is loaded on demand within GPU context."""
42
+ # Set HF token
43
  if self.config.HF_TOKEN:
44
  HfFolder.save_token(self.config.HF_TOKEN)
45
  logging.info("HuggingFace token set successfully")
46
 
47
+ # YOLO detection on CPU (force CPU)
48
  try:
49
+ self.models_cache['det'] = YOLO(
50
+ self.config.YOLO_MODEL_PATH,
51
+ device='cpu'
52
+ )
53
  logging.info("✅ YOLO detection model loaded on CPU")
54
  except Exception as e:
55
+ logging.error(
56
+ f"Failed to load YOLO model at '{self.config.YOLO_MODEL_PATH}': {e}"
57
+ )
58
+ # fail fast so you’ll immediately see why 'det' was never set
59
+ raise
60
 
61
  # Segmentation model on CPU
62
  try:
63
+ self.models_cache['seg'] = load_model(
64
+ self.config.SEG_MODEL_PATH,
65
+ compile=False
66
+ )
67
  logging.info("✅ Segmentation model loaded on CPU")
68
  except Exception as e:
69
  logging.warning(f"Segmentation model not available: {e}")
 
80
  except Exception as e:
81
  logging.warning(f"Wound classification model not available: {e}")
82
 
83
+ # Embeddings for KB on CPU
84
  try:
85
  self.models_cache['embedding_model'] = HuggingFaceEmbeddings(
86
  model_name='sentence-transformers/all-MiniLM-L6-v2',
 
90
  except Exception as e:
91
  logging.warning(f"Embedding model not available: {e}")
92
 
93
+ # Load PDF guidelines
94
  self._load_knowledge_base()
95
 
96
  def _load_knowledge_base(self):
 
97
  docs = []
98
  for pdf in self.config.GUIDELINE_PDFS:
99
  if os.path.exists(pdf):
 
102
  logging.info(f"Loaded PDF: {pdf}")
103
 
104
  if docs and 'embedding_model' in self.models_cache:
105
+ splitter = RecursiveCharacterTextSplitter(
106
+ chunk_size=1000, chunk_overlap=100
107
+ )
108
  chunks = splitter.split_documents(docs)
109
+ vs = FAISS.from_documents(
110
+ chunks, self.models_cache['embedding_model']
111
+ )
112
  self.knowledge_base_cache['vectorstore'] = vs
113
  logging.info(f"✅ Knowledge base loaded ({len(chunks)} chunks)")
114
  else:
115
  self.knowledge_base_cache['vectorstore'] = None
116
  logging.warning("Knowledge base unavailable")
117
 
 
118
  def perform_visual_analysis(self, image_pil):
119
  """Detect & segment on CPU; return only paths + metrics."""
120
+ if 'det' not in self.models_cache:
121
+ raise RuntimeError(
122
+ "YOLO detection model ('det') not loaded; cannot perform visual analysis"
123
+ )
124
+
125
  try:
126
+ img_cv = cv2.cvtColor(
127
+ np.array(image_pil), cv2.COLOR_RGB2BGR
128
+ )
129
+ res = self.models_cache['det'].predict(
130
+ img_cv, verbose=False
131
+ )[0]
132
  if not res.boxes:
133
  raise ValueError("No wound detected")
134
+
135
  # Bounding box
136
  x1, y1, x2, y2 = res.boxes.xyxy[0].cpu().numpy().astype(int)
137
  region = img_cv[y1:y2, x1:x2]
138
+
139
  # Save detection overlay
140
  det_vis = img_cv.copy()
141
+ cv2.rectangle(det_vis, (x1, y1), (x2, y2), (0, 255, 0), 2)
142
  os.makedirs(f"{self.config.UPLOADS_DIR}/analysis", exist_ok=True)
143
  ts = datetime.now().strftime('%Y%m%d_%H%M%S')
144
+ det_path = (
145
+ f"{self.config.UPLOADS_DIR}/analysis/detection_{ts}.png"
146
+ )
147
  cv2.imwrite(det_path, det_vis)
148
+
149
  # Initialize metrics & seg
150
  length = breadth = area = 0
151
  seg_path = None
152
+
153
+ # Segmentation (if available)
154
  if 'seg' in self.models_cache:
155
  h, w = self.models_cache['seg'].input_shape[1:3]
156
+ inp = cv2.resize(region, (w, h)) / 255.0
157
+ mask = (
158
+ self.models_cache['seg']
159
+ .predict(np.expand_dims(inp, 0))[0, :, :, 0] > 0.5
160
+ ).astype(np.uint8)
161
+ mask_rs = cv2.resize(
162
+ mask,
163
+ (region.shape[1], region.shape[0]),
164
+ interpolation=cv2.INTER_NEAREST
165
+ )
166
  ov = region.copy()
167
+ ov[mask_rs == 1] = [0, 0, 255]
168
+ seg_vis = cv2.addWeighted(region, 0.7, ov, 0.3, 0)
169
+ seg_path = (
170
+ f"{self.config.UPLOADS_DIR}/analysis/segmentation_{ts}.png"
171
+ )
172
  cv2.imwrite(seg_path, seg_vis)
173
+
174
+ cnts, _ = cv2.findContours(
175
+ mask_rs,
176
+ cv2.RETR_EXTERNAL,
177
+ cv2.CHAIN_APPROX_SIMPLE
178
+ )
179
  if cnts:
180
  cnt = max(cnts, key=cv2.contourArea)
181
+ _, _, w0, h0 = cv2.boundingRect(cnt)
182
+ length = round(h0 / self.px_per_cm, 2)
183
+ breadth = round(w0 / self.px_per_cm, 2)
184
+ area = round(
185
+ cv2.contourArea(cnt) / (self.px_per_cm ** 2), 2
186
+ )
187
+
188
  # Classification
189
  wound_type = 'Unknown'
190
  if 'cls' in self.models_cache:
191
  try:
192
+ labels = self.models_cache['cls'](
193
+ Image.fromarray(cv2.cvtColor(region, cv2.COLOR_BGR2RGB))
194
+ )
195
+ wound_type = max(labels, key=lambda x: x['score'])['label']
196
  except Exception:
197
  pass
198
+
199
  return {
200
  'wound_type': wound_type,
201
  'length_cm': length,
202
  'breadth_cm': breadth,
203
  'surface_area_cm2': area,
204
+ 'detection_confidence': float(
205
+ res.boxes.conf[0].cpu().item()
206
+ ),
207
  'detection_image_path': det_path,
208
  'segmentation_image_path': seg_path
209
  }
210
+
211
  except Exception as e:
212
  logging.error(f"Visual analysis error: {e}")
213
  raise
214
 
215
  def query_guidelines(self, query: str):
 
216
  vs = self.knowledge_base_cache.get('vectorstore')
217
  if not vs:
218
  return "Clinical guidelines unavailable"
219
  docs = vs.as_retriever(search_kwargs={'k':10}).invoke(query)
220
  return '\n\n'.join(
221
+ f"Source: {d.metadata.get('source','?')}, Page: {d.metadata.get('page','?')}\n"
222
+ f"{d.page_content}"
223
  for d in docs
224
  )
225
 
226
  @spaces.GPU(enable_queue=True, duration=120)
227
+ def generate_final_report(
228
+ self, patient_info, visual_results, guideline_context, image_pil, max_new_tokens=None
229
+ ):
230
  """Run MedGemma on GPU; return markdown report."""
 
231
  if 'medgemma_pipe' not in self.models_cache:
232
  try:
233
  self.models_cache['medgemma_pipe'] = pipeline(
 
241
  logging.info("✅ MedGemma pipeline loaded on GPU")
242
  except Exception as e:
243
  logging.warning(f"MedGemma pipeline not available: {e}")
244
+ return self._generate_fallback_report(
245
+ patient_info, visual_results, guideline_context
246
+ )
247
 
 
248
  msgs = [
249
  {'role':'system','content':[{'type':'text','text':default_system_prompt}]},
250
  {'role':'user','content':[]}
251
  ]
 
252
  if image_pil:
253
  msgs[1]['content'].append({'type':'image','image':image_pil})
254
  for key in ('detection_image_path','segmentation_image_path'):
255
  p = visual_results.get(key)
256
  if p and os.path.exists(p):
257
+ msgs[1]['content'].append(
258
+ {'type':'image','image':Image.open(p)}
259
+ )
260
+ prompt = (
261
+ f"## Patient\n{patient_info}\n"
262
+ f"## Wound Type: {visual_results['wound_type']}"
263
+ )
264
  msgs[1]['content'].append({'type':'text','text':prompt})
265
 
266
  out = self.models_cache['medgemma_pipe'](
 
274
  )
275
 
276
  def _generate_fallback_report(self, patient_info, visual_results, guideline_context):
 
277
  dp = visual_results.get('detection_image_path','N/A')
278
  sp = visual_results.get('segmentation_image_path','N/A')
279
  return (
 
285
  )
286
 
287
  def save_and_commit_image(self, image_pil):
 
288
  os.makedirs(self.config.UPLOADS_DIR, exist_ok=True)
289
  fn = f"{datetime.now():%Y%m%d_%H%M%S}.png"
290
  path = os.path.join(self.config.UPLOADS_DIR, fn)
291
  image_pil.convert('RGB').save(path)
292
+ if self.config.HF_TOKEN and getattr(self.config,'DATASET_ID', None):
293
  try:
294
  api = HfApi()
295
  api.upload_file(
 
303
  return path
304
 
305
  def full_analysis_pipeline(self, image_pil, questionnaire_data):
 
306
  try:
307
  saved = self.save_and_commit_image(image_pil)
308
  vis = self.perform_visual_analysis(image_pil)
309
+ info = ", ".join(
310
+ f"{k}:{v}" for k,v in questionnaire_data.items() if v
311
+ )
312
  gc = self.query_guidelines(info)
313
  report = self.generate_final_report(info, vis, gc, image_pil)
314
+ return {
315
+ 'success': True,
316
+ 'visual_analysis': vis,
317
+ 'report': report,
318
+ 'saved_image_path': saved
319
+ }
320
  except Exception as e:
321
  logging.error(f"Pipeline error: {e}")
322
+ return {'success': False, 'error': str(e)}
323
 
324
  def analyze_wound(self, image, questionnaire_data):
325
+ if isinstance(image, str):
 
326
  image = Image.open(image)
327
  return self.full_analysis_pipeline(image, questionnaire_data)
328
 
329
  def _assess_risk_legacy(self, questionnaire_data):
330
+ risk_factors, risk_score = [], 0
 
 
 
331
  try:
 
332
  age = questionnaire_data.get('patient_age', 0)
333
  if age > 65:
334
+ risk_factors.append("Advanced age (>65)"); risk_score+=2
 
335
  elif age > 50:
336
+ risk_factors.append("Older adult (50-65)"); risk_score+=1
337
+ duration = questionnaire_data.get('wound_duration','').lower()
338
+ if any(term in duration for term in ['month','year']):
339
+ risk_factors.append("Chronic wound (>4 weeks)"); risk_score+=3
340
+ pain = questionnaire_data.get('pain_level',0)
341
+ if pain>=7: risk_factors.append("High pain level"); risk_score+=2
342
+ hist = questionnaire_data.get('medical_history','').lower()
343
+ if 'diabetes' in hist: risk_factors.append("Diabetes mellitus"); risk_score+=3
344
+ if 'vascular' in hist: risk_factors.append("Vascular issues"); risk_score+=2
345
+ if 'immune' in hist: risk_factors.append("Immune compromise"); risk_score+=2
346
+
347
+ if risk_score>=7: level="High"
348
+ elif risk_score>=4: level="Moderate"
349
+ else: level="Low"
350
+ return {'risk_score':risk_score,'risk_level':level,'risk_factors':risk_factors}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  except Exception as e:
352
  logging.error(f"Risk assessment error: {e}")
353
+ return {'risk_score':0,'risk_level':'Unknown','risk_factors':[]}