Rajan Sharma commited on
Commit
e324bb0
·
verified ·
1 Parent(s): 8af0435

Update schema_mapper.py

Browse files
Files changed (1) hide show
  1. schema_mapper.py +105 -388
schema_mapper.py CHANGED
@@ -1,411 +1,128 @@
1
- from __future__ import annotations
 
2
  import re
3
- from dataclasses import dataclass, field
4
- from typing import Dict, List, Any, Tuple, Optional, Set
5
- import pandas as pd
6
- from data_registry import DataRegistry
7
 
8
- # Generic concept patterns that work across domains
9
- UNIVERSAL_CONCEPT_PATTERNS = {
10
- # Entity/grouping concepts
11
- "facility": [r"\bfacilit(y|ies)\b", r"\bhospital\b", r"\bsite\b", r"\bcentre\b", r"\bcenter\b", r"\blocation\b", r"\bprovider\b"],
12
- "organization": [r"\borganization\b", r"\bcompany\b", r"\bbusiness\b", r"\bfirm\b", r"\bentity\b"],
13
- "department": [r"\bdepartment\b", r"\bdivision\b", r"\bunit\b", r"\bsection\b"],
14
- "specialty": [r"\bspecialt(y|ies)\b", r"\bservice\b", r"\btype\b", r"\bcategory\b", r"\bkind\b"],
15
- "region": [r"\bzone\b", r"\bregion\b", r"\barea\b", r"\bdistrict\b", r"\bterritory\b"],
16
-
17
- # Time-based metrics
18
- "wait_time": [r"\bwait", r"\bdelay", r"\btime", r"\bduration", r"\blength"],
19
- "wait_median": [r"\bmedian\b.*\bwait", r"\bP50\b", r"\bwait.*\bmedian", r"median.*time"],
20
- "wait_p90": [r"\bp90\b", r"\b90(th)?\s*percentile\b", r"\bwait.*p90", r"90.*wait"],
21
- "response_time": [r"\bresponse\b.*\btime\b", r"\bprocessing\b.*\btime\b"],
22
-
23
- # Performance metrics
24
- "score": [r"\bscore\b", r"\brating\b", r"\bindex\b", r"\brank\b"],
25
- "efficiency": [r"\befficiency\b", r"\bthroughput\b", r"\bproductivity\b"],
26
- "quality": [r"\bquality\b", r"\bperformance\b", r"\boutcome\b"],
27
- "satisfaction": [r"\bsatisfaction\b", r"\bfeedback\b", r"\brating\b"],
28
 
29
  # Capacity metrics
30
- "capacity": [r"\bcapacity\b", r"\bvolume\b", r"\bsize\b", r"\blimit\b"],
31
- "utilization": [r"\butilization\b", r"\boccupancy\b", r"\busage\b"],
32
- "availability": [r"\bavailab\w+", r"\bopen\b", r"\bfree\b"],
33
-
34
- # Cost/financial metrics
35
- "cost": [r"\bcost\b", r"\bprice\b", r"\bexpense\b", r"\bfee\b", r"\bcharge\b"],
36
- "budget": [r"\bbudget\b", r"\bfunding\b", r"\ballocation\b"],
37
- "revenue": [r"\brevenue\b", r"\bincome\b", r"\bearnings\b"],
38
-
39
- # Count/volume metrics
40
- "count": [r"\bcount\b", r"\bnumber\b", r"\bquantity\b", r"\btotal\b"],
41
- "rate": [r"\brate\b", r"\bratio\b", r"\bpercent\b", r"\bfrequency\b"],
42
- "volume": [r"\bvolume\b", r"\bamount\b", r"\bquantity\b"]
 
 
 
43
  }
44
 
45
- def _extract_key_terms_from_scenario(scenario_text: str) -> Set[str]:
46
- """Extract important terms from scenario text to guide concept detection."""
47
- if not scenario_text:
48
- return set()
49
-
50
- # Extract meaningful words, filtering out common stop words
51
- stop_words = {
52
- 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
53
- 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did',
54
- 'a', 'an', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they'
55
- }
56
-
57
- words = re.findall(r'\b[a-zA-Z]{3,}\b', scenario_text.lower())
58
- key_terms = {word for word in words if word not in stop_words}
59
-
60
- return key_terms
61
-
62
- def _generate_dynamic_patterns(scenario_terms: Set[str], existing_patterns: Dict[str, List[str]]) -> Dict[str, List[str]]:
63
- """Generate additional concept patterns based on scenario content."""
64
- dynamic_patterns = existing_patterns.copy()
65
-
66
- # Add scenario-specific terms as potential concepts
67
- for term in scenario_terms:
68
- if len(term) >= 4: # Only meaningful terms
69
- # Check if term relates to existing concepts
70
- term_pattern = rf"\b{re.escape(term)}\b"
71
-
72
- # Add as potential entity if it sounds like one
73
- if any(indicator in term for indicator in ['hospital', 'clinic', 'school', 'department', 'facility']):
74
- if 'facility' not in dynamic_patterns:
75
- dynamic_patterns['facility'] = []
76
- dynamic_patterns['facility'].append(term_pattern)
77
-
78
- # Add as potential metric if it sounds like one
79
- elif any(indicator in term for indicator in ['time', 'score', 'rate', 'cost', 'wait']):
80
- concept_key = f"metric_{term}"
81
- dynamic_patterns[concept_key] = [term_pattern]
82
-
83
- return dynamic_patterns
84
-
85
- def _score_column_match(col_name: str, patterns: List[str], scenario_terms: Set[str] = None) -> int:
86
- """Score how well a column matches concept patterns."""
87
- col_lower = col_name.lower()
88
- score = 0
89
-
90
- # Pattern matching
91
- for i, pattern in enumerate(patterns):
92
- if re.search(pattern, col_lower):
93
- score += 100 - (i * 10) # Higher score for earlier patterns
94
- break
95
-
96
- # Boost score if column name contains scenario-relevant terms
97
- if scenario_terms:
98
- for term in scenario_terms:
99
- if term in col_lower:
100
- score += 25
101
-
102
- return score
103
-
104
- def _detect_column_types(df: pd.DataFrame) -> Dict[str, str]:
105
- """Detect the likely type/purpose of each column."""
106
- column_types = {}
107
-
108
- for col in df.columns:
109
- col_lower = col.lower()
110
-
111
- # Detect numeric columns that could be converted
112
- sample = df[col].dropna().head(50)
113
- numeric_convertible = False
114
- if len(sample) > 0:
115
- try:
116
- numeric_sample = pd.to_numeric(sample, errors='coerce')
117
- if numeric_sample.notna().sum() > len(sample) * 0.7:
118
- numeric_convertible = True
119
- except:
120
- pass
121
-
122
- # Categorize columns
123
- if numeric_convertible:
124
- if any(term in col_lower for term in ['id', 'number', 'code', 'index']):
125
- column_types[col] = 'identifier'
126
- elif any(term in col_lower for term in ['time', 'date', 'duration', 'wait', 'delay']):
127
- column_types[col] = 'time_metric'
128
- elif any(term in col_lower for term in ['cost', 'price', 'budget', 'fee', 'expense']):
129
- column_types[col] = 'cost_metric'
130
- elif any(term in col_lower for term in ['count', 'number', 'quantity', 'volume']):
131
- column_types[col] = 'count_metric'
132
- elif any(term in col_lower for term in ['rate', 'ratio', 'percent', 'score']):
133
- column_types[col] = 'performance_metric'
134
- else:
135
- column_types[col] = 'numeric_metric'
136
- else:
137
- # String/categorical columns
138
- unique_ratio = df[col].nunique() / len(df)
139
- if unique_ratio < 0.1:
140
- column_types[col] = 'category'
141
- elif unique_ratio < 0.5:
142
- column_types[col] = 'grouping'
143
- else:
144
- column_types[col] = 'text'
145
-
146
- return column_types
147
-
148
- @dataclass
149
  class MappingResult:
150
- resolved: Dict[str, Tuple[str, str]] = field(default_factory=dict)
151
- ambiguous: Dict[str, List[Tuple[str, str]]] = field(default_factory=dict)
152
- missing: List[str] = field(default_factory=list)
153
- discovered: Dict[str, str] = field(default_factory=dict) # Discovered column types
154
 
155
- def _extract_explicit_mappings_from_scenario(scenario_text: str, available_columns: List[Tuple[str, str]]) -> Dict[str, Tuple[str, str]]:
156
- """Extract explicit column mappings from scenario text."""
157
- explicit_mappings = {}
158
-
159
- if not scenario_text:
160
- return explicit_mappings
161
-
162
- scenario_lower = scenario_text.lower()
163
-
164
- # Create a lookup of available columns (case-insensitive)
165
- column_lookup = {}
166
- for table_name, col_name in available_columns:
167
- column_lookup[col_name.lower()] = (table_name, col_name)
168
-
169
- # Pattern 1: Direct column descriptions like "Surgery_Median column contains..."
170
- column_desc_patterns = [
171
- r'(\w+)\s+column\s+(?:contains|reports|shows|includes|represents)',
172
- r'column\s+(\w+)\s+(?:contains|reports|shows|includes|represents)',
173
- r'(\w+)\s+(?:contains|reports|shows|includes|represents)'
174
- ]
175
-
176
- for pattern in column_desc_patterns:
177
- matches = re.findall(pattern, scenario_text, re.IGNORECASE)
178
- for match in matches:
179
- col_name = match.lower()
180
- if col_name in column_lookup:
181
- # Determine the concept based on context around the column name
182
- context = scenario_text[max(0, scenario_text.lower().find(col_name)-50):scenario_text.lower().find(col_name)+100].lower()
183
-
184
- if any(term in context for term in ['wait', 'time', 'delay', 'duration']):
185
- if 'median' in col_name:
186
- explicit_mappings['wait_median'] = column_lookup[col_name]
187
- elif '90' in col_name or 'percentile' in col_name:
188
- explicit_mappings['wait_p90'] = column_lookup[col_name]
189
- else:
190
- explicit_mappings['wait_time'] = column_lookup[col_name]
191
-
192
- elif any(term in context for term in ['facility', 'hospital', 'clinic', 'site']):
193
- explicit_mappings['facility'] = column_lookup[col_name]
194
-
195
- elif any(term in context for term in ['specialty', 'service', 'department']):
196
- explicit_mappings['specialty'] = column_lookup[col_name]
197
-
198
- elif any(term in context for term in ['zone', 'region', 'area', 'district']):
199
- explicit_mappings['region'] = column_lookup[col_name]
200
-
201
- # Pattern 2: Task-based column identification like "calculate average for each facility"
202
- task_patterns = [
203
- (r'(?:for each|by)\s+(\w+)', ['facility', 'specialty', 'region']),
204
- (r'(?:identify|rank|list)\s+(\w+)', ['facility', 'specialty', 'region']),
205
- (r'average\s+(\w+)\s+(?:wait|time)', ['wait_median', 'wait_time']),
206
- (r'median\s+(\w+)', ['wait_median']),
207
- (r'90th\s+percentile\s+(\w+)', ['wait_p90'])
208
- ]
209
-
210
- for pattern, concepts in task_patterns:
211
- matches = re.findall(pattern, scenario_lower)
212
- for match in matches:
213
- match_lower = match.lower()
214
- if match_lower in column_lookup:
215
- for concept in concepts:
216
- if concept not in explicit_mappings:
217
- explicit_mappings[concept] = column_lookup[match_lower]
218
- break
219
-
220
- # Pattern 3: Direct column name matches from scenario
221
- explicit_columns = re.findall(r'\b([A-Za-z_][A-Za-z0-9_]*)\b', scenario_text)
222
- for col_candidate in explicit_columns:
223
- col_lower = col_candidate.lower()
224
- if col_lower in column_lookup:
225
- # Smart concept assignment based on column name patterns
226
- if not any(concept in explicit_mappings for concept in ['facility', 'organization', 'department']):
227
- if re.search(r'facility|hospital|clinic|site|provider', col_lower):
228
- explicit_mappings['facility'] = column_lookup[col_lower]
229
-
230
- if not any(concept in explicit_mappings for concept in ['specialty', 'service']):
231
- if re.search(r'specialty|service|department|type', col_lower):
232
- explicit_mappings['specialty'] = column_lookup[col_lower]
233
-
234
- if not any(concept in explicit_mappings for concept in ['region', 'zone']):
235
- if re.search(r'zone|region|area|district', col_lower):
236
- explicit_mappings['region'] = column_lookup[col_lower]
237
-
238
- if not any(concept in explicit_mappings for concept in ['wait_median', 'wait_time']):
239
- if re.search(r'.*median.*', col_lower) and re.search(r'wait|time|surgery|consult', col_lower):
240
- explicit_mappings['wait_median'] = column_lookup[col_lower]
241
-
242
- if not any(concept in explicit_mappings for concept in ['wait_p90']):
243
- if re.search(r'.*(90|percentile).*', col_lower) and re.search(r'wait|time|surgery|consult', col_lower):
244
- explicit_mappings['wait_p90'] = column_lookup[col_lower]
245
-
246
- return explicit_mappings
247
-
248
- def _extract_explicit_tasks_from_scenario(scenario_text: str) -> List[str]:
249
- """Extract explicit task requirements from scenario text."""
250
- tasks = []
251
-
252
- if not scenario_text:
253
- return tasks
254
-
255
- scenario_lower = scenario_text.lower()
256
-
257
- # Task extraction patterns
258
- task_patterns = [
259
- r'(?:your tasks?(?:\s+are)?[:\s]+)([^.]*?)(?:\.|$)',
260
- r'(?:you (?:should|need to|are to|must)[:\s]+)([^.]*?)(?:\.|$)',
261
- r'(?:tasks?[:\s]+)([^.]*?)(?:\.|deliverables|$)',
262
- r'(?:\d+\.?\s*)([^.]*?)(?:\.|$)' # Numbered tasks
263
- ]
264
-
265
- for pattern in task_patterns:
266
- matches = re.findall(pattern, scenario_text, re.IGNORECASE | re.DOTALL)
267
- for match in matches:
268
- task = match.strip()
269
- if len(task) > 10 and any(verb in task.lower() for verb in ['identify', 'calculate', 'analyze', 'compare', 'assess', 'determine', 'rank', 'list']):
270
- tasks.append(task)
271
-
272
- return tasks
273
-
274
- def map_concepts(scenario_text: str, registry: DataRegistry) -> MappingResult:
275
- """Enhanced mapping that extracts explicit information from scenario text."""
276
  result = MappingResult()
277
 
278
- if not registry.names():
279
- result.missing = list(UNIVERSAL_CONCEPT_PATTERNS.keys())
280
- return result
281
-
282
  # Extract key terms from scenario
283
- scenario_terms = _extract_key_terms_from_scenario(scenario_text)
284
-
285
- # Collect all available columns
286
- all_columns = []
287
- for table in registry.iter_tables():
288
- # Detect column types for this table
289
- column_types = _detect_column_types(table.df)
290
- result.discovered.update({f"{table.name}.{col}": col_type for col, col_type in column_types.items()})
291
-
292
- for col in table.df.columns:
293
- all_columns.append((table.name, str(col)))
294
-
295
- # STEP 1: Extract explicit mappings from scenario text
296
- explicit_mappings = _extract_explicit_mappings_from_scenario(scenario_text, all_columns)
297
-
298
- # STEP 2: Use explicit mappings first
299
- for concept, (table_name, col_name) in explicit_mappings.items():
300
- result.resolved[concept] = (table_name, col_name)
301
-
302
- # STEP 3: For unmapped concepts, use pattern matching with scenario context
303
- remaining_patterns = {k: v for k, v in UNIVERSAL_CONCEPT_PATTERNS.items() if k not in result.resolved}
304
 
305
- if remaining_patterns:
306
- # Generate dynamic patterns based on scenario
307
- concept_patterns = _generate_dynamic_patterns(scenario_terms, remaining_patterns)
 
308
 
309
- # Map remaining concepts to columns
310
- for concept, patterns in concept_patterns.items():
311
- if concept in result.resolved:
312
- continue # Skip already resolved
313
-
314
- scores = [
315
- ((tbl, col), _score_column_match(col, patterns, scenario_terms))
316
- for (tbl, col) in all_columns
317
- ]
318
-
319
- scores.sort(key=lambda x: x[1], reverse=True)
320
-
321
- if not scores or scores[0][1] == 0:
322
- result.missing.append(concept)
323
- continue
324
-
325
- top_score = scores[0][1]
326
-
327
- # Find all columns with similar high scores (potential ambiguity)
328
- threshold = max(70, top_score - 15) # Higher threshold for explicit scenarios
329
- high_scoring = [pair for pair, score in scores if score >= threshold]
330
-
331
- if len(high_scoring) == 1:
332
- tbl, col = high_scoring[0]
333
- result.resolved[concept] = (tbl, col)
334
  else:
335
- # Check if scenario text makes disambiguation obvious
336
- disambiguated = False
337
- for (tbl, col), score in scores[:3]: # Check top 3
338
- col_mentioned = col.lower() in scenario_text.lower()
339
- if col_mentioned and score >= threshold:
340
- result.resolved[concept] = (tbl, col)
341
- disambiguated = True
342
- break
343
-
344
- if not disambiguated:
345
- result.ambiguous[concept] = high_scoring[:3] # Limit to top 3
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  return result
348
 
349
- def build_phase1_questions(scenario_text: str, registry: DataRegistry, mapping: MappingResult, max_questions: int = 4) -> str:
350
- """Build minimal clarifying questions, only when truly necessary."""
351
-
352
- # Extract explicit tasks from scenario
353
- explicit_tasks = _extract_explicit_tasks_from_scenario(scenario_text)
354
-
355
- # Check if scenario provides comprehensive instructions
356
- has_detailed_tasks = len(explicit_tasks) >= 3
357
- has_data_descriptions = any(term in scenario_text.lower() for term in [
358
- 'column', 'dataset', 'file', 'csv', 'records', 'contains', 'includes'
359
- ])
360
-
361
- # If scenario is comprehensive, minimize questions
362
- if has_detailed_tasks and has_data_descriptions:
363
- # Only ask about truly ambiguous mappings where scenario doesn't clarify
364
- critical_questions = []
365
-
366
- # Only ask about ambiguities that can't be resolved from context
367
- for concept, options in mapping.ambiguous.items():
368
- if len(options) > 1:
369
- # Check if scenario text clearly indicates which column to use
370
- scenario_lower = scenario_text.lower()
371
- clear_preference = None
372
-
373
- for table_name, col_name in options:
374
- if col_name.lower() in scenario_lower:
375
- mentions = scenario_lower.count(col_name.lower())
376
- if mentions > 0:
377
- clear_preference = f"{table_name}.{col_name}"
378
- break
379
-
380
- if not clear_preference and len(critical_questions) < max_questions:
381
- option_strs = [f"{tbl}.{col}" for tbl, col in options[:3]]
382
- critical_questions.append(f"**Column Clarification**: For {concept.replace('_', ' ')}, use: {', '.join(option_strs)}?")
383
-
384
- if not critical_questions:
385
- return "**Proceeding with Analysis**: Scenario and data mappings are clear. Analyzing now..."
386
-
387
- return "**Quick Clarification**\n\n" + "\n".join(critical_questions)
388
-
389
- # Fallback to standard question generation for less comprehensive scenarios
390
  questions = []
391
- scenario_lower = scenario_text.lower() if scenario_text else ""
392
 
393
- # Ambiguous mappings - ask for clarification
394
- important_concepts = ['facility', 'organization', 'department', 'specialty', 'region']
395
- for concept in important_concepts:
396
- if concept in mapping.ambiguous and len(questions) < max_questions:
397
- options = [f"{tbl}.{col}" for tbl, col in mapping.ambiguous[concept][:3]]
398
- questions.append(f"**Entity**: Which column represents {concept.replace('_', ' ')}? Options: {', '.join(options)}")
 
 
 
 
 
 
 
 
399
 
400
- # Missing critical data
401
- if len(questions) < max_questions:
402
- if not any(concept in mapping.resolved for concept in ['facility', 'organization', 'department']):
403
- questions.append("**Grouping**: What entities should be analyzed? (facilities, departments, regions, etc.)")
404
-
405
- if not any(concept in mapping.resolved for concept in ['wait_time', 'wait_median', 'score', 'performance']):
406
- questions.append("**Metric**: What is the primary metric to analyze? (wait times, scores, costs, etc.)")
407
 
408
  if not questions:
409
- return "**Analysis Ready**: Data structure understood. Proceeding with analysis..."
410
 
411
- return "**Clarification Questions**\n\n" + "\n".join(f"{i+1}. {q}" for i, q in enumerate(questions))
 
1
+ # schema_mapper.py
2
+ from typing import Dict, List, Any, Set
3
  import re
 
 
 
 
4
 
5
+ # Healthcare terminology mappings
6
+ HEALTHCARE_CONCEPTS = {
7
+ # Facility types
8
+ "hospital": ["hospital", "medical center", "health centre", "clinic"],
9
+ "nursing_facility": ["nursing home", "long-term care", "residential care", "care facility"],
10
+ "ambulatory_care": ["ambulatory", "outpatient", "clinic", "surgery center"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # Capacity metrics
13
+ "bed_capacity": ["beds", "capacity", "bed count", "staffed beds"],
14
+ "occupancy_rate": ["occupancy", "utilization", "bed occupancy"],
15
+
16
+ # Geographic terms
17
+ "zone": ["zone", "region", "area", "district"],
18
+ "province": ["province", "state", "territory"],
19
+
20
+ # Time periods
21
+ "fiscal_year": ["fiscal year", "fy", "financial year"],
22
+ "current_period": ["current", "2023-24", "present", "latest"],
23
+ "previous_period": ["previous", "2022-23", "past", "last"],
24
+
25
+ # Healthcare operations
26
+ "patient_flow": ["patient flow", "throughput", "patient movement"],
27
+ "resource_allocation": ["resource allocation", "staffing", "resource distribution"],
28
+ "surge_capacity": ["surge", "overflow", "emergency capacity"],
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  class MappingResult:
32
+ def __init__(self):
33
+ self.resolved = {} # Successfully mapped concepts
34
+ self.ambiguous = {} # Concepts with multiple possible mappings
35
+ self.missing = set() # Concepts that couldn't be mapped
36
 
37
+ def map_concepts(scenario_text: str, data_registry) -> MappingResult:
38
+ """Map healthcare concepts from scenario text to data registry."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  result = MappingResult()
40
 
 
 
 
 
41
  # Extract key terms from scenario
42
+ scenario_lower = scenario_text.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ # Check for healthcare concepts
45
+ for concept, synonyms in HEALTHCARE_CONCEPTS.items():
46
+ # Check if any synonym appears in the scenario
47
+ found_synonyms = [syn for syn in synonyms if syn in scenario_lower]
48
 
49
+ if found_synonyms:
50
+ # Try to map to data registry
51
+ mapped_to = _map_to_data_registry(concept, data_registry)
52
+ if mapped_to:
53
+ result.resolved[concept] = mapped_to
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  else:
55
+ result.missing.add(concept)
56
+
57
+ # Additional mapping for specific healthcare patterns
58
+ # Check for facility distribution patterns
59
+ if any(phrase in scenario_lower for phrase in ["facility distribution", "facility count", "number of facilities"]):
60
+ if any("facility" in name.lower() for name in data_registry.names()):
61
+ result.resolved["facility_distribution"] = next(
62
+ (name for name in data_registry.names() if "facility" in name.lower()), None
63
+ )
64
+ else:
65
+ result.missing.add("facility_distribution")
66
+
67
+ # Check for bed capacity patterns
68
+ if any(phrase in scenario_lower for phrase in ["bed capacity", "bed count", "staffed beds"]):
69
+ if any("bed" in name.lower() for name in data_registry.names()):
70
+ result.resolved["bed_capacity"] = next(
71
+ (name for name in data_registry.names() if "bed" in name.lower()), None
72
+ )
73
+ else:
74
+ result.missing.add("bed_capacity")
75
+
76
+ # Check for long-term care patterns
77
+ if any(phrase in scenario_lower for phrase in ["long-term care", "ltc", "nursing capacity"]):
78
+ result.resolved["long_term_care"] = "facility_distribution" # Usually in facility data
79
 
80
  return result
81
 
82
+ def _map_to_data_registry(concept: str, data_registry) -> Any:
83
+ """Helper to map a concept to the data registry."""
84
+ file_names = data_registry.names()
85
+
86
+ if concept in ["hospital", "facility_distribution", "long_term_care"]:
87
+ return next((name for name in file_names if "facility" in name.lower() or "health" in name.lower()), None)
88
+ elif concept == "bed_capacity":
89
+ return next((name for name in file_names if "bed" in name.lower()), None)
90
+ elif concept == "zone":
91
+ # Check if any dataframe has a 'zone' column
92
+ for name in file_names:
93
+ df = data_registry.get(name)
94
+ if df is not None and 'zone' in df.columns:
95
+ return name
96
+ return None
97
+
98
+ return None
99
+
100
+ def build_phase1_questions(scenario_text: str, registry, mapping: MappingResult) -> str:
101
+ """Build clarifying questions based on mapping results."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  questions = []
 
103
 
104
+ # If we have good mapping, we might not need questions
105
+ if len(mapping.resolved) > len(mapping.missing) and len(mapping.ambiguous) == 0:
106
+ return "**Data Analysis Ready**: Your data appears well-structured. Please provide any additional context about your analysis goals."
107
+
108
+ # Questions for missing concepts
109
+ if mapping.missing:
110
+ questions.append("### Missing Information")
111
+ for concept in mapping.missing:
112
+ if concept == "facility_distribution":
113
+ questions.append("- Do you have data about healthcare facilities and their distribution?")
114
+ elif concept == "bed_capacity":
115
+ questions.append("- Do you have data about hospital bed capacity and changes over time?")
116
+ else:
117
+ questions.append(f"- Can you provide more information about {concept}?")
118
 
119
+ # Questions for ambiguous concepts
120
+ if mapping.ambiguous:
121
+ questions.append("### Clarification Needed")
122
+ for concept, options in mapping.ambiguous.items():
123
+ questions.append(f"- For '{concept}', did you mean: {', '.join(options)}?")
 
 
124
 
125
  if not questions:
126
+ return "**Data Analysis Ready**: Your data appears well-structured. Please provide any additional context about your analysis goals."
127
 
128
+ return "\n".join(questions)