Coverage for src/local_deep_research/error_handling/report_generator.py: 99%

116 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-22 21:20 +0000

1""" 

2ErrorReportGenerator - Create user-friendly error reports 

3""" 

4 

5import re 

6from typing import Any, Dict, Optional 

7 

8from loguru import logger 

9 

10from .error_messages import ( 

11 MAX_ERROR_CLASSIFICATION_CHARS, 

12 TYPED_RESEARCH_ERROR_MESSAGES, 

13 get_known_research_error_message, 

14) 

15from .error_reporter import ErrorReporter 

16 

17 

18_GENERIC_ERROR_MESSAGE = ( 

19 "Research failed due to an unexpected error. " 

20 "Check the server logs for details." 

21) 

22 

23 

24class ErrorReportGenerator: 

25 """ 

26 Generates comprehensive, user-friendly error reports 

27 """ 

28 

29 def __init__(self, llm=None): 

30 """ 

31 Initialize error report generator 

32 

33 Args: 

34 llm: Optional LLM instance (unused, kept for compatibility) 

35 """ 

36 self.error_reporter = ErrorReporter() 

37 

38 def generate_error_report( 

39 self, 

40 error_message: str, 

41 query: str, 

42 partial_results: Optional[Dict[str, Any]] = None, 

43 search_iterations: int = 0, 

44 research_id: Optional[str] = None, 

45 ) -> str: 

46 """ 

47 Generate a comprehensive error report 

48 

49 Args: 

50 error_message: The error that occurred 

51 query: The research query 

52 partial_results: Any partial results that were collected 

53 search_iterations: Number of search iterations completed 

54 research_id: Research ID for reference 

55 

56 Returns: 

57 str: Formatted error report in Markdown 

58 """ 

59 try: 

60 # Analyze the error 

61 context = { 

62 "query": query, 

63 "search_iterations": search_iterations, 

64 "research_id": research_id, 

65 "partial_results": partial_results, 

66 } 

67 

68 if partial_results: 

69 context.update(partial_results) 

70 

71 error_analysis = self.error_reporter.analyze_error( 

72 error_message, context 

73 ) 

74 

75 # Build the simplified report 

76 report_parts = [] 

77 

78 # Header with user-friendly error message and logs reference 

79 user_friendly_message = self._make_error_user_friendly( 

80 error_message 

81 ) 

82 category_title = error_analysis.get("title", "Error") 

83 

84 report_parts.append("# ⚠️ Research Failed") 

85 report_parts.append(f"\n**Error Type:** {category_title}") 

86 report_parts.append(f"\n**What happened:** {user_friendly_message}") 

87 report_parts.append( 

88 '\n*For detailed error information, scroll down to the research logs and select "Errors" from the filter.*' 

89 ) 

90 

91 # Support links - moved up for better visibility 

92 report_parts.append("\n## 💬 Get Help") 

93 report_parts.append("We're here to help you get this working:") 

94 report_parts.append( 

95 "- 📖 **Documentation & guides:** [Wiki](https://github.com/LearningCircuit/local-deep-research/wiki)" 

96 ) 

97 report_parts.append( 

98 "- 💬 **Chat with the community:** [Discord #help-and-support](https://discord.gg/ttcqQeFcJ3)" 

99 ) 

100 report_parts.append( 

101 "- 🐛 **Report bugs or get help:** [GitHub Issues](https://github.com/LearningCircuit/local-deep-research/issues) *(don't hesitate to ask if you're stuck!)*" 

102 ) 

103 report_parts.append( 

104 "- 💭 **Join discussions:** [Reddit r/LocalDeepResearch](https://www.reddit.com/r/LocalDeepResearch/) *(checked less frequently)*" 

105 ) 

106 

107 # Show partial results if available, as a plain Markdown 

108 # section. The report is shown through renderMarkdown() 

109 # (services/ui.js), which displays raw HTML in Markdown as 

110 # literal text, so a <details> wrapper would show its tags. 

111 if error_analysis.get("has_partial_results"): 

112 partial_content = self._format_partial_results(partial_results) 

113 if partial_content: 

114 report_parts.append( 

115 f"\n## 📊 Partial Results Available\n\n{partial_content}" 

116 ) 

117 

118 return "\n".join(report_parts) 

119 

120 except Exception: 

121 # Fallback: always return something, even if error report generation fails 

122 logger.exception("Failed to generate error report") 

123 return f"""# ⚠️ Research Failed 

124 

125**What happened:** {_GENERIC_ERROR_MESSAGE} 

126 

127## 💬 Get Help 

128We're here to help you get this working: 

129- 📖 **Documentation & guides:** [Wiki](https://github.com/LearningCircuit/local-deep-research/wiki) 

130- 💬 **Chat with the community:** [Discord #help-and-support](https://discord.gg/ttcqQeFcJ3) 

131- 🐛 **Report bugs or get help:** [GitHub Issues](https://github.com/LearningCircuit/local-deep-research/issues) *(don't hesitate to ask if you're stuck!)* 

132 

133*Note: Error report generation failed - showing basic error information.*""" 

134 

135 def _format_partial_results( 

136 self, partial_results: Optional[Dict[str, Any]] 

137 ) -> str: 

138 """ 

139 Format partial results for display 

140 

141 Args: 

142 partial_results: Partial results data 

143 

144 Returns: 

145 str: Formatted partial results 

146 """ 

147 if not partial_results: 

148 return "" 

149 

150 formatted_parts = [] 

151 

152 # Current knowledge summary 

153 if "current_knowledge" in partial_results: 

154 knowledge = partial_results["current_knowledge"] 

155 if ( 

156 isinstance(knowledge, str) 

157 and not knowledge.lstrip().lower().startswith("error:") 

158 and len(knowledge.strip()) > 50 

159 ): 

160 formatted_parts.append("### Research Summary\n") 

161 formatted_parts.append( 

162 knowledge[:1000] + "..." 

163 if len(knowledge) > 1000 

164 else knowledge 

165 ) 

166 formatted_parts.append("") 

167 

168 # Search results 

169 if "search_results" in partial_results: 

170 results = partial_results["search_results"] 

171 if results: 171 ↛ 182line 171 didn't jump to line 182 because the condition on line 171 was always true

172 formatted_parts.append("### Search Results Found\n") 

173 for i, result in enumerate(results[:5], 1): # Show top 5 

174 title = result.get("title", "Untitled") 

175 url = result.get("url", "") 

176 formatted_parts.append(f"{i}. **{title}**") 

177 if url: 

178 formatted_parts.append(f" - URL: {url}") 

179 formatted_parts.append("") 

180 

181 # Findings 

182 if "findings" in partial_results: 

183 findings = partial_results["findings"] 

184 if findings: 184 ↛ 206line 184 didn't jump to line 206 because the condition on line 184 was always true

185 valid_findings = [ 

186 (i, finding) 

187 for i, finding in enumerate(findings[:3], 1) 

188 if isinstance(finding.get("content"), str) 

189 and finding["content"] 

190 and not finding["content"] 

191 .lstrip() 

192 .lower() 

193 .startswith("error:") 

194 ] 

195 if valid_findings: 

196 formatted_parts.append("### Research Findings\n") 

197 for i, finding in valid_findings: 

198 content = finding.get("content", "") 

199 phase = finding.get("phase", f"Finding {i}") 

200 formatted_parts.append(f"**{phase}:**") 

201 formatted_parts.append( 

202 content[:500] + "..." if len(content) > 500 else content 

203 ) 

204 formatted_parts.append("") 

205 

206 if formatted_parts: 

207 formatted_parts.append( 

208 "*Note: The above results were successfully collected before the error occurred.*" 

209 ) 

210 

211 return "\n".join(formatted_parts) if formatted_parts else "" 

212 

213 def _get_technical_context( 

214 self, 

215 error_analysis: Dict[str, Any], 

216 partial_results: Optional[Dict[str, Any]], 

217 ) -> str: 

218 """ 

219 Get additional technical context for the error 

220 

221 Args: 

222 error_analysis: Error analysis results 

223 partial_results: Partial results if available 

224 

225 Returns: 

226 str: Technical context information 

227 """ 

228 context_parts = [] 

229 

230 # Add timing information if available 

231 if partial_results: 

232 if "start_time" in partial_results: 

233 context_parts.append( 

234 f"- **Start Time:** {partial_results['start_time']}" 

235 ) 

236 

237 if "last_activity" in partial_results: 

238 context_parts.append( 

239 f"- **Last Activity:** {partial_results['last_activity']}" 

240 ) 

241 

242 # Add model information 

243 if "model_config" in partial_results: 

244 config = partial_results["model_config"] 

245 context_parts.append( 

246 f"- **Model:** {config.get('model_name', 'Unknown')}" 

247 ) 

248 context_parts.append( 

249 f"- **Provider:** {config.get('provider', 'Unknown')}" 

250 ) 

251 

252 # Add search information 

253 if "search_config" in partial_results: 

254 search_config = partial_results["search_config"] 

255 context_parts.append( 

256 f"- **Search Engine:** {search_config.get('engine', 'Unknown')}" 

257 ) 

258 context_parts.append( 

259 f"- **Max Results:** {search_config.get('max_results', 'Unknown')}" 

260 ) 

261 

262 # Add any error codes or HTTP status 

263 if "status_code" in partial_results: 

264 context_parts.append( 

265 f"- **Status Code:** {partial_results['status_code']}" 

266 ) 

267 

268 if "error_code" in partial_results: 

269 context_parts.append( 

270 f"- **Error Code:** {partial_results['error_code']}" 

271 ) 

272 

273 # Add error-specific context based on category 

274 category = error_analysis.get("category") 

275 if category: 

276 if "connection" in category.value.lower(): 

277 context_parts.append( 

278 "- **Network Error:** Connection-related issue detected" 

279 ) 

280 context_parts.append( 

281 "- **Retry Recommended:** Check service status and try again" 

282 ) 

283 elif "model" in category.value.lower(): 

284 context_parts.append( 

285 "- **Model Error:** Issue with AI model or configuration" 

286 ) 

287 context_parts.append( 

288 "- **Check:** Model service availability and parameters" 

289 ) 

290 

291 return "\n".join(context_parts) if context_parts else "" 

292 

293 def generate_quick_error_summary( 

294 self, error_message: str 

295 ) -> Dict[str, str]: 

296 """ 

297 Generate a quick error summary for API responses 

298 

299 Args: 

300 error_message: The error message 

301 

302 Returns: 

303 dict: Quick error summary 

304 """ 

305 error_analysis = self.error_reporter.analyze_error(error_message) 

306 

307 return { 

308 "title": error_analysis["title"], 

309 "category": error_analysis["category"].value, 

310 "severity": error_analysis["severity"], 

311 "recoverable": error_analysis["recoverable"], 

312 } 

313 

314 def _make_error_user_friendly(self, error_message: str) -> str: 

315 """ 

316 Replace cryptic technical error messages with user-friendly versions 

317 

318 Args: 

319 error_message: The original technical error message 

320 

321 Returns: 

322 str: User-friendly error message without raw exception details 

323 """ 

324 worker_message = get_known_research_error_message(error_message) 

325 if worker_message is not None: 

326 return worker_message 

327 

328 # Bound both typed-token and fallback regex scans. Preserve the exact 

329 # worker-message check above: truncation must not turn an untrusted 

330 # message with appended details into an authored exact match. 

331 error_message = error_message[:MAX_ERROR_CLASSIFICATION_CHARS] 

332 

333 # A typed upstream message can still contain a provider URL or a raw 

334 # "| Details:" suffix. Select only authored text for its code. Unknown 

335 # types fall through to the pattern table for a second classification. 

336 typed_error = re.search( 

337 r"\(Error type: (\w+)\)", error_message, re.IGNORECASE 

338 ) 

339 if typed_error and typed_error.group(1).lower() != "unknown": 

340 return TYPED_RESEARCH_ERROR_MESSAGES.get( 

341 typed_error.group(1).lower(), _GENERIC_ERROR_MESSAGE 

342 ) 

343 # Dictionary of technical errors to user-friendly messages 

344 error_replacements = { 

345 "Egress policy refused this run: a sensitive source": ( 

346 "Egress policy refused this run: a sensitive source would " 

347 "reach an exposing destination." 

348 ), 

349 "Egress policy refused this run": "Egress policy refused this run.", 

350 "max_workers must be greater than 0": ( 

351 "The LLM failed to generate search questions. This usually means the LLM service isn't responding properly.\n\n" 

352 "**Try this:**\n" 

353 "- Check if your LLM service (Ollama/LM Studio) is running\n" 

354 "- Restart the LLM service\n" 

355 "- Try a different model" 

356 ), 

357 "POST predict.*EOF": ( 

358 "Lost connection to Ollama. This usually means Ollama stopped responding or there's a network issue.\n\n" 

359 "**Try this:**\n" 

360 "- Restart Ollama: `ollama serve`\n" 

361 "- Check if Ollama is still running: `ps aux | grep ollama`\n" 

362 "- Try a different port if 11434 is in use" 

363 ), 

364 "HTTP error 404.*research results": ( 

365 "The research completed but the results can't be displayed. The files were likely generated successfully.\n\n" 

366 "**Try this:**\n" 

367 "- Check the `research_outputs` folder for your report\n" 

368 "- Ensure the folder has proper read/write permissions\n" 

369 "- Restart the LDR web interface" 

370 ), 

371 "Connection refused|\\[Errno 111\\]": ( 

372 "Cannot connect to the LLM service. The service might not be running or is using a different address.\n\n" 

373 "**Try this:**\n" 

374 "- Start your LLM service (Ollama: `ollama serve`, LM Studio: launch the app)\n" 

375 "- **Docker on Mac/Windows:** Change URL from `http://localhost:1234` to `http://host.docker.internal:1234`\n" 

376 "- **Docker on Linux:** Use your host IP instead of localhost (find with `hostname -I`)\n" 

377 "- Check the service URL in settings matches where your LLM is running\n" 

378 "- Verify the port number is correct (Ollama: 11434, LM Studio: 1234)" 

379 ), 

380 "The search is longer than 256 characters": ( 

381 "Your search query is too long for GitHub's API (max 256 characters).\n\n" 

382 "**Try this:**\n" 

383 "- Shorten your research query\n" 

384 "- Use a different search engine (SearXNG, Wikipedia, etc.)\n" 

385 "- Break your research into smaller, focused queries" 

386 ), 

387 "No module named.*local_deep_research": ( 

388 "Installation issue detected. The package isn't properly installed.\n\n" 

389 "**Try this:**\n" 

390 "- Reinstall: `pip install -e .` from the project directory\n" 

391 "- Check you're using the right Python environment\n" 

392 "- For Docker users: rebuild the container" 

393 ), 

394 "Failed to create search engine|search engine.*could not be found": ( 

395 "Search engine configuration problem.\n\n" 

396 "**Try this:**\n" 

397 "- Use the default search engine (auto)\n" 

398 "- Check search engine settings in Advanced Options\n" 

399 "- Ensure required API keys are set for external search engines" 

400 ), 

401 "No search results found|All search engines.*blocked.*rate.*limited": ( 

402 "No search results were found for your query. This could mean all search engines are unavailable.\n\n" 

403 "**Try this:**\n" 

404 "- **If using SearXNG:** Check if your SearXNG Docker container is running: `docker ps`\n" 

405 "- **Start SearXNG:** `docker run -d -p 8080:8080 searxng/searxng` then set URL to `http://localhost:8080`\n" 

406 "- **Try different search terms:** Use broader, more general keywords\n" 

407 "- **Check network connection:** Ensure you can access the internet\n" 

408 "- **Switch search engines:** Try SearXNG, Brave, or Google (if API key configured)\n" 

409 "- **Check for typos** in your research query" 

410 ), 

411 "TypeError.*Context.*Size|'<' not supported between instances of .* and 'NoneType'": ( 

412 "Model configuration issue. The context size setting might not be compatible with your model.\n\n" 

413 "**Try this:**\n" 

414 "- Check your model's maximum context size\n" 

415 "- Leave context size settings at default\n" 

416 "- Try a different model" 

417 ), 

418 "Model.*not found in Ollama": ( 

419 "The specified model isn't available in Ollama.\n\n" 

420 "**Try this:**\n" 

421 "- Check available models: `ollama list`\n" 

422 "- Pull the model: `ollama pull <model-name>`\n" 

423 "- Use the exact model name shown in `ollama list` (e.g., 'gemma2:9b' not 'gemma:latest')" 

424 ), 

425 "No auth credentials found|401.*API key": ( 

426 "API key is missing or incorrectly configured.\n\n" 

427 "**Try this:**\n" 

428 "- Set API key in the web UI settings (LDR reads only the server environment, not a .env file)\n" 

429 "- Go to Settings → Advanced → enter your API key\n" 

430 "- For custom endpoints, ensure the key format matches what your provider expects" 

431 ), 

432 "Attempt to write readonly database": ( 

433 "Permission issue with the database file.\n\n" 

434 "**Try this:**\n" 

435 "- On Windows: Run as Administrator\n" 

436 "- On Linux/Mac: Check folder permissions\n" 

437 "- Delete and recreate the database file if corrupted" 

438 ), 

439 "database is locked|database table is locked": ( 

440 "The local database is temporarily locked, usually because " 

441 "another operation is writing to it at the same time.\n\n" 

442 "**Try this:**\n" 

443 "- Wait a few seconds and run the research again\n" 

444 "- Avoid starting multiple researches that write at the same moment\n" 

445 "- If it persists, restart the LDR server to clear stuck connections" 

446 ), 

447 "does not support tools|tool calling": ( 

448 "Your current model does not support tool calling, which is required by the **langgraph-agent** strategy.\n\n" 

449 "**Try this:**\n" 

450 "- **Switch strategy:** Go to Settings → Search Strategy and select **source-based** or **focused-iteration** instead\n" 

451 "- **Switch model:** Use a model that supports tool calling (e.g., qwen3, gpt-oss:20b, mistral, llama3.1)\n" 

452 "- The langgraph-agent strategy requires models with native tool/function calling support" 

453 ), 

454 "object has no attribute 'model_dump'": ( 

455 "The agent received tool calls in a shape LangChain could not parse into structured messages. " 

456 "This is a known failure mode of the **langgraph-agent** strategy when the LLM, the OpenAI-compatible server, " 

457 "or a proxy in front of either of them produces non-standard tool-call responses.\n\n" 

458 "**Try this:**\n" 

459 "- **Bypass any proxy/shim** sitting in front of your local LLM server. Tool-call schema translation is the single most common place OpenAI-compatible proxies break, especially when they inject vendor extension fields (e.g. llama.cpp's `timings_per_token`, `return_progress`).\n" 

460 "- **Update your serving stack.** llama.cpp has documented OpenAI tool-call format bugs (e.g. arguments emitted as object instead of JSON string). Recent builds of llama.cpp / vLLM / Ollama fix many of these.\n" 

461 "- **Use a model with native tool-calling support.** Models without an explicit tool-call template fall back to generic prompting, which frequently produces unparseable outputs.\n" 

462 "- **Switch strategy.** If you don't need an agentic loop, Settings → Search Strategy → **source-based** or **focused-iteration** sidesteps tool-calling entirely.\n" 

463 "- See [issue #3897](https://github.com/LearningCircuit/local-deep-research/issues/3897) for the original report and discussion." 

464 ), 

465 "Invalid value.*SearXNG": ( 

466 "SearXNG configuration or rate limiting issue.\n\n" 

467 "**Try this:**\n" 

468 "- Keep 'Search snippets only' enabled (don't turn it off)\n" 

469 "- Restart SearXNG: `docker restart searxng`\n" 

470 "- If rate limited, wait a few minutes or use a VPN" 

471 ), 

472 "host.*localhost.*Docker|127\\.0\\.0\\.1.*Docker|localhost.*1234.*Docker|LM.*Studio.*Docker.*Mac": ( 

473 "Docker networking issue - can't connect to services on host.\n\n" 

474 "**Try this:**\n" 

475 "- **On Mac/Windows Docker:** Replace 'localhost' or '127.0.0.1' with 'host.docker.internal'\n" 

476 "- **On Linux Docker:** Use your host's actual IP address (find with `hostname -I`)\n" 

477 "- **Example:** Change `http://localhost:1234` to `http://host.docker.internal:1234`\n" 

478 "- Ensure the service port isn't blocked by firewall\n" 

479 "- Alternative: Use host networking mode (see wiki for setup)" 

480 ), 

481 "token limit|context length": ( 

482 "The research exceeded the model's context limit.\n\n" 

483 "**Try this:**\n" 

484 "- Reduce the research scope or number of search iterations\n" 

485 "- Try a model with a larger context window" 

486 ), 

487 "timeout|timed out": ( 

488 "The LLM request took too long to complete.\n\n" 

489 "**Try this:**\n" 

490 "- Retry after checking that your LLM service is running\n" 

491 "- Reduce the research scope or increase the request timeout" 

492 ), 

493 "rate limit": ( 

494 "The research reached the LLM provider's request limit.\n\n" 

495 "**Try this:**\n" 

496 "- Wait a few minutes before retrying\n" 

497 "- Reduce concurrent research requests or check your provider's quota" 

498 ), 

499 "connection|network": ( 

500 "The connection to the LLM service failed.\n\n" 

501 "**Try this:**\n" 

502 "- Check your network connection and that the LLM service is running\n" 

503 "- Verify the service address in settings, then retry" 

504 ), 

505 "llm error|final answer synthesis fail": ( 

506 "The language model could not complete the final answer.\n\n" 

507 "**Try this:**\n" 

508 "- Check that your LLM service is running, then retry\n" 

509 "- Try a different model or reduce the research scope" 

510 ), 

511 } 

512 

513 # Check each pattern and replace if found 

514 for pattern, replacement in error_replacements.items(): 

515 if re.search(pattern, error_message, re.IGNORECASE): 

516 return replacement 

517 

518 return _GENERIC_ERROR_MESSAGE