jmisak commited on
Commit
56da263
Β·
verified Β·
1 Parent(s): d17fde8

Upload 2 files

Browse files
Files changed (2) hide show
  1. fix_llm_timeout.py +312 -0
  2. llm_robust.py +262 -0
fix_llm_timeout.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ LLM Timeout Fixer and Configuration Utility
4
+
5
+ This script helps diagnose and fix LLM timeout issues, particularly
6
+ when the node.js server or model loading causes the app to hang.
7
+
8
+ Usage:
9
+ python fix_llm_timeout.py --test # Test LLM connectivity
10
+ python fix_llm_timeout.py --fix # Apply recommended fixes
11
+ python fix_llm_timeout.py --config # Show current configuration
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import argparse
17
+
18
+ def print_banner():
19
+ print("=" * 70)
20
+ print(" TranscriptorAI - LLM Timeout Diagnostic & Fix Utility")
21
+ print("=" * 70)
22
+ print()
23
+
24
+ def test_llm_connectivity():
25
+ """Test if LLM backends are accessible"""
26
+ print("[1/4] Testing LLM Backend Connectivity...")
27
+ print()
28
+
29
+ # Test HuggingFace API
30
+ print(" Testing HuggingFace API...")
31
+ hf_token = os.getenv("HUGGINGFACE_TOKEN", "")
32
+
33
+ if not hf_token:
34
+ print(" βœ— HUGGINGFACE_TOKEN not set")
35
+ print(" Set it with: export HUGGINGFACE_TOKEN='your_token_here'")
36
+ hf_available = False
37
+ else:
38
+ try:
39
+ from huggingface_hub import InferenceClient
40
+ client = InferenceClient(token=hf_token)
41
+ # Quick test
42
+ result = client.text_generation(
43
+ "Test",
44
+ model="mistralai/Mixtral-8x7B-Instruct-v0.1",
45
+ max_new_tokens=10,
46
+ timeout=10
47
+ )
48
+ print(" βœ“ HuggingFace API is accessible")
49
+ hf_available = True
50
+ except Exception as e:
51
+ print(f" βœ— HuggingFace API failed: {e}")
52
+ hf_available = False
53
+
54
+ print()
55
+
56
+ # Test LMStudio
57
+ print(" Testing LMStudio...")
58
+ lmstudio_url = os.getenv("LM_STUDIO_URL", "http://192.168.1.245:1234")
59
+
60
+ try:
61
+ import requests
62
+ response = requests.get(f"{lmstudio_url}/v1/models", timeout=5)
63
+ if response.status_code == 200:
64
+ print(f" βœ“ LMStudio is accessible at {lmstudio_url}")
65
+ lmstudio_available = True
66
+ else:
67
+ print(f" βœ— LMStudio returned status {response.status_code}")
68
+ lmstudio_available = False
69
+ except Exception as e:
70
+ print(f" βœ— LMStudio not accessible: {e}")
71
+ print(f" Checked URL: {lmstudio_url}")
72
+ lmstudio_available = False
73
+
74
+ print()
75
+ print("=" * 70)
76
+ print("SUMMARY:")
77
+ print(f" HuggingFace API: {'βœ“ Available' if hf_available else 'βœ— Not Available'}")
78
+ print(f" LMStudio: {'βœ“ Available' if lmstudio_available else 'βœ— Not Available'}")
79
+ print("=" * 70)
80
+ print()
81
+
82
+ if not hf_available and not lmstudio_available:
83
+ print("⚠ WARNING: No LLM backends are available!")
84
+ print()
85
+ print("RECOMMENDED ACTIONS:")
86
+ print("1. For HuggingFace API:")
87
+ print(" export HUGGINGFACE_TOKEN='your_hf_token_here'")
88
+ print()
89
+ print("2. For LMStudio:")
90
+ print(" - Start LMStudio server")
91
+ print(" - Load a model (recommended: Mistral 7B or smaller)")
92
+ print(" - Verify it's running at: http://localhost:1234")
93
+ print(" - Set URL: export LM_STUDIO_URL='http://localhost:1234'")
94
+ print()
95
+ return False
96
+
97
+ return True
98
+
99
+ def show_current_config():
100
+ """Display current configuration"""
101
+ print("[2/4] Current Configuration...")
102
+ print()
103
+
104
+ config_items = [
105
+ ("LLM Backend", os.getenv("LLM_BACKEND", "hf_api")),
106
+ ("HuggingFace Model", os.getenv("HF_MODEL", "mistralai/Mixtral-8x7B-Instruct-v0.1")),
107
+ ("LMStudio URL", os.getenv("LM_STUDIO_URL", "http://192.168.1.245:1234")),
108
+ ("Max Tokens", os.getenv("MAX_TOKENS_PER_REQUEST", "300")),
109
+ ("LLM Timeout", os.getenv("LLM_TIMEOUT", "120")),
110
+ ("Temperature", os.getenv("LLM_TEMPERATURE", "0.3")),
111
+ ]
112
+
113
+ for key, value in config_items:
114
+ print(f" {key:20s}: {value}")
115
+
116
+ print()
117
+
118
+ def apply_fixes():
119
+ """Apply recommended configuration fixes"""
120
+ print("[3/4] Applying Recommended Fixes...")
121
+ print()
122
+
123
+ fixes_applied = []
124
+
125
+ # Create .env file with recommended settings
126
+ env_content = """# TranscriptorAI LLM Configuration - Optimized for Stability
127
+ # Generated by fix_llm_timeout.py
128
+
129
+ # Use HuggingFace API (more stable than local models)
130
+ LLM_BACKEND=hf_api
131
+
132
+ # Set your HuggingFace token here
133
+ HUGGINGFACE_TOKEN=your_token_here
134
+
135
+ # Use a lighter, faster model
136
+ HF_MODEL=mistralai/Mistral-7B-Instruct-v0.2
137
+
138
+ # Reduce token requirements to prevent timeouts
139
+ MAX_TOKENS_PER_REQUEST=200
140
+
141
+ # Aggressive timeout (60 seconds instead of 120)
142
+ LLM_TIMEOUT=60
143
+
144
+ # Lower temperature for more consistent output
145
+ LLM_TEMPERATURE=0.3
146
+
147
+ # LMStudio configuration (if using local)
148
+ LM_STUDIO_URL=http://localhost:1234
149
+
150
+ # Chunking optimization
151
+ MAX_CHUNK_TOKENS=4000
152
+ OVERLAP_TOKENS=100
153
+ """
154
+
155
+ env_path = "/home/john/TranscriptorEnhanced/.env"
156
+
157
+ try:
158
+ with open(env_path, 'w') as f:
159
+ f.write(env_content)
160
+ print(f" βœ“ Created optimized .env file at {env_path}")
161
+ fixes_applied.append("Created .env configuration")
162
+ except Exception as e:
163
+ print(f" βœ— Failed to create .env file: {e}")
164
+
165
+ # Create a startup script
166
+ startup_script = """#!/bin/bash
167
+ # TranscriptorAI Startup Script with LLM Health Check
168
+
169
+ echo "==================================="
170
+ echo " TranscriptorAI Startup"
171
+ echo "==================================="
172
+ echo
173
+
174
+ # Load environment variables
175
+ if [ -f .env ]; then
176
+ export $(cat .env | grep -v '^#' | xargs)
177
+ echo "βœ“ Loaded .env configuration"
178
+ else
179
+ echo "⚠ No .env file found, using defaults"
180
+ fi
181
+
182
+ echo
183
+ echo "Testing LLM connectivity..."
184
+ python fix_llm_timeout.py --test
185
+
186
+ if [ $? -ne 0 ]; then
187
+ echo
188
+ echo "⚠ LLM connectivity issues detected!"
189
+ echo "Continue anyway? (y/n)"
190
+ read -r response
191
+ if [ "$response" != "y" ]; then
192
+ echo "Startup cancelled"
193
+ exit 1
194
+ fi
195
+ fi
196
+
197
+ echo
198
+ echo "Starting application..."
199
+ python app.py
200
+ """
201
+
202
+ startup_path = "/home/john/TranscriptorEnhanced/start.sh"
203
+
204
+ try:
205
+ with open(startup_path, 'w') as f:
206
+ f.write(startup_script)
207
+ os.chmod(startup_path, 0o755)
208
+ print(f" βœ“ Created startup script at {startup_path}")
209
+ print(f" Run with: ./start.sh")
210
+ fixes_applied.append("Created startup script")
211
+ except Exception as e:
212
+ print(f" βœ— Failed to create startup script: {e}")
213
+
214
+ print()
215
+ print("=" * 70)
216
+ print("FIXES APPLIED:")
217
+ for fix in fixes_applied:
218
+ print(f" - {fix}")
219
+ print("=" * 70)
220
+ print()
221
+
222
+ print("NEXT STEPS:")
223
+ print("1. Edit .env file and add your HUGGINGFACE_TOKEN")
224
+ print("2. Run: ./start.sh")
225
+ print(" OR: source .env && python app.py")
226
+ print()
227
+
228
+ def diagnose_hanging_issue():
229
+ """Diagnose why the app might be hanging"""
230
+ print("[4/4] Diagnosing Potential Hang Issues...")
231
+ print()
232
+
233
+ issues_found = []
234
+
235
+ # Check if we're using a heavy model
236
+ model = os.getenv("HF_MODEL", "mistralai/Mixtral-8x7B-Instruct-v0.1")
237
+ if "Mixtral-8x7B" in model or "70B" in model or "33B" in model:
238
+ issues_found.append({
239
+ "issue": "Using a large model that may cause timeouts",
240
+ "solution": "Switch to a lighter model like Mistral-7B-Instruct-v0.2"
241
+ })
242
+
243
+ # Check timeout settings
244
+ timeout = int(os.getenv("LLM_TIMEOUT", "120"))
245
+ if timeout > 90:
246
+ issues_found.append({
247
+ "issue": f"LLM timeout is high ({timeout}s), may cause hanging appearance",
248
+ "solution": "Reduce to 60 seconds for faster failure detection"
249
+ })
250
+
251
+ # Check max tokens
252
+ max_tokens = int(os.getenv("MAX_TOKENS_PER_REQUEST", "300"))
253
+ if max_tokens > 500:
254
+ issues_found.append({
255
+ "issue": f"Max tokens is high ({max_tokens}), slows generation",
256
+ "solution": "Reduce to 200-300 tokens"
257
+ })
258
+
259
+ if not issues_found:
260
+ print(" βœ“ No obvious configuration issues detected")
261
+ else:
262
+ print(" Issues detected:")
263
+ for i, item in enumerate(issues_found, 1):
264
+ print(f"\n {i}. {item['issue']}")
265
+ print(f" Solution: {item['solution']}")
266
+
267
+ print()
268
+ print("=" * 70)
269
+ print("COMMON CAUSES OF HANGING:")
270
+ print(" 1. Model server (LMStudio/node.js) running out of memory")
271
+ print(" 2. Network timeout to HuggingFace API")
272
+ print(" 3. Model too large for available resources")
273
+ print(" 4. Multiple concurrent requests overloading server")
274
+ print()
275
+ print("PREVENTION:")
276
+ print(" - Use the robust LLM wrapper (llm_robust.py) - already integrated")
277
+ print(" - Set aggressive timeouts (60s max)")
278
+ print(" - Use lighter models (Mistral-7B instead of Mixtral-8x7B)")
279
+ print(" - Process transcripts in smaller batches")
280
+ print("=" * 70)
281
+ print()
282
+
283
+ def main():
284
+ parser = argparse.ArgumentParser(description="Fix LLM timeout issues")
285
+ parser.add_argument("--test", action="store_true", help="Test LLM connectivity")
286
+ parser.add_argument("--fix", action="store_true", help="Apply recommended fixes")
287
+ parser.add_argument("--config", action="store_true", help="Show current config")
288
+ parser.add_argument("--diagnose", action="store_true", help="Diagnose hanging issues")
289
+
290
+ args = parser.parse_args()
291
+
292
+ print_banner()
293
+
294
+ if not any(vars(args).values()):
295
+ # No arguments, run all
296
+ test_llm_connectivity()
297
+ show_current_config()
298
+ apply_fixes()
299
+ diagnose_hanging_issue()
300
+ else:
301
+ if args.test:
302
+ success = test_llm_connectivity()
303
+ sys.exit(0 if success else 1)
304
+ if args.config:
305
+ show_current_config()
306
+ if args.fix:
307
+ apply_fixes()
308
+ if args.diagnose:
309
+ diagnose_hanging_issue()
310
+
311
+ if __name__ == "__main__":
312
+ main()
llm_robust.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Robust LLM wrapper with aggressive timeout protection and lightweight fallbacks
3
+ Prevents node.js/model server crashes during summarization
4
+ """
5
+
6
+ import os
7
+ import signal
8
+ import time
9
+ from contextlib import contextmanager
10
+ from typing import Tuple, Dict, Optional
11
+
12
+ class TimeoutException(Exception):
13
+ pass
14
+
15
+ @contextmanager
16
+ def timeout(seconds):
17
+ """Context manager for enforcing hard timeouts"""
18
+ def signal_handler(signum, frame):
19
+ raise TimeoutException(f"Operation timed out after {seconds} seconds")
20
+
21
+ # Set the signal handler
22
+ old_handler = signal.signal(signal.SIGALRM, signal_handler)
23
+ signal.alarm(seconds)
24
+
25
+ try:
26
+ yield
27
+ finally:
28
+ signal.alarm(0)
29
+ signal.signal(signal.SIGALRM, old_handler)
30
+
31
+ def query_llm_with_timeout(
32
+ prompt: str,
33
+ user_context: str,
34
+ interviewee_type: str,
35
+ extract_structured: bool = True,
36
+ is_summary: bool = False,
37
+ max_timeout: int = 60 # Reduced from 120 to 60 seconds
38
+ ) -> Tuple[str, Dict]:
39
+ """
40
+ Query LLM with aggressive timeout protection
41
+ Falls back to lightweight processing if heavy models fail
42
+ """
43
+
44
+ print(f"[LLM] Starting {'summary' if is_summary else 'analysis'} generation...")
45
+ print(f"[LLM] Timeout limit: {max_timeout}s")
46
+
47
+ # Import here to avoid circular dependencies
48
+ from llm import query_llm
49
+
50
+ try:
51
+ # Try with timeout protection
52
+ with timeout(max_timeout):
53
+ result = query_llm(
54
+ prompt,
55
+ user_context,
56
+ interviewee_type,
57
+ extract_structured=extract_structured,
58
+ is_summary=is_summary
59
+ )
60
+ print(f"[LLM] βœ“ Completed successfully")
61
+ return result
62
+
63
+ except TimeoutException as e:
64
+ print(f"[LLM] βœ— Timeout after {max_timeout}s")
65
+ print(f"[LLM] Generating lightweight fallback...")
66
+
67
+ # Generate lightweight fallback
68
+ if is_summary:
69
+ return generate_lightweight_summary(prompt, interviewee_type)
70
+ else:
71
+ return generate_lightweight_analysis(prompt, interviewee_type)
72
+
73
+ except Exception as e:
74
+ print(f"[LLM] βœ— Error: {type(e).__name__}: {str(e)}")
75
+ print(f"[LLM] Generating emergency fallback...")
76
+
77
+ # Emergency fallback
78
+ if is_summary:
79
+ return generate_emergency_summary(interviewee_type)
80
+ else:
81
+ return generate_emergency_analysis(interviewee_type)
82
+
83
+ def generate_lightweight_summary(prompt: str, interviewee_type: str) -> Tuple[str, Dict]:
84
+ """
85
+ Generate a lightweight summary without heavy LLM processing
86
+ Extracts key points from the prompt itself
87
+ """
88
+
89
+ print("[Fallback] Creating lightweight summary from prompt data...")
90
+
91
+ # Extract numbers from prompt
92
+ import re
93
+
94
+ # Find participant counts
95
+ participant_matches = re.findall(r'(\d+)\s+(?:participants|transcripts|interviews)', prompt, re.IGNORECASE)
96
+ num_participants = int(participant_matches[0]) if participant_matches else 0
97
+
98
+ # Find percentages
99
+ percentages = re.findall(r'(\d+)%', prompt)
100
+
101
+ # Find mentions of conditions/themes
102
+ lines = prompt.split('\n')
103
+ themes = []
104
+ for line in lines:
105
+ if ':' in line and not line.strip().startswith(('#', '-', '*', '=')):
106
+ parts = line.split(':', 1)
107
+ if len(parts) == 2:
108
+ theme = parts[0].strip()
109
+ if len(theme) < 50: # Reasonable theme length
110
+ themes.append(theme)
111
+
112
+ summary = f"""LIGHTWEIGHT SUMMARY REPORT
113
+ (Generated due to LLM timeout - data extracted from available information)
114
+
115
+ SAMPLE OVERVIEW:
116
+ Total {interviewee_type} interviews analyzed: {num_participants}
117
+
118
+ KEY OBSERVATIONS:
119
+ This analysis is based on structured data extraction rather than full LLM synthesis.
120
+ For detailed narrative analysis, please:
121
+ 1. Reduce the number of transcripts being analyzed simultaneously
122
+ 2. Check LLM server (LMStudio/HuggingFace) connectivity
123
+ 3. Consider using a lighter model
124
+
125
+ DATA EXTRACTED:
126
+ """
127
+
128
+ if themes:
129
+ summary += f"\nIdentified themes ({len(themes)} total):\n"
130
+ for i, theme in enumerate(themes[:10], 1):
131
+ summary += f"{i}. {theme}\n"
132
+
133
+ if percentages:
134
+ summary += f"\nPercentages mentioned: {', '.join(set(percentages))}%\n"
135
+
136
+ summary += f"""
137
+
138
+ RECOMMENDATIONS:
139
+ 1. Review the CSV output file for structured data
140
+ 2. Individual transcript analyses contain detailed information
141
+ 3. For full narrative synthesis, retry with:
142
+ - Fewer transcripts per batch
143
+ - Increased timeout limits
144
+ - Verified LLM server connectivity
145
+
146
+ This lightweight summary preserves data integrity while avoiding server crashes.
147
+ For production use, ensure LLM backend is properly configured and responsive.
148
+ """
149
+
150
+ return summary, {}
151
+
152
+ def generate_emergency_summary(interviewee_type: str) -> Tuple[str, Dict]:
153
+ """Emergency fallback when even lightweight processing fails"""
154
+
155
+ summary = f"""EMERGENCY FALLBACK REPORT
156
+
157
+ LLM PROCESSING UNAVAILABLE
158
+
159
+ The system encountered critical errors during summary generation.
160
+ All structured data has been preserved in the CSV output file.
161
+
162
+ IMMEDIATE ACTIONS REQUIRED:
163
+ 1. Check LLM server status (LMStudio/HuggingFace API)
164
+ 2. Verify network connectivity
165
+ 3. Review console logs for specific error messages
166
+ 4. Check available system memory
167
+
168
+ DATA PRESERVATION:
169
+ βœ“ Individual transcript analyses completed
170
+ βœ“ Structured data extracted to CSV
171
+ βœ“ Quality scores calculated
172
+ βœ— Cross-transcript narrative synthesis failed
173
+
174
+ NEXT STEPS:
175
+ 1. Review the CSV file: Contains all extracted structured data
176
+ 2. Check individual transcript results below this summary
177
+ 3. Resolve LLM connectivity issues
178
+ 4. Re-run summary generation once service is restored
179
+
180
+ This emergency report ensures no data loss while protecting system stability.
181
+ """
182
+
183
+ return summary, {}
184
+
185
+ def generate_lightweight_analysis(prompt: str, interviewee_type: str) -> Tuple[str, Dict]:
186
+ """Lightweight analysis without heavy LLM"""
187
+
188
+ # Extract basic structured data from prompt
189
+ import re
190
+
191
+ structured_data = {}
192
+
193
+ if interviewee_type == "HCP":
194
+ # Extract medical terms
195
+ medical_pattern = r'\b(diagnos\w+|prescri\w+|treatment|medication|therapy)\b'
196
+ terms = re.findall(medical_pattern, prompt, re.IGNORECASE)
197
+ structured_data = {
198
+ "diagnoses": list(set([t for t in terms if 'diagnos' in t.lower()])),
199
+ "prescriptions": list(set([t for t in terms if 'prescri' in t.lower()])),
200
+ "treatment_rationale": [],
201
+ "key_insights": [f"Lightweight extraction: {len(terms)} medical terms identified"]
202
+ }
203
+
204
+ elif interviewee_type == "Patient":
205
+ # Extract patient terms
206
+ patient_pattern = r'\b(symptom|pain|concern|treatment|medication|side effect)\b'
207
+ terms = re.findall(patient_pattern, prompt, re.IGNORECASE)
208
+ structured_data = {
209
+ "symptoms": list(set([t for t in terms if 'symptom' in t.lower() or 'pain' in t.lower()])),
210
+ "concerns": [],
211
+ "treatment_response": [],
212
+ "key_insights": [f"Lightweight extraction: {len(terms)} patient-related terms identified"]
213
+ }
214
+ else:
215
+ structured_data = {
216
+ "key_insights": ["Lightweight analysis - full LLM processing unavailable"]
217
+ }
218
+
219
+ analysis = f"""[LIGHTWEIGHT ANALYSIS]
220
+ Due to LLM timeout, basic pattern extraction was used.
221
+ Structured data contains {sum(len(v) for v in structured_data.values() if isinstance(v, list))} items.
222
+
223
+ For full analysis, ensure LLM server is responsive.
224
+ """
225
+
226
+ return analysis, structured_data
227
+
228
+ def generate_emergency_analysis(interviewee_type: str) -> Tuple[str, Dict]:
229
+ """Emergency fallback for individual transcript analysis"""
230
+
231
+ structured_data = {
232
+ "key_insights": ["Emergency fallback - LLM processing failed"],
233
+ "processing_status": "FALLBACK_MODE"
234
+ }
235
+
236
+ analysis = "[EMERGENCY FALLBACK] LLM processing unavailable. Minimal data extraction performed."
237
+
238
+ return analysis, structured_data
239
+
240
+ # Utility function to test LLM connectivity before processing
241
+ def test_llm_connection(timeout_seconds: int = 10) -> bool:
242
+ """Test if LLM backend is responsive"""
243
+
244
+ print("[LLM] Testing backend connectivity...")
245
+
246
+ test_prompt = "Test"
247
+
248
+ try:
249
+ with timeout(timeout_seconds):
250
+ from llm import query_llm
251
+ result = query_llm(
252
+ test_prompt,
253
+ "",
254
+ "Other",
255
+ extract_structured=False,
256
+ is_summary=False
257
+ )
258
+ print("[LLM] βœ“ Backend responsive")
259
+ return True
260
+ except Exception as e:
261
+ print(f"[LLM] βœ— Backend not responsive: {e}")
262
+ return False