| import os |
| import json |
| import re |
| from typing import Tuple, Dict, List |
| from concurrent.futures import ThreadPoolExecutor, TimeoutError as ThreadTimeout |
|
|
|
|
| |
| |
| |
|
|
| DEBUG_MODE = os.getenv("DEBUG_MODE", "False").lower() == "true" |
| USE_HF_API = os.getenv("USE_HF_API", "False").lower() == "true" |
| HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN", "") |
|
|
| |
| |
| |
| def log(msg): |
| if DEBUG_MODE: |
| print(f"[LLM Debug] {msg}") |
|
|
|
|
| def get_system_prompt(interviewee_type: str, is_summary: bool = False) -> str: |
| """Generate context-aware system prompts""" |
| |
| base_prompt = """You are an expert medical transcript analyzer specializing in healthcare interviews. |
| |
| Your task is to extract structured, actionable insights from interview transcripts. |
| |
| Core Principles: |
| - Focus on factual, verifiable medical information |
| - Distinguish between speaker roles accurately |
| - Filter out pleasantries, disclaimers, and off-topic content |
| - Extract specific medical terms, dosages, and treatment details |
| - Identify patterns and clinical reasoning |
| """ |
| |
| if is_summary: |
| return base_prompt + """ |
| CROSS-INTERVIEW SYNTHESIS & VALIDATION TASK: |
| |
| You are analyzing multiple transcripts. Extract verified patterns and flag inconsistencies. |
| |
| STEP 1 - PATTERN IDENTIFICATION: |
| For each theme, count occurrences across transcripts: |
| - How many participants mentioned X? (e.g., "7 out of 10 participants") |
| - Calculate percentages when relevant |
| - What's the range of perspectives? |
| |
| STEP 2 - CLASSIFY BY CONSENSUS LEVEL: |
| - STRONG CONSENSUS (80%+ agreement): Findings most participants agree on |
| - MAJORITY VIEW (60-79%): Significant but not universal agreement |
| - SPLIT PERSPECTIVES (40-59%): Where views diverge |
| - OUTLIERS (<40%): Unique but noteworthy perspectives |
| |
| STEP 3 - CROSS-VALIDATE: |
| - Check for contradictions between transcripts |
| - Note where perspectives differ and why |
| - Flag quality issues (brief transcripts, vague responses) |
| |
| STEP 4 - CITE EVIDENCE: |
| - Reference specific transcript numbers |
| - Include brief supporting quotes/details |
| - Distinguish fact from interpretation |
| |
| OUTPUT FORMAT: |
| Start with 2-3 sentence executive overview, then: |
| |
| **STRONG CONSENSUS FINDINGS:** |
| [List with counts and evidence] |
| |
| **MAJORITY FINDINGS:** |
| [List with counts] |
| |
| **DIVERGENT PERSPECTIVES:** |
| [Where participants disagreed and context] |
| |
| **NOTABLE OUTLIERS:** |
| [Unique but important points] |
| |
| **QUALITY NOTES:** |
| [Any gaps or transcript issues] |
| |
| CRITICAL RULES: |
| - NEVER use vague terms like "many," "most," "some" - always use specific numbers |
| - ALWAYS cite transcript numbers for claims |
| - FLAG weak evidence explicitly |
| - Separate facts from interpretations |
| - NO JSON output - write in clear narrative prose |
| """ |
| |
| if interviewee_type == "HCP": |
| return base_prompt + """ |
| Healthcare Professional Analysis Focus: |
| - Prescribing patterns and medication choices |
| - Diagnostic reasoning and clinical decision-making |
| - Treatment protocols and guidelines referenced |
| - Peer perspectives on efficacy and safety |
| - Barriers to treatment or adoption |
| - Off-label uses or emerging practices |
| |
| Extract and structure: |
| 1. Diagnoses mentioned with context |
| 2. Prescriptions with dosage, frequency, and rationale |
| 3. Treatment strategies and their justifications |
| 4. Clinical guidelines or studies referenced |
| 5. Challenges or barriers discussed |
| 6. Key clinical insights or pearls |
| """ |
| |
| elif interviewee_type == "Patient": |
| return base_prompt + """ |
| Patient Interview Analysis Focus: |
| - Symptom descriptions and severity |
| - Treatment experiences and outcomes |
| - Side effects and tolerability |
| - Quality of life impacts |
| - Adherence challenges and enablers |
| - Emotional and psychological factors |
| - Healthcare system interactions |
| |
| Extract and structure: |
| 1. Primary symptoms with duration and severity |
| 2. Current and past treatments |
| 3. Treatment effectiveness and satisfaction |
| 4. Side effects experienced |
| 5. Concerns and unmet needs |
| 6. Quality of life impacts |
| 7. Support systems and resources |
| """ |
| |
| else: |
| return base_prompt + """ |
| General Interview Analysis Focus: |
| - Main themes and topics discussed |
| - Key insights and observations |
| - Recommendations or suggestions |
| - Contextual factors |
| - Areas of emphasis or concern |
| |
| Extract and structure relevant information based on interview content. |
| """ |
|
|
|
|
| def build_extraction_template(interviewee_type: str) -> str: |
| """Create JSON template for structured data extraction""" |
| |
| if interviewee_type == "HCP": |
| return """{ |
| "diagnoses": ["condition 1", "condition 2"], |
| "prescriptions": ["medication (dose, frequency, indication)"], |
| "treatment_rationale": ["reason for treatment choice"], |
| "guidelines_mentioned": ["guideline or study name"], |
| "clinical_decisions": ["key clinical decision with reasoning"], |
| "barriers": ["barrier to treatment"], |
| "key_insights": ["notable clinical insight"] |
| }""" |
| |
| elif interviewee_type == "Patient": |
| return """{ |
| "symptoms": ["symptom (severity, duration)"], |
| "concerns": ["patient concern or question"], |
| "treatments_current": ["current treatment"], |
| "treatments_past": ["past treatment with outcome"], |
| "treatment_response": ["description of how treatment is working"], |
| "side_effects": ["side effect experienced"], |
| "quality_of_life": ["impact on daily life"], |
| "adherence_factors": ["factor affecting medication adherence"] |
| }""" |
| |
| else: |
| return """{ |
| "key_insights": ["main insight or finding"], |
| "themes": ["recurring theme"], |
| "recommendations": ["recommendation or suggestion"], |
| "context": ["important contextual information"] |
| }""" |
|
|
|
|
| def parse_structured_response(text: str, interviewee_type: str) -> Dict: |
| """Extract structured data from LLM response""" |
| |
| |
| json_match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL) |
| |
| if json_match: |
| try: |
| data = json.loads(json_match.group()) |
| log(f"Successfully extracted JSON: {data}") |
| return data |
| except json.JSONDecodeError: |
| log("Failed to parse JSON from response") |
| |
| |
| data = {} |
| |
| if interviewee_type == "HCP": |
| |
| diag_pattern = r'(?:diagnos[ei]s|condition):\s*([^\n]+)' |
| data["diagnoses"] = re.findall(diag_pattern, text, re.IGNORECASE) |
| |
| |
| rx_pattern = r'(?:prescri[bp]\w*|medication):\s*([^\n]+)' |
| data["prescriptions"] = re.findall(rx_pattern, text, re.IGNORECASE) |
| |
| |
| treat_pattern = r'(?:treatment|therapy|rationale):\s*([^\n]+)' |
| data["treatment_rationale"] = re.findall(treat_pattern, text, re.IGNORECASE) |
| |
| elif interviewee_type == "Patient": |
| |
| symptom_pattern = r'(?:symptom|complaint|experienc\w*):\s*([^\n]+)' |
| data["symptoms"] = re.findall(symptom_pattern, text, re.IGNORECASE) |
| |
| |
| concern_pattern = r'(?:concern|worry|question|anxious):\s*([^\n]+)' |
| data["concerns"] = re.findall(concern_pattern, text, re.IGNORECASE) |
| |
| |
| se_pattern = r'(?:side effect|adverse|reaction):\s*([^\n]+)' |
| data["side_effects"] = re.findall(se_pattern, text, re.IGNORECASE) |
| |
| |
| for key in data: |
| data[key] = list(set([item.strip() for item in data[key] if item.strip()])) |
| |
| log(f"Extracted data from text: {data}") |
| return data |
|
|
|
|
| def query_llm_hf_api(prompt: str, max_tokens: int = 500) -> str: |
| """Use Hugging Face Inference API for better quality""" |
| try: |
| from huggingface_hub import InferenceClient |
| |
| client = InferenceClient(token=HF_TOKEN) |
| |
| |
| messages = [ |
| {"role": "system", "content": "You are an expert transcript analyzer. Provide detailed, structured analysis."}, |
| {"role": "user", "content": prompt} |
| ] |
| |
| response = client.chat_completion( |
| messages=messages, |
| model="microsoft/Phi-3-mini-4k-instruct", |
| max_tokens=max_tokens, |
| temperature=0.3 |
| ) |
| |
| return response.choices[0].message.content.strip() |
| |
| except Exception as e: |
| import traceback |
| full_error = traceback.format_exc() |
| log(f"HF API error: {e}\n{full_error}") |
| print(f"[HF API Full Error]\n{full_error}") |
| return f"[Error] HF API failed: {e}" |
|
|
|
|
| def query_llm_local(prompt: str, max_tokens: int = 500) -> str: |
| """Local model optimized for L4 GPU""" |
| try: |
| from transformers import AutoModelForSeq2SeqLM, AutoTokenizer |
| import torch |
| |
| if not hasattr(query_llm_local, 'model'): |
| log("Loading local model on L4...") |
| query_llm_local.tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-xxl") |
| query_llm_local.model = AutoModelForSeq2SeqLM.from_pretrained( |
| "google/flan-t5-xxl", |
| torch_dtype=torch.float16, |
| device_map="auto" |
| ) |
| |
| |
| inputs = query_llm_local.tokenizer( |
| prompt, |
| return_tensors="pt", |
| truncation=True, |
| max_length=512 |
| ).to("cuda") |
| |
| outputs = query_llm_local.model.generate( |
| **inputs, |
| max_new_tokens=max_tokens, |
| do_sample=False |
| ) |
| |
| response = query_llm_local.tokenizer.decode(outputs[0], skip_special_tokens=True) |
| return response.strip() |
| |
| except Exception as e: |
| log(f"Local model error: {e}") |
| return f"[Error] Local model failed: {e}" |
|
|
|
|
| def query_llm( |
| chunk: str, |
| user_context: str, |
| interviewee_type: str, |
| extract_structured: bool = False, |
| is_summary: bool = False, |
| timeout: int = 120 |
| ) -> Tuple[str, Dict]: |
| """ |
| Main LLM query function with structured extraction |
| |
| Returns: |
| Tuple of (response_text, structured_data_dict) |
| """ |
| |
| system_prompt = get_system_prompt(interviewee_type, is_summary) |
| extraction_template = build_extraction_template(interviewee_type) if extract_structured else "" |
| |
| |
| full_prompt = f"""{system_prompt} |
| |
| User Instructions: |
| {user_context} |
| |
| Transcript Segment to Analyze: |
| {chunk} |
| |
| """ |
| |
| if extract_structured: |
| full_prompt += f""" |
| IMPORTANT: Provide your analysis in two parts: |
| 1. A clear narrative summary (3-5 sentences) |
| 2. Structured data in this exact JSON format: |
| {extraction_template} |
| |
| Be specific and include relevant details (dosages, durations, severity levels, etc.) |
| """ |
| |
| |
| max_prompt_length = 6000 |
| if len(full_prompt) > max_prompt_length: |
| chunk_limit = max_prompt_length - len(system_prompt) - len(user_context) - len(extraction_template) - 500 |
| chunk = chunk[:chunk_limit] |
| full_prompt = f"{system_prompt}\n\nUser Instructions:\n{user_context}\n\nTranscript Segment:\n{chunk}\n\n" |
| if extract_structured: |
| full_prompt += f"Provide analysis and structured JSON: {extraction_template}" |
| log(f"Prompt truncated to {len(full_prompt)} characters") |
| |
| def generate(): |
| if os.getenv("USE_LMSTUDIO", "False").lower() == "true": |
| return query_llm_lmstudio(full_prompt, max_tokens=600) |
| elif USE_HF_API and HF_TOKEN: |
| return query_llm_hf_api(full_prompt, max_tokens=600) |
| else: |
| return query_llm_local(full_prompt, max_tokens=600) |
| |
| |
| with ThreadPoolExecutor(max_workers=1) as executor: |
| future = executor.submit(generate) |
| try: |
| response = future.result(timeout=timeout) |
| log(f"LLM response received ({len(response)} chars)") |
| |
| |
| structured_data = {} |
| if extract_structured: |
| structured_data = parse_structured_response(response, interviewee_type) |
| |
| return response, structured_data |
| |
| except ThreadTimeout: |
| log("LLM generation timed out") |
| return "[Error] LLM generation timed out.", {} |
| except Exception as e: |
| log(f"LLM generation failed: {e}") |
| return f"[Error] LLM generation failed: {e}", {} |
| |
|
|
| def extract_structured_data(text: str, interviewee_type: str) -> Dict: |
| """ |
| Standalone function to extract structured data from existing text |
| Useful for post-processing |
| """ |
| return parse_structured_response(text, interviewee_type) |