Spaces:
Sleeping
Sleeping
Commit
·
b9333d0
1
Parent(s):
9b13398
Update main.py and docker files
Browse files- Dockerfile +1 -1
- final.py +0 -708
- main.py +706 -1
- testing.py +50 -0
Dockerfile
CHANGED
@@ -94,7 +94,7 @@ exec uvicorn main:app --host 0.0.0.0 --port 8000 --log-level info\n\
|
|
94 |
' > /app/start.sh && chmod +x /app/start.sh
|
95 |
|
96 |
# Expose ports
|
97 |
-
EXPOSE 8000 11434
|
98 |
|
99 |
# Switch to non-root user
|
100 |
USER appuser
|
|
|
94 |
' > /app/start.sh && chmod +x /app/start.sh
|
95 |
|
96 |
# Expose ports
|
97 |
+
# EXPOSE 8000 11434
|
98 |
|
99 |
# Switch to non-root user
|
100 |
USER appuser
|
final.py
DELETED
@@ -1,708 +0,0 @@
|
|
1 |
-
import re
|
2 |
-
import json
|
3 |
-
import requests
|
4 |
-
import traceback
|
5 |
-
import time
|
6 |
-
import os
|
7 |
-
from typing import Dict, Any, List, Optional
|
8 |
-
from datetime import datetime, timedelta
|
9 |
-
|
10 |
-
# Updated imports for pydantic
|
11 |
-
from pydantic import BaseModel, Field
|
12 |
-
|
13 |
-
# Updated imports for LangChain
|
14 |
-
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
|
15 |
-
from langchain_core.output_parsers import JsonOutputParser
|
16 |
-
from langchain_ollama import OllamaLLM
|
17 |
-
from langchain.chains import LLMChain
|
18 |
-
from langchain.callbacks.manager import CallbackManager
|
19 |
-
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
20 |
-
from langchain_huggingface.embeddings import HuggingFaceEmbeddings
|
21 |
-
|
22 |
-
# Enhanced HuggingFace imports for improved functionality
|
23 |
-
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
|
24 |
-
import numpy as np
|
25 |
-
|
26 |
-
# Import endpoints documentation
|
27 |
-
from endpoints_documentation import endpoints_documentation
|
28 |
-
|
29 |
-
# Set environment variables for HuggingFace
|
30 |
-
# os.environ["HF_HOME"] = "/tmp/huggingface"
|
31 |
-
if os.name == 'posix' and os.uname().sysname == 'Darwin': # Check if running on macOS
|
32 |
-
# Use macOS appropriate paths
|
33 |
-
os.environ["HF_HOME"] = os.path.expanduser("~/Library/Caches/huggingface")
|
34 |
-
os.environ["TRANSFORMERS_CACHE"] = os.path.expanduser("~/Library/Caches/huggingface/transformers")
|
35 |
-
else:
|
36 |
-
# Default paths for Linux/Windows
|
37 |
-
os.environ["HF_HOME"] = "/tmp/huggingface"
|
38 |
-
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
39 |
-
|
40 |
-
class EndpointRequest(BaseModel):
|
41 |
-
"""Data model for API endpoint requests"""
|
42 |
-
endpoint: str = Field(..., description="The API endpoint path to call")
|
43 |
-
method: str = Field(..., description="The HTTP method to use (GET or POST)")
|
44 |
-
params: Dict[str, Any] = Field(default_factory=dict, description="Parameters for the API call")
|
45 |
-
missing_required: List[str] = Field(default_factory=list, description="Any required parameters that are missing")
|
46 |
-
|
47 |
-
|
48 |
-
class AIAgent:
|
49 |
-
def __init__(self):
|
50 |
-
self.endpoints_documentation = endpoints_documentation
|
51 |
-
self.ollama_base_url = "http://localhost:11434" # Default Ollama URL
|
52 |
-
self.model_name = "mistral" # Using mistral model for better multilingual support
|
53 |
-
# self.model_name = 'llama3'
|
54 |
-
self.BASE_URL = 'https://agent.serveo.net'
|
55 |
-
self.headers = {
|
56 |
-
'Content-type': 'application/json'
|
57 |
-
}
|
58 |
-
self.user_id = 'd8507df8-cec6-49f9-adcc-367b13805e73'
|
59 |
-
self.max_retries = 3
|
60 |
-
self.retry_delay = 2 # seconds
|
61 |
-
|
62 |
-
# Enhanced language detection using HuggingFace models
|
63 |
-
self._initialize_language_tools()
|
64 |
-
|
65 |
-
# Initialize LangChain components
|
66 |
-
self._initialize_llm()
|
67 |
-
self._initialize_parsers_and_chains()
|
68 |
-
|
69 |
-
# Add date parsing capabilities
|
70 |
-
self._initialize_date_parser()
|
71 |
-
|
72 |
-
def _initialize_language_tools(self):
|
73 |
-
"""Initialize more sophisticated language processing tools"""
|
74 |
-
# Use multilingual embeddings for semantic understanding
|
75 |
-
self.embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
|
76 |
-
|
77 |
-
# Initialize language identification model
|
78 |
-
try:
|
79 |
-
self.language_classifier = pipeline(
|
80 |
-
"text-classification",
|
81 |
-
model="papluca/xlm-roberta-base-language-detection",
|
82 |
-
top_k=1
|
83 |
-
)
|
84 |
-
print("Language classification model loaded successfully")
|
85 |
-
except Exception as e:
|
86 |
-
print(f"Failed to load language classification model: {e}")
|
87 |
-
# Fallback to basic regex detection if model fails to load
|
88 |
-
self.language_classifier = None
|
89 |
-
|
90 |
-
# Add sentiment analysis for enhanced response generation
|
91 |
-
try:
|
92 |
-
self.sentiment_analyzer = pipeline(
|
93 |
-
"sentiment-analysis",
|
94 |
-
model="cardiffnlp/twitter-xlm-roberta-base-sentiment"
|
95 |
-
)
|
96 |
-
print("Sentiment analysis model loaded successfully")
|
97 |
-
except Exception as e:
|
98 |
-
print(f"Failed to load sentiment analysis model: {e}")
|
99 |
-
self.sentiment_analyzer = None
|
100 |
-
|
101 |
-
def _initialize_date_parser(self):
|
102 |
-
"""Initialize date parsing model for handling relative date expressions"""
|
103 |
-
try:
|
104 |
-
self.date_parser = pipeline(
|
105 |
-
"token-classification",
|
106 |
-
model="Jean-Baptiste/roberta-large-ner-english",
|
107 |
-
aggregation_strategy="simple"
|
108 |
-
)
|
109 |
-
print("Date parsing model loaded successfully")
|
110 |
-
except Exception as e:
|
111 |
-
print(f"Failed to load date parsing model: {e}")
|
112 |
-
self.date_parser = None
|
113 |
-
|
114 |
-
def detect_language(self, text):
|
115 |
-
"""
|
116 |
-
Enhanced language detection using HuggingFace models
|
117 |
-
"""
|
118 |
-
# First try using the HuggingFace language classification model if available
|
119 |
-
if self.language_classifier and len(text.strip()) > 3:
|
120 |
-
try:
|
121 |
-
result = self.language_classifier(text)
|
122 |
-
detected_lang = result[0][0]['label']
|
123 |
-
confidence = result[0][0]['score']
|
124 |
-
|
125 |
-
print(f"Language detected: {detected_lang} with confidence {confidence:.4f}")
|
126 |
-
|
127 |
-
# Map the detected language to our simplified language set
|
128 |
-
if detected_lang in ['ar', 'arabic']:
|
129 |
-
return "arabic"
|
130 |
-
elif detected_lang in ['en', 'english']:
|
131 |
-
return "english"
|
132 |
-
elif confidence > 0.8: # If confident but not English/Arabic
|
133 |
-
# We currently only support English/Arabic, but log other languages
|
134 |
-
print(f"Detected unsupported language: {detected_lang}")
|
135 |
-
# Default to English for other languages for now
|
136 |
-
return "english"
|
137 |
-
except Exception as e:
|
138 |
-
print(f"Error in language detection model: {e}")
|
139 |
-
# Continue to fallback methods
|
140 |
-
|
141 |
-
# Fallback: Basic detection of Arabic text using regex
|
142 |
-
arabic_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+')
|
143 |
-
if arabic_pattern.search(text):
|
144 |
-
return "arabic"
|
145 |
-
|
146 |
-
# Default to English
|
147 |
-
return "english"
|
148 |
-
|
149 |
-
def analyze_sentiment(self, text):
|
150 |
-
"""Analyze the sentiment of the input text"""
|
151 |
-
if self.sentiment_analyzer and len(text.strip()) > 3:
|
152 |
-
try:
|
153 |
-
result = self.sentiment_analyzer(text)
|
154 |
-
sentiment = result[0]['label']
|
155 |
-
score = result[0]['score']
|
156 |
-
return {
|
157 |
-
"sentiment": sentiment,
|
158 |
-
"score": score
|
159 |
-
}
|
160 |
-
except Exception as e:
|
161 |
-
print(f"Error in sentiment analysis: {e}")
|
162 |
-
|
163 |
-
# Default neutral sentiment if analysis fails
|
164 |
-
return {"sentiment": "NEUTRAL", "score": 0.5}
|
165 |
-
|
166 |
-
def extract_semantic_keywords(self, text, top_n=5):
|
167 |
-
"""Extract semantic keywords from text using embeddings"""
|
168 |
-
try:
|
169 |
-
# Simple keyword extraction using embeddings comparison
|
170 |
-
# This is a basic implementation - could be enhanced further
|
171 |
-
words = re.findall(r'\b\w+\b', text.lower())
|
172 |
-
unique_words = list(set([w for w in words if len(w) > 3]))
|
173 |
-
|
174 |
-
if not unique_words:
|
175 |
-
return []
|
176 |
-
|
177 |
-
# Get embeddings for all words
|
178 |
-
embeddings_list = []
|
179 |
-
for word in unique_words:
|
180 |
-
try:
|
181 |
-
emb = self.embeddings.embed_query(word)
|
182 |
-
embeddings_list.append((word, emb))
|
183 |
-
except Exception as e:
|
184 |
-
print(f"Error embedding word {word}: {e}")
|
185 |
-
|
186 |
-
# Get embedding for full text
|
187 |
-
text_embedding = self.embeddings.embed_query(text)
|
188 |
-
|
189 |
-
# Calculate similarity to full text
|
190 |
-
similarities = []
|
191 |
-
for word, emb in embeddings_list:
|
192 |
-
similarity = np.dot(emb, text_embedding) / (np.linalg.norm(emb) * np.linalg.norm(text_embedding))
|
193 |
-
similarities.append((word, similarity))
|
194 |
-
|
195 |
-
# Sort by similarity
|
196 |
-
similarities.sort(key=lambda x: x[1], reverse=True)
|
197 |
-
|
198 |
-
# Return top N keywords
|
199 |
-
return [word for word, _ in similarities[:top_n]]
|
200 |
-
|
201 |
-
except Exception as e:
|
202 |
-
print(f"Error extracting keywords: {e}")
|
203 |
-
return []
|
204 |
-
|
205 |
-
def _initialize_llm(self):
|
206 |
-
"""Initialize the LLM with appropriate configuration"""
|
207 |
-
# Set up the callback manager for streaming (optional)
|
208 |
-
callbacks = [StreamingStdOutCallbackHandler()]
|
209 |
-
|
210 |
-
# Initialize the Ollama LLM with updated parameters
|
211 |
-
self.llm = OllamaLLM(
|
212 |
-
model=self.model_name,
|
213 |
-
base_url=self.ollama_base_url,
|
214 |
-
callbacks=callbacks,
|
215 |
-
temperature=0.7,
|
216 |
-
num_ctx=8192, # Increased context window
|
217 |
-
top_p=0.9,
|
218 |
-
request_timeout=60, # Timeout in seconds
|
219 |
-
)
|
220 |
-
|
221 |
-
def _initialize_parsers_and_chains(self):
|
222 |
-
"""Initialize output parsers and LLM chains"""
|
223 |
-
# Setup JSON parser for structured output
|
224 |
-
self.json_parser = JsonOutputParser(pydantic_object=EndpointRequest)
|
225 |
-
|
226 |
-
# Create multilingual router prompt template with enhanced context
|
227 |
-
self.router_prompt_template = PromptTemplate(
|
228 |
-
template="""
|
229 |
-
You are a precise API routing assistant. Your job is to analyze user queries and select the correct API endpoint with proper parameters.
|
230 |
-
|
231 |
-
=== ENDPOINT DOCUMENTATION ===
|
232 |
-
{endpoints_documentation}
|
233 |
-
|
234 |
-
=== USER REQUEST ANALYSIS ===
|
235 |
-
User Query: {user_query}
|
236 |
-
Language: {detected_language}
|
237 |
-
Keywords: {extracted_keywords}
|
238 |
-
Sentiment: {sentiment_analysis}
|
239 |
-
|
240 |
-
=== ROUTING PROCESS ===
|
241 |
-
Follow these steps in order:
|
242 |
-
|
243 |
-
STEP 1: INTENT ANALYSIS
|
244 |
-
- What is the user trying to accomplish?
|
245 |
-
- What type of operation are they requesting? (create, read, update, delete, search, etc.)
|
246 |
-
- What entity/resource are they working with?
|
247 |
-
|
248 |
-
STEP 2: ENDPOINT MATCHING
|
249 |
-
- Review each endpoint in the documentation
|
250 |
-
- Match the user's intent to the endpoint's PURPOSE/DESCRIPTION
|
251 |
-
- Consider the HTTP method (GET for retrieval, POST for creation, etc.)
|
252 |
-
- Verify the endpoint can handle the user's specific request
|
253 |
-
|
254 |
-
STEP 3: PARAMETER EXTRACTION
|
255 |
-
- Identify ALL required parameters from the endpoint documentation
|
256 |
-
- Extract parameter values from the user query
|
257 |
-
- Convert data types as needed (dates to ISO 8601, numbers to integers, etc.)
|
258 |
-
- Set appropriate defaults for optional parameters if beneficial
|
259 |
-
|
260 |
-
STEP 4: VALIDATION
|
261 |
-
- Ensure ALL required parameters are provided or identified as missing
|
262 |
-
- Verify parameter formats match documentation requirements
|
263 |
-
- Check that the selected endpoint actually solves the user's problem
|
264 |
-
|
265 |
-
=== RESPONSE FORMAT ===
|
266 |
-
Provide your analysis and decision in this exact JSON structure:
|
267 |
-
|
268 |
-
{{
|
269 |
-
"reasoning": {{
|
270 |
-
"user_intent": "Brief description of what the user wants to accomplish",
|
271 |
-
"selected_endpoint": "Why this endpoint was chosen over others",
|
272 |
-
"parameter_mapping": "How user query maps to endpoint parameters"
|
273 |
-
}},
|
274 |
-
"endpoint": "/exact_endpoint_path_from_documentation",
|
275 |
-
"method": "HTTP_METHOD",
|
276 |
-
"params": {{
|
277 |
-
"required_param_1": "extracted_or_converted_value",
|
278 |
-
"required_param_2": "extracted_or_converted_value",
|
279 |
-
"optional_param": "value_if_applicable"
|
280 |
-
}},
|
281 |
-
"missing_required": ["list", "of", "missing", "required", "parameters"],
|
282 |
-
"confidence": 0.95
|
283 |
-
}}
|
284 |
-
|
285 |
-
=== CRITICAL RULES ===
|
286 |
-
1. ONLY select endpoints that exist in the provided documentation
|
287 |
-
2. NEVER fabricate or assume endpoint parameters not in documentation
|
288 |
-
3. ALL required parameters MUST be included or listed as missing
|
289 |
-
4. Convert dates/times to ISO 8601 format (YYYY-MM-DDTHH:MM:SS)
|
290 |
-
5. If patient_id is required and not provided, add it to missing_required
|
291 |
-
6. Match endpoints by PURPOSE, not just keywords in the path
|
292 |
-
7. If multiple endpoints could work, choose the most specific one
|
293 |
-
8. If no endpoint matches, set endpoint to null and explain in reasoning
|
294 |
-
|
295 |
-
=== EXAMPLES OF GOOD MATCHING ===
|
296 |
-
- User wants "patient records" → Use patient retrieval endpoint, not general search
|
297 |
-
- User wants to "schedule appointment" → Use appointment creation endpoint
|
298 |
-
- User asks "what appointments today" → Use appointment listing with date filter
|
299 |
-
- User wants to "update medication" → Use medication update endpoint with patient_id
|
300 |
-
|
301 |
-
Think step by step and be precise with your endpoint selection and parameter extraction.
|
302 |
-
""",
|
303 |
-
input_variables=["endpoints_documentation", "user_query", "detected_language",
|
304 |
-
"extracted_keywords", "sentiment_analysis"],
|
305 |
-
partial_variables={"format_instructions": self.json_parser.get_format_instructions()}
|
306 |
-
)
|
307 |
-
|
308 |
-
# # Create user-friendly response template with enhanced context awareness
|
309 |
-
# self.user_response_template = PromptTemplate(
|
310 |
-
# template="""
|
311 |
-
# You are a professional and friendly virtual assistant for a healthcare system.
|
312 |
-
# Your task is to generate clear, concise, and professional responses to user queries.
|
313 |
-
|
314 |
-
# IMPORTANT RULES:
|
315 |
-
# - Respond ONLY in {detected_language}
|
316 |
-
# - For Arabic, use Modern Standard Arabic (فصحى)
|
317 |
-
# - Keep responses SHORT and DIRECT
|
318 |
-
# - Include ONLY essential information
|
319 |
-
# - NEVER mix languages
|
320 |
-
# - ALWAYS use the EXACT data from the system response
|
321 |
-
# - NEVER make up or modify hospital information
|
322 |
-
# - Use professional and polite tone
|
323 |
-
|
324 |
-
# Original query: {user_query}
|
325 |
-
# System result: {api_response}
|
326 |
-
# User sentiment: {sentiment_analysis}
|
327 |
-
|
328 |
-
# ARABIC RESPONSE RULES:
|
329 |
-
# - Use Arabic numbers (١، ٢، ٣)
|
330 |
-
# - Use proper date format (١٥ مايو ٢٠٢٥)
|
331 |
-
# - Use proper time format (الساعة ٨ صباحاً)
|
332 |
-
# - Use formal medical terms
|
333 |
-
# - Keep sentences short and clear
|
334 |
-
# - Use exact hospital names and addresses from the data
|
335 |
-
# - Use exact working hours from the data
|
336 |
-
# - Use professional healthcare terminology
|
337 |
-
|
338 |
-
# ENGLISH RESPONSE RULES:
|
339 |
-
# - Use clear, direct language
|
340 |
-
# - Include only essential details
|
341 |
-
# - Use proper medical terms
|
342 |
-
# - Keep responses concise
|
343 |
-
# - Use exact hospital names and addresses from the data
|
344 |
-
# - Use exact working hours from the data
|
345 |
-
# - Use professional healthcare terminology
|
346 |
-
|
347 |
-
# Remember:
|
348 |
-
# - Keep responses SHORT and FOCUSED
|
349 |
-
# - Use ONLY data from the system response
|
350 |
-
# - NEVER modify or make up hospital information
|
351 |
-
# - Include only what's necessary to answer the query
|
352 |
-
# - Maintain professional and polite tone
|
353 |
-
# - Use proper healthcare terminology
|
354 |
-
# """,
|
355 |
-
# input_variables=["user_query", "api_response", "detected_language",
|
356 |
-
# "sentiment_analysis", "extracted_keywords"]
|
357 |
-
# )
|
358 |
-
# Create user-friendly response template with enhanced context awareness
|
359 |
-
# Create user-friendly response template with enhanced context awareness
|
360 |
-
# Create user-friendly response template with enhanced context awareness
|
361 |
-
self.user_response_template = PromptTemplate(
|
362 |
-
template="""
|
363 |
-
You are a professional healthcare assistant. Generate clear, accurate responses using EXACT data from the system.
|
364 |
-
|
365 |
-
=== STRICT REQUIREMENTS ===
|
366 |
-
- Respond ONLY in {detected_language}
|
367 |
-
- Use EXACT information from api_response - NO modifications
|
368 |
-
- Keep responses SHORT, SIMPLE, and DIRECT
|
369 |
-
- Use professional healthcare tone
|
370 |
-
- NEVER mix languages or make up information
|
371 |
-
|
372 |
-
=== ORIGINAL REQUEST ===
|
373 |
-
User Query: {user_query}
|
374 |
-
User Sentiment: {sentiment_analysis}
|
375 |
-
|
376 |
-
=== SYSTEM DATA ===
|
377 |
-
{api_response}
|
378 |
-
|
379 |
-
=== LANGUAGE-SPECIFIC FORMATTING ===
|
380 |
-
|
381 |
-
FOR ARABIC RESPONSES:
|
382 |
-
- Use Modern Standard Arabic (الفصحى)
|
383 |
-
- Use Arabic numerals: ١، ٢، ٣، ٤، ٥، ٦، ٧، ٨، ٩، ١٠
|
384 |
-
- Time format: "من الساعة ٨:٠٠ صباحاً إلى ٥:٠٠ مساءً"
|
385 |
-
- Date format: "١٥ مايو ٢٠٢٥"
|
386 |
-
- Use proper Arabic medical terminology
|
387 |
-
- Keep sentences short and grammatically correct
|
388 |
-
- Example format for hospitals:
|
389 |
-
"مستشفى [الاسم] - العنوان: [العنوان الكامل] - أوقات العمل: من [الوقت] إلى [الوقت]"
|
390 |
-
|
391 |
-
FOR ENGLISH RESPONSES:
|
392 |
-
- Use clear, professional language
|
393 |
-
- Time format: "8:00 AM to 5:00 PM"
|
394 |
-
- Date format: "May 15, 2025"
|
395 |
-
- Keep sentences concise and direct
|
396 |
-
- Example format for hospitals:
|
397 |
-
"[Hospital Name] - Address: [Full Address] - Hours: [Opening Time] to [Closing Time]"
|
398 |
-
|
399 |
-
=== RESPONSE STRUCTURE ===
|
400 |
-
1. Direct answer to the user's question
|
401 |
-
2. Essential details only (names, addresses, hours, contact info)
|
402 |
-
3. Brief helpful note if needed
|
403 |
-
4. No unnecessary introductions or conclusions
|
404 |
-
|
405 |
-
=== CRITICAL RULES ===
|
406 |
-
- Extract information EXACTLY as provided in api_response
|
407 |
-
- Do NOT include technical URLs, IDs, or system codes in the response
|
408 |
-
- Do NOT show raw links or booking URLs to users
|
409 |
-
- Present information in natural, conversational language
|
410 |
-
- Do NOT use bullet points or technical formatting
|
411 |
-
- Write as if you're speaking to the patient directly
|
412 |
-
- If data is missing, state "المعلومات غير متوفرة" (Arabic) or "Information not available" (English)
|
413 |
-
- Convert technical data into human-readable format
|
414 |
-
- NEVER add translations or explanations in other languages
|
415 |
-
- NEVER include "Translated response" or similar phrases
|
416 |
-
- END your response immediately after providing the requested information
|
417 |
-
- Do NOT add any English translation when responding in Arabic
|
418 |
-
- Do NOT add any Arabic translation when responding in English
|
419 |
-
|
420 |
-
=== HUMAN-LIKE FORMATTING RULES ===
|
421 |
-
FOR ARABIC:
|
422 |
-
- Instead of "رابط الحجز: [URL]" → say "تم حجز موعدك بنجاح"
|
423 |
-
- Instead of "الأزمة: غير متوفرة" → omit or say "بدون أعراض محددة"
|
424 |
-
- Use natural sentences like "موعدك مع الدكتور [Name] يوم [Date] في تمام الساعة [Time]"
|
425 |
-
- Avoid technical terms and system language
|
426 |
-
|
427 |
-
FOR ENGLISH:
|
428 |
-
- Instead of "Booking URL: [link]" → say "Your appointment has been scheduled"
|
429 |
-
- Use natural sentences like "You have an appointment with Dr. [Name] on [Date] at [Time]"
|
430 |
-
- Avoid showing raw URLs, IDs, or technical data
|
431 |
-
|
432 |
-
=== QUALITY CHECKS ===
|
433 |
-
Before responding, verify:
|
434 |
-
✓ Response sounds natural and conversational
|
435 |
-
✓ No technical URLs, IDs, or system codes are shown
|
436 |
-
✓ Information is presented in human-friendly language
|
437 |
-
✓ Grammar is correct in the target language
|
438 |
-
✓ Response directly answers the user's question
|
439 |
-
✓ No bullet points or technical formatting
|
440 |
-
✓ Sounds like a helpful human assistant, not a system
|
441 |
-
|
442 |
-
Generate a response that is accurate, helpful, and professionally formatted.
|
443 |
-
|
444 |
-
=== FINAL INSTRUCTION ===
|
445 |
-
Respond ONLY in the requested language. Do NOT provide translations, explanations, or additional text in any other language. Stop immediately after answering the user's question.
|
446 |
-
""",
|
447 |
-
input_variables=["user_query", "api_response", "detected_language",
|
448 |
-
"sentiment_analysis", "extracted_keywords"]
|
449 |
-
)
|
450 |
-
|
451 |
-
# Create LLM chains
|
452 |
-
self.router_chain = LLMChain(
|
453 |
-
llm=self.llm,
|
454 |
-
prompt=self.router_prompt_template,
|
455 |
-
output_key="route_result"
|
456 |
-
)
|
457 |
-
|
458 |
-
self.user_response_chain = LLMChain(
|
459 |
-
llm=self.llm,
|
460 |
-
prompt=self.user_response_template,
|
461 |
-
output_key="user_friendly_response"
|
462 |
-
)
|
463 |
-
|
464 |
-
def parse_relative_date(self, text, detected_language):
|
465 |
-
"""
|
466 |
-
Parse relative dates from text using a combination of methods
|
467 |
-
"""
|
468 |
-
today = datetime.now()
|
469 |
-
|
470 |
-
# Handle common relative date patterns in English and Arabic
|
471 |
-
tomorrow_patterns = {
|
472 |
-
'english': [r'\btomorrow\b', r'\bnext day\b'],
|
473 |
-
'arabic': [r'\bغدا\b', r'\bبكرة\b', r'\bغدًا\b', r'\bالغد\b']
|
474 |
-
}
|
475 |
-
|
476 |
-
next_week_patterns = {
|
477 |
-
'english': [r'\bnext week\b'],
|
478 |
-
'arabic': [r'\bالأسبوع القادم\b', r'\bالأسبوع المقبل\b', r'\bالاسبوع الجاي\b']
|
479 |
-
}
|
480 |
-
|
481 |
-
# Check for "tomorrow" patterns
|
482 |
-
for pattern in tomorrow_patterns.get(detected_language, []) + tomorrow_patterns.get('english', []):
|
483 |
-
if re.search(pattern, text, re.IGNORECASE):
|
484 |
-
return (today + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S')
|
485 |
-
|
486 |
-
# Check for "next week" patterns
|
487 |
-
for pattern in next_week_patterns.get(detected_language, []) + next_week_patterns.get('english', []):
|
488 |
-
if re.search(pattern, text, re.IGNORECASE):
|
489 |
-
return (today + timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%S')
|
490 |
-
|
491 |
-
# If NER model is available, use it to extract date entities
|
492 |
-
if self.date_parser and detected_language == 'english':
|
493 |
-
try:
|
494 |
-
date_entities = self.date_parser(text)
|
495 |
-
for entity in date_entities:
|
496 |
-
if entity['entity_group'] == 'DATE':
|
497 |
-
# Here you would need more complex date parsing logic
|
498 |
-
# This is just a placeholder
|
499 |
-
print(f"Found date entity: {entity['word']}")
|
500 |
-
# For now, just default to tomorrow if we detect any date
|
501 |
-
return (today + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S')
|
502 |
-
except Exception as e:
|
503 |
-
print(f"Error in date parsing: {e}")
|
504 |
-
|
505 |
-
# Default return None if no date pattern is recognized
|
506 |
-
return None
|
507 |
-
|
508 |
-
def process_user_query(self, user_query: str) -> Dict[str, Any]:
|
509 |
-
"""
|
510 |
-
Process the user query through the LangChain pipeline and return a response
|
511 |
-
"""
|
512 |
-
try:
|
513 |
-
start_time = time.time()
|
514 |
-
|
515 |
-
# Detect language of the query
|
516 |
-
detected_language = self.detect_language(user_query)
|
517 |
-
print(f"Detected language: {detected_language}")
|
518 |
-
|
519 |
-
# Enhanced context using Hugging Face models
|
520 |
-
sentiment_result = self.analyze_sentiment(user_query)
|
521 |
-
print(f"Sentiment analysis: {sentiment_result}")
|
522 |
-
|
523 |
-
extracted_keywords = self.extract_semantic_keywords(user_query)
|
524 |
-
print(f"Extracted keywords: {extracted_keywords}")
|
525 |
-
|
526 |
-
# Try to extract dates from query
|
527 |
-
parsed_date = self.parse_relative_date(user_query, detected_language)
|
528 |
-
if parsed_date:
|
529 |
-
print(f"Parsed relative date: {parsed_date}")
|
530 |
-
|
531 |
-
# 1. Route the query to determine which API endpoint to call
|
532 |
-
router_result = self.router_chain.invoke({
|
533 |
-
"endpoints_documentation": json.dumps(self.endpoints_documentation, indent=2),
|
534 |
-
"user_query": user_query,
|
535 |
-
"detected_language": detected_language,
|
536 |
-
"extracted_keywords": ", ".join(extracted_keywords),
|
537 |
-
"sentiment_analysis": json.dumps(sentiment_result)
|
538 |
-
})
|
539 |
-
|
540 |
-
# 2. Parse the router response
|
541 |
-
route_result = router_result["route_result"]
|
542 |
-
parsed_route = None
|
543 |
-
|
544 |
-
# Clean the response first
|
545 |
-
cleaned_response = route_result
|
546 |
-
|
547 |
-
# Remove any comments (both single-line and multi-line)
|
548 |
-
cleaned_response = re.sub(r'//.*?$', '', cleaned_response, flags=re.MULTILINE)
|
549 |
-
cleaned_response = re.sub(r'/\*.*?\*/', '', cleaned_response, flags=re.DOTALL)
|
550 |
-
|
551 |
-
# Remove any trailing commas
|
552 |
-
cleaned_response = re.sub(r',(\s*[}\]])', r'\1', cleaned_response)
|
553 |
-
|
554 |
-
# Try different methods to parse the JSON response
|
555 |
-
try:
|
556 |
-
# First attempt: direct JSON parsing of cleaned response
|
557 |
-
parsed_route = json.loads(cleaned_response)
|
558 |
-
except json.JSONDecodeError:
|
559 |
-
try:
|
560 |
-
# Second attempt: extract JSON from markdown code block
|
561 |
-
json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', cleaned_response, re.DOTALL)
|
562 |
-
if json_match:
|
563 |
-
parsed_route = json.loads(json_match.group(1))
|
564 |
-
except (json.JSONDecodeError, AttributeError):
|
565 |
-
try:
|
566 |
-
# Third attempt: find JSON-like content using regex
|
567 |
-
json_pattern = r'\{\s*"endpoint"\s*:.*?\}'
|
568 |
-
json_match = re.search(json_pattern, cleaned_response, re.DOTALL)
|
569 |
-
if json_match:
|
570 |
-
json_str = json_match.group(0)
|
571 |
-
# Additional cleaning for the extracted JSON
|
572 |
-
json_str = re.sub(r'//.*?$', '', json_str, flags=re.MULTILINE)
|
573 |
-
json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
|
574 |
-
parsed_route = json.loads(json_str)
|
575 |
-
except (json.JSONDecodeError, AttributeError):
|
576 |
-
print(f"Failed to parse JSON. Raw response: {route_result}")
|
577 |
-
print(f"Cleaned response: {cleaned_response}")
|
578 |
-
raise ValueError("Could not extract valid JSON from LLM response")
|
579 |
-
|
580 |
-
if not parsed_route:
|
581 |
-
raise ValueError("Failed to parse LLM response into valid JSON")
|
582 |
-
|
583 |
-
# Replace any placeholder values and inject parsed dates if available
|
584 |
-
if 'params' in parsed_route:
|
585 |
-
if 'patient_id' in parsed_route['params']:
|
586 |
-
parsed_route['params']['patient_id'] = self.user_id
|
587 |
-
|
588 |
-
# Inject parsed date if available and a date parameter exists
|
589 |
-
date_params = ['appointment_date', 'date', 'schedule_date', 'date_time', 'new_date_time']
|
590 |
-
if parsed_date:
|
591 |
-
for param in date_params:
|
592 |
-
if param in parsed_route['params']:
|
593 |
-
parsed_route['params'][param] = parsed_date
|
594 |
-
|
595 |
-
print('Parsed route: ', parsed_route)
|
596 |
-
print(f"Routing completed in {time.time() - start_time:.2f} seconds")
|
597 |
-
|
598 |
-
# 3. Make the backend API call
|
599 |
-
backend_response = self.backend_call(parsed_route)
|
600 |
-
|
601 |
-
# 4. Generate user-friendly response
|
602 |
-
user_friendly_result = self.user_response_chain.invoke({
|
603 |
-
"user_query": user_query,
|
604 |
-
"api_response": json.dumps(backend_response, indent=2),
|
605 |
-
"detected_language": detected_language,
|
606 |
-
"sentiment_analysis": json.dumps(sentiment_result),
|
607 |
-
"extracted_keywords": ", ".join(extracted_keywords)
|
608 |
-
})
|
609 |
-
print('user response: ', user_friendly_result["user_friendly_response"])
|
610 |
-
|
611 |
-
print(f"Total processing time: {time.time() - start_time:.2f} seconds")
|
612 |
-
|
613 |
-
return {
|
614 |
-
"routing_info": parsed_route,
|
615 |
-
"api_response": backend_response,
|
616 |
-
"user_friendly_response": user_friendly_result["user_friendly_response"],
|
617 |
-
"detected_language": detected_language,
|
618 |
-
"sentiment": sentiment_result,
|
619 |
-
"keywords": extracted_keywords
|
620 |
-
}
|
621 |
-
|
622 |
-
except Exception as e:
|
623 |
-
error_detail = {
|
624 |
-
"error": f"Error processing query: {str(e)}",
|
625 |
-
"type": type(e).__name__,
|
626 |
-
"traceback": traceback.format_exc()
|
627 |
-
}
|
628 |
-
print(f"Error: {error_detail['error']}")
|
629 |
-
print(f"Traceback: {error_detail['traceback']}")
|
630 |
-
return error_detail
|
631 |
-
|
632 |
-
def backend_call(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
633 |
-
"""
|
634 |
-
Make the actual API call to the backend with retry logic
|
635 |
-
"""
|
636 |
-
endpoint_url = data.get('endpoint')
|
637 |
-
endpoint_method = data.get('method')
|
638 |
-
endpoint_params = data.get('params', {}).copy() # Create a copy to avoid modifying the original
|
639 |
-
|
640 |
-
print('Endpoint url: ' + endpoint_url)
|
641 |
-
print('Method: ', endpoint_method)
|
642 |
-
print('Params: ', endpoint_params)
|
643 |
-
|
644 |
-
# Add retry logic for more robust API calls
|
645 |
-
retries = 0
|
646 |
-
while retries < self.max_retries:
|
647 |
-
try:
|
648 |
-
if endpoint_method.upper() == 'GET':
|
649 |
-
response = requests.get(
|
650 |
-
self.BASE_URL + endpoint_url,
|
651 |
-
params=endpoint_params,
|
652 |
-
headers=self.headers,
|
653 |
-
timeout=10 # Add timeout for backend calls
|
654 |
-
)
|
655 |
-
elif endpoint_method.upper() == 'POST': # POST or other methods
|
656 |
-
response = requests.post(
|
657 |
-
self.BASE_URL + endpoint_url,
|
658 |
-
json=endpoint_params,
|
659 |
-
headers=self.headers,
|
660 |
-
timeout=10
|
661 |
-
)
|
662 |
-
elif endpoint_method.upper() == 'PUT':
|
663 |
-
response = requests.put(
|
664 |
-
self.BASE_URL + endpoint_url,
|
665 |
-
json=endpoint_params,
|
666 |
-
headers=self.headers,
|
667 |
-
timeout=10
|
668 |
-
)
|
669 |
-
|
670 |
-
# Check if response status is success
|
671 |
-
response.raise_for_status()
|
672 |
-
return response.json()
|
673 |
-
|
674 |
-
except requests.exceptions.RequestException as e:
|
675 |
-
retries += 1
|
676 |
-
if retries >= self.max_retries:
|
677 |
-
return {
|
678 |
-
"error": "Backend API call failed after multiple retries",
|
679 |
-
"details": str(e),
|
680 |
-
"status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None
|
681 |
-
}
|
682 |
-
|
683 |
-
print(f"API call attempt {retries} failed, retrying in {self.retry_delay} seconds...")
|
684 |
-
time.sleep(self.retry_delay)
|
685 |
-
|
686 |
-
|
687 |
-
# Initialize the AI agent singleton
|
688 |
-
# ai_agent = AIAgent()
|
689 |
-
|
690 |
-
|
691 |
-
# Test the agent directly
|
692 |
-
# if __name__ == "__main__":
|
693 |
-
# agent = AIAgent()
|
694 |
-
|
695 |
-
# # Test with English query
|
696 |
-
# # print("\n---Testing English Query---")
|
697 |
-
# # english_response = agent.process_user_query("I need to book an appointment with Dr. Smith tomorrow at 8 PM")
|
698 |
-
# # print("\nEnglish response:")
|
699 |
-
# # print(english_response["user_friendly_response"])
|
700 |
-
|
701 |
-
# # Test with Arabic query
|
702 |
-
# print("\n---Testing Arabic Query---")
|
703 |
-
# # arabic_response = agent.process_user_query(" اريد الغاء الحجز مع الدكتور Smith")
|
704 |
-
# arabic_response = agent.process_user_query("اريد حجز ميعاد غدا في الساعه الثامنه مساء مع الدكتور Smith")
|
705 |
-
# # arabic_response = agent.process_user_query("متى يفتح المستشفى؟")
|
706 |
-
# # arabic_response = agent.process_user_query("اريد معرفه كل الحجوزات الخاصه بي")
|
707 |
-
# print("\nArabic response:")
|
708 |
-
# print(arabic_response["user_friendly_response"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
@@ -1,7 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from fastapi import FastAPI, HTTPException
|
2 |
from pydantic import BaseModel
|
3 |
from typing import Dict, Any, Optional
|
4 |
-
|
5 |
|
6 |
app = FastAPI(
|
7 |
title="Healthcare AI Assistant",
|
|
|
1 |
+
import re
|
2 |
+
import json
|
3 |
+
import requests
|
4 |
+
import traceback
|
5 |
+
import time
|
6 |
+
import os
|
7 |
+
from typing import Dict, Any, List, Optional
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
|
10 |
+
# Updated imports for pydantic
|
11 |
+
from pydantic import BaseModel, Field
|
12 |
+
|
13 |
+
# Updated imports for LangChain
|
14 |
+
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
|
15 |
+
from langchain_core.output_parsers import JsonOutputParser
|
16 |
+
from langchain_ollama import OllamaLLM
|
17 |
+
from langchain.chains import LLMChain
|
18 |
+
from langchain.callbacks.manager import CallbackManager
|
19 |
+
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
20 |
+
from langchain_huggingface.embeddings import HuggingFaceEmbeddings
|
21 |
+
|
22 |
+
# Enhanced HuggingFace imports for improved functionality
|
23 |
+
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
|
24 |
+
import numpy as np
|
25 |
+
|
26 |
+
# Import endpoints documentation
|
27 |
+
from endpoints_documentation import endpoints_documentation
|
28 |
+
|
29 |
+
# Set environment variables for HuggingFace
|
30 |
+
# os.environ["HF_HOME"] = "/tmp/huggingface"
|
31 |
+
os.environ["HF_HOME"] = "/tmp/huggingface"
|
32 |
+
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
33 |
+
|
34 |
+
class EndpointRequest(BaseModel):
|
35 |
+
"""Data model for API endpoint requests"""
|
36 |
+
endpoint: str = Field(..., description="The API endpoint path to call")
|
37 |
+
method: str = Field(..., description="The HTTP method to use (GET or POST)")
|
38 |
+
params: Dict[str, Any] = Field(default_factory=dict, description="Parameters for the API call")
|
39 |
+
missing_required: List[str] = Field(default_factory=list, description="Any required parameters that are missing")
|
40 |
+
|
41 |
+
|
42 |
+
class AIAgent:
|
43 |
+
def __init__(self):
|
44 |
+
self.endpoints_documentation = endpoints_documentation
|
45 |
+
self.ollama_base_url = "http://localhost:11434" # Default Ollama URL
|
46 |
+
self.model_name = "mistral" # Using mistral model for better multilingual support
|
47 |
+
# self.model_name = 'llama3'
|
48 |
+
self.BASE_URL = 'https://agent.serveo.net'
|
49 |
+
self.headers = {
|
50 |
+
'Content-type': 'application/json'
|
51 |
+
}
|
52 |
+
self.user_id = 'd8507df8-cec6-49f9-adcc-367b13805e73'
|
53 |
+
self.max_retries = 3
|
54 |
+
self.retry_delay = 2 # seconds
|
55 |
+
|
56 |
+
# Enhanced language detection using HuggingFace models
|
57 |
+
self._initialize_language_tools()
|
58 |
+
|
59 |
+
# Initialize LangChain components
|
60 |
+
self._initialize_llm()
|
61 |
+
self._initialize_parsers_and_chains()
|
62 |
+
|
63 |
+
# Add date parsing capabilities
|
64 |
+
self._initialize_date_parser()
|
65 |
+
|
66 |
+
def _initialize_language_tools(self):
|
67 |
+
"""Initialize more sophisticated language processing tools"""
|
68 |
+
# Use multilingual embeddings for semantic understanding
|
69 |
+
self.embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
|
70 |
+
|
71 |
+
# Initialize language identification model
|
72 |
+
try:
|
73 |
+
self.language_classifier = pipeline(
|
74 |
+
"text-classification",
|
75 |
+
model="papluca/xlm-roberta-base-language-detection",
|
76 |
+
top_k=1
|
77 |
+
)
|
78 |
+
print("Language classification model loaded successfully")
|
79 |
+
except Exception as e:
|
80 |
+
print(f"Failed to load language classification model: {e}")
|
81 |
+
# Fallback to basic regex detection if model fails to load
|
82 |
+
self.language_classifier = None
|
83 |
+
|
84 |
+
# Add sentiment analysis for enhanced response generation
|
85 |
+
try:
|
86 |
+
self.sentiment_analyzer = pipeline(
|
87 |
+
"sentiment-analysis",
|
88 |
+
model="cardiffnlp/twitter-xlm-roberta-base-sentiment"
|
89 |
+
)
|
90 |
+
print("Sentiment analysis model loaded successfully")
|
91 |
+
except Exception as e:
|
92 |
+
print(f"Failed to load sentiment analysis model: {e}")
|
93 |
+
self.sentiment_analyzer = None
|
94 |
+
|
95 |
+
def _initialize_date_parser(self):
|
96 |
+
"""Initialize date parsing model for handling relative date expressions"""
|
97 |
+
try:
|
98 |
+
self.date_parser = pipeline(
|
99 |
+
"token-classification",
|
100 |
+
model="Jean-Baptiste/roberta-large-ner-english",
|
101 |
+
aggregation_strategy="simple"
|
102 |
+
)
|
103 |
+
print("Date parsing model loaded successfully")
|
104 |
+
except Exception as e:
|
105 |
+
print(f"Failed to load date parsing model: {e}")
|
106 |
+
self.date_parser = None
|
107 |
+
|
108 |
+
def detect_language(self, text):
|
109 |
+
"""
|
110 |
+
Enhanced language detection using HuggingFace models
|
111 |
+
"""
|
112 |
+
# First try using the HuggingFace language classification model if available
|
113 |
+
if self.language_classifier and len(text.strip()) > 3:
|
114 |
+
try:
|
115 |
+
result = self.language_classifier(text)
|
116 |
+
detected_lang = result[0][0]['label']
|
117 |
+
confidence = result[0][0]['score']
|
118 |
+
|
119 |
+
print(f"Language detected: {detected_lang} with confidence {confidence:.4f}")
|
120 |
+
|
121 |
+
# Map the detected language to our simplified language set
|
122 |
+
if detected_lang in ['ar', 'arabic']:
|
123 |
+
return "arabic"
|
124 |
+
elif detected_lang in ['en', 'english']:
|
125 |
+
return "english"
|
126 |
+
elif confidence > 0.8: # If confident but not English/Arabic
|
127 |
+
# We currently only support English/Arabic, but log other languages
|
128 |
+
print(f"Detected unsupported language: {detected_lang}")
|
129 |
+
# Default to English for other languages for now
|
130 |
+
return "english"
|
131 |
+
except Exception as e:
|
132 |
+
print(f"Error in language detection model: {e}")
|
133 |
+
# Continue to fallback methods
|
134 |
+
|
135 |
+
# Fallback: Basic detection of Arabic text using regex
|
136 |
+
arabic_pattern = re.compile(r'[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]+')
|
137 |
+
if arabic_pattern.search(text):
|
138 |
+
return "arabic"
|
139 |
+
|
140 |
+
# Default to English
|
141 |
+
return "english"
|
142 |
+
|
143 |
+
def analyze_sentiment(self, text):
|
144 |
+
"""Analyze the sentiment of the input text"""
|
145 |
+
if self.sentiment_analyzer and len(text.strip()) > 3:
|
146 |
+
try:
|
147 |
+
result = self.sentiment_analyzer(text)
|
148 |
+
sentiment = result[0]['label']
|
149 |
+
score = result[0]['score']
|
150 |
+
return {
|
151 |
+
"sentiment": sentiment,
|
152 |
+
"score": score
|
153 |
+
}
|
154 |
+
except Exception as e:
|
155 |
+
print(f"Error in sentiment analysis: {e}")
|
156 |
+
|
157 |
+
# Default neutral sentiment if analysis fails
|
158 |
+
return {"sentiment": "NEUTRAL", "score": 0.5}
|
159 |
+
|
160 |
+
def extract_semantic_keywords(self, text, top_n=5):
|
161 |
+
"""Extract semantic keywords from text using embeddings"""
|
162 |
+
try:
|
163 |
+
# Simple keyword extraction using embeddings comparison
|
164 |
+
# This is a basic implementation - could be enhanced further
|
165 |
+
words = re.findall(r'\b\w+\b', text.lower())
|
166 |
+
unique_words = list(set([w for w in words if len(w) > 3]))
|
167 |
+
|
168 |
+
if not unique_words:
|
169 |
+
return []
|
170 |
+
|
171 |
+
# Get embeddings for all words
|
172 |
+
embeddings_list = []
|
173 |
+
for word in unique_words:
|
174 |
+
try:
|
175 |
+
emb = self.embeddings.embed_query(word)
|
176 |
+
embeddings_list.append((word, emb))
|
177 |
+
except Exception as e:
|
178 |
+
print(f"Error embedding word {word}: {e}")
|
179 |
+
|
180 |
+
# Get embedding for full text
|
181 |
+
text_embedding = self.embeddings.embed_query(text)
|
182 |
+
|
183 |
+
# Calculate similarity to full text
|
184 |
+
similarities = []
|
185 |
+
for word, emb in embeddings_list:
|
186 |
+
similarity = np.dot(emb, text_embedding) / (np.linalg.norm(emb) * np.linalg.norm(text_embedding))
|
187 |
+
similarities.append((word, similarity))
|
188 |
+
|
189 |
+
# Sort by similarity
|
190 |
+
similarities.sort(key=lambda x: x[1], reverse=True)
|
191 |
+
|
192 |
+
# Return top N keywords
|
193 |
+
return [word for word, _ in similarities[:top_n]]
|
194 |
+
|
195 |
+
except Exception as e:
|
196 |
+
print(f"Error extracting keywords: {e}")
|
197 |
+
return []
|
198 |
+
|
199 |
+
def _initialize_llm(self):
|
200 |
+
"""Initialize the LLM with appropriate configuration"""
|
201 |
+
# Set up the callback manager for streaming (optional)
|
202 |
+
callbacks = [StreamingStdOutCallbackHandler()]
|
203 |
+
|
204 |
+
# Initialize the Ollama LLM with updated parameters
|
205 |
+
self.llm = OllamaLLM(
|
206 |
+
model=self.model_name,
|
207 |
+
base_url=self.ollama_base_url,
|
208 |
+
callbacks=callbacks,
|
209 |
+
temperature=0.7,
|
210 |
+
num_ctx=8192, # Increased context window
|
211 |
+
top_p=0.9,
|
212 |
+
request_timeout=60, # Timeout in seconds
|
213 |
+
)
|
214 |
+
|
215 |
+
def _initialize_parsers_and_chains(self):
|
216 |
+
"""Initialize output parsers and LLM chains"""
|
217 |
+
# Setup JSON parser for structured output
|
218 |
+
self.json_parser = JsonOutputParser(pydantic_object=EndpointRequest)
|
219 |
+
|
220 |
+
# Create multilingual router prompt template with enhanced context
|
221 |
+
self.router_prompt_template = PromptTemplate(
|
222 |
+
template="""
|
223 |
+
You are a precise API routing assistant. Your job is to analyze user queries and select the correct API endpoint with proper parameters.
|
224 |
+
|
225 |
+
=== ENDPOINT DOCUMENTATION ===
|
226 |
+
{endpoints_documentation}
|
227 |
+
|
228 |
+
=== USER REQUEST ANALYSIS ===
|
229 |
+
User Query: {user_query}
|
230 |
+
Language: {detected_language}
|
231 |
+
Keywords: {extracted_keywords}
|
232 |
+
Sentiment: {sentiment_analysis}
|
233 |
+
|
234 |
+
=== ROUTING PROCESS ===
|
235 |
+
Follow these steps in order:
|
236 |
+
|
237 |
+
STEP 1: INTENT ANALYSIS
|
238 |
+
- What is the user trying to accomplish?
|
239 |
+
- What type of operation are they requesting? (create, read, update, delete, search, etc.)
|
240 |
+
- What entity/resource are they working with?
|
241 |
+
|
242 |
+
STEP 2: ENDPOINT MATCHING
|
243 |
+
- Review each endpoint in the documentation
|
244 |
+
- Match the user's intent to the endpoint's PURPOSE/DESCRIPTION
|
245 |
+
- Consider the HTTP method (GET for retrieval, POST for creation, etc.)
|
246 |
+
- Verify the endpoint can handle the user's specific request
|
247 |
+
|
248 |
+
STEP 3: PARAMETER EXTRACTION
|
249 |
+
- Identify ALL required parameters from the endpoint documentation
|
250 |
+
- Extract parameter values from the user query
|
251 |
+
- Convert data types as needed (dates to ISO 8601, numbers to integers, etc.)
|
252 |
+
- Set appropriate defaults for optional parameters if beneficial
|
253 |
+
|
254 |
+
STEP 4: VALIDATION
|
255 |
+
- Ensure ALL required parameters are provided or identified as missing
|
256 |
+
- Verify parameter formats match documentation requirements
|
257 |
+
- Check that the selected endpoint actually solves the user's problem
|
258 |
+
|
259 |
+
=== RESPONSE FORMAT ===
|
260 |
+
Provide your analysis and decision in this exact JSON structure:
|
261 |
+
|
262 |
+
{{
|
263 |
+
"reasoning": {{
|
264 |
+
"user_intent": "Brief description of what the user wants to accomplish",
|
265 |
+
"selected_endpoint": "Why this endpoint was chosen over others",
|
266 |
+
"parameter_mapping": "How user query maps to endpoint parameters"
|
267 |
+
}},
|
268 |
+
"endpoint": "/exact_endpoint_path_from_documentation",
|
269 |
+
"method": "HTTP_METHOD",
|
270 |
+
"params": {{
|
271 |
+
"required_param_1": "extracted_or_converted_value",
|
272 |
+
"required_param_2": "extracted_or_converted_value",
|
273 |
+
"optional_param": "value_if_applicable"
|
274 |
+
}},
|
275 |
+
"missing_required": ["list", "of", "missing", "required", "parameters"],
|
276 |
+
"confidence": 0.95
|
277 |
+
}}
|
278 |
+
|
279 |
+
=== CRITICAL RULES ===
|
280 |
+
1. ONLY select endpoints that exist in the provided documentation
|
281 |
+
2. NEVER fabricate or assume endpoint parameters not in documentation
|
282 |
+
3. ALL required parameters MUST be included or listed as missing
|
283 |
+
4. Convert dates/times to ISO 8601 format (YYYY-MM-DDTHH:MM:SS)
|
284 |
+
5. If patient_id is required and not provided, add it to missing_required
|
285 |
+
6. Match endpoints by PURPOSE, not just keywords in the path
|
286 |
+
7. If multiple endpoints could work, choose the most specific one
|
287 |
+
8. If no endpoint matches, set endpoint to null and explain in reasoning
|
288 |
+
|
289 |
+
=== EXAMPLES OF GOOD MATCHING ===
|
290 |
+
- User wants "patient records" → Use patient retrieval endpoint, not general search
|
291 |
+
- User wants to "schedule appointment" → Use appointment creation endpoint
|
292 |
+
- User asks "what appointments today" → Use appointment listing with date filter
|
293 |
+
- User wants to "update medication" → Use medication update endpoint with patient_id
|
294 |
+
|
295 |
+
Think step by step and be precise with your endpoint selection and parameter extraction.
|
296 |
+
""",
|
297 |
+
input_variables=["endpoints_documentation", "user_query", "detected_language",
|
298 |
+
"extracted_keywords", "sentiment_analysis"],
|
299 |
+
partial_variables={"format_instructions": self.json_parser.get_format_instructions()}
|
300 |
+
)
|
301 |
+
|
302 |
+
# # Create user-friendly response template with enhanced context awareness
|
303 |
+
# self.user_response_template = PromptTemplate(
|
304 |
+
# template="""
|
305 |
+
# You are a professional and friendly virtual assistant for a healthcare system.
|
306 |
+
# Your task is to generate clear, concise, and professional responses to user queries.
|
307 |
+
|
308 |
+
# IMPORTANT RULES:
|
309 |
+
# - Respond ONLY in {detected_language}
|
310 |
+
# - For Arabic, use Modern Standard Arabic (فصحى)
|
311 |
+
# - Keep responses SHORT and DIRECT
|
312 |
+
# - Include ONLY essential information
|
313 |
+
# - NEVER mix languages
|
314 |
+
# - ALWAYS use the EXACT data from the system response
|
315 |
+
# - NEVER make up or modify hospital information
|
316 |
+
# - Use professional and polite tone
|
317 |
+
|
318 |
+
# Original query: {user_query}
|
319 |
+
# System result: {api_response}
|
320 |
+
# User sentiment: {sentiment_analysis}
|
321 |
+
|
322 |
+
# ARABIC RESPONSE RULES:
|
323 |
+
# - Use Arabic numbers (١، ٢، ٣)
|
324 |
+
# - Use proper date format (١٥ مايو ٢٠٢٥)
|
325 |
+
# - Use proper time format (الساعة ٨ صباحاً)
|
326 |
+
# - Use formal medical terms
|
327 |
+
# - Keep sentences short and clear
|
328 |
+
# - Use exact hospital names and addresses from the data
|
329 |
+
# - Use exact working hours from the data
|
330 |
+
# - Use professional healthcare terminology
|
331 |
+
|
332 |
+
# ENGLISH RESPONSE RULES:
|
333 |
+
# - Use clear, direct language
|
334 |
+
# - Include only essential details
|
335 |
+
# - Use proper medical terms
|
336 |
+
# - Keep responses concise
|
337 |
+
# - Use exact hospital names and addresses from the data
|
338 |
+
# - Use exact working hours from the data
|
339 |
+
# - Use professional healthcare terminology
|
340 |
+
|
341 |
+
# Remember:
|
342 |
+
# - Keep responses SHORT and FOCUSED
|
343 |
+
# - Use ONLY data from the system response
|
344 |
+
# - NEVER modify or make up hospital information
|
345 |
+
# - Include only what's necessary to answer the query
|
346 |
+
# - Maintain professional and polite tone
|
347 |
+
# - Use proper healthcare terminology
|
348 |
+
# """,
|
349 |
+
# input_variables=["user_query", "api_response", "detected_language",
|
350 |
+
# "sentiment_analysis", "extracted_keywords"]
|
351 |
+
# )
|
352 |
+
# Create user-friendly response template with enhanced context awareness
|
353 |
+
# Create user-friendly response template with enhanced context awareness
|
354 |
+
# Create user-friendly response template with enhanced context awareness
|
355 |
+
self.user_response_template = PromptTemplate(
|
356 |
+
template="""
|
357 |
+
You are a professional healthcare assistant. Generate clear, accurate responses using EXACT data from the system.
|
358 |
+
|
359 |
+
=== STRICT REQUIREMENTS ===
|
360 |
+
- Respond ONLY in {detected_language}
|
361 |
+
- Use EXACT information from api_response - NO modifications
|
362 |
+
- Keep responses SHORT, SIMPLE, and DIRECT
|
363 |
+
- Use professional healthcare tone
|
364 |
+
- NEVER mix languages or make up information
|
365 |
+
|
366 |
+
=== ORIGINAL REQUEST ===
|
367 |
+
User Query: {user_query}
|
368 |
+
User Sentiment: {sentiment_analysis}
|
369 |
+
|
370 |
+
=== SYSTEM DATA ===
|
371 |
+
{api_response}
|
372 |
+
|
373 |
+
=== LANGUAGE-SPECIFIC FORMATTING ===
|
374 |
+
|
375 |
+
FOR ARABIC RESPONSES:
|
376 |
+
- Use Modern Standard Arabic (الفصحى)
|
377 |
+
- Use Arabic numerals: ١، ٢، ٣، ٤، ٥، ٦، ٧، ٨، ٩، ١٠
|
378 |
+
- Time format: "من الساعة ٨:٠٠ صباحاً إلى ٥:٠٠ مساءً"
|
379 |
+
- Date format: "١٥ مايو ٢٠٢٥"
|
380 |
+
- Use proper Arabic medical terminology
|
381 |
+
- Keep sentences short and grammatically correct
|
382 |
+
- Example format for hospitals:
|
383 |
+
"مستشفى [الاسم] - العنوان: [العنوان الكامل] - أوقات العمل: من [الوقت] إلى [الوقت]"
|
384 |
+
|
385 |
+
FOR ENGLISH RESPONSES:
|
386 |
+
- Use clear, professional language
|
387 |
+
- Time format: "8:00 AM to 5:00 PM"
|
388 |
+
- Date format: "May 15, 2025"
|
389 |
+
- Keep sentences concise and direct
|
390 |
+
- Example format for hospitals:
|
391 |
+
"[Hospital Name] - Address: [Full Address] - Hours: [Opening Time] to [Closing Time]"
|
392 |
+
|
393 |
+
=== RESPONSE STRUCTURE ===
|
394 |
+
1. Direct answer to the user's question
|
395 |
+
2. Essential details only (names, addresses, hours, contact info)
|
396 |
+
3. Brief helpful note if needed
|
397 |
+
4. No unnecessary introductions or conclusions
|
398 |
+
|
399 |
+
=== CRITICAL RULES ===
|
400 |
+
- Extract information EXACTLY as provided in api_response
|
401 |
+
- Do NOT include technical URLs, IDs, or system codes in the response
|
402 |
+
- Do NOT show raw links or booking URLs to users
|
403 |
+
- Present information in natural, conversational language
|
404 |
+
- Do NOT use bullet points or technical formatting
|
405 |
+
- Write as if you're speaking to the patient directly
|
406 |
+
- If data is missing, state "المعلومات غير متوفرة" (Arabic) or "Information not available" (English)
|
407 |
+
- Convert technical data into human-readable format
|
408 |
+
- NEVER add translations or explanations in other languages
|
409 |
+
- NEVER include "Translated response" or similar phrases
|
410 |
+
- END your response immediately after providing the requested information
|
411 |
+
- Do NOT add any English translation when responding in Arabic
|
412 |
+
- Do NOT add any Arabic translation when responding in English
|
413 |
+
|
414 |
+
=== HUMAN-LIKE FORMATTING RULES ===
|
415 |
+
FOR ARABIC:
|
416 |
+
- Instead of "رابط الحجز: [URL]" → say "تم حجز موعدك بنجاح"
|
417 |
+
- Instead of "الأزمة: غير متوفرة" → omit or say "بدون أعراض محددة"
|
418 |
+
- Use natural sentences like "موعدك مع الدكتور [Name] يوم [Date] في تمام الساعة [Time]"
|
419 |
+
- Avoid technical terms and system language
|
420 |
+
|
421 |
+
FOR ENGLISH:
|
422 |
+
- Instead of "Booking URL: [link]" → say "Your appointment has been scheduled"
|
423 |
+
- Use natural sentences like "You have an appointment with Dr. [Name] on [Date] at [Time]"
|
424 |
+
- Avoid showing raw URLs, IDs, or technical data
|
425 |
+
|
426 |
+
=== QUALITY CHECKS ===
|
427 |
+
Before responding, verify:
|
428 |
+
✓ Response sounds natural and conversational
|
429 |
+
✓ No technical URLs, IDs, or system codes are shown
|
430 |
+
✓ Information is presented in human-friendly language
|
431 |
+
✓ Grammar is correct in the target language
|
432 |
+
✓ Response directly answers the user's question
|
433 |
+
✓ No bullet points or technical formatting
|
434 |
+
✓ Sounds like a helpful human assistant, not a system
|
435 |
+
|
436 |
+
Generate a response that is accurate, helpful, and professionally formatted.
|
437 |
+
|
438 |
+
=== FINAL INSTRUCTION ===
|
439 |
+
Respond ONLY in the requested language. Do NOT provide translations, explanations, or additional text in any other language. Stop immediately after answering the user's question.
|
440 |
+
""",
|
441 |
+
input_variables=["user_query", "api_response", "detected_language",
|
442 |
+
"sentiment_analysis", "extracted_keywords"]
|
443 |
+
)
|
444 |
+
|
445 |
+
# Create LLM chains
|
446 |
+
self.router_chain = LLMChain(
|
447 |
+
llm=self.llm,
|
448 |
+
prompt=self.router_prompt_template,
|
449 |
+
output_key="route_result"
|
450 |
+
)
|
451 |
+
|
452 |
+
self.user_response_chain = LLMChain(
|
453 |
+
llm=self.llm,
|
454 |
+
prompt=self.user_response_template,
|
455 |
+
output_key="user_friendly_response"
|
456 |
+
)
|
457 |
+
|
458 |
+
def parse_relative_date(self, text, detected_language):
|
459 |
+
"""
|
460 |
+
Parse relative dates from text using a combination of methods
|
461 |
+
"""
|
462 |
+
today = datetime.now()
|
463 |
+
|
464 |
+
# Handle common relative date patterns in English and Arabic
|
465 |
+
tomorrow_patterns = {
|
466 |
+
'english': [r'\btomorrow\b', r'\bnext day\b'],
|
467 |
+
'arabic': [r'\bغدا\b', r'\bبكرة\b', r'\bغدًا\b', r'\bالغد\b']
|
468 |
+
}
|
469 |
+
|
470 |
+
next_week_patterns = {
|
471 |
+
'english': [r'\bnext week\b'],
|
472 |
+
'arabic': [r'\bالأسبوع القادم\b', r'\bالأسبوع المقبل\b', r'\bالاسبوع الجاي\b']
|
473 |
+
}
|
474 |
+
|
475 |
+
# Check for "tomorrow" patterns
|
476 |
+
for pattern in tomorrow_patterns.get(detected_language, []) + tomorrow_patterns.get('english', []):
|
477 |
+
if re.search(pattern, text, re.IGNORECASE):
|
478 |
+
return (today + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S')
|
479 |
+
|
480 |
+
# Check for "next week" patterns
|
481 |
+
for pattern in next_week_patterns.get(detected_language, []) + next_week_patterns.get('english', []):
|
482 |
+
if re.search(pattern, text, re.IGNORECASE):
|
483 |
+
return (today + timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%S')
|
484 |
+
|
485 |
+
# If NER model is available, use it to extract date entities
|
486 |
+
if self.date_parser and detected_language == 'english':
|
487 |
+
try:
|
488 |
+
date_entities = self.date_parser(text)
|
489 |
+
for entity in date_entities:
|
490 |
+
if entity['entity_group'] == 'DATE':
|
491 |
+
# Here you would need more complex date parsing logic
|
492 |
+
# This is just a placeholder
|
493 |
+
print(f"Found date entity: {entity['word']}")
|
494 |
+
# For now, just default to tomorrow if we detect any date
|
495 |
+
return (today + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%S')
|
496 |
+
except Exception as e:
|
497 |
+
print(f"Error in date parsing: {e}")
|
498 |
+
|
499 |
+
# Default return None if no date pattern is recognized
|
500 |
+
return None
|
501 |
+
|
502 |
+
def process_user_query(self, user_query: str) -> Dict[str, Any]:
|
503 |
+
"""
|
504 |
+
Process the user query through the LangChain pipeline and return a response
|
505 |
+
"""
|
506 |
+
try:
|
507 |
+
start_time = time.time()
|
508 |
+
|
509 |
+
# Detect language of the query
|
510 |
+
detected_language = self.detect_language(user_query)
|
511 |
+
print(f"Detected language: {detected_language}")
|
512 |
+
|
513 |
+
# Enhanced context using Hugging Face models
|
514 |
+
sentiment_result = self.analyze_sentiment(user_query)
|
515 |
+
print(f"Sentiment analysis: {sentiment_result}")
|
516 |
+
|
517 |
+
extracted_keywords = self.extract_semantic_keywords(user_query)
|
518 |
+
print(f"Extracted keywords: {extracted_keywords}")
|
519 |
+
|
520 |
+
# Try to extract dates from query
|
521 |
+
parsed_date = self.parse_relative_date(user_query, detected_language)
|
522 |
+
if parsed_date:
|
523 |
+
print(f"Parsed relative date: {parsed_date}")
|
524 |
+
|
525 |
+
# 1. Route the query to determine which API endpoint to call
|
526 |
+
router_result = self.router_chain.invoke({
|
527 |
+
"endpoints_documentation": json.dumps(self.endpoints_documentation, indent=2),
|
528 |
+
"user_query": user_query,
|
529 |
+
"detected_language": detected_language,
|
530 |
+
"extracted_keywords": ", ".join(extracted_keywords),
|
531 |
+
"sentiment_analysis": json.dumps(sentiment_result)
|
532 |
+
})
|
533 |
+
|
534 |
+
# 2. Parse the router response
|
535 |
+
route_result = router_result["route_result"]
|
536 |
+
parsed_route = None
|
537 |
+
|
538 |
+
# Clean the response first
|
539 |
+
cleaned_response = route_result
|
540 |
+
|
541 |
+
# Remove any comments (both single-line and multi-line)
|
542 |
+
cleaned_response = re.sub(r'//.*?$', '', cleaned_response, flags=re.MULTILINE)
|
543 |
+
cleaned_response = re.sub(r'/\*.*?\*/', '', cleaned_response, flags=re.DOTALL)
|
544 |
+
|
545 |
+
# Remove any trailing commas
|
546 |
+
cleaned_response = re.sub(r',(\s*[}\]])', r'\1', cleaned_response)
|
547 |
+
|
548 |
+
# Try different methods to parse the JSON response
|
549 |
+
try:
|
550 |
+
# First attempt: direct JSON parsing of cleaned response
|
551 |
+
parsed_route = json.loads(cleaned_response)
|
552 |
+
except json.JSONDecodeError:
|
553 |
+
try:
|
554 |
+
# Second attempt: extract JSON from markdown code block
|
555 |
+
json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', cleaned_response, re.DOTALL)
|
556 |
+
if json_match:
|
557 |
+
parsed_route = json.loads(json_match.group(1))
|
558 |
+
except (json.JSONDecodeError, AttributeError):
|
559 |
+
try:
|
560 |
+
# Third attempt: find JSON-like content using regex
|
561 |
+
json_pattern = r'\{\s*"endpoint"\s*:.*?\}'
|
562 |
+
json_match = re.search(json_pattern, cleaned_response, re.DOTALL)
|
563 |
+
if json_match:
|
564 |
+
json_str = json_match.group(0)
|
565 |
+
# Additional cleaning for the extracted JSON
|
566 |
+
json_str = re.sub(r'//.*?$', '', json_str, flags=re.MULTILINE)
|
567 |
+
json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
|
568 |
+
parsed_route = json.loads(json_str)
|
569 |
+
except (json.JSONDecodeError, AttributeError):
|
570 |
+
print(f"Failed to parse JSON. Raw response: {route_result}")
|
571 |
+
print(f"Cleaned response: {cleaned_response}")
|
572 |
+
raise ValueError("Could not extract valid JSON from LLM response")
|
573 |
+
|
574 |
+
if not parsed_route:
|
575 |
+
raise ValueError("Failed to parse LLM response into valid JSON")
|
576 |
+
|
577 |
+
# Replace any placeholder values and inject parsed dates if available
|
578 |
+
if 'params' in parsed_route:
|
579 |
+
if 'patient_id' in parsed_route['params']:
|
580 |
+
parsed_route['params']['patient_id'] = self.user_id
|
581 |
+
|
582 |
+
# Inject parsed date if available and a date parameter exists
|
583 |
+
date_params = ['appointment_date', 'date', 'schedule_date', 'date_time', 'new_date_time']
|
584 |
+
if parsed_date:
|
585 |
+
for param in date_params:
|
586 |
+
if param in parsed_route['params']:
|
587 |
+
parsed_route['params'][param] = parsed_date
|
588 |
+
|
589 |
+
print('Parsed route: ', parsed_route)
|
590 |
+
print(f"Routing completed in {time.time() - start_time:.2f} seconds")
|
591 |
+
|
592 |
+
# 3. Make the backend API call
|
593 |
+
backend_response = self.backend_call(parsed_route)
|
594 |
+
|
595 |
+
# 4. Generate user-friendly response
|
596 |
+
user_friendly_result = self.user_response_chain.invoke({
|
597 |
+
"user_query": user_query,
|
598 |
+
"api_response": json.dumps(backend_response, indent=2),
|
599 |
+
"detected_language": detected_language,
|
600 |
+
"sentiment_analysis": json.dumps(sentiment_result),
|
601 |
+
"extracted_keywords": ", ".join(extracted_keywords)
|
602 |
+
})
|
603 |
+
print('user response: ', user_friendly_result["user_friendly_response"])
|
604 |
+
|
605 |
+
print(f"Total processing time: {time.time() - start_time:.2f} seconds")
|
606 |
+
|
607 |
+
return {
|
608 |
+
"routing_info": parsed_route,
|
609 |
+
"api_response": backend_response,
|
610 |
+
"user_friendly_response": user_friendly_result["user_friendly_response"],
|
611 |
+
"detected_language": detected_language,
|
612 |
+
"sentiment": sentiment_result,
|
613 |
+
"keywords": extracted_keywords
|
614 |
+
}
|
615 |
+
|
616 |
+
except Exception as e:
|
617 |
+
error_detail = {
|
618 |
+
"error": f"Error processing query: {str(e)}",
|
619 |
+
"type": type(e).__name__,
|
620 |
+
"traceback": traceback.format_exc()
|
621 |
+
}
|
622 |
+
print(f"Error: {error_detail['error']}")
|
623 |
+
print(f"Traceback: {error_detail['traceback']}")
|
624 |
+
return error_detail
|
625 |
+
|
626 |
+
def backend_call(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
627 |
+
"""
|
628 |
+
Make the actual API call to the backend with retry logic
|
629 |
+
"""
|
630 |
+
endpoint_url = data.get('endpoint')
|
631 |
+
endpoint_method = data.get('method')
|
632 |
+
endpoint_params = data.get('params', {}).copy() # Create a copy to avoid modifying the original
|
633 |
+
|
634 |
+
print('Endpoint url: ' + endpoint_url)
|
635 |
+
print('Method: ', endpoint_method)
|
636 |
+
print('Params: ', endpoint_params)
|
637 |
+
|
638 |
+
# Add retry logic for more robust API calls
|
639 |
+
retries = 0
|
640 |
+
while retries < self.max_retries:
|
641 |
+
try:
|
642 |
+
if endpoint_method.upper() == 'GET':
|
643 |
+
response = requests.get(
|
644 |
+
self.BASE_URL + endpoint_url,
|
645 |
+
params=endpoint_params,
|
646 |
+
headers=self.headers,
|
647 |
+
timeout=10 # Add timeout for backend calls
|
648 |
+
)
|
649 |
+
elif endpoint_method.upper() == 'POST': # POST or other methods
|
650 |
+
response = requests.post(
|
651 |
+
self.BASE_URL + endpoint_url,
|
652 |
+
json=endpoint_params,
|
653 |
+
headers=self.headers,
|
654 |
+
timeout=10
|
655 |
+
)
|
656 |
+
elif endpoint_method.upper() == 'PUT':
|
657 |
+
response = requests.put(
|
658 |
+
self.BASE_URL + endpoint_url,
|
659 |
+
json=endpoint_params,
|
660 |
+
headers=self.headers,
|
661 |
+
timeout=10
|
662 |
+
)
|
663 |
+
|
664 |
+
# Check if response status is success
|
665 |
+
response.raise_for_status()
|
666 |
+
return response.json()
|
667 |
+
|
668 |
+
except requests.exceptions.RequestException as e:
|
669 |
+
retries += 1
|
670 |
+
if retries >= self.max_retries:
|
671 |
+
return {
|
672 |
+
"error": "Backend API call failed after multiple retries",
|
673 |
+
"details": str(e),
|
674 |
+
"status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None
|
675 |
+
}
|
676 |
+
|
677 |
+
print(f"API call attempt {retries} failed, retrying in {self.retry_delay} seconds...")
|
678 |
+
time.sleep(self.retry_delay)
|
679 |
+
|
680 |
+
|
681 |
+
# Initialize the AI agent singleton
|
682 |
+
# ai_agent = AIAgent()
|
683 |
+
|
684 |
+
|
685 |
+
# Test the agent directly
|
686 |
+
# if __name__ == "__main__":
|
687 |
+
# agent = AIAgent()
|
688 |
+
|
689 |
+
# # Test with English query
|
690 |
+
# # print("\n---Testing English Query---")
|
691 |
+
# # english_response = agent.process_user_query("I need to book an appointment with Dr. Smith tomorrow at 8 PM")
|
692 |
+
# # print("\nEnglish response:")
|
693 |
+
# # print(english_response["user_friendly_response"])
|
694 |
+
|
695 |
+
# # Test with Arabic query
|
696 |
+
# print("\n---Testing Arabic Query---")
|
697 |
+
# # arabic_response = agent.process_user_query(" اريد الغاء الحجز مع الدكتور Smith")
|
698 |
+
# arabic_response = agent.process_user_query("اريد حجز ميعاد غدا في الساعه الثامنه مساء مع الدكتور Smith")
|
699 |
+
# # arabic_response = agent.process_user_query("متى يفتح المستشفى؟")
|
700 |
+
# # arabic_response = agent.process_user_query("اريد معرفه كل الحجوزات الخاصه بي")
|
701 |
+
# print("\nArabic response:")
|
702 |
+
# print(arabic_response["user_friendly_response"])
|
703 |
+
|
704 |
+
|
705 |
+
# Fast api section
|
706 |
from fastapi import FastAPI, HTTPException
|
707 |
from pydantic import BaseModel
|
708 |
from typing import Dict, Any, Optional
|
709 |
+
|
710 |
|
711 |
app = FastAPI(
|
712 |
title="Healthcare AI Assistant",
|
testing.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException
|
2 |
+
from pydantic import BaseModel
|
3 |
+
from typing import Dict, Any, Optional
|
4 |
+
from final import AIAgent
|
5 |
+
|
6 |
+
app = FastAPI(
|
7 |
+
title="Healthcare AI Assistant",
|
8 |
+
description="An AI-powered healthcare assistant that handles appointment booking and queries",
|
9 |
+
version="1.0.0"
|
10 |
+
)
|
11 |
+
|
12 |
+
# Initialize the AI agent
|
13 |
+
agent = AIAgent()
|
14 |
+
|
15 |
+
class QueryRequest(BaseModel):
|
16 |
+
query: str
|
17 |
+
language: Optional[str] = None
|
18 |
+
|
19 |
+
class QueryResponse(BaseModel):
|
20 |
+
routing_info: Dict[str, Any]
|
21 |
+
api_response: Dict[str, Any]
|
22 |
+
user_friendly_response: str
|
23 |
+
detected_language: str
|
24 |
+
sentiment: Dict[str, Any]
|
25 |
+
|
26 |
+
@app.post("/query", response_model=QueryResponse)
|
27 |
+
async def process_query(request: QueryRequest):
|
28 |
+
"""
|
29 |
+
Process a user query and return a response
|
30 |
+
"""
|
31 |
+
try:
|
32 |
+
response = agent.process_user_query(request.query)
|
33 |
+
return response
|
34 |
+
except Exception as e:
|
35 |
+
raise HTTPException(status_code=500, detail=str(e))
|
36 |
+
|
37 |
+
@app.get("/health")
|
38 |
+
async def health_check():
|
39 |
+
"""
|
40 |
+
Health check endpoint
|
41 |
+
"""
|
42 |
+
return {"status": "healthy", "service": "healthcare-ai-assistant"}
|
43 |
+
|
44 |
+
@app.get("/")
|
45 |
+
async def root():
|
46 |
+
return {"message": "Hello World"}
|
47 |
+
|
48 |
+
if __name__ == "__main__":
|
49 |
+
import uvicorn
|
50 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|