FabsAgo commited on
Commit
68929e4
Β·
verified Β·
1 Parent(s): fd3c0ce

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +490 -0
app.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py β€” AI Visit Plan Generator (Hugging Face Spaces Version)
3
+
4
+ This is a single-file application that:
5
+ 1. Accepts free-text care referral instructions
6
+ 2. Sends them to an AI model on Hugging Face
7
+ 3. Extracts a structured Visit Plan (JSON)
8
+ 4. Displays the result in a web interface
9
+
10
+ Deployed as a Hugging Face Space with Gradio.
11
+ """
12
+
13
+ import os
14
+ import json
15
+ import re
16
+ import gradio as gr
17
+ import requests
18
+ from pydantic import BaseModel, Field
19
+ from typing import Optional
20
+ from enum import Enum
21
+
22
+
23
+ # ══════════════════════════════════════════════════════════════════
24
+ # SECTION 1: CONFIGURATION
25
+ # ══════════════════════════════════════════════════════════════════
26
+
27
+ # Read the API token from the Space's secrets (set in Settings)
28
+ HUGGINGFACE_API_TOKEN = os.environ.get("HUGGINGFACE_API_TOKEN", "")
29
+
30
+ if not HUGGINGFACE_API_TOKEN:
31
+ print("WARNING: HUGGINGFACE_API_TOKEN not found in environment.")
32
+ print("Go to your Space's Settings > Repository secrets to add it.")
33
+
34
+ # The default AI model
35
+ DEFAULT_MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
36
+
37
+
38
+ # ══════════════════════════════════════════════════════════════════
39
+ # SECTION 2: DATA MODELS (What a Visit Plan looks like)
40
+ # ══════════════════════════════════════════════════════════════════
41
+
42
+ class DayOfWeek(str, Enum):
43
+ MONDAY = "Monday"
44
+ TUESDAY = "Tuesday"
45
+ WEDNESDAY = "Wednesday"
46
+ THURSDAY = "Thursday"
47
+ FRIDAY = "Friday"
48
+ SATURDAY = "Saturday"
49
+ SUNDAY = "Sunday"
50
+
51
+
52
+ class Confidence(str, Enum):
53
+ HIGH = "high"
54
+ MEDIUM = "medium"
55
+ LOW = "low"
56
+
57
+
58
+ class VisitOccurrence(BaseModel):
59
+ """A single scheduled visit within the plan."""
60
+ day_of_week: DayOfWeek = Field(..., description="Day the visit occurs")
61
+ start_time: str = Field(..., description="Preferred start time, e.g. '08:00'")
62
+ end_time: str = Field(..., description="Preferred end time, e.g. '09:00'")
63
+ duration_minutes: int = Field(..., description="Duration in minutes")
64
+ care_tasks: list[str] = Field(
65
+ default_factory=list,
66
+ description="Specific tasks, e.g. ['medication administration', 'personal care']"
67
+ )
68
+ notes: Optional[str] = Field(None, description="Additional notes for this visit")
69
+ confidence: Confidence = Field(
70
+ Confidence.MEDIUM,
71
+ description="Confidence level for this occurrence extraction"
72
+ )
73
+
74
+
75
+ class VisitPlan(BaseModel):
76
+ """Top-level visit plan generated from referral text."""
77
+ visit_frequency: str = Field(..., description="e.g. 'Twice daily, 7 days a week'")
78
+ preferred_times: list[str] = Field(
79
+ default_factory=list, description="e.g. ['08:00-09:00', '20:00-21:00']"
80
+ )
81
+ qualifications_required: list[str] = Field(
82
+ default_factory=list, description="e.g. ['medication-trained']"
83
+ )
84
+ gender_preference: Optional[str] = Field(
85
+ None, description="e.g. 'Female carer preferred'"
86
+ )
87
+ environmental_requirements: list[str] = Field(
88
+ default_factory=list, description="e.g. ['key-safe code: 1234']"
89
+ )
90
+ occurrences: list[VisitOccurrence] = Field(
91
+ default_factory=list, description="Individual visit occurrences"
92
+ )
93
+ summary: str = Field(..., description="Brief plain-English summary")
94
+ extraction_notes: list[str] = Field(
95
+ default_factory=list, description="Flags for ambiguous or missing info"
96
+ )
97
+
98
+
99
+ # ══════════════════════════════════════════════════════════════════
100
+ # SECTION 3: THE PROMPT (Instructions for the AI)
101
+ # ══════════════════════════════════════════════════════════════════
102
+
103
+ VISIT_PLAN_SCHEMA = json.dumps(VisitPlan.model_json_schema(), indent=2)
104
+
105
+ SYSTEM_PROMPT = f"""You are a care coordination assistant for a domiciliary care platform.
106
+ Your task is to read free-text referral documents (from GPs, hospitals, or social workers)
107
+ and extract a structured visit plan.
108
+
109
+ ## Output Format
110
+ Return ONLY valid JSON matching this schema - no markdown, no explanation, no preamble:
111
+
112
+ {VISIT_PLAN_SCHEMA}
113
+
114
+ ## Rules
115
+ 1. Extract all visit-related information from the referral text.
116
+ 2. Map clinical shorthand to full terms:
117
+ - "BD" = "twice daily", "OD" = "once daily", "TDS" = "three times daily"
118
+ - "PRN" = "as needed", "QDS" = "four times daily"
119
+ 3. If information is ambiguous or missing, set the field to a sensible default
120
+ and add a note to "extraction_notes" explaining what was assumed.
121
+ 4. Set confidence to "high" when the referral explicitly states the value,
122
+ "medium" when inferred from context, "low" when guessed or defaulted.
123
+ 5. Never invent care tasks that are not mentioned or implied in the referral.
124
+ 6. If gender preference is not mentioned, set it to null.
125
+ 7. Include ALL days of the week if the referral says "daily" or "7 days a week".
126
+ 8. Times should be in 24-hour format (e.g. "08:00" not "8am").
127
+ """
128
+
129
+ # Few-shot example (teaches the AI by showing an example input and expected output)
130
+ FEW_SHOT_INPUT = """
131
+ Mrs Joan Smith, 82. Lives alone, ground floor flat. Key-safe code 4521.
132
+ Requires personal care morning and evening, 7 days. Medication administration BD
133
+ (insulin). Female carer preferred due to personal care. Moving and handling required
134
+ - uses a zimmer frame. Daughter (Sarah, 07700 900123) is primary contact.
135
+ """
136
+
137
+ FEW_SHOT_OUTPUT = json.dumps({
138
+ "visit_frequency": "Twice daily, 7 days a week",
139
+ "preferred_times": ["08:00-09:00", "20:00-21:00"],
140
+ "qualifications_required": [
141
+ "medication-trained (insulin administration)",
142
+ "moving-and-handling"
143
+ ],
144
+ "gender_preference": "Female carer preferred",
145
+ "environmental_requirements": [
146
+ "Key-safe code: 4521",
147
+ "Ground floor flat",
148
+ "Uses zimmer frame"
149
+ ],
150
+ "occurrences": [
151
+ {
152
+ "day_of_week": "Monday",
153
+ "start_time": "08:00",
154
+ "end_time": "09:00",
155
+ "duration_minutes": 60,
156
+ "care_tasks": ["personal care", "medication administration (insulin)"],
157
+ "notes": "Morning visit",
158
+ "confidence": "high"
159
+ },
160
+ {
161
+ "day_of_week": "Monday",
162
+ "start_time": "20:00",
163
+ "end_time": "21:00",
164
+ "duration_minutes": 60,
165
+ "care_tasks": ["personal care", "medication administration (insulin)"],
166
+ "notes": "Evening visit",
167
+ "confidence": "high"
168
+ }
169
+ ],
170
+ "summary": "Mrs Joan Smith, 82, requires twice-daily personal care and insulin "
171
+ "administration, 7 days a week. Female carer preferred. Key-safe access, "
172
+ "ground floor, zimmer frame user.",
173
+ "extraction_notes": [
174
+ "Only Monday occurrences shown - repeat pattern for all 7 days",
175
+ "Visit times assumed as 08:00 and 20:00 - not explicitly stated in referral"
176
+ ]
177
+ }, indent=2)
178
+
179
+
180
+ # ══════════════════════════════════════════════════════════════════
181
+ # SECTION 4: LLM CLIENT (Talks to Hugging Face's AI)
182
+ # ══════════════════════════════════════════════════════════════════
183
+
184
+ def call_huggingface_model(referral_text: str, model_id: str = DEFAULT_MODEL) -> str:
185
+ """
186
+ Send the referral text to a Hugging Face model and get the response.
187
+
188
+ Args:
189
+ referral_text: The free-text referral from the coordinator.
190
+ model_id: Which AI model to use.
191
+
192
+ Returns:
193
+ The raw text response from the AI model.
194
+ """
195
+ # Build the conversation messages
196
+ messages = [
197
+ {"role": "system", "content": SYSTEM_PROMPT},
198
+ {"role": "user", "content": f"Example referral:\n{FEW_SHOT_INPUT}"},
199
+ {"role": "assistant", "content": FEW_SHOT_OUTPUT},
200
+ {"role": "user", "content": f"Now extract a visit plan from this referral:\n\n{referral_text}"},
201
+ ]
202
+
203
+ # Format the prompt for Mistral-style models
204
+ prompt_parts = []
205
+ for msg in messages:
206
+ role = msg["role"]
207
+ content = msg["content"]
208
+ if role in ("system", "user"):
209
+ prompt_parts.append(f"[INST] {content} [/INST]")
210
+ elif role == "assistant":
211
+ prompt_parts.append(content)
212
+ formatted_prompt = "\n".join(prompt_parts)
213
+
214
+ # Call the Hugging Face Inference API
215
+ url = f"https://api-inference.huggingface.co/models/{model_id}"
216
+ headers = {
217
+ "Authorization": f"Bearer {HUGGINGFACE_API_TOKEN}",
218
+ "Content-Type": "application/json",
219
+ }
220
+ payload = {
221
+ "inputs": formatted_prompt,
222
+ "parameters": {
223
+ "max_new_tokens": 2048,
224
+ "temperature": 0.1,
225
+ "return_full_text": False,
226
+ },
227
+ }
228
+
229
+ response = requests.post(url, headers=headers, json=payload, timeout=120)
230
+ response.raise_for_status()
231
+
232
+ result = response.json()
233
+ if isinstance(result, list) and len(result) > 0:
234
+ return result[0].get("generated_text", "")
235
+ return str(result)
236
+
237
+
238
+ # ══════════════════════════════════════════════════════════════════
239
+ # SECTION 5: EXTRACTION & VALIDATION
240
+ # ══════════════════════════════════════════════════════════════════
241
+
242
+ def extract_json_from_text(text: str) -> str | None:
243
+ """Find JSON in the AI's response (handles markdown code fences)."""
244
+ # Try markdown code fence first
245
+ match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', text, re.DOTALL)
246
+ if match:
247
+ return match.group(1)
248
+ # Try raw JSON
249
+ match = re.search(r'(\{.*\})', text, re.DOTALL)
250
+ if match:
251
+ return match.group(1)
252
+ return None
253
+
254
+
255
+ def validate_and_warn(visit_plan: dict) -> list[str]:
256
+ """Check the visit plan for potential issues."""
257
+ warnings = []
258
+
259
+ for i, occ in enumerate(visit_plan.get("occurrences", [])):
260
+ if occ.get("confidence") == "low":
261
+ day = occ.get("day_of_week", "Unknown")
262
+ warnings.append(
263
+ f"Visit {i+1} ({day}) has LOW confidence - please verify."
264
+ )
265
+
266
+ if not visit_plan.get("qualifications_required"):
267
+ warnings.append("No qualifications extracted - check the referral.")
268
+
269
+ if not visit_plan.get("occurrences"):
270
+ warnings.append("No visit times generated - the AI may have struggled to parse the schedule.")
271
+
272
+ if not visit_plan.get("environmental_requirements"):
273
+ warnings.append("No environmental requirements found - check for key-safe codes, access info, etc.")
274
+
275
+ return warnings
276
+
277
+
278
+ def process_referral(referral_text: str, model_id: str) -> dict:
279
+ """
280
+ Main pipeline: referral text -> validated VisitPlan.
281
+
282
+ Returns a dict with success, visit_plan, raw_response, and errors.
283
+ """
284
+ # Call the AI
285
+ try:
286
+ raw_response = call_huggingface_model(referral_text, model_id)
287
+ except Exception as e:
288
+ return {
289
+ "success": False,
290
+ "visit_plan": None,
291
+ "raw_response": "",
292
+ "errors": [f"Error calling AI model: {str(e)}"],
293
+ }
294
+
295
+ # Extract JSON
296
+ json_str = extract_json_from_text(raw_response)
297
+ if not json_str:
298
+ return {
299
+ "success": False,
300
+ "visit_plan": None,
301
+ "raw_response": raw_response,
302
+ "errors": ["AI did not return valid JSON. Try again or use a different model."],
303
+ }
304
+
305
+ # Validate
306
+ try:
307
+ data = json.loads(json_str)
308
+ plan = VisitPlan(**data)
309
+ return {
310
+ "success": True,
311
+ "visit_plan": plan.model_dump(),
312
+ "raw_response": raw_response,
313
+ "errors": [],
314
+ }
315
+ except json.JSONDecodeError as e:
316
+ return {
317
+ "success": False,
318
+ "visit_plan": None,
319
+ "raw_response": raw_response,
320
+ "errors": [f"Invalid JSON from AI: {str(e)}"],
321
+ }
322
+ except Exception as e:
323
+ return {
324
+ "success": False,
325
+ "visit_plan": None,
326
+ "raw_response": raw_response,
327
+ "errors": [f"Output didn't match expected format: {str(e)}"],
328
+ }
329
+
330
+
331
+ # ══════════════════════════════════════════════════════════════════
332
+ # SECTION 6: SAMPLE REFERRALS (for testing)
333
+ # ══════════════════════════════════════════════════════════════════
334
+
335
+ SAMPLE_REFERRAL_1 = """
336
+ Mr Arthur Davies, 78 years old. Referred by Dr Patel, Elm Surgery.
337
+ Diagnosis: Type 2 diabetes, early-stage dementia, limited mobility (uses walking stick).
338
+ Lives with wife (Margaret, 81) in a two-storey house - bedroom is upstairs.
339
+
340
+ Care required:
341
+ - Morning visit (approx 8am): assist with washing, dressing, breakfast prep, and
342
+ morning medication (metformin 500mg, donepezil 10mg).
343
+ - Lunchtime check (approx 12:30): ensure lunch is prepared, prompt medication if
344
+ needed, welfare check.
345
+ - Evening visit (approx 6pm): evening meal support, help preparing for bed,
346
+ evening medication (metformin 500mg).
347
+
348
+ Frequency: 7 days a week. Carer must be medication-trained.
349
+ No gender preference expressed. Wife is sometimes out at day centre (Tues/Thurs)
350
+ so key-safe access needed - code 7890. Please note: Mr Davies can become confused
351
+ in the late afternoon (sundowning).
352
+ """
353
+
354
+ SAMPLE_REFERRAL_2 = """
355
+ DISCHARGE SUMMARY - NHS Trust Reference: DIS-2024-8891
356
+ Patient: Mr James Thompson, DOB 15/03/1945 (79 years)
357
+ Diagnosis: Right hip replacement (total), Type 1 diabetes (insulin-dependent),
358
+ mild cognitive impairment.
359
+
360
+ Discharge to home. Lives with wife (Betty, 77) in semi-detached house.
361
+ Stairs present - bedroom moved to ground floor temporarily.
362
+
363
+ Post-operative care plan:
364
+ AM visit 0730-0830: Personal care, wound dressing check, insulin
365
+ administration (NovoRapid 8 units pre-breakfast), compression stockings.
366
+ Must be moving and handling trained - patient cannot weight-bear fully.
367
+ PM visit 1900-2000: Evening personal care, insulin administration
368
+ (Lantus 20 units), wound check, help into bed.
369
+
370
+ Duration: 6 weeks initially, review at 4 weeks.
371
+ Frequency: BD, 7/7. Female carer preferred (patient request).
372
+ Key-safe code: 5566. OT assessment pending for stair lift.
373
+ District nurse visiting alternate days for wound management.
374
+ """
375
+
376
+ SAMPLE_REFERRAL_3 = """
377
+ Mrs Margaret Williams, 75. Lives alone in a bungalow. Key-safe: 3344.
378
+ Needs help with personal care every morning, 7 days a week.
379
+ Medication: paracetamol 500mg BD. No gender preference.
380
+ Mobile with walking frame. Contact: son David 07700 111222.
381
+ """
382
+
383
+
384
+ # ══════════════════════════════════════════════════════════════════
385
+ # SECTION 7: GRADIO USER INTERFACE
386
+ # ══════════════════════════════════════════════════════════════════
387
+
388
+ def generate_visit_plan(referral_text: str, model_choice: str):
389
+ """Called when the user clicks 'Generate Visit Plan'."""
390
+ if not referral_text.strip():
391
+ return "Please paste referral text in the box on the left.", "", ""
392
+
393
+ if not HUGGINGFACE_API_TOKEN:
394
+ return (
395
+ "ERROR: No API token found.\n\n"
396
+ "Go to your Space's Settings > Repository secrets\n"
397
+ "and add a secret called HUGGINGFACE_API_TOKEN\n"
398
+ "with your Hugging Face token (starts with hf_...).",
399
+ "",
400
+ ""
401
+ )
402
+
403
+ result = process_referral(referral_text, model_choice)
404
+
405
+ if result["success"]:
406
+ plan_json = json.dumps(result["visit_plan"], indent=2)
407
+ warnings = validate_and_warn(result["visit_plan"])
408
+ all_notes = result["visit_plan"].get("extraction_notes", []) + warnings
409
+ if all_notes:
410
+ notes_text = "\n".join(f">> {note}" for note in all_notes)
411
+ else:
412
+ notes_text = "No issues detected. Please still review against the original referral."
413
+ return plan_json, notes_text, result["raw_response"]
414
+ else:
415
+ error_text = "ERRORS:\n" + "\n".join(result["errors"])
416
+ return error_text, "", result.get("raw_response", "")
417
+
418
+
419
+ # Build the Gradio interface
420
+ with gr.Blocks(
421
+ title="AI Visit Plan Generator",
422
+ theme=gr.themes.Soft(),
423
+ ) as demo:
424
+
425
+ gr.Markdown("# AI Visit Plan Generator")
426
+ gr.Markdown(
427
+ "Paste a free-text care referral below. The AI will extract a structured "
428
+ "visit plan for review. **Always verify the output against the original referral.**"
429
+ )
430
+
431
+ with gr.Row():
432
+ with gr.Column(scale=1):
433
+ gr.Markdown("### Input")
434
+ model_dropdown = gr.Dropdown(
435
+ choices=[
436
+ "mistralai/Mistral-7B-Instruct-v0.3",
437
+ "HuggingFaceH4/zephyr-7b-beta",
438
+ "meta-llama/Llama-2-13b-chat-hf",
439
+ "tiiuae/falcon-7b-instruct",
440
+ ],
441
+ value="mistralai/Mistral-7B-Instruct-v0.3",
442
+ label="AI Model",
443
+ info="Try different models to compare results"
444
+ )
445
+ referral_input = gr.Textbox(
446
+ label="Referral Text",
447
+ placeholder="Paste the GP referral, hospital discharge summary, "
448
+ "or social worker assessment here...",
449
+ lines=15,
450
+ )
451
+ with gr.Row():
452
+ generate_btn = gr.Button("Generate Visit Plan", variant="primary")
453
+ with gr.Row():
454
+ example1_btn = gr.Button("Example 1: GP Referral", size="sm")
455
+ example2_btn = gr.Button("Example 2: Hospital Discharge", size="sm")
456
+ example3_btn = gr.Button("Example 3: Simple", size="sm")
457
+
458
+ with gr.Column(scale=1):
459
+ gr.Markdown("### Output")
460
+ plan_output = gr.Textbox(
461
+ label="Generated Visit Plan (JSON)",
462
+ lines=20,
463
+ interactive=False,
464
+ )
465
+ notes_output = gr.Textbox(
466
+ label="Notes & Warnings",
467
+ lines=5,
468
+ interactive=False,
469
+ )
470
+
471
+ with gr.Accordion("Debug: Raw Model Response", open=False):
472
+ gr.Markdown(
473
+ "*Shows the raw text from the AI model. Useful for troubleshooting.*"
474
+ )
475
+ raw_output = gr.Textbox(label="Raw Response", lines=10, interactive=False)
476
+
477
+ # Connect buttons to functions
478
+ generate_btn.click(
479
+ fn=generate_visit_plan,
480
+ inputs=[referral_input, model_dropdown],
481
+ outputs=[plan_output, notes_output, raw_output],
482
+ )
483
+ example1_btn.click(fn=lambda: SAMPLE_REFERRAL_1, outputs=referral_input)
484
+ example2_btn.click(fn=lambda: SAMPLE_REFERRAL_2, outputs=referral_input)
485
+ example3_btn.click(fn=lambda: SAMPLE_REFERRAL_3, outputs=referral_input)
486
+
487
+
488
+ # Launch the app
489
+ if __name__ == "__main__":
490
+ demo.launch()