Spaces:
Running
Upgrade all 3 LLM prompts for intelligent RAG responses
Browse filesMAJOR IMPROVEMENTS:
1. Entity Extraction (API Call #1):
- Comprehensive synonym generation using LLM intelligence
- Examples: "Pfizer COVID vaccine" β Comirnaty, BNT162b2, tozinameran
- Increased max_tokens: 256 β 500 for expansive synonyms
- Temperature: 0.1 β 0.3 for creative synonym brainstorming
2. Planning Agent (API Call #2):
- Defaults to SEARCH_TRIALS for data-driven answers
- Added FOCUS field to guide final answer (efficacy, safety, etc.)
- Clearer routing rules with "when in doubt, search trials"
- Better handling of edge cases
3. Final Summary (API Call #3) - COMPLETE REWRITE:
- NEVER says "no relevant trials found"
- Intelligently extracts insights from partial matches
- Context window: 6000 β 12,000 chars (2x increase)
- Max tokens: 1024 β 2,000 (comprehensive answers)
- Recognizes synonyms (Sinopharm = BBIBP-CorV)
- New structure: DIRECT ANSWER β KEY EVIDENCE β INSIGHTS β CONTEXT
- Works with imperfect matches (same drug, different disease = valuable!)
FIXES:
- "No relevant information" responses when trials exist
- Weak answers from company-filtered searches (Sinopharm example)
- Missed trials due to synonym mismatches
π€ Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- foundation_engine.py +567 -104
|
@@ -26,7 +26,7 @@ hf_token = os.getenv("HF_TOKEN")
|
|
| 26 |
DATASET_FILE = Path(__file__).parent / "complete_dataset_WITH_RESULTS_FULL.txt"
|
| 27 |
CHUNKS_FILE = Path(__file__).parent / "dataset_chunks_TRIAL_AWARE.pkl"
|
| 28 |
EMBEDDINGS_FILE = Path(__file__).parent / "dataset_embeddings_TRIAL_AWARE_FIXED.npy" # FIXED version to avoid cache
|
| 29 |
-
INVERTED_INDEX_FILE = Path(__file__).parent / "
|
| 30 |
|
| 31 |
# HF Dataset containing the large files
|
| 32 |
DATASET_REPO = "gmkdigitalmedia/foundation1.2-data"
|
|
@@ -38,6 +38,52 @@ doc_embeddings = None
|
|
| 38 |
bm25_index = None # BM25 index for fast keyword search
|
| 39 |
inverted_index = None # Inverted index for instant drug lookup
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# ============================================================================
|
| 42 |
# RAG FUNCTIONS
|
| 43 |
# ============================================================================
|
|
@@ -222,6 +268,7 @@ def load_embeddings():
|
|
| 222 |
chunks_path = CHUNKS_FILE
|
| 223 |
embeddings_path = EMBEDDINGS_FILE
|
| 224 |
dataset_path = DATASET_FILE
|
|
|
|
| 225 |
|
| 226 |
if not CHUNKS_FILE.exists():
|
| 227 |
downloaded = download_from_dataset("dataset_chunks_TRIAL_AWARE.pkl")
|
|
@@ -235,6 +282,12 @@ def load_embeddings():
|
|
| 235 |
downloaded = download_from_dataset("complete_dataset_WITH_RESULTS_FULL.txt")
|
| 236 |
if downloaded:
|
| 237 |
dataset_path = downloaded
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
if chunks_path.exists() and embeddings_path.exists():
|
| 240 |
try:
|
|
@@ -339,18 +392,19 @@ def load_embeddings():
|
|
| 339 |
# Skip BM25 (too memory-heavy for Docker), use inverted index only
|
| 340 |
global inverted_index
|
| 341 |
|
| 342 |
-
# Try to load pre-built inverted index (
|
| 343 |
-
if
|
| 344 |
-
logger.info(f"Loading
|
| 345 |
try:
|
| 346 |
-
with open(
|
| 347 |
inverted_index = pickle.load(f)
|
| 348 |
-
logger.info(f"β Loaded
|
|
|
|
| 349 |
except Exception as e:
|
| 350 |
-
logger.warning(f"Failed to load
|
| 351 |
inverted_index = build_inverted_index(doc_chunks)
|
| 352 |
else:
|
| 353 |
-
logger.info("
|
| 354 |
inverted_index = build_inverted_index(doc_chunks)
|
| 355 |
|
| 356 |
logger.info("Will use inverted index + semantic search (no BM25)")
|
|
@@ -454,14 +508,19 @@ def filter_trial_for_clinical_summary(trial_text):
|
|
| 454 |
return '\n'.join(filtered_lines)
|
| 455 |
|
| 456 |
|
| 457 |
-
def retrieve_context_with_embeddings(query, top_k=10):
|
| 458 |
"""
|
| 459 |
-
ENTERPRISE HYBRID SEARCH
|
| 460 |
-
-
|
|
|
|
| 461 |
- Scores each trial by keyword frequency (TF-IDF style)
|
| 462 |
- Also gets semantic similarity scores
|
| 463 |
- Merges both scores with weighted combination
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
"""
|
| 466 |
import time
|
| 467 |
import re
|
|
@@ -567,6 +626,56 @@ def retrieve_context_with_embeddings(query, top_k=10):
|
|
| 567 |
|
| 568 |
logger.info(f"[HYBRID] Inverted index scoring: {len(keyword_scores)} trials matched ({time.time()-t_kw:.2f}s)")
|
| 569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
# 2. SEMANTIC SCORING
|
| 571 |
load_embedder()
|
| 572 |
t_sem = time.time()
|
|
@@ -665,6 +774,7 @@ def retrieve_context_with_embeddings(query, top_k=10):
|
|
| 665 |
logger.info(f"[355M RANKING] Added ranking metadata to context for final LLM")
|
| 666 |
|
| 667 |
context = "\n\n---\n\n".join(context_chunks) # Use --- as separator between trials
|
|
|
|
| 668 |
logger.info(f"[HYBRID] TOTAL TIME: {time.time()-t0:.2f}s")
|
| 669 |
logger.info(f"[HYBRID] Filtered context length: {len(context)} chars (was ~{sum(len(c) for c in raw_chunks)} chars)")
|
| 670 |
|
|
@@ -940,39 +1050,54 @@ def parse_query_with_llm(query, hf_token=None):
|
|
| 940 |
logger.info("[QUERY PARSER] Analyzing user query with LLM...")
|
| 941 |
client = InferenceClient(token=hf_token, timeout=30)
|
| 942 |
|
| 943 |
-
parse_prompt = f"""
|
| 944 |
|
| 945 |
Query: "{query}"
|
| 946 |
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
|
| 971 |
response = client.chat_completion(
|
| 972 |
model="meta-llama/Llama-3.1-70B-Instruct",
|
| 973 |
messages=[{"role": "user", "content": parse_prompt}],
|
| 974 |
-
max_tokens=
|
| 975 |
-
temperature=0.
|
| 976 |
)
|
| 977 |
|
| 978 |
parsed = response.choices[0].message.content.strip()
|
|
@@ -1024,40 +1149,224 @@ Now parse the query above:"""
|
|
| 1024 |
'raw_parsed': ''
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
-
|
|
|
|
| 1028 |
"""
|
| 1029 |
-
|
| 1030 |
|
| 1031 |
-
|
| 1032 |
-
-
|
| 1033 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
try:
|
| 1036 |
# Try Groq first (much faster), fallback to HuggingFace
|
| 1037 |
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 1038 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
if groq_api_key:
|
| 1040 |
logger.info("Generating response with Llama-3.1-70B via GROQ (fast)...")
|
| 1041 |
from groq import Groq
|
| 1042 |
client = Groq(api_key=groq_api_key)
|
| 1043 |
|
| 1044 |
-
# Simplified prompt for faster generation
|
| 1045 |
-
system_prompt = """You are a medical research assistant. Answer based ONLY on the provided clinical trial data. Be concise and cite NCT IDs."""
|
| 1046 |
-
|
| 1047 |
-
user_prompt = f"""Clinical trials:
|
| 1048 |
-
{rag_context[:6000]}
|
| 1049 |
-
|
| 1050 |
-
Question: {query}
|
| 1051 |
-
|
| 1052 |
-
Provide a concise answer citing specific NCT trial IDs."""
|
| 1053 |
-
|
| 1054 |
response = client.chat.completions.create(
|
| 1055 |
-
model="llama-3.1-70b-versatile",
|
| 1056 |
messages=[
|
| 1057 |
{"role": "system", "content": system_prompt},
|
| 1058 |
{"role": "user", "content": user_prompt}
|
| 1059 |
],
|
| 1060 |
-
max_tokens=
|
| 1061 |
temperature=0.3,
|
| 1062 |
timeout=30
|
| 1063 |
)
|
|
@@ -1070,24 +1379,13 @@ Provide a concise answer citing specific NCT trial IDs."""
|
|
| 1070 |
from huggingface_hub import InferenceClient
|
| 1071 |
client = InferenceClient(token=hf_token, timeout=120)
|
| 1072 |
|
| 1073 |
-
system_prompt = """You are a medical research assistant. Answer based ONLY on the provided clinical trial data. Be concise and cite NCT IDs."""
|
| 1074 |
-
|
| 1075 |
-
user_prompt = f"""Clinical trials:
|
| 1076 |
-
{rag_context[:6000]}
|
| 1077 |
-
|
| 1078 |
-
Question: {query}
|
| 1079 |
-
|
| 1080 |
-
Provide a concise answer citing specific NCT trial IDs."""
|
| 1081 |
-
|
| 1082 |
-
messages = [
|
| 1083 |
-
{"role": "system", "content": system_prompt},
|
| 1084 |
-
{"role": "user", "content": user_prompt}
|
| 1085 |
-
]
|
| 1086 |
-
|
| 1087 |
response = client.chat_completion(
|
| 1088 |
model="meta-llama/Meta-Llama-3.1-70B-Instruct",
|
| 1089 |
-
messages=
|
| 1090 |
-
|
|
|
|
|
|
|
|
|
|
| 1091 |
temperature=0.3
|
| 1092 |
)
|
| 1093 |
|
|
@@ -1114,7 +1412,7 @@ def process_query_simple_test(conversation):
|
|
| 1114 |
|
| 1115 |
# Try to search
|
| 1116 |
start = time.time()
|
| 1117 |
-
context = retrieve_context_with_embeddings(conversation, top_k=
|
| 1118 |
search_time = time.time() - start
|
| 1119 |
|
| 1120 |
if not context:
|
|
@@ -1134,15 +1432,16 @@ def process_query_simple_test(conversation):
|
|
| 1134 |
|
| 1135 |
def process_query(conversation):
|
| 1136 |
"""
|
| 1137 |
-
Complete pipeline with LLM query parsing and natural language generation
|
| 1138 |
|
| 1139 |
Flow:
|
| 1140 |
0. LLM Parser - Extract drugs, diseases, companies, endpoints (~2-3s)
|
| 1141 |
-
|
|
|
|
| 1142 |
2. Skipped - 355M ranking removed (was broken)
|
| 1143 |
3. LLM Response - Llama 70B generates natural language (~15s)
|
| 1144 |
|
| 1145 |
-
Total: ~
|
| 1146 |
"""
|
| 1147 |
import time
|
| 1148 |
import traceback
|
|
@@ -1176,26 +1475,158 @@ def process_query(conversation):
|
|
| 1176 |
logger.warning(error_msg)
|
| 1177 |
output_parts.append(f"{error_msg}\n")
|
| 1178 |
search_query = conversation # Fallback to original
|
|
|
|
| 1179 |
|
| 1180 |
-
# Step
|
| 1181 |
try:
|
| 1182 |
-
|
| 1183 |
-
logger.info("Step
|
| 1184 |
-
output_parts.append("β Step
|
| 1185 |
-
context = retrieve_context_with_embeddings(search_query, top_k=3)
|
| 1186 |
|
| 1187 |
-
|
| 1188 |
-
return "No matching trials found in RAG search."
|
| 1189 |
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
output_parts.append(f"
|
| 1193 |
-
|
|
|
|
| 1194 |
|
| 1195 |
except Exception as e:
|
| 1196 |
-
error_msg = f"β Step
|
| 1197 |
-
logger.
|
| 1198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
|
| 1200 |
# Step 2: Skipped (355M ranking removed - was broken)
|
| 1201 |
output_parts.append("β Step 2: Skipped (using hybrid search + recency)\n")
|
|
@@ -1205,7 +1636,13 @@ def process_query(conversation):
|
|
| 1205 |
step3_start = time.time()
|
| 1206 |
logger.info("Step 3: Generating response with Llama-3.1-70B...")
|
| 1207 |
output_parts.append("β Step 3: Llama 70B generation started...\n")
|
| 1208 |
-
llama_response = generate_llama_response(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
step3_time = time.time() - step3_start
|
| 1210 |
output_parts.append(f"β Step 3 Complete: Llama 70B response generated ({step3_time:.1f}s)\n")
|
| 1211 |
logger.info(f"Llama 70B generation successful in {step3_time:.1f}s")
|
|
@@ -1237,6 +1674,10 @@ RAG RETRIEVED TRIALS (Top 3 Most Relevant):
|
|
| 1237 |
---
|
| 1238 |
Total Time: {total_time:.1f}s
|
| 1239 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1240 |
return output
|
| 1241 |
except Exception as e:
|
| 1242 |
# Absolute fallback
|
|
@@ -1275,9 +1716,44 @@ System Info:
|
|
| 1275 |
========================================
|
| 1276 |
"""
|
| 1277 |
logger.error(master_error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
return master_error_msg
|
| 1279 |
|
| 1280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1281 |
# ============================================================================
|
| 1282 |
# GRADIO INTERFACE
|
| 1283 |
# ============================================================================
|
|
@@ -1304,21 +1780,8 @@ with gr.Blocks(title="Foundation 1.2") as demo:
|
|
| 1304 |
)
|
| 1305 |
|
| 1306 |
gr.Markdown("""
|
| 1307 |
-
**Production
|
| 1308 |
-
|
| 1309 |
-
**Search (3-Stage Hybrid):**
|
| 1310 |
-
1. Keyword matching (70%) + Semantic search (30%) β 10 candidates
|
| 1311 |
-
2. 355M Clinical Trial GPT re-ranks by relevance
|
| 1312 |
-
3. Returns top 3 trials with best clinical relevance scores
|
| 1313 |
-
|
| 1314 |
-
**Generation (Qwen2.5-14B-Instruct):**
|
| 1315 |
-
- 14B parameter model via HuggingFace Inference API
|
| 1316 |
-
- Structured clinical summaries with clear headings
|
| 1317 |
-
- Cites specific NCT trial IDs
|
| 1318 |
-
- Includes actual trial results and efficacy data
|
| 1319 |
-
- High-quality medical reasoning and analysis
|
| 1320 |
|
| 1321 |
-
*355M model used for ranking (not generation) + Qwen2.5-14B for responses*
|
| 1322 |
""")
|
| 1323 |
|
| 1324 |
|
|
|
|
| 26 |
DATASET_FILE = Path(__file__).parent / "complete_dataset_WITH_RESULTS_FULL.txt"
|
| 27 |
CHUNKS_FILE = Path(__file__).parent / "dataset_chunks_TRIAL_AWARE.pkl"
|
| 28 |
EMBEDDINGS_FILE = Path(__file__).parent / "dataset_embeddings_TRIAL_AWARE_FIXED.npy" # FIXED version to avoid cache
|
| 29 |
+
INVERTED_INDEX_FILE = Path(__file__).parent / "inverted_index_COMPREHENSIVE.pkl" # Pre-built inverted index (307MB)
|
| 30 |
|
| 31 |
# HF Dataset containing the large files
|
| 32 |
DATASET_REPO = "gmkdigitalmedia/foundation1.2-data"
|
|
|
|
| 38 |
bm25_index = None # BM25 index for fast keyword search
|
| 39 |
inverted_index = None # Inverted index for instant drug lookup
|
| 40 |
|
| 41 |
+
# ============================================================================
|
| 42 |
+
# ANALYTICS TRACKING
|
| 43 |
+
# ============================================================================
|
| 44 |
+
|
| 45 |
+
from collections import defaultdict, Counter
|
| 46 |
+
import time as time_module
|
| 47 |
+
|
| 48 |
+
class QueryAnalytics:
|
| 49 |
+
"""Track query patterns and performance for monitoring"""
|
| 50 |
+
|
| 51 |
+
def __init__(self):
|
| 52 |
+
self.query_types = Counter()
|
| 53 |
+
self.response_times = defaultdict(list)
|
| 54 |
+
self.error_count = 0
|
| 55 |
+
self.total_queries = 0
|
| 56 |
+
self.start_time = time_module.time()
|
| 57 |
+
|
| 58 |
+
def record_query(self, query_type: str, response_time: float, success: bool = True):
|
| 59 |
+
"""Record a query execution"""
|
| 60 |
+
self.total_queries += 1
|
| 61 |
+
self.query_types[query_type] += 1
|
| 62 |
+
self.response_times[query_type].append(response_time)
|
| 63 |
+
if not success:
|
| 64 |
+
self.error_count += 1
|
| 65 |
+
logger.info(f"[ANALYTICS] Recorded: {query_type}, {response_time:.2f}s, success={success}")
|
| 66 |
+
|
| 67 |
+
def get_stats(self):
|
| 68 |
+
"""Get analytics summary"""
|
| 69 |
+
uptime = time_module.time() - self.start_time
|
| 70 |
+
stats = {
|
| 71 |
+
'total_queries': self.total_queries,
|
| 72 |
+
'uptime_seconds': uptime,
|
| 73 |
+
'error_rate': self.error_count / self.total_queries if self.total_queries > 0 else 0,
|
| 74 |
+
'query_type_distribution': dict(self.query_types),
|
| 75 |
+
'avg_response_times': {}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
for query_type, times in self.response_times.items():
|
| 79 |
+
if times:
|
| 80 |
+
stats['avg_response_times'][query_type] = sum(times) / len(times)
|
| 81 |
+
|
| 82 |
+
return stats
|
| 83 |
+
|
| 84 |
+
# Initialize global analytics
|
| 85 |
+
query_analytics = QueryAnalytics()
|
| 86 |
+
|
| 87 |
# ============================================================================
|
| 88 |
# RAG FUNCTIONS
|
| 89 |
# ============================================================================
|
|
|
|
| 268 |
chunks_path = CHUNKS_FILE
|
| 269 |
embeddings_path = EMBEDDINGS_FILE
|
| 270 |
dataset_path = DATASET_FILE
|
| 271 |
+
index_path = INVERTED_INDEX_FILE
|
| 272 |
|
| 273 |
if not CHUNKS_FILE.exists():
|
| 274 |
downloaded = download_from_dataset("dataset_chunks_TRIAL_AWARE.pkl")
|
|
|
|
| 282 |
downloaded = download_from_dataset("complete_dataset_WITH_RESULTS_FULL.txt")
|
| 283 |
if downloaded:
|
| 284 |
dataset_path = downloaded
|
| 285 |
+
# Download inverted index from dataset (307 MB, truly comprehensive)
|
| 286 |
+
if not INVERTED_INDEX_FILE.exists():
|
| 287 |
+
downloaded = download_from_dataset("inverted_index_COMPREHENSIVE.pkl")
|
| 288 |
+
if downloaded:
|
| 289 |
+
index_path = downloaded
|
| 290 |
+
logger.info(f"β Downloaded comprehensive inverted index from dataset")
|
| 291 |
|
| 292 |
if chunks_path.exists() and embeddings_path.exists():
|
| 293 |
try:
|
|
|
|
| 392 |
# Skip BM25 (too memory-heavy for Docker), use inverted index only
|
| 393 |
global inverted_index
|
| 394 |
|
| 395 |
+
# Try to load pre-built comprehensive inverted index (77MB) from dataset
|
| 396 |
+
if index_path.exists():
|
| 397 |
+
logger.info(f"Loading comprehensive inverted index from {index_path.name}...")
|
| 398 |
try:
|
| 399 |
+
with open(index_path, 'rb') as f:
|
| 400 |
inverted_index = pickle.load(f)
|
| 401 |
+
logger.info(f"β Loaded comprehensive index with {len(inverted_index):,} terms")
|
| 402 |
+
logger.info(f" Includes: TITLE (all words), INTERVENTION, CONDITIONS, SPONSOR, SUMMARY/DESCRIPTION (companies)")
|
| 403 |
except Exception as e:
|
| 404 |
+
logger.warning(f"Failed to load comprehensive index: {e}, building basic index...")
|
| 405 |
inverted_index = build_inverted_index(doc_chunks)
|
| 406 |
else:
|
| 407 |
+
logger.info("Comprehensive inverted index not found, building basic index (15 minutes)...")
|
| 408 |
inverted_index = build_inverted_index(doc_chunks)
|
| 409 |
|
| 410 |
logger.info("Will use inverted index + semantic search (no BM25)")
|
|
|
|
| 508 |
return '\n'.join(filtered_lines)
|
| 509 |
|
| 510 |
|
| 511 |
+
def retrieve_context_with_embeddings(query, top_k=10, entities=None):
|
| 512 |
"""
|
| 513 |
+
ENTERPRISE HYBRID SEARCH with STRICT ENTITY FILTERING
|
| 514 |
+
- Enforces HARD FILTERS for companies (sponsor/collaborator)
|
| 515 |
+
- Extracts meaningful terms from query (case-insensitive)
|
| 516 |
- Scores each trial by keyword frequency (TF-IDF style)
|
| 517 |
- Also gets semantic similarity scores
|
| 518 |
- Merges both scores with weighted combination
|
| 519 |
+
|
| 520 |
+
Args:
|
| 521 |
+
query: Search query string
|
| 522 |
+
top_k: Number of results to return
|
| 523 |
+
entities: Dict with 'drugs', 'diseases', 'companies' - if provided, STRICTLY filters
|
| 524 |
"""
|
| 525 |
import time
|
| 526 |
import re
|
|
|
|
| 626 |
|
| 627 |
logger.info(f"[HYBRID] Inverted index scoring: {len(keyword_scores)} trials matched ({time.time()-t_kw:.2f}s)")
|
| 628 |
|
| 629 |
+
# 1.5. STRICT COMPANY FILTERING (if companies specified)
|
| 630 |
+
company_filter_failed = False
|
| 631 |
+
if entities and entities.get('companies'):
|
| 632 |
+
companies = [c.lower() for c in entities['companies']]
|
| 633 |
+
logger.info(f"[STRICT FILTER] Enforcing company filter: {companies}")
|
| 634 |
+
|
| 635 |
+
# Save original scores in case we need to fall back
|
| 636 |
+
original_keyword_scores = keyword_scores.copy()
|
| 637 |
+
|
| 638 |
+
# Filter keyword_scores to ONLY trials with these companies
|
| 639 |
+
filtered_keyword_scores = {}
|
| 640 |
+
sponsor_field_patterns = ['sponsor:', 'collaborator:', 'manufacturer:']
|
| 641 |
+
|
| 642 |
+
for idx, score in keyword_scores.items():
|
| 643 |
+
chunk_data = doc_chunks[idx]
|
| 644 |
+
chunk_text = chunk_data[1] if isinstance(chunk_data, tuple) else chunk_data
|
| 645 |
+
chunk_lower = chunk_text.lower()
|
| 646 |
+
|
| 647 |
+
# Check if ANY company appears in sponsor/collaborator/manufacturer fields
|
| 648 |
+
has_company = False
|
| 649 |
+
for company in companies:
|
| 650 |
+
# Look for company name in sponsor-related fields
|
| 651 |
+
for field in sponsor_field_patterns:
|
| 652 |
+
if field in chunk_lower:
|
| 653 |
+
field_start = chunk_lower.find(field)
|
| 654 |
+
field_text = chunk_lower[field_start:field_start+500] # Next 500 chars
|
| 655 |
+
if company in field_text:
|
| 656 |
+
has_company = True
|
| 657 |
+
logger.info(f"[COMPANY MATCH] Trial {idx} has '{company}' in {field}")
|
| 658 |
+
break
|
| 659 |
+
if has_company:
|
| 660 |
+
break
|
| 661 |
+
|
| 662 |
+
if has_company:
|
| 663 |
+
filtered_keyword_scores[idx] = score * 10.0 # 10x boost for company match
|
| 664 |
+
# If no company match, EXCLUDE this trial
|
| 665 |
+
|
| 666 |
+
before_count = len(keyword_scores)
|
| 667 |
+
after_count = len(filtered_keyword_scores)
|
| 668 |
+
|
| 669 |
+
logger.info(f"[STRICT FILTER] Filtered {before_count} β {after_count} trials (only those from {companies})")
|
| 670 |
+
|
| 671 |
+
# If no company matches, fall back to original search but flag it
|
| 672 |
+
if len(filtered_keyword_scores) == 0:
|
| 673 |
+
logger.warning(f"[STRICT FILTER] No trials found from companies {companies}, falling back to general search")
|
| 674 |
+
company_filter_failed = True
|
| 675 |
+
keyword_scores = original_keyword_scores # Restore original
|
| 676 |
+
else:
|
| 677 |
+
keyword_scores = filtered_keyword_scores
|
| 678 |
+
|
| 679 |
# 2. SEMANTIC SCORING
|
| 680 |
load_embedder()
|
| 681 |
t_sem = time.time()
|
|
|
|
| 774 |
logger.info(f"[355M RANKING] Added ranking metadata to context for final LLM")
|
| 775 |
|
| 776 |
context = "\n\n---\n\n".join(context_chunks) # Use --- as separator between trials
|
| 777 |
+
|
| 778 |
logger.info(f"[HYBRID] TOTAL TIME: {time.time()-t0:.2f}s")
|
| 779 |
logger.info(f"[HYBRID] Filtered context length: {len(context)} chars (was ~{sum(len(c) for c in raw_chunks)} chars)")
|
| 780 |
|
|
|
|
| 1050 |
logger.info("[QUERY PARSER] Analyzing user query with LLM...")
|
| 1051 |
client = InferenceClient(token=hf_token, timeout=30)
|
| 1052 |
|
| 1053 |
+
parse_prompt = f"""You are an expert in clinical trial terminology. Extract and expand entities from this query.
|
| 1054 |
|
| 1055 |
Query: "{query}"
|
| 1056 |
|
| 1057 |
+
Your task is to think creatively about ALL possible ways these entities might appear in clinical trial databases.
|
| 1058 |
+
|
| 1059 |
+
For each entity type, brainstorm extensively:
|
| 1060 |
+
|
| 1061 |
+
DRUGS:
|
| 1062 |
+
- Start with drugs explicitly mentioned
|
| 1063 |
+
- Add ALL possible names: brand names, generic names, research codes (like BNT162b2),
|
| 1064 |
+
manufacturer+drug combos (Pfizer-BioNTech), chemical names, common abbreviations
|
| 1065 |
+
- Think: "What would a pharmaceutical company call this in a trial?"
|
| 1066 |
+
- Example: "Pfizer COVID vaccine" β ["Comirnaty", "BNT162b2", "tozinameran", "Pfizer-BioNTech COVID-19 vaccine", "mRNA-1273"]
|
| 1067 |
+
|
| 1068 |
+
DISEASES:
|
| 1069 |
+
- Include the disease/condition mentioned
|
| 1070 |
+
- Add medical synonyms, ICD-10 terms, related conditions
|
| 1071 |
+
- Both technical and colloquial terms
|
| 1072 |
+
- Example: "COVID" β ["COVID-19", "SARS-CoV-2", "coronavirus disease 2019", "severe acute respiratory syndrome coronavirus 2"]
|
| 1073 |
+
|
| 1074 |
+
COMPANIES:
|
| 1075 |
+
- Company mentioned plus parent companies, subsidiaries
|
| 1076 |
+
- Include previous names, merged entities, partnership names
|
| 1077 |
+
- Example: "Pfizer" β ["Pfizer", "Pfizer Inc.", "Pfizer-BioNTech", "BioNTech SE"]
|
| 1078 |
+
|
| 1079 |
+
ENDPOINTS:
|
| 1080 |
+
- Any specific outcomes, measures, or endpoints mentioned
|
| 1081 |
+
- Include related clinical measures
|
| 1082 |
+
|
| 1083 |
+
SEARCH_TERMS:
|
| 1084 |
+
- Comprehensive keywords combining above entities
|
| 1085 |
+
- Include partial matches that might be relevant
|
| 1086 |
+
|
| 1087 |
+
Format EXACTLY as:
|
| 1088 |
+
DRUGS: [list or "none"]
|
| 1089 |
+
DISEASES: [list or "none"]
|
| 1090 |
+
COMPANIES: [list or "none"]
|
| 1091 |
+
ENDPOINTS: [list or "none"]
|
| 1092 |
+
SEARCH_TERMS: [comprehensive keyword list]
|
| 1093 |
+
|
| 1094 |
+
Be expansive - more synonyms mean better trial matching."""
|
| 1095 |
|
| 1096 |
response = client.chat_completion(
|
| 1097 |
model="meta-llama/Llama-3.1-70B-Instruct",
|
| 1098 |
messages=[{"role": "user", "content": parse_prompt}],
|
| 1099 |
+
max_tokens=500, # Increased for comprehensive synonyms
|
| 1100 |
+
temperature=0.3 # Slightly higher for creative synonym generation
|
| 1101 |
)
|
| 1102 |
|
| 1103 |
parsed = response.choices[0].message.content.strip()
|
|
|
|
| 1149 |
'raw_parsed': ''
|
| 1150 |
}
|
| 1151 |
|
| 1152 |
+
|
| 1153 |
+
def plan_query_action(query, parsed_entities, hf_token=None):
|
| 1154 |
"""
|
| 1155 |
+
Use HuggingFace Llama-70B to decide the best action for this query.
|
| 1156 |
|
| 1157 |
+
Actions:
|
| 1158 |
+
- SEARCH_TRIALS: Specific drug/disease questions (use RAG with top 30 trials)
|
| 1159 |
+
- COUNT_AGGREGATE: "How many" or "list all" questions (use index counts)
|
| 1160 |
+
- COMPARE: Compare two or more treatments
|
| 1161 |
+
- GENERAL_KNOWLEDGE: Definitions or general info (skip RAG, use LLM knowledge)
|
| 1162 |
+
|
| 1163 |
+
Returns: Dict with action, reasoning, and parameters
|
| 1164 |
"""
|
| 1165 |
+
try:
|
| 1166 |
+
from huggingface_hub import InferenceClient
|
| 1167 |
+
|
| 1168 |
+
logger.info("[PLANNING AGENT] Deciding action with HuggingFace Llama-70B...")
|
| 1169 |
+
client = InferenceClient(token=hf_token, timeout=30)
|
| 1170 |
+
|
| 1171 |
+
planning_prompt = f"""You are a clinical trial search strategist. Route this query to the best action.
|
| 1172 |
+
|
| 1173 |
+
Query: "{query}"
|
| 1174 |
+
|
| 1175 |
+
Extracted entities:
|
| 1176 |
+
- Drugs: {parsed_entities.get('drugs', [])}
|
| 1177 |
+
- Diseases: {parsed_entities.get('diseases', [])}
|
| 1178 |
+
- Companies: {parsed_entities.get('companies', [])}
|
| 1179 |
+
- Endpoints: {parsed_entities.get('endpoints', [])}
|
| 1180 |
+
|
| 1181 |
+
ROUTING RULES:
|
| 1182 |
+
1. SEARCH_TRIALS (default): Any question about specific drugs, treatments, efficacy, safety, trial results, side effects, or when entities are extracted
|
| 1183 |
+
2. COUNT_AGGREGATE: Only when explicitly asking "how many", "list all", "total number"
|
| 1184 |
+
3. COMPARE: Only when explicitly comparing with "vs", "versus", "compare", "better than", "difference between"
|
| 1185 |
+
4. GENERAL_KNOWLEDGE: Only for pure definitions with no trial data needed
|
| 1186 |
+
|
| 1187 |
+
When in doubt, choose SEARCH_TRIALS - real trial data is almost always helpful.
|
| 1188 |
+
|
| 1189 |
+
Analyze the user's intent:
|
| 1190 |
+
- Are they asking about specific trial outcomes? β SEARCH_TRIALS
|
| 1191 |
+
- Do they want data about a drug/disease? β SEARCH_TRIALS
|
| 1192 |
+
- Are they asking for counts or lists? β COUNT_AGGREGATE
|
| 1193 |
+
- Are they comparing treatments? β COMPARE
|
| 1194 |
+
- Is this purely definitional? β GENERAL_KNOWLEDGE
|
| 1195 |
+
|
| 1196 |
+
Respond with:
|
| 1197 |
+
ACTION: [choose one action]
|
| 1198 |
+
REASONING: [one clear sentence explaining why]
|
| 1199 |
+
SEARCH_TERMS: [refined search terms to find the most relevant trials]
|
| 1200 |
+
FOCUS: [what aspect to emphasize in the final answer - efficacy, safety, trial status, etc.]"""
|
| 1201 |
+
|
| 1202 |
+
response = client.chat_completion(
|
| 1203 |
+
model="meta-llama/Llama-3.1-70B-Instruct",
|
| 1204 |
+
messages=[{"role": "user", "content": planning_prompt}],
|
| 1205 |
+
max_tokens=150,
|
| 1206 |
+
temperature=0.1 # Low temp for consistent routing
|
| 1207 |
+
)
|
| 1208 |
+
|
| 1209 |
+
result_text = response.choices[0].message.content.strip()
|
| 1210 |
+
logger.info(f"[PLANNING AGENT] Decision:\n{result_text}")
|
| 1211 |
+
|
| 1212 |
+
# Parse the response
|
| 1213 |
+
result = {
|
| 1214 |
+
'action': 'SEARCH_TRIALS', # Default fallback
|
| 1215 |
+
'reasoning': 'Could not parse response',
|
| 1216 |
+
'params': query,
|
| 1217 |
+
'focus': 'comprehensive trial data', # New field
|
| 1218 |
+
'raw': result_text
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
lines = result_text.split('\n')
|
| 1222 |
+
for line in lines:
|
| 1223 |
+
line = line.strip()
|
| 1224 |
+
if line.startswith('ACTION:'):
|
| 1225 |
+
action = line.replace('ACTION:', '').strip()
|
| 1226 |
+
if action in ['SEARCH_TRIALS', 'COUNT_AGGREGATE', 'COMPARE', 'GENERAL_KNOWLEDGE']:
|
| 1227 |
+
result['action'] = action
|
| 1228 |
+
elif line.startswith('REASONING:'):
|
| 1229 |
+
result['reasoning'] = line.replace('REASONING:', '').strip()
|
| 1230 |
+
elif line.startswith('SEARCH_TERMS:'):
|
| 1231 |
+
params = line.replace('SEARCH_TERMS:', '').strip()
|
| 1232 |
+
if params.lower() != 'none':
|
| 1233 |
+
result['params'] = params
|
| 1234 |
+
elif line.startswith('FOCUS:'):
|
| 1235 |
+
result['focus'] = line.replace('FOCUS:', '').strip()
|
| 1236 |
+
|
| 1237 |
+
logger.info(f"[PLANNING AGENT] β Action: {result['action']}, Focus: {result['focus']}, Reasoning: {result['reasoning']}")
|
| 1238 |
+
return result
|
| 1239 |
+
|
| 1240 |
+
except Exception as e:
|
| 1241 |
+
logger.warning(f"[PLANNING AGENT] Failed: {e}, defaulting to SEARCH_TRIALS")
|
| 1242 |
+
return {
|
| 1243 |
+
'action': 'SEARCH_TRIALS',
|
| 1244 |
+
'reasoning': f'Planning failed: {e}',
|
| 1245 |
+
'params': query,
|
| 1246 |
+
'focus': 'available trial data'
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
|
| 1250 |
+
def generate_llama_response(query, rag_context, hf_token=None, parsed_entities=None, planning_context=None):
|
| 1251 |
+
"""
|
| 1252 |
+
Intelligent synthesis that ALWAYS provides substantive answers from available data
|
| 1253 |
+
|
| 1254 |
+
Args:
|
| 1255 |
+
query: User's question
|
| 1256 |
+
rag_context: Retrieved trial data
|
| 1257 |
+
hf_token: HuggingFace API token
|
| 1258 |
+
parsed_entities: Dict with extracted entities (drugs, diseases, companies)
|
| 1259 |
+
planning_context: Dict with planning agent output (action, focus, reasoning)
|
| 1260 |
+
"""
|
| 1261 |
+
# Build entity context string for better guidance
|
| 1262 |
+
entity_context = ""
|
| 1263 |
+
if parsed_entities:
|
| 1264 |
+
drugs_list = parsed_entities.get('drugs', [])[:10]
|
| 1265 |
+
diseases_list = parsed_entities.get('diseases', [])[:10]
|
| 1266 |
+
companies_list = parsed_entities.get('companies', [])[:10]
|
| 1267 |
+
|
| 1268 |
+
if drugs_list or diseases_list or companies_list:
|
| 1269 |
+
entity_context = f"""
|
| 1270 |
+
Key entities to look for (including synonyms):
|
| 1271 |
+
- Drugs/Treatments: {', '.join(drugs_list) if drugs_list else 'none'}
|
| 1272 |
+
- Diseases: {', '.join(diseases_list) if diseases_list else 'none'}
|
| 1273 |
+
- Companies: {', '.join(companies_list) if companies_list else 'none'}"""
|
| 1274 |
+
|
| 1275 |
+
# Focus area from planning
|
| 1276 |
+
focus_area = planning_context.get('focus', 'comprehensive analysis') if planning_context else 'comprehensive analysis'
|
| 1277 |
+
|
| 1278 |
try:
|
| 1279 |
# Try Groq first (much faster), fallback to HuggingFace
|
| 1280 |
groq_api_key = os.getenv("GROQ_API_KEY")
|
| 1281 |
|
| 1282 |
+
system_prompt = """You are a leading clinical trials analyst. Your role is to provide the most helpful, informative answer possible using available trial data. You excel at finding connections and insights even from imperfect data matches.
|
| 1283 |
+
|
| 1284 |
+
CORE PRINCIPLES:
|
| 1285 |
+
1. ALWAYS provide substantive, useful answers
|
| 1286 |
+
2. Find relevant information even in partially-matching trials
|
| 1287 |
+
3. Extract specific numbers, dates, phases, outcomes wherever available
|
| 1288 |
+
4. Connect information across trials to build comprehensive insights
|
| 1289 |
+
5. Never say "no relevant trials found" - work with what you have"""
|
| 1290 |
+
|
| 1291 |
+
user_prompt = f"""Question: {query}
|
| 1292 |
+
|
| 1293 |
+
Focus for this analysis: {focus_area}
|
| 1294 |
+
{entity_context}
|
| 1295 |
+
|
| 1296 |
+
Available Clinical Trial Data:
|
| 1297 |
+
{rag_context[:12000]}
|
| 1298 |
+
|
| 1299 |
+
YOUR MISSION:
|
| 1300 |
+
Provide the most comprehensive, helpful answer possible by intelligently analyzing ALL available trials.
|
| 1301 |
+
|
| 1302 |
+
ANALYSIS APPROACH:
|
| 1303 |
+
1. SCAN all trials for ANY relevance to the query:
|
| 1304 |
+
- Direct matches (same drug + disease) β Primary focus
|
| 1305 |
+
- Same drug, different disease β Still valuable (shows drug profile)
|
| 1306 |
+
- Same disease, different drug β Provides treatment landscape context
|
| 1307 |
+
- Same company β Shows research pipeline
|
| 1308 |
+
- Similar mechanisms/drug classes β Offers comparative insights
|
| 1309 |
+
|
| 1310 |
+
2. EXTRACT concrete information:
|
| 1311 |
+
- Trial phases, enrollment numbers, completion dates
|
| 1312 |
+
- Efficacy percentages, response rates, survival data
|
| 1313 |
+
- Safety profiles, adverse events, tolerability
|
| 1314 |
+
- Dosing regimens, administration routes
|
| 1315 |
+
- Patient populations, inclusion/exclusion criteria
|
| 1316 |
+
|
| 1317 |
+
3. SYNTHESIZE intelligently:
|
| 1318 |
+
- If asking about Drug X for Disease Y but only find Drug X for Disease Z,
|
| 1319 |
+
discuss what this reveals about Drug X's mechanism and potential
|
| 1320 |
+
- Find patterns across trials (e.g., consistent safety profile)
|
| 1321 |
+
- Note trial progression (Phase 1 β 2 β 3) showing development status
|
| 1322 |
+
|
| 1323 |
+
## YOUR RESPONSE STRUCTURE:
|
| 1324 |
+
|
| 1325 |
+
### DIRECT ANSWER
|
| 1326 |
+
[Immediately address the query with the best available information. Be confident and helpful.
|
| 1327 |
+
If asking about "Sinopharm COVID vaccine" and trials mention "BBIBP-CorV" - recognize these as the same.
|
| 1328 |
+
Lead with what you KNOW, not what you don't know.]
|
| 1329 |
+
|
| 1330 |
+
### KEY CLINICAL TRIALS EVIDENCE
|
| 1331 |
+
[For each relevant trial, extract meaningful information:]
|
| 1332 |
+
- **NCT#####**: [Specific findings relevant to query - be detailed with numbers/outcomes]
|
| 1333 |
+
- **NCT#####**: [What this tells us - phases, enrollment, results if available]
|
| 1334 |
+
[Include even partially relevant trials with appropriate context]
|
| 1335 |
+
|
| 1336 |
+
### CLINICAL INSIGHTS
|
| 1337 |
+
[Synthesize patterns and meaningful conclusions:]
|
| 1338 |
+
- What do these trials collectively reveal?
|
| 1339 |
+
- Treatment landscape and development status
|
| 1340 |
+
- Efficacy signals or safety patterns
|
| 1341 |
+
- How different trials complement each other
|
| 1342 |
+
- Comparison with similar drugs/approaches if relevant
|
| 1343 |
+
|
| 1344 |
+
### ADDITIONAL CONTEXT
|
| 1345 |
+
[Brief, if needed - but keep positive and informative:]
|
| 1346 |
+
- If data is from different indications, explain transferable insights
|
| 1347 |
+
- If only early phase data, discuss what this means for development
|
| 1348 |
+
- Focus on what the data DOES tell us
|
| 1349 |
+
|
| 1350 |
+
REMEMBER:
|
| 1351 |
+
- Users want actionable information, not disclaimers
|
| 1352 |
+
- Even Phase 1 safety data is valuable information
|
| 1353 |
+
- Cross-indication data provides mechanism insights
|
| 1354 |
+
- Company trial portfolios reveal strategic priorities
|
| 1355 |
+
- Similar drug classes offer comparative context
|
| 1356 |
+
- ALWAYS find something valuable to report"""
|
| 1357 |
+
|
| 1358 |
if groq_api_key:
|
| 1359 |
logger.info("Generating response with Llama-3.1-70B via GROQ (fast)...")
|
| 1360 |
from groq import Groq
|
| 1361 |
client = Groq(api_key=groq_api_key)
|
| 1362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
response = client.chat.completions.create(
|
| 1364 |
+
model="llama-3.1-70b-versatile",
|
| 1365 |
messages=[
|
| 1366 |
{"role": "system", "content": system_prompt},
|
| 1367 |
{"role": "user", "content": user_prompt}
|
| 1368 |
],
|
| 1369 |
+
max_tokens=2000, # Increased for comprehensive answers
|
| 1370 |
temperature=0.3,
|
| 1371 |
timeout=30
|
| 1372 |
)
|
|
|
|
| 1379 |
from huggingface_hub import InferenceClient
|
| 1380 |
client = InferenceClient(token=hf_token, timeout=120)
|
| 1381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
response = client.chat_completion(
|
| 1383 |
model="meta-llama/Meta-Llama-3.1-70B-Instruct",
|
| 1384 |
+
messages=[
|
| 1385 |
+
{"role": "system", "content": system_prompt},
|
| 1386 |
+
{"role": "user", "content": user_prompt}
|
| 1387 |
+
],
|
| 1388 |
+
max_tokens=2000, # Increased for comprehensive answers
|
| 1389 |
temperature=0.3
|
| 1390 |
)
|
| 1391 |
|
|
|
|
| 1412 |
|
| 1413 |
# Try to search
|
| 1414 |
start = time.time()
|
| 1415 |
+
context = retrieve_context_with_embeddings(conversation, top_k=30)
|
| 1416 |
search_time = time.time() - start
|
| 1417 |
|
| 1418 |
if not context:
|
|
|
|
| 1432 |
|
| 1433 |
def process_query(conversation):
|
| 1434 |
"""
|
| 1435 |
+
Complete pipeline with LLM query parsing, planning agent, and natural language generation
|
| 1436 |
|
| 1437 |
Flow:
|
| 1438 |
0. LLM Parser - Extract drugs, diseases, companies, endpoints (~2-3s)
|
| 1439 |
+
0.5. Planning Agent - Decide action: SEARCH_TRIALS / COUNT_AGGREGATE / GENERAL_KNOWLEDGE (~1s)
|
| 1440 |
+
1. Execute Action - Based on plan: RAG search, index count, or skip to LLM (~2s)
|
| 1441 |
2. Skipped - 355M ranking removed (was broken)
|
| 1442 |
3. LLM Response - Llama 70B generates natural language (~15s)
|
| 1443 |
|
| 1444 |
+
Total: ~21 seconds
|
| 1445 |
"""
|
| 1446 |
import time
|
| 1447 |
import traceback
|
|
|
|
| 1475 |
logger.warning(error_msg)
|
| 1476 |
output_parts.append(f"{error_msg}\n")
|
| 1477 |
search_query = conversation # Fallback to original
|
| 1478 |
+
parsed_query = {'drugs': [], 'diseases': [], 'companies': []}
|
| 1479 |
|
| 1480 |
+
# Step 0.5: Planning agent decides action
|
| 1481 |
try:
|
| 1482 |
+
planning_start = time.time()
|
| 1483 |
+
logger.info("Step 0.5: Planning agent deciding action...")
|
| 1484 |
+
output_parts.append("β Step 0.5: Planning agent started...\n")
|
|
|
|
| 1485 |
|
| 1486 |
+
plan = plan_query_action(conversation, parsed_query, hf_token=hf_token)
|
|
|
|
| 1487 |
|
| 1488 |
+
planning_time = time.time() - planning_start
|
| 1489 |
+
output_parts.append(f"β Step 0.5 Complete: Action decided ({planning_time:.1f}s)\n")
|
| 1490 |
+
output_parts.append(f" Action: {plan['action']}\n")
|
| 1491 |
+
output_parts.append(f" Reasoning: {plan['reasoning']}\n")
|
| 1492 |
+
logger.info(f"Planning complete: {plan['action']} - {plan['reasoning']}")
|
| 1493 |
|
| 1494 |
except Exception as e:
|
| 1495 |
+
error_msg = f"β Step 0.5 WARNING (Planning): {str(e)}, defaulting to SEARCH_TRIALS"
|
| 1496 |
+
logger.warning(error_msg)
|
| 1497 |
+
output_parts.append(f"{error_msg}\n")
|
| 1498 |
+
plan = {'action': 'SEARCH_TRIALS', 'reasoning': 'Planning failed', 'params': search_query}
|
| 1499 |
+
|
| 1500 |
+
# Step 1: Execute action based on plan
|
| 1501 |
+
if plan['action'] == 'GENERAL_KNOWLEDGE':
|
| 1502 |
+
# Skip RAG entirely, go straight to LLM
|
| 1503 |
+
try:
|
| 1504 |
+
step1_start = time.time()
|
| 1505 |
+
logger.info("Step 1: GENERAL_KNOWLEDGE - Skipping RAG...")
|
| 1506 |
+
output_parts.append("β Step 1: Skipped RAG (general knowledge query)\n")
|
| 1507 |
+
context = "" # Empty context
|
| 1508 |
+
step1_time = time.time() - step1_start
|
| 1509 |
+
output_parts.append(f"β Step 1 Complete: Using LLM knowledge only ({step1_time:.1f}s)\n")
|
| 1510 |
+
except Exception as e:
|
| 1511 |
+
error_msg = f"β Step 1 FAILED: {str(e)}"
|
| 1512 |
+
logger.error(error_msg)
|
| 1513 |
+
return error_msg
|
| 1514 |
+
|
| 1515 |
+
elif plan['action'] == 'COUNT_AGGREGATE':
|
| 1516 |
+
# Use index to count, pass summary to LLM
|
| 1517 |
+
try:
|
| 1518 |
+
step1_start = time.time()
|
| 1519 |
+
logger.info("Step 1: COUNT_AGGREGATE - Using inverted index...")
|
| 1520 |
+
output_parts.append("β Step 1: Count/aggregation started...\n")
|
| 1521 |
+
|
| 1522 |
+
# Get search terms from plan
|
| 1523 |
+
search_terms = plan['params'].lower().split()
|
| 1524 |
+
|
| 1525 |
+
# Find matching trials from inverted index
|
| 1526 |
+
global inverted_index
|
| 1527 |
+
matching_trial_ids = set()
|
| 1528 |
+
|
| 1529 |
+
if inverted_index:
|
| 1530 |
+
for term in search_terms:
|
| 1531 |
+
if term in inverted_index:
|
| 1532 |
+
matching_trial_ids.update(inverted_index[term])
|
| 1533 |
+
logger.info(f" Found {len(inverted_index[term])} trials for '{term}'")
|
| 1534 |
+
|
| 1535 |
+
# Create summary context
|
| 1536 |
+
if matching_trial_ids:
|
| 1537 |
+
context = f"Found {len(matching_trial_ids)} trials matching the query.\n\n"
|
| 1538 |
+
context += f"Note: This is an aggregate count. For detailed information about specific trials, "
|
| 1539 |
+
context += f"please ask a more specific question about individual drugs or treatments."
|
| 1540 |
+
else:
|
| 1541 |
+
context = "No trials found matching the query."
|
| 1542 |
+
|
| 1543 |
+
step1_time = time.time() - step1_start
|
| 1544 |
+
output_parts.append(f"β Step 1 Complete: Found {len(matching_trial_ids)} matching trials ({step1_time:.1f}s)\n")
|
| 1545 |
+
logger.info(f"Count aggregation complete - {len(matching_trial_ids)} trials in {step1_time:.1f}s")
|
| 1546 |
+
|
| 1547 |
+
except Exception as e:
|
| 1548 |
+
error_msg = f"β Step 1 FAILED (Count): {str(e)}\n{traceback.format_exc()}"
|
| 1549 |
+
logger.error(error_msg)
|
| 1550 |
+
return error_msg
|
| 1551 |
+
|
| 1552 |
+
elif plan['action'] == 'COMPARE':
|
| 1553 |
+
# Compare treatments - retrieve trials for each and let LLM analyze
|
| 1554 |
+
try:
|
| 1555 |
+
step1_start = time.time()
|
| 1556 |
+
logger.info("Step 1: COMPARE - Retrieving trials for comparison...")
|
| 1557 |
+
output_parts.append("β Step 1: Comparison search started...\n")
|
| 1558 |
+
|
| 1559 |
+
# Extract treatments to compare from parsed drugs
|
| 1560 |
+
treatments = parsed_query.get('drugs', [])
|
| 1561 |
+
|
| 1562 |
+
if len(treatments) < 2:
|
| 1563 |
+
# Try to extract from query text if not in parsed drugs
|
| 1564 |
+
import re
|
| 1565 |
+
compare_patterns = [
|
| 1566 |
+
r'(\w+)\s+(?:vs|versus|vs\.)\s+(\w+)',
|
| 1567 |
+
r'compare\s+(\w+)\s+(?:and|with|to)\s+(\w+)'
|
| 1568 |
+
]
|
| 1569 |
+
for pattern in compare_patterns:
|
| 1570 |
+
match = re.search(pattern, conversation.lower())
|
| 1571 |
+
if match:
|
| 1572 |
+
treatments = [match.group(1), match.group(2)]
|
| 1573 |
+
break
|
| 1574 |
+
|
| 1575 |
+
if len(treatments) < 2:
|
| 1576 |
+
context = "Could not identify two treatments to compare. Please specify which treatments you'd like to compare."
|
| 1577 |
+
else:
|
| 1578 |
+
logger.info(f"[COMPARE] Comparing: {treatments[0]} vs {treatments[1]}")
|
| 1579 |
+
|
| 1580 |
+
# Search for trials for each treatment
|
| 1581 |
+
context_parts = []
|
| 1582 |
+
for i, treatment in enumerate(treatments[:2], 1): # Compare first 2
|
| 1583 |
+
logger.info(f"[COMPARE] Searching trials for {treatment}...")
|
| 1584 |
+
treatment_trials = retrieve_context_with_embeddings(treatment, top_k=30, entities=parsed_query)
|
| 1585 |
+
|
| 1586 |
+
if treatment_trials:
|
| 1587 |
+
context_parts.append(f"=== TRIALS FOR {treatment.upper()} ===\n{treatment_trials}\n")
|
| 1588 |
+
else:
|
| 1589 |
+
context_parts.append(f"=== TRIALS FOR {treatment.upper()} ===\nNo trials found.\n")
|
| 1590 |
+
|
| 1591 |
+
# Combine all trials for LLM comparison
|
| 1592 |
+
context = "\n".join(context_parts)
|
| 1593 |
+
context += f"\n\nPLEASE COMPARE: {treatments[0]} vs {treatments[1]}\n"
|
| 1594 |
+
context += "Analyze the trials above and provide a side-by-side comparison including:\n"
|
| 1595 |
+
context += "- Number of trials for each\n"
|
| 1596 |
+
context += "- Key indications/diseases studied\n"
|
| 1597 |
+
context += "- Trial phases\n"
|
| 1598 |
+
context += "- Notable efficacy or safety findings\n"
|
| 1599 |
+
context += "- Head-to-head comparison trials (if any)"
|
| 1600 |
+
|
| 1601 |
+
step1_time = time.time() - step1_start
|
| 1602 |
+
output_parts.append(f"β Step 1 Complete: Retrieved comparison data ({step1_time:.1f}s)\n")
|
| 1603 |
+
logger.info(f"Comparison search complete in {step1_time:.1f}s")
|
| 1604 |
+
|
| 1605 |
+
except Exception as e:
|
| 1606 |
+
error_msg = f"β Step 1 FAILED (Compare): {str(e)}\n{traceback.format_exc()}"
|
| 1607 |
+
logger.error(error_msg)
|
| 1608 |
+
return error_msg
|
| 1609 |
+
|
| 1610 |
+
else: # SEARCH_TRIALS - normal RAG search (using optimized search query)
|
| 1611 |
+
try:
|
| 1612 |
+
step1_start = time.time()
|
| 1613 |
+
logger.info("Step 1: RAG search...")
|
| 1614 |
+
output_parts.append("β Step 1: RAG search started...\n")
|
| 1615 |
+
# Pass entities for STRICT company filtering
|
| 1616 |
+
context = retrieve_context_with_embeddings(search_query, top_k=30, entities=parsed_query)
|
| 1617 |
+
|
| 1618 |
+
if not context:
|
| 1619 |
+
return "No matching trials found in RAG search."
|
| 1620 |
+
|
| 1621 |
+
# No limit - use complete trials
|
| 1622 |
+
step1_time = time.time() - step1_start
|
| 1623 |
+
output_parts.append(f"β Step 1 Complete: Found {context.count('NCT')} trials ({step1_time:.1f}s)\n")
|
| 1624 |
+
logger.info(f"RAG search successful - found trials in {step1_time:.1f}s")
|
| 1625 |
+
|
| 1626 |
+
except Exception as e:
|
| 1627 |
+
error_msg = f"β Step 1 FAILED (RAG search): {str(e)}\n{traceback.format_exc()}"
|
| 1628 |
+
logger.error(error_msg)
|
| 1629 |
+
return error_msg
|
| 1630 |
|
| 1631 |
# Step 2: Skipped (355M ranking removed - was broken)
|
| 1632 |
output_parts.append("β Step 2: Skipped (using hybrid search + recency)\n")
|
|
|
|
| 1636 |
step3_start = time.time()
|
| 1637 |
logger.info("Step 3: Generating response with Llama-3.1-70B...")
|
| 1638 |
output_parts.append("β Step 3: Llama 70B generation started...\n")
|
| 1639 |
+
llama_response = generate_llama_response(
|
| 1640 |
+
conversation,
|
| 1641 |
+
context,
|
| 1642 |
+
hf_token=hf_token,
|
| 1643 |
+
parsed_entities=parsed_query,
|
| 1644 |
+
planning_context=plan
|
| 1645 |
+
)
|
| 1646 |
step3_time = time.time() - step3_start
|
| 1647 |
output_parts.append(f"β Step 3 Complete: Llama 70B response generated ({step3_time:.1f}s)\n")
|
| 1648 |
logger.info(f"Llama 70B generation successful in {step3_time:.1f}s")
|
|
|
|
| 1674 |
---
|
| 1675 |
Total Time: {total_time:.1f}s
|
| 1676 |
"""
|
| 1677 |
+
# Record analytics
|
| 1678 |
+
query_type = plan.get('action', 'UNKNOWN') if 'plan' in locals() else 'UNKNOWN'
|
| 1679 |
+
query_analytics.record_query(query_type, total_time, success=True)
|
| 1680 |
+
|
| 1681 |
return output
|
| 1682 |
except Exception as e:
|
| 1683 |
# Absolute fallback
|
|
|
|
| 1716 |
========================================
|
| 1717 |
"""
|
| 1718 |
logger.error(master_error_msg)
|
| 1719 |
+
|
| 1720 |
+
# Record analytics for error
|
| 1721 |
+
elapsed_time = time.time() - start_time if 'start_time' in locals() else 0
|
| 1722 |
+
query_analytics.record_query('ERROR', elapsed_time, success=False)
|
| 1723 |
+
|
| 1724 |
return master_error_msg
|
| 1725 |
|
| 1726 |
|
| 1727 |
+
def get_analytics_report():
|
| 1728 |
+
"""
|
| 1729 |
+
Get analytics report for monitoring
|
| 1730 |
+
|
| 1731 |
+
Returns formatted string with query statistics
|
| 1732 |
+
"""
|
| 1733 |
+
stats = query_analytics.get_stats()
|
| 1734 |
+
|
| 1735 |
+
uptime_hours = stats['uptime_seconds'] / 3600
|
| 1736 |
+
|
| 1737 |
+
report = f"""
|
| 1738 |
+
=== ANALYTICS REPORT ===
|
| 1739 |
+
|
| 1740 |
+
Uptime: {uptime_hours:.1f} hours
|
| 1741 |
+
Total Queries: {stats['total_queries']}
|
| 1742 |
+
Error Rate: {stats['error_rate']*100:.1f}%
|
| 1743 |
+
|
| 1744 |
+
Query Type Distribution:
|
| 1745 |
+
"""
|
| 1746 |
+
|
| 1747 |
+
for query_type, count in stats['query_type_distribution'].items():
|
| 1748 |
+
percentage = (count / stats['total_queries'] * 100) if stats['total_queries'] > 0 else 0
|
| 1749 |
+
avg_time = stats['avg_response_times'].get(query_type, 0)
|
| 1750 |
+
report += f" {query_type}: {count} queries ({percentage:.1f}%) - avg {avg_time:.2f}s\n"
|
| 1751 |
+
|
| 1752 |
+
report += "\n=== END REPORT ===\n"
|
| 1753 |
+
|
| 1754 |
+
return report
|
| 1755 |
+
|
| 1756 |
+
|
| 1757 |
# ============================================================================
|
| 1758 |
# GRADIO INTERFACE
|
| 1759 |
# ============================================================================
|
|
|
|
| 1780 |
)
|
| 1781 |
|
| 1782 |
gr.Markdown("""
|
| 1783 |
+
**Production Pipeline - Optimized for Clinical Accuracy**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1784 |
|
|
|
|
| 1785 |
""")
|
| 1786 |
|
| 1787 |
|