Spaces:
Sleeping
Sleeping
| """ | |
| Robust LLM wrapper with aggressive timeout protection and lightweight fallbacks | |
| Prevents node.js/model server crashes during summarization | |
| """ | |
| import os | |
| import signal | |
| import time | |
| from contextlib import contextmanager | |
| from typing import Tuple, Dict, Optional | |
| class TimeoutException(Exception): | |
| pass | |
| def timeout(seconds): | |
| """Context manager for enforcing hard timeouts""" | |
| def signal_handler(signum, frame): | |
| raise TimeoutException(f"Operation timed out after {seconds} seconds") | |
| # Set the signal handler | |
| old_handler = signal.signal(signal.SIGALRM, signal_handler) | |
| signal.alarm(seconds) | |
| try: | |
| yield | |
| finally: | |
| signal.alarm(0) | |
| signal.signal(signal.SIGALRM, old_handler) | |
| def query_llm_with_timeout( | |
| prompt: str, | |
| user_context: str, | |
| interviewee_type: str, | |
| extract_structured: bool = True, | |
| is_summary: bool = False, | |
| max_timeout: int = 60 # Reduced from 120 to 60 seconds | |
| ) -> Tuple[str, Dict]: | |
| """ | |
| Query LLM with aggressive timeout protection | |
| Falls back to lightweight processing if heavy models fail | |
| """ | |
| print(f"[LLM] Starting {'summary' if is_summary else 'analysis'} generation...") | |
| print(f"[LLM] Timeout limit: {max_timeout}s") | |
| # Import here to avoid circular dependencies | |
| from llm import query_llm | |
| try: | |
| # Try with timeout protection | |
| with timeout(max_timeout): | |
| result = query_llm( | |
| prompt, | |
| user_context, | |
| interviewee_type, | |
| extract_structured=extract_structured, | |
| is_summary=is_summary | |
| ) | |
| print(f"[LLM] β Completed successfully") | |
| return result | |
| except TimeoutException as e: | |
| print(f"[LLM] β Timeout after {max_timeout}s") | |
| print(f"[LLM] Generating lightweight fallback...") | |
| # Generate lightweight fallback | |
| if is_summary: | |
| return generate_lightweight_summary(prompt, interviewee_type) | |
| else: | |
| return generate_lightweight_analysis(prompt, interviewee_type) | |
| except Exception as e: | |
| print(f"[LLM] β Error: {type(e).__name__}: {str(e)}") | |
| print(f"[LLM] Generating emergency fallback...") | |
| # Emergency fallback | |
| if is_summary: | |
| return generate_emergency_summary(interviewee_type) | |
| else: | |
| return generate_emergency_analysis(interviewee_type) | |
| def generate_lightweight_summary(prompt: str, interviewee_type: str) -> Tuple[str, Dict]: | |
| """ | |
| Generate a lightweight summary without heavy LLM processing | |
| Extracts key points from the prompt itself | |
| """ | |
| print("[Fallback] Creating lightweight summary from prompt data...") | |
| # Extract numbers from prompt | |
| import re | |
| # Find participant counts | |
| participant_matches = re.findall(r'(\d+)\s+(?:participants|transcripts|interviews)', prompt, re.IGNORECASE) | |
| num_participants = int(participant_matches[0]) if participant_matches else 0 | |
| # Find percentages | |
| percentages = re.findall(r'(\d+)%', prompt) | |
| # Find mentions of conditions/themes | |
| lines = prompt.split('\n') | |
| themes = [] | |
| for line in lines: | |
| if ':' in line and not line.strip().startswith(('#', '-', '*', '=')): | |
| parts = line.split(':', 1) | |
| if len(parts) == 2: | |
| theme = parts[0].strip() | |
| if len(theme) < 50: # Reasonable theme length | |
| themes.append(theme) | |
| summary = f"""LIGHTWEIGHT SUMMARY REPORT | |
| (Generated due to LLM timeout - data extracted from available information) | |
| SAMPLE OVERVIEW: | |
| Total {interviewee_type} interviews analyzed: {num_participants} | |
| KEY OBSERVATIONS: | |
| This analysis is based on structured data extraction rather than full LLM synthesis. | |
| For detailed narrative analysis, please: | |
| 1. Reduce the number of transcripts being analyzed simultaneously | |
| 2. Check LLM server (LMStudio/HuggingFace) connectivity | |
| 3. Consider using a lighter model | |
| DATA EXTRACTED: | |
| """ | |
| if themes: | |
| summary += f"\nIdentified themes ({len(themes)} total):\n" | |
| for i, theme in enumerate(themes[:10], 1): | |
| summary += f"{i}. {theme}\n" | |
| if percentages: | |
| summary += f"\nPercentages mentioned: {', '.join(set(percentages))}%\n" | |
| summary += f""" | |
| RECOMMENDATIONS: | |
| 1. Review the CSV output file for structured data | |
| 2. Individual transcript analyses contain detailed information | |
| 3. For full narrative synthesis, retry with: | |
| - Fewer transcripts per batch | |
| - Increased timeout limits | |
| - Verified LLM server connectivity | |
| This lightweight summary preserves data integrity while avoiding server crashes. | |
| For production use, ensure LLM backend is properly configured and responsive. | |
| """ | |
| return summary, {} | |
| def generate_emergency_summary(interviewee_type: str) -> Tuple[str, Dict]: | |
| """Emergency fallback when even lightweight processing fails""" | |
| summary = f"""EMERGENCY FALLBACK REPORT | |
| LLM PROCESSING UNAVAILABLE | |
| The system encountered critical errors during summary generation. | |
| All structured data has been preserved in the CSV output file. | |
| IMMEDIATE ACTIONS REQUIRED: | |
| 1. Check LLM server status (LMStudio/HuggingFace API) | |
| 2. Verify network connectivity | |
| 3. Review console logs for specific error messages | |
| 4. Check available system memory | |
| DATA PRESERVATION: | |
| β Individual transcript analyses completed | |
| β Structured data extracted to CSV | |
| β Quality scores calculated | |
| β Cross-transcript narrative synthesis failed | |
| NEXT STEPS: | |
| 1. Review the CSV file: Contains all extracted structured data | |
| 2. Check individual transcript results below this summary | |
| 3. Resolve LLM connectivity issues | |
| 4. Re-run summary generation once service is restored | |
| This emergency report ensures no data loss while protecting system stability. | |
| """ | |
| return summary, {} | |
| def generate_lightweight_analysis(prompt: str, interviewee_type: str) -> Tuple[str, Dict]: | |
| """Lightweight analysis without heavy LLM""" | |
| # Extract basic structured data from prompt | |
| import re | |
| structured_data = {} | |
| if interviewee_type == "HCP": | |
| # Extract medical terms | |
| medical_pattern = r'\b(diagnos\w+|prescri\w+|treatment|medication|therapy)\b' | |
| terms = re.findall(medical_pattern, prompt, re.IGNORECASE) | |
| structured_data = { | |
| "diagnoses": list(set([t for t in terms if 'diagnos' in t.lower()])), | |
| "prescriptions": list(set([t for t in terms if 'prescri' in t.lower()])), | |
| "treatment_rationale": [], | |
| "key_insights": [f"Lightweight extraction: {len(terms)} medical terms identified"] | |
| } | |
| elif interviewee_type == "Patient": | |
| # Extract patient terms | |
| patient_pattern = r'\b(symptom|pain|concern|treatment|medication|side effect)\b' | |
| terms = re.findall(patient_pattern, prompt, re.IGNORECASE) | |
| structured_data = { | |
| "symptoms": list(set([t for t in terms if 'symptom' in t.lower() or 'pain' in t.lower()])), | |
| "concerns": [], | |
| "treatment_response": [], | |
| "key_insights": [f"Lightweight extraction: {len(terms)} patient-related terms identified"] | |
| } | |
| else: | |
| structured_data = { | |
| "key_insights": ["Lightweight analysis - full LLM processing unavailable"] | |
| } | |
| analysis = f"""[LIGHTWEIGHT ANALYSIS] | |
| Due to LLM timeout, basic pattern extraction was used. | |
| Structured data contains {sum(len(v) for v in structured_data.values() if isinstance(v, list))} items. | |
| For full analysis, ensure LLM server is responsive. | |
| """ | |
| return analysis, structured_data | |
| def generate_emergency_analysis(interviewee_type: str) -> Tuple[str, Dict]: | |
| """Emergency fallback for individual transcript analysis""" | |
| structured_data = { | |
| "key_insights": ["Emergency fallback - LLM processing failed"], | |
| "processing_status": "FALLBACK_MODE" | |
| } | |
| analysis = "[EMERGENCY FALLBACK] LLM processing unavailable. Minimal data extraction performed." | |
| return analysis, structured_data | |
| # Utility function to test LLM connectivity before processing | |
| def test_llm_connection(timeout_seconds: int = 10) -> bool: | |
| """Test if LLM backend is responsive""" | |
| print("[LLM] Testing backend connectivity...") | |
| test_prompt = "Test" | |
| try: | |
| with timeout(timeout_seconds): | |
| from llm import query_llm | |
| result = query_llm( | |
| test_prompt, | |
| "", | |
| "Other", | |
| extract_structured=False, | |
| is_summary=False | |
| ) | |
| print("[LLM] β Backend responsive") | |
| return True | |
| except Exception as e: | |
| print(f"[LLM] β Backend not responsive: {e}") | |
| return False | |