447 lines
20 KiB
Python
447 lines
20 KiB
Python
"""
|
||
OpenRouter API service for AI-powered patient experience comment analysis.
|
||
Handles authentication, requests, and response parsing for sentiment analysis,
|
||
keyword extraction, topic identification, and entity recognition optimized for healthcare.
|
||
"""
|
||
import logging
|
||
import json
|
||
from typing import Dict, List, Any, Optional
|
||
import httpx
|
||
|
||
from django.conf import settings
|
||
from django.utils import timezone
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class OpenRouterService:
|
||
"""
|
||
Service for interacting with OpenRouter API to analyze patient experience comments.
|
||
Provides healthcare-focused sentiment analysis, keyword extraction, topic identification,
|
||
and entity recognition with actionable business insights.
|
||
"""
|
||
|
||
DEFAULT_MODEL = "anthropic/claude-3-haiku"
|
||
DEFAULT_MAX_TOKENS = 2048
|
||
DEFAULT_TEMPERATURE = 0.1
|
||
|
||
def __init__(
|
||
self,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
timeout: int = 30
|
||
):
|
||
"""
|
||
Initialize OpenRouter service.
|
||
|
||
Args:
|
||
api_key: OpenRouter API key (defaults to settings.OPENROUTER_API_KEY)
|
||
model: Model to use (defaults to settings.OPENROUTER_MODEL or DEFAULT_MODEL)
|
||
timeout: Request timeout in seconds
|
||
"""
|
||
self.api_key = api_key or getattr(settings, 'OPENROUTER_API_KEY', None)
|
||
self.model = model or getattr(settings, 'AI_MODEL', self.DEFAULT_MODEL)
|
||
self.timeout = timeout
|
||
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
||
|
||
if not self.api_key:
|
||
logger.warning(
|
||
"OpenRouter API key not configured. "
|
||
"Set OPENROUTER_API_KEY in your .env file."
|
||
)
|
||
|
||
logger.info(f"OpenRouter service initialized with model: {self.model}")
|
||
|
||
def _build_analysis_prompt(self, comments: List[Dict[str, Any]]) -> str:
|
||
"""
|
||
Build prompt for batch comment analysis with bilingual output.
|
||
Note: This method is kept for compatibility but not actively used.
|
||
|
||
Args:
|
||
comments: List of comment dictionaries with 'id' and 'text' keys
|
||
|
||
Returns:
|
||
Formatted prompt string
|
||
"""
|
||
# Kept for backward compatibility
|
||
comments_text = "\n".join([
|
||
f"Comment {i+1}: {c['text']}"
|
||
for i, c in enumerate(comments)
|
||
])
|
||
|
||
prompt = """You are a bilingual healthcare experience analyst. Analyze patient comments..."""
|
||
return prompt
|
||
|
||
def analyze_comment(self, comment_id: str, text: str) -> Dict[str, Any]:
|
||
"""
|
||
Analyze a single patient experience comment using OpenRouter API.
|
||
|
||
Args:
|
||
comment_id: Comment ID
|
||
text: Comment text
|
||
|
||
Returns:
|
||
Dictionary with success status and analysis results
|
||
"""
|
||
logger.info("=" * 80)
|
||
logger.info("STARTING PATIENT EXPERIENCE ANALYSIS")
|
||
logger.info("=" * 80)
|
||
|
||
if not self.api_key:
|
||
logger.error("API KEY NOT CONFIGURED")
|
||
return {
|
||
'success': False,
|
||
'error': 'OpenRouter API key not configured'
|
||
}
|
||
|
||
if not text:
|
||
logger.warning("No comment text to analyze")
|
||
return {
|
||
'success': False,
|
||
'error': 'Comment text is empty'
|
||
}
|
||
|
||
try:
|
||
logger.info(f"Building prompt for comment {comment_id}...")
|
||
|
||
# Enhanced healthcare-focused prompt
|
||
prompt = f"""You are an expert healthcare patient experience analyst specializing in analyzing patient feedback for hospital quality improvement and business intelligence. Analyze the following patient comment and provide a COMPREHENSIVE bilingual analysis in BOTH English and Arabic that helps hospital management make data-driven decisions.
|
||
|
||
PATIENT COMMENT:
|
||
{text}
|
||
|
||
CRITICAL REQUIREMENTS:
|
||
1. ALL analysis MUST be provided in BOTH English and Arabic
|
||
2. Use clear, modern Arabic (فصحى معاصرة) that all Arabic speakers understand
|
||
3. Detect the comment's original language and provide accurate translations
|
||
4. Maintain cultural sensitivity and medical terminology accuracy
|
||
5. Focus on actionable insights for hospital improvement
|
||
|
||
PROVIDE THE FOLLOWING ANALYSIS:
|
||
|
||
A. SENTIMENT ANALYSIS (Bilingual)
|
||
- classification: {{"en": "positive|neutral|negative|mixed", "ar": "إيجابي|محايد|سلبي|مختلط"}}
|
||
- score: number from -1.0 (very negative) to 1.0 (very positive)
|
||
- confidence: number from 0.0 to 1.0
|
||
- urgency_level: {{"en": "low|medium|high|critical", "ar": "منخفض|متوسط|عالي|حرج"}}
|
||
|
||
B. DETAILED SUMMARY (Bilingual)
|
||
- en: 3-4 sentence English summary covering: main complaint/praise, specific incidents, patient expectations, and emotional tone
|
||
- ar: 3-4 sentence Arabic summary (ملخص تفصيلي) with equivalent depth and nuance
|
||
|
||
C. KEYWORDS (Bilingual - 7-10 each)
|
||
Focus on: medical terms, service aspects, staff mentions, facility features, emotional descriptors
|
||
- en: ["keyword1", "keyword2", ...]
|
||
- ar: ["كلمة1", "كلمة2", ...]
|
||
|
||
D. HEALTHCARE-SPECIFIC TOPICS (Bilingual - 4-6 each)
|
||
Categories: Clinical Care, Nursing Care, Medical Staff, Administrative Services, Facility/Environment,
|
||
Wait Times, Communication, Billing/Finance, Food Services, Cleanliness, Privacy, Technology/Equipment
|
||
- en: ["topic1", "topic2", ...]
|
||
- ar: ["موضوع1", "موضوع2", ...]
|
||
|
||
E. ENTITIES (Bilingual)
|
||
Extract: Doctor names, Department names, Staff roles, Locations, Medical conditions, Treatments, Medications
|
||
- For each entity: {{"text": {{"en": "...", "ar": "..."}}, "type": {{"en": "DOCTOR|NURSE|DEPARTMENT|STAFF|LOCATION|CONDITION|TREATMENT|MEDICATION|OTHER", "ar": "طبيب|ممرض|قسم|موظف|موقع|حالة|علاج|دواء|أخرى"}}}}
|
||
|
||
F. EMOTIONS (Granular Analysis)
|
||
- joy: 0.0 to 1.0 (satisfaction, happiness, gratitude)
|
||
- anger: 0.0 to 1.0 (frustration, irritation, rage)
|
||
- sadness: 0.0 to 1.0 (disappointment, grief, despair)
|
||
- fear: 0.0 to 1.0 (anxiety, worry, panic)
|
||
- surprise: 0.0 to 1.0 (shock, amazement)
|
||
- disgust: 0.0 to 1.0 (revulsion, contempt)
|
||
- trust: 0.0 to 1.0 (confidence in care, safety)
|
||
- anticipation: 0.0 to 1.0 (hope, expectation)
|
||
- labels: {{"emotion": {{"en": "English", "ar": "عربي"}}}}
|
||
|
||
G. ACTIONABLE INSIGHTS (NEW - Critical for Business)
|
||
- primary_concern: {{"en": "Main issue identified", "ar": "المشكلة الرئيسية"}}
|
||
- affected_department: {{"en": "Department name", "ar": "اسم القسم"}}
|
||
- service_quality_indicators: {{
|
||
"clinical_care": 0-10,
|
||
"staff_behavior": 0-10,
|
||
"facility_condition": 0-10,
|
||
"wait_time": 0-10,
|
||
"communication": 0-10,
|
||
"overall_experience": 0-10
|
||
}}
|
||
- complaint_type: {{"en": "clinical|service|administrative|facility|staff_behavior|billing|other", "ar": "سريري|خدمة|إداري|منشأة|سلوك_موظفين|فوترة|أخرى"}}
|
||
- requires_followup: true/false
|
||
- followup_priority: {{"en": "low|medium|high|urgent", "ar": "منخفضة|متوسطة|عالية|عاجلة"}}
|
||
- recommended_actions: {{
|
||
"en": ["Action 1", "Action 2", "Action 3"],
|
||
"ar": ["إجراء 1", "إجراء 2", "إجراء 3"]
|
||
}}
|
||
|
||
H. BUSINESS INTELLIGENCE METRICS (NEW)
|
||
- patient_satisfaction_score: 0-100 (overall satisfaction estimate)
|
||
- nps_likelihood: -100 to 100 (Net Promoter Score estimate: would recommend hospital?)
|
||
- retention_risk: {{"level": "low|medium|high", "score": 0.0-1.0}}
|
||
- reputation_impact: {{"level": "positive|neutral|negative|severe", "score": -1.0 to 1.0}}
|
||
- compliance_concerns: {{"present": true/false, "types": ["HIPAA|safety|ethics|other"]}}
|
||
|
||
I. PATIENT JOURNEY TOUCHPOINTS (NEW)
|
||
Identify which touchpoints are mentioned:
|
||
- touchpoints: {{
|
||
"admission": true/false,
|
||
"waiting_area": true/false,
|
||
"consultation": true/false,
|
||
"diagnosis": true/false,
|
||
"treatment": true/false,
|
||
"nursing_care": true/false,
|
||
"medication": true/false,
|
||
"discharge": true/false,
|
||
"billing": true/false,
|
||
"follow_up": true/false
|
||
}}
|
||
|
||
J. COMPETITIVE INSIGHTS (NEW)
|
||
- mentions_competitors: true/false
|
||
- comparison_sentiment: {{"en": "favorable|unfavorable|neutral", "ar": "مواتي|غير_مواتي|محايد"}}
|
||
- unique_selling_points: {{"en": ["USP1", "USP2"], "ar": ["نقطة1", "نقطة2"]}}
|
||
- improvement_opportunities: {{"en": ["Opp1", "Opp2"], "ar": ["فرصة1", "فرصة2"]}}
|
||
|
||
RETURN ONLY VALID JSON IN THIS EXACT FORMAT:
|
||
{{
|
||
"comment_index": 0,
|
||
"sentiment": {{
|
||
"classification": {{"en": "positive", "ar": "إيجابي"}},
|
||
"score": 0.85,
|
||
"confidence": 0.92,
|
||
"urgency_level": {{"en": "low", "ar": "منخفض"}}
|
||
}},
|
||
"summaries": {{
|
||
"en": "The patient expressed high satisfaction with Dr. Ahmed's thorough examination and clear explanation of the treatment plan. They appreciated the nursing staff's attentiveness but mentioned a 45-minute wait time in the cardiology department. Overall positive experience with room for improvement in scheduling.",
|
||
"ar": "أعرب المريض عن رضاه الكبير عن فحص د. أحمد الشامل وشرحه الواضح لخطة العلاج. وأشاد باهتمام طاقم التمريض لكنه أشار إلى وقت انتظار 45 دقيقة في قسم القلب. تجربة إيجابية بشكل عام مع مجال للتحسين في الجدولة."
|
||
}},
|
||
"keywords": {{
|
||
"en": ["excellent care", "Dr. Ahmed", "thorough examination", "wait time", "cardiology", "nursing staff", "treatment plan"],
|
||
"ar": ["رعاية ممتازة", "د. أحمد", "فحص شامل", "وقت الانتظار", "قسم القلب", "طاقم التمريض", "خطة العلاج"]
|
||
}},
|
||
"topics": {{
|
||
"en": ["Clinical Care Quality", "Doctor-Patient Communication", "Wait Times", "Nursing Care", "Cardiology Services"],
|
||
"ar": ["جودة الرعاية السريرية", "التواصل بين الطبيب والمريض", "أوقات الانتظار", "الرعاية التمريضية", "خدمات القلب"]
|
||
}},
|
||
"entities": [
|
||
{{
|
||
"text": {{"en": "Dr. Ahmed", "ar": "د. أحمد"}},
|
||
"type": {{"en": "DOCTOR", "ar": "طبيب"}}
|
||
}},
|
||
{{
|
||
"text": {{"en": "Cardiology Department", "ar": "قسم القلب"}},
|
||
"type": {{"en": "DEPARTMENT", "ar": "قسم"}}
|
||
}}
|
||
],
|
||
"emotions": {{
|
||
"joy": 0.8,
|
||
"anger": 0.15,
|
||
"sadness": 0.0,
|
||
"fear": 0.05,
|
||
"surprise": 0.1,
|
||
"disgust": 0.0,
|
||
"trust": 0.85,
|
||
"anticipation": 0.7,
|
||
"labels": {{
|
||
"joy": {{"en": "Satisfaction/Gratitude", "ar": "رضا/امتنان"}},
|
||
"anger": {{"en": "Frustration", "ar": "إحباط"}},
|
||
"sadness": {{"en": "Disappointment", "ar": "خيبة أمل"}},
|
||
"fear": {{"en": "Anxiety", "ar": "قلق"}},
|
||
"surprise": {{"en": "Surprise", "ar": "مفاجأة"}},
|
||
"disgust": {{"en": "Disgust", "ar": "اشمئزاز"}},
|
||
"trust": {{"en": "Trust/Confidence", "ar": "ثقة/طمأنينة"}},
|
||
"anticipation": {{"en": "Hope/Expectation", "ar": "أمل/توقع"}}
|
||
}}
|
||
}},
|
||
"actionable_insights": {{
|
||
"primary_concern": {{"en": "Extended wait times in cardiology", "ar": "أوقات انتظار طويلة في قسم القلب"}},
|
||
"affected_department": {{"en": "Cardiology", "ar": "قسم القلب"}},
|
||
"service_quality_indicators": {{
|
||
"clinical_care": 9,
|
||
"staff_behavior": 9,
|
||
"facility_condition": 8,
|
||
"wait_time": 6,
|
||
"communication": 9,
|
||
"overall_experience": 8
|
||
}},
|
||
"complaint_type": {{"en": "service", "ar": "خدمة"}},
|
||
"requires_followup": false,
|
||
"followup_priority": {{"en": "low", "ar": "منخفضة"}},
|
||
"recommended_actions": {{
|
||
"en": [
|
||
"Review cardiology department scheduling system",
|
||
"Recognize Dr. Ahmed for excellent patient communication",
|
||
"Implement wait time reduction strategies in cardiology"
|
||
],
|
||
"ar": [
|
||
"مراجعة نظام الجدولة في قسم القلب",
|
||
"تكريم د. أحمد لتميزه في التواصل مع المرضى",
|
||
"تطبيق استراتيجيات تقليل وقت الانتظار في قسم القلب"
|
||
]
|
||
}}
|
||
}},
|
||
"business_intelligence": {{
|
||
"patient_satisfaction_score": 82,
|
||
"nps_likelihood": 65,
|
||
"retention_risk": {{"level": "low", "score": 0.15}},
|
||
"reputation_impact": {{"level": "positive", "score": 0.7}},
|
||
"compliance_concerns": {{"present": false, "types": []}}
|
||
}},
|
||
"patient_journey": {{
|
||
"touchpoints": {{
|
||
"admission": false,
|
||
"waiting_area": true,
|
||
"consultation": true,
|
||
"diagnosis": false,
|
||
"treatment": true,
|
||
"nursing_care": true,
|
||
"medication": false,
|
||
"discharge": false,
|
||
"billing": false,
|
||
"follow_up": false
|
||
}}
|
||
}},
|
||
"competitive_insights": {{
|
||
"mentions_competitors": false,
|
||
"comparison_sentiment": {{"en": "neutral", "ar": "محايد"}},
|
||
"unique_selling_points": {{
|
||
"en": ["Excellent physician communication", "Attentive nursing staff"],
|
||
"ar": ["تواصل ممتاز للأطباء", "طاقم تمريض منتبه"]
|
||
}},
|
||
"improvement_opportunities": {{
|
||
"en": ["Optimize appointment scheduling", "Reduce cardiology wait times"],
|
||
"ar": ["تحسين جدولة المواعيد", "تقليل أوقات الانتظار في القلب"]
|
||
}}
|
||
}}
|
||
}}
|
||
|
||
IMPORTANT: Return ONLY the JSON object, no additional text or markdown formatting."""
|
||
|
||
logger.info(f"Prompt length: {len(prompt)} characters")
|
||
|
||
headers = {
|
||
'Authorization': f'Bearer {self.api_key}',
|
||
'Content-Type': 'application/json',
|
||
'HTTP-Referer': getattr(settings, 'SITE_URL', 'http://localhost'),
|
||
'X-Title': 'Healthcare Patient Experience Analyzer'
|
||
}
|
||
|
||
payload = {
|
||
'model': self.model,
|
||
'messages': [
|
||
{
|
||
'role': 'system',
|
||
'content': 'You are an expert healthcare patient experience analyst specializing in converting patient feedback into actionable business intelligence for hospital quality improvement. Always respond with valid JSON only, no markdown formatting.'
|
||
},
|
||
{
|
||
'role': 'user',
|
||
'content': prompt
|
||
}
|
||
],
|
||
'max_tokens': self.DEFAULT_MAX_TOKENS,
|
||
'temperature': self.DEFAULT_TEMPERATURE
|
||
}
|
||
|
||
logger.info(f"Request payload prepared:")
|
||
logger.info(f" - Model: {payload['model']}")
|
||
logger.info(f" - Max tokens: {payload['max_tokens']}")
|
||
logger.info(f" - Temperature: {payload['temperature']}")
|
||
|
||
with httpx.Client(timeout=self.timeout) as client:
|
||
response = client.post(
|
||
self.api_url,
|
||
headers=headers,
|
||
json=payload
|
||
)
|
||
|
||
logger.info(f"Response status: {response.status_code}")
|
||
|
||
if response.status_code != 200:
|
||
logger.error(f"API returned status {response.status_code}: {response.text}")
|
||
return {
|
||
'success': False,
|
||
'error': f'API error: {response.status_code} - {response.text}'
|
||
}
|
||
|
||
data = response.json()
|
||
|
||
# Extract analysis from response
|
||
if 'choices' in data and len(data['choices']) > 0:
|
||
content = data['choices'][0]['message']['content']
|
||
|
||
# Parse JSON response
|
||
try:
|
||
# Clean up response
|
||
content = content.strip()
|
||
|
||
# Remove markdown code blocks if present
|
||
if content.startswith('```json'):
|
||
content = content[7:]
|
||
elif content.startswith('```'):
|
||
content = content[3:]
|
||
|
||
if content.endswith('```'):
|
||
content = content[:-3]
|
||
|
||
content = content.strip()
|
||
|
||
analysis_data = json.loads(content)
|
||
|
||
# Extract metadata
|
||
metadata = {
|
||
'model': self.model,
|
||
'prompt_tokens': data.get('usage', {}).get('prompt_tokens', 0),
|
||
'completion_tokens': data.get('usage', {}).get('completion_tokens', 0),
|
||
'total_tokens': data.get('usage', {}).get('total_tokens', 0),
|
||
'analyzed_at': timezone.now().isoformat()
|
||
}
|
||
|
||
logger.info(f"Analysis completed successfully for comment {comment_id}")
|
||
logger.info(f" - Patient Satisfaction Score: {analysis_data.get('business_intelligence', {}).get('patient_satisfaction_score', 'N/A')}")
|
||
logger.info(f" - Sentiment: {analysis_data.get('sentiment', {}).get('classification', {}).get('en', 'N/A')}")
|
||
logger.info(f" - Requires Follow-up: {analysis_data.get('actionable_insights', {}).get('requires_followup', 'N/A')}")
|
||
|
||
return {
|
||
'success': True,
|
||
'comment_id': comment_id,
|
||
'analysis': analysis_data,
|
||
'metadata': metadata
|
||
}
|
||
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"JSON parse error: {e}")
|
||
logger.error(f"Content: {content[:500]}...")
|
||
return {
|
||
'success': False,
|
||
'error': f'Invalid JSON response from API: {str(e)}'
|
||
}
|
||
else:
|
||
logger.error(f"No choices found in response: {data}")
|
||
return {
|
||
'success': False,
|
||
'error': 'No analysis returned from API'
|
||
}
|
||
|
||
except httpx.HTTPStatusError as e:
|
||
logger.error(f"HTTP status error: {e}")
|
||
return {
|
||
'success': False,
|
||
'error': f'API error: {e.response.status_code} - {str(e)}'
|
||
}
|
||
except httpx.RequestError as e:
|
||
logger.error(f"Request error: {e}")
|
||
return {
|
||
'success': False,
|
||
'error': f'Request failed: {str(e)}'
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Unexpected error: {e}", exc_info=True)
|
||
return {
|
||
'success': False,
|
||
'error': f'Unexpected error: {str(e)}'
|
||
}
|
||
|
||
def is_configured(self) -> bool:
|
||
"""Check if service is properly configured."""
|
||
return bool(self.api_key) |