omgy commited on
Commit
d1d3632
·
verified ·
1 Parent(s): 18af589

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +755 -0
app.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CareFlow Nexus - Pharmacy Agent
3
+ Hugging Face Deployment Ready
4
+
5
+ This agent manages medication dispensing, inventory tracking, and prescription fulfillment
6
+ for the CareFlow Nexus hospital operating system.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import uuid
13
+ from datetime import datetime, timedelta
14
+ from enum import Enum
15
+ from typing import Any, Dict, List, Literal, Optional
16
+
17
+ import google.generativeai as genai
18
+ import uvicorn
19
+ from fastapi import BackgroundTasks, FastAPI, HTTPException
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fastapi.responses import StreamingResponse
22
+ from pydantic import BaseModel, Field
23
+
24
+ # ============================================================================
25
+ # Configuration
26
+ # ============================================================================
27
+
28
+ AGENT_VERSION = "1.0.0"
29
+ AGENT_NAME = "Pharmacy Agent"
30
+ AGENT_ID = "pharmacy-agent-001"
31
+
32
+ # Configure Gemini API
33
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
34
+ if GEMINI_API_KEY:
35
+ genai.configure(api_key=GEMINI_API_KEY)
36
+ gemini_model = genai.GenerativeModel("gemini-pro")
37
+ else:
38
+ gemini_model = None
39
+ print("⚠️ Warning: GEMINI_API_KEY not found. AI features will be disabled.")
40
+
41
+ # ============================================================================
42
+ # Data Models
43
+ # ============================================================================
44
+
45
+
46
+ class MedicationStatus(str, Enum):
47
+ PENDING = "pending"
48
+ IN_PROGRESS = "in_progress"
49
+ READY = "ready"
50
+ DISPENSED = "dispensed"
51
+ CANCELLED = "cancelled"
52
+ OUT_OF_STOCK = "out_of_stock"
53
+
54
+
55
+ class PrescriptionRequest(BaseModel):
56
+ prescription_id: Optional[str] = None
57
+ patient_id: str = Field(..., description="Patient identifier")
58
+ patient_name: str = Field(..., description="Patient name")
59
+ doctor_id: str = Field(..., description="Prescribing doctor ID")
60
+ medications: List[Dict[str, Any]] = Field(..., description="List of medications")
61
+ priority: Literal["low", "medium", "high", "urgent"] = "medium"
62
+ notes: Optional[str] = None
63
+
64
+ class Config:
65
+ json_schema_extra = {
66
+ "example": {
67
+ "patient_id": "P12345",
68
+ "patient_name": "John Doe",
69
+ "doctor_id": "D001",
70
+ "medications": [
71
+ {
72
+ "name": "Amoxicillin",
73
+ "dosage": "500mg",
74
+ "frequency": "3x daily",
75
+ "duration": "7 days",
76
+ "quantity": 21,
77
+ }
78
+ ],
79
+ "priority": "high",
80
+ "notes": "Patient has penicillin allergy - verify alternative",
81
+ }
82
+ }
83
+
84
+
85
+ class InventoryItem(BaseModel):
86
+ medication_name: str
87
+ stock_quantity: int
88
+ unit: str = "units"
89
+ expiry_date: Optional[str] = None
90
+ reorder_level: int = 50
91
+ location: Optional[str] = None
92
+
93
+
94
+ class PrescriptionResponse(BaseModel):
95
+ prescription_id: str
96
+ status: MedicationStatus
97
+ patient_id: str
98
+ patient_name: str
99
+ medications: List[Dict[str, Any]]
100
+ estimated_time: Optional[str] = None
101
+ pharmacist_notes: Optional[str] = None
102
+ created_at: str
103
+ updated_at: str
104
+
105
+
106
+ class TaskStatusResponse(BaseModel):
107
+ task_id: str
108
+ status: Literal["pending", "in_progress", "completed", "failed"]
109
+ progress: int = Field(ge=0, le=100)
110
+ message: str
111
+ result: Optional[Dict[str, Any]] = None
112
+ created_at: str
113
+ updated_at: str
114
+
115
+
116
+ class HealthResponse(BaseModel):
117
+ status: Literal["healthy", "degraded", "unhealthy"]
118
+ agent: str
119
+ version: str
120
+ uptime_seconds: float
121
+ active_tasks: int
122
+ total_processed: int
123
+ timestamp: str
124
+
125
+
126
+ # ============================================================================
127
+ # In-Memory Storage (Replace with database in production)
128
+ # ============================================================================
129
+
130
+ prescriptions_db: Dict[str, Dict[str, Any]] = {}
131
+ tasks_db: Dict[str, Dict[str, Any]] = {}
132
+ inventory_db: Dict[str, InventoryItem] = {
133
+ "Amoxicillin": InventoryItem(
134
+ medication_name="Amoxicillin",
135
+ stock_quantity=500,
136
+ unit="tablets",
137
+ expiry_date="2025-12-31",
138
+ reorder_level=100,
139
+ location="A-12",
140
+ ),
141
+ "Ibuprofen": InventoryItem(
142
+ medication_name="Ibuprofen",
143
+ stock_quantity=800,
144
+ unit="tablets",
145
+ expiry_date="2025-11-30",
146
+ reorder_level=150,
147
+ location="B-05",
148
+ ),
149
+ "Paracetamol": InventoryItem(
150
+ medication_name="Paracetamol",
151
+ stock_quantity=1200,
152
+ unit="tablets",
153
+ expiry_date="2026-03-15",
154
+ reorder_level=200,
155
+ location="B-06",
156
+ ),
157
+ "Insulin": InventoryItem(
158
+ medication_name="Insulin",
159
+ stock_quantity=75,
160
+ unit="vials",
161
+ expiry_date="2025-06-30",
162
+ reorder_level=20,
163
+ location="C-01-Refrigerated",
164
+ ),
165
+ "Aspirin": InventoryItem(
166
+ medication_name="Aspirin",
167
+ stock_quantity=600,
168
+ unit="tablets",
169
+ expiry_date="2025-10-20",
170
+ reorder_level=100,
171
+ location="A-15",
172
+ ),
173
+ }
174
+
175
+ # Agent statistics
176
+ agent_stats = {"start_time": datetime.now(), "total_processed": 0, "active_tasks": 0}
177
+
178
+ # ============================================================================
179
+ # FastAPI App
180
+ # ============================================================================
181
+
182
+ app = FastAPI(
183
+ title="CareFlow Nexus - Pharmacy Agent",
184
+ description="AI-powered medication management and dispensing agent",
185
+ version=AGENT_VERSION,
186
+ docs_url="/docs",
187
+ redoc_url="/redoc",
188
+ )
189
+
190
+ # CORS Configuration
191
+ app.add_middleware(
192
+ CORSMiddleware,
193
+ allow_origins=["*"], # Configure appropriately for production
194
+ allow_credentials=True,
195
+ allow_methods=["*"],
196
+ allow_headers=["*"],
197
+ )
198
+
199
+ # ============================================================================
200
+ # Helper Functions
201
+ # ============================================================================
202
+
203
+
204
+ async def check_drug_interactions(medications: List[Dict[str, Any]]) -> Dict[str, Any]:
205
+ """Use Gemini AI to check for potential drug interactions"""
206
+ if not gemini_model:
207
+ return {
208
+ "has_interactions": False,
209
+ "message": "AI check unavailable",
210
+ "warnings": [],
211
+ }
212
+
213
+ try:
214
+ med_names = [med.get("name", "") for med in medications]
215
+ prompt = f"""As a clinical pharmacist AI, analyze these medications for potential drug interactions:
216
+
217
+ Medications: {", ".join(med_names)}
218
+
219
+ Provide:
220
+ 1. Any potential drug-drug interactions
221
+ 2. Severity level (low/medium/high/critical)
222
+ 3. Recommended actions or alternatives
223
+ 4. Special monitoring requirements
224
+
225
+ Format response as JSON with keys: has_interactions (bool), severity (string), interactions (list), recommendations (list)"""
226
+
227
+ response = gemini_model.generate_content(prompt)
228
+
229
+ # Parse AI response
230
+ ai_analysis = {
231
+ "has_interactions": "interaction" in response.text.lower(),
232
+ "ai_response": response.text,
233
+ "checked_at": datetime.now().isoformat(),
234
+ }
235
+
236
+ return ai_analysis
237
+ except Exception as e:
238
+ return {
239
+ "has_interactions": False,
240
+ "error": str(e),
241
+ "warnings": ["AI check failed"],
242
+ }
243
+
244
+
245
+ async def generate_pharmacist_notes(prescription: Dict[str, Any]) -> str:
246
+ """Use Gemini AI to generate professional pharmacist notes"""
247
+ if not gemini_model:
248
+ return "Prescription verified and approved for dispensing."
249
+
250
+ try:
251
+ prompt = f"""As a clinical pharmacist, provide brief professional notes for this prescription:
252
+
253
+ Patient: {prescription.get("patient_name")}
254
+ Medications: {json.dumps(prescription.get("medications", []), indent=2)}
255
+ Priority: {prescription.get("priority")}
256
+ Doctor's Notes: {prescription.get("notes", "None")}
257
+
258
+ Provide:
259
+ 1. Key counseling points for the patient
260
+ 2. Administration instructions
261
+ 3. Important warnings or precautions
262
+ 4. Storage requirements
263
+
264
+ Keep it concise (max 3-4 sentences)."""
265
+
266
+ response = gemini_model.generate_content(prompt)
267
+ return response.text.strip()
268
+ except Exception as e:
269
+ return f"Prescription approved. Standard counseling recommended. (AI note generation failed: {str(e)})"
270
+
271
+
272
+ def check_inventory(medications: List[Dict[str, Any]]) -> tuple[bool, List[str]]:
273
+ """Check if all medications are in stock"""
274
+ out_of_stock = []
275
+
276
+ for med in medications:
277
+ med_name = med.get("name", "")
278
+ quantity = med.get("quantity", 0)
279
+
280
+ if med_name in inventory_db:
281
+ if inventory_db[med_name].stock_quantity < quantity:
282
+ out_of_stock.append(
283
+ f"{med_name} (need {quantity}, have {inventory_db[med_name].stock_quantity})"
284
+ )
285
+ else:
286
+ out_of_stock.append(f"{med_name} (not in inventory)")
287
+
288
+ return len(out_of_stock) == 0, out_of_stock
289
+
290
+
291
+ def update_inventory(medications: List[Dict[str, Any]]):
292
+ """Update inventory after dispensing"""
293
+ for med in medications:
294
+ med_name = med.get("name", "")
295
+ quantity = med.get("quantity", 0)
296
+
297
+ if med_name in inventory_db:
298
+ inventory_db[med_name].stock_quantity -= quantity
299
+
300
+
301
+ async def process_prescription_task(prescription_id: str):
302
+ """Background task to process prescription"""
303
+ try:
304
+ # Update task status
305
+ tasks_db[prescription_id]["status"] = "in_progress"
306
+ tasks_db[prescription_id]["progress"] = 10
307
+ tasks_db[prescription_id]["message"] = "Verifying prescription..."
308
+ await asyncio.sleep(1)
309
+
310
+ # AI-powered drug interaction check
311
+ prescription = prescriptions_db[prescription_id]
312
+ tasks_db[prescription_id]["progress"] = 20
313
+ tasks_db[prescription_id]["message"] = "Checking drug interactions with AI..."
314
+
315
+ interaction_check = await check_drug_interactions(prescription["medications"])
316
+ if interaction_check.get("has_interactions"):
317
+ prescriptions_db[prescription_id]["pharmacist_notes"] = (
318
+ f"⚠️ AI Alert: {interaction_check.get('ai_response', 'Potential interactions detected')}"
319
+ )
320
+
321
+ await asyncio.sleep(1)
322
+
323
+ # Check inventory
324
+ tasks_db[prescription_id]["progress"] = 30
325
+ tasks_db[prescription_id]["message"] = "Checking inventory..."
326
+ await asyncio.sleep(1)
327
+
328
+ # Check inventory
329
+ prescription = prescriptions_db[prescription_id]
330
+ in_stock, out_of_stock_items = check_inventory(prescription["medications"])
331
+
332
+ if not in_stock:
333
+ tasks_db[prescription_id]["status"] = "failed"
334
+ tasks_db[prescription_id]["progress"] = 100
335
+ tasks_db[prescription_id]["message"] = (
336
+ f"Out of stock: {', '.join(out_of_stock_items)}"
337
+ )
338
+ prescriptions_db[prescription_id]["status"] = MedicationStatus.OUT_OF_STOCK
339
+ prescriptions_db[prescription_id]["pharmacist_notes"] = (
340
+ f"Out of stock: {', '.join(out_of_stock_items)}"
341
+ )
342
+ return
343
+
344
+ # Simulate medication preparation
345
+ tasks_db[prescription_id]["progress"] = 50
346
+ tasks_db[prescription_id]["message"] = "Preparing medications..."
347
+ await asyncio.sleep(2)
348
+
349
+ tasks_db[prescription_id]["progress"] = 70
350
+ tasks_db[prescription_id]["message"] = "Labeling and packaging..."
351
+ await asyncio.sleep(1)
352
+
353
+ tasks_db[prescription_id]["progress"] = 90
354
+ tasks_db[prescription_id]["message"] = "Final quality check..."
355
+ await asyncio.sleep(1)
356
+
357
+ # Update inventory
358
+ update_inventory(prescription["medications"])
359
+
360
+ # Generate AI-powered pharmacist notes
361
+ tasks_db[prescription_id]["progress"] = 95
362
+ tasks_db[prescription_id]["message"] = "Generating counseling notes..."
363
+
364
+ ai_notes = await generate_pharmacist_notes(prescription)
365
+ if not prescriptions_db[prescription_id].get("pharmacist_notes"):
366
+ prescriptions_db[prescription_id]["pharmacist_notes"] = ai_notes
367
+
368
+ # Complete
369
+ tasks_db[prescription_id]["status"] = "completed"
370
+ tasks_db[prescription_id]["progress"] = 100
371
+ tasks_db[prescription_id]["message"] = "Prescription ready for pickup"
372
+ tasks_db[prescription_id]["result"] = {
373
+ "prescription_id": prescription_id,
374
+ "ready_time": datetime.now().isoformat(),
375
+ "ai_enhanced": gemini_model is not None,
376
+ }
377
+
378
+ prescriptions_db[prescription_id]["status"] = MedicationStatus.READY
379
+ prescriptions_db[prescription_id]["updated_at"] = datetime.now().isoformat()
380
+
381
+ agent_stats["total_processed"] += 1
382
+ agent_stats["active_tasks"] -= 1
383
+
384
+ except Exception as e:
385
+ tasks_db[prescription_id]["status"] = "failed"
386
+ tasks_db[prescription_id]["progress"] = 100
387
+ tasks_db[prescription_id]["message"] = f"Error: {str(e)}"
388
+ agent_stats["active_tasks"] -= 1
389
+
390
+
391
+ # ============================================================================
392
+ # API Endpoints
393
+ # ============================================================================
394
+
395
+
396
+ @app.get("/", tags=["General"])
397
+ async def root():
398
+ """Root endpoint"""
399
+ return {
400
+ "agent": AGENT_NAME,
401
+ "version": AGENT_VERSION,
402
+ "status": "online",
403
+ "endpoints": {
404
+ "health": "/health",
405
+ "docs": "/docs",
406
+ "prescriptions": "/api/pharmacy/prescriptions",
407
+ "inventory": "/api/pharmacy/inventory",
408
+ },
409
+ }
410
+
411
+
412
+ @app.get("/health", response_model=HealthResponse, tags=["General"])
413
+ async def health_check():
414
+ """Health check endpoint"""
415
+ uptime = (datetime.now() - agent_stats["start_time"]).total_seconds()
416
+
417
+ return HealthResponse(
418
+ status="healthy" if gemini_model else "degraded",
419
+ agent=AGENT_NAME,
420
+ version=AGENT_VERSION,
421
+ uptime_seconds=uptime,
422
+ active_tasks=agent_stats["active_tasks"],
423
+ total_processed=agent_stats["total_processed"],
424
+ timestamp=datetime.now().isoformat(),
425
+ )
426
+
427
+
428
+ @app.post(
429
+ "/api/pharmacy/prescriptions",
430
+ response_model=PrescriptionResponse,
431
+ tags=["Pharmacy"],
432
+ )
433
+ async def create_prescription(
434
+ request: PrescriptionRequest, background_tasks: BackgroundTasks
435
+ ):
436
+ """
437
+ Submit a new prescription for processing
438
+
439
+ The agent will:
440
+ 1. Verify prescription details
441
+ 2. Check inventory availability
442
+ 3. Prepare medications
443
+ 4. Package and label
444
+ 5. Notify when ready for pickup
445
+ """
446
+ # Generate prescription ID
447
+ prescription_id = request.prescription_id or f"RX-{uuid.uuid4().hex[:8].upper()}"
448
+
449
+ # Check if prescription already exists
450
+ if prescription_id in prescriptions_db:
451
+ raise HTTPException(status_code=400, detail="Prescription ID already exists")
452
+
453
+ # Quick inventory check
454
+ in_stock, out_of_stock_items = check_inventory(request.medications)
455
+
456
+ # Create prescription record
457
+ prescription = {
458
+ "prescription_id": prescription_id,
459
+ "status": MedicationStatus.PENDING
460
+ if in_stock
461
+ else MedicationStatus.OUT_OF_STOCK,
462
+ "patient_id": request.patient_id,
463
+ "patient_name": request.patient_name,
464
+ "doctor_id": request.doctor_id,
465
+ "medications": request.medications,
466
+ "priority": request.priority,
467
+ "notes": request.notes,
468
+ "estimated_time": (datetime.now() + timedelta(minutes=15)).isoformat()
469
+ if in_stock
470
+ else None,
471
+ "pharmacist_notes": None
472
+ if in_stock
473
+ else f"Out of stock: {', '.join(out_of_stock_items)}",
474
+ "created_at": datetime.now().isoformat(),
475
+ "updated_at": datetime.now().isoformat(),
476
+ }
477
+
478
+ prescriptions_db[prescription_id] = prescription
479
+
480
+ # Create task record
481
+ tasks_db[prescription_id] = {
482
+ "task_id": prescription_id,
483
+ "status": "pending" if in_stock else "failed",
484
+ "progress": 0 if in_stock else 100,
485
+ "message": "Prescription submitted"
486
+ if in_stock
487
+ else f"Out of stock: {', '.join(out_of_stock_items)}",
488
+ "result": None,
489
+ "created_at": datetime.now().isoformat(),
490
+ "updated_at": datetime.now().isoformat(),
491
+ }
492
+
493
+ # Start background processing if in stock
494
+ if in_stock:
495
+ agent_stats["active_tasks"] += 1
496
+ background_tasks.add_task(process_prescription_task, prescription_id)
497
+
498
+ return PrescriptionResponse(**prescription)
499
+
500
+
501
+ @app.get(
502
+ "/api/pharmacy/prescriptions/{prescription_id}",
503
+ response_model=PrescriptionResponse,
504
+ tags=["Pharmacy"],
505
+ )
506
+ async def get_prescription(prescription_id: str):
507
+ """Get prescription status by ID"""
508
+ if prescription_id not in prescriptions_db:
509
+ raise HTTPException(status_code=404, detail="Prescription not found")
510
+
511
+ return PrescriptionResponse(**prescriptions_db[prescription_id])
512
+
513
+
514
+ @app.get(
515
+ "/api/pharmacy/prescriptions",
516
+ response_model=List[PrescriptionResponse],
517
+ tags=["Pharmacy"],
518
+ )
519
+ async def list_prescriptions(
520
+ status: Optional[MedicationStatus] = None,
521
+ patient_id: Optional[str] = None,
522
+ limit: int = 50,
523
+ ):
524
+ """List all prescriptions with optional filters"""
525
+ results = list(prescriptions_db.values())
526
+
527
+ if status:
528
+ results = [p for p in results if p["status"] == status]
529
+
530
+ if patient_id:
531
+ results = [p for p in results if p["patient_id"] == patient_id]
532
+
533
+ results = sorted(results, key=lambda x: x["created_at"], reverse=True)
534
+ results = results[:limit]
535
+
536
+ return [PrescriptionResponse(**p) for p in results]
537
+
538
+
539
+ @app.post("/api/pharmacy/prescriptions/{prescription_id}/dispense", tags=["Pharmacy"])
540
+ async def dispense_prescription(prescription_id: str):
541
+ """Mark prescription as dispensed (picked up by patient)"""
542
+ if prescription_id not in prescriptions_db:
543
+ raise HTTPException(status_code=404, detail="Prescription not found")
544
+
545
+ prescription = prescriptions_db[prescription_id]
546
+
547
+ if prescription["status"] != MedicationStatus.READY:
548
+ raise HTTPException(
549
+ status_code=400,
550
+ detail=f"Prescription not ready for dispensing. Current status: {prescription['status']}",
551
+ )
552
+
553
+ prescription["status"] = MedicationStatus.DISPENSED
554
+ prescription["updated_at"] = datetime.now().isoformat()
555
+
556
+ return {
557
+ "message": "Prescription dispensed successfully",
558
+ "prescription_id": prescription_id,
559
+ "dispensed_at": prescription["updated_at"],
560
+ }
561
+
562
+
563
+ @app.post("/api/pharmacy/prescriptions/{prescription_id}/cancel", tags=["Pharmacy"])
564
+ async def cancel_prescription(prescription_id: str, reason: Optional[str] = None):
565
+ """Cancel a prescription"""
566
+ if prescription_id not in prescriptions_db:
567
+ raise HTTPException(status_code=404, detail="Prescription not found")
568
+
569
+ prescription = prescriptions_db[prescription_id]
570
+
571
+ if prescription["status"] == MedicationStatus.DISPENSED:
572
+ raise HTTPException(
573
+ status_code=400, detail="Cannot cancel dispensed prescription"
574
+ )
575
+
576
+ prescription["status"] = MedicationStatus.CANCELLED
577
+ prescription["pharmacist_notes"] = f"Cancelled: {reason or 'No reason provided'}"
578
+ prescription["updated_at"] = datetime.now().isoformat()
579
+
580
+ return {
581
+ "message": "Prescription cancelled successfully",
582
+ "prescription_id": prescription_id,
583
+ }
584
+
585
+
586
+ @app.get(
587
+ "/api/pharmacy/inventory", response_model=List[InventoryItem], tags=["Pharmacy"]
588
+ )
589
+ async def get_inventory(low_stock_only: bool = False):
590
+ """Get current medication inventory"""
591
+ items = list(inventory_db.values())
592
+
593
+ if low_stock_only:
594
+ items = [item for item in items if item.stock_quantity <= item.reorder_level]
595
+
596
+ return items
597
+
598
+
599
+ @app.put(
600
+ "/api/pharmacy/inventory/{medication_name}",
601
+ response_model=InventoryItem,
602
+ tags=["Pharmacy"],
603
+ )
604
+ async def update_inventory_item(medication_name: str, item: InventoryItem):
605
+ """Update inventory for a medication"""
606
+ inventory_db[medication_name] = item
607
+ return item
608
+
609
+
610
+ @app.get(
611
+ "/api/pharmacy/tasks/{task_id}", response_model=TaskStatusResponse, tags=["Tasks"]
612
+ )
613
+ async def get_task_status(task_id: str):
614
+ """Get task processing status"""
615
+ if task_id not in tasks_db:
616
+ raise HTTPException(status_code=404, detail="Task not found")
617
+
618
+ return TaskStatusResponse(**tasks_db[task_id])
619
+
620
+
621
+ @app.get("/api/pharmacy/stream/{prescription_id}", tags=["Streaming"])
622
+ async def stream_prescription_status(prescription_id: str):
623
+ """
624
+ Server-Sent Events (SSE) endpoint for real-time prescription updates
625
+ """
626
+ if prescription_id not in prescriptions_db:
627
+ raise HTTPException(status_code=404, detail="Prescription not found")
628
+
629
+ async def event_generator():
630
+ last_status = None
631
+
632
+ while True:
633
+ if prescription_id in tasks_db:
634
+ task = tasks_db[prescription_id]
635
+ current_status = task["status"]
636
+
637
+ # Send update if status changed
638
+ if current_status != last_status:
639
+ event_data = {
640
+ "prescription_id": prescription_id,
641
+ "status": task["status"],
642
+ "progress": task["progress"],
643
+ "message": task["message"],
644
+ "timestamp": datetime.now().isoformat(),
645
+ }
646
+
647
+ yield f"data: {json.dumps(event_data)}\n\n"
648
+ last_status = current_status
649
+
650
+ # Stop streaming if completed or failed
651
+ if current_status in ["completed", "failed"]:
652
+ break
653
+
654
+ await asyncio.sleep(1)
655
+
656
+ # Send final message
657
+ yield f"data: {json.dumps({'status': 'stream_ended'})}\n\n"
658
+
659
+ return StreamingResponse(
660
+ event_generator(),
661
+ media_type="text/event-stream",
662
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
663
+ )
664
+
665
+
666
+ @app.get("/api/pharmacy/stats", tags=["Analytics"])
667
+ async def get_statistics():
668
+ """Get pharmacy agent statistics"""
669
+ return {
670
+ "total_prescriptions": len(prescriptions_db),
671
+ "active_prescriptions": len(
672
+ [
673
+ p
674
+ for p in prescriptions_db.values()
675
+ if p["status"]
676
+ in [MedicationStatus.PENDING, MedicationStatus.IN_PROGRESS]
677
+ ]
678
+ ),
679
+ "completed_today": agent_stats["total_processed"],
680
+ "active_tasks": agent_stats["active_tasks"],
681
+ "inventory_items": len(inventory_db),
682
+ "low_stock_items": len(
683
+ [
684
+ item
685
+ for item in inventory_db.values()
686
+ if item.stock_quantity <= item.reorder_level
687
+ ]
688
+ ),
689
+ "uptime_seconds": (datetime.now() - agent_stats["start_time"]).total_seconds(),
690
+ "ai_enabled": gemini_model is not None,
691
+ "gemini_configured": GEMINI_API_KEY is not None,
692
+ }
693
+
694
+
695
+ @app.post("/api/pharmacy/ai/analyze", tags=["AI Features"])
696
+ async def ai_analyze_prescription(
697
+ patient_name: str,
698
+ medications: List[Dict[str, Any]],
699
+ medical_history: Optional[str] = None,
700
+ ):
701
+ """
702
+ Use Gemini AI to analyze prescription for potential issues
703
+
704
+ Provides:
705
+ - Drug interaction analysis
706
+ - Dosage recommendations
707
+ - Patient counseling points
708
+ - Safety warnings
709
+ """
710
+ if not gemini_model:
711
+ raise HTTPException(
712
+ status_code=503,
713
+ detail="AI features unavailable. GEMINI_API_KEY not configured.",
714
+ )
715
+
716
+ try:
717
+ prompt = f"""As an expert clinical pharmacist AI, analyze this prescription:
718
+
719
+ Patient: {patient_name}
720
+ Medical History: {medical_history or "Not provided"}
721
+ Medications: {json.dumps(medications, indent=2)}
722
+
723
+ Provide comprehensive analysis:
724
+ 1. Drug-drug interactions (if any)
725
+ 2. Contraindications based on medical history
726
+ 3. Dosage appropriateness
727
+ 4. Patient counseling points
728
+ 5. Monitoring recommendations
729
+ 6. Safety warnings
730
+
731
+ Be specific and clinically relevant."""
732
+
733
+ response = gemini_model.generate_content(prompt)
734
+
735
+ # Check for interactions
736
+ interaction_check = await check_drug_interactions(medications)
737
+
738
+ return {
739
+ "success": True,
740
+ "analysis": response.text,
741
+ "interaction_check": interaction_check,
742
+ "timestamp": datetime.now().isoformat(),
743
+ "model": "gemini-pro",
744
+ }
745
+ except Exception as e:
746
+ raise HTTPException(status_code=500, detail=f"AI analysis failed: {str(e)}")
747
+
748
+
749
+ # ============================================================================
750
+ # Main Entry Point
751
+ # ============================================================================
752
+
753
+ if __name__ == "__main__":
754
+ port = int(os.environ.get("PORT", 7860)) # Hugging Face uses port 7860
755
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False, log_level="info")