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

108 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-20 01:24 +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_reporter import ErrorReporter 

11 

12 

13class ErrorReportGenerator: 

14 """ 

15 Generates comprehensive, user-friendly error reports 

16 """ 

17 

18 def __init__(self, llm=None): 

19 """ 

20 Initialize error report generator 

21 

22 Args: 

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

24 """ 

25 self.error_reporter = ErrorReporter() 

26 

27 def generate_error_report( 

28 self, 

29 error_message: str, 

30 query: str, 

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

32 search_iterations: int = 0, 

33 research_id: Optional[str] = None, 

34 ) -> str: 

35 """ 

36 Generate a comprehensive error report 

37 

38 Args: 

39 error_message: The error that occurred 

40 query: The research query 

41 partial_results: Any partial results that were collected 

42 search_iterations: Number of search iterations completed 

43 research_id: Research ID for reference 

44 

45 Returns: 

46 str: Formatted error report in Markdown 

47 """ 

48 try: 

49 # Analyze the error 

50 context = { 

51 "query": query, 

52 "search_iterations": search_iterations, 

53 "research_id": research_id, 

54 "partial_results": partial_results, 

55 } 

56 

57 if partial_results: 

58 context.update(partial_results) 

59 

60 error_analysis = self.error_reporter.analyze_error( 

61 error_message, context 

62 ) 

63 

64 # Build the simplified report 

65 report_parts = [] 

66 

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

68 user_friendly_message = self._make_error_user_friendly( 

69 error_message 

70 ) 

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

72 

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

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

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

76 report_parts.append( 

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

78 ) 

79 

80 # Support links - moved up for better visibility 

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

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

83 report_parts.append( 

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

85 ) 

86 report_parts.append( 

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

88 ) 

89 report_parts.append( 

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

91 ) 

92 report_parts.append( 

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

94 ) 

95 

96 # Show partial results if available (in expandable section) 

97 if error_analysis.get("has_partial_results"): 

98 partial_content = self._format_partial_results(partial_results) 

99 if partial_content: 99 ↛ 104line 99 didn't jump to line 104 because the condition on line 99 was always true

100 report_parts.append( 

101 f"\n<details>\n<summary>📊 Partial Results Available</summary>\n\n{partial_content}\n</details>" 

102 ) 

103 

104 return "\n".join(report_parts) 

105 

106 except Exception: 

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

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

109 return f"""# ⚠️ Research Failed 

110 

111**What happened:** {error_message} 

112 

113## 💬 Get Help 

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

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

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

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

118 

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

120 

121 def _format_partial_results( 

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

123 ) -> str: 

124 """ 

125 Format partial results for display 

126 

127 Args: 

128 partial_results: Partial results data 

129 

130 Returns: 

131 str: Formatted partial results 

132 """ 

133 if not partial_results: 

134 return "" 

135 

136 formatted_parts = [] 

137 

138 # Current knowledge summary 

139 if "current_knowledge" in partial_results: 

140 knowledge = partial_results["current_knowledge"] 

141 if knowledge and len(knowledge.strip()) > 50: 

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

143 formatted_parts.append( 

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

145 if len(knowledge) > 1000 

146 else knowledge 

147 ) 

148 formatted_parts.append("") 

149 

150 # Search results 

151 if "search_results" in partial_results: 

152 results = partial_results["search_results"] 

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

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

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

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

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

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

159 if url: 

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

161 formatted_parts.append("") 

162 

163 # Findings 

164 if "findings" in partial_results: 

165 findings = partial_results["findings"] 

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

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

168 for i, finding in enumerate(findings[:3], 1): # Show top 3 

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

170 if content and not content.startswith("Error:"): 

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

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

173 formatted_parts.append( 

174 content[:500] + "..." 

175 if len(content) > 500 

176 else content 

177 ) 

178 formatted_parts.append("") 

179 

180 if formatted_parts: 

181 formatted_parts.append( 

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

183 ) 

184 

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

186 

187 def _get_technical_context( 

188 self, 

189 error_analysis: Dict[str, Any], 

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

191 ) -> str: 

192 """ 

193 Get additional technical context for the error 

194 

195 Args: 

196 error_analysis: Error analysis results 

197 partial_results: Partial results if available 

198 

199 Returns: 

200 str: Technical context information 

201 """ 

202 context_parts = [] 

203 

204 # Add timing information if available 

205 if partial_results: 

206 if "start_time" in partial_results: 

207 context_parts.append( 

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

209 ) 

210 

211 if "last_activity" in partial_results: 

212 context_parts.append( 

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

214 ) 

215 

216 # Add model information 

217 if "model_config" in partial_results: 

218 config = partial_results["model_config"] 

219 context_parts.append( 

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

221 ) 

222 context_parts.append( 

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

224 ) 

225 

226 # Add search information 

227 if "search_config" in partial_results: 

228 search_config = partial_results["search_config"] 

229 context_parts.append( 

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

231 ) 

232 context_parts.append( 

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

234 ) 

235 

236 # Add any error codes or HTTP status 

237 if "status_code" in partial_results: 

238 context_parts.append( 

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

240 ) 

241 

242 if "error_code" in partial_results: 

243 context_parts.append( 

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

245 ) 

246 

247 # Add error-specific context based on category 

248 category = error_analysis.get("category") 

249 if category: 

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

251 context_parts.append( 

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

253 ) 

254 context_parts.append( 

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

256 ) 

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

258 context_parts.append( 

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

260 ) 

261 context_parts.append( 

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

263 ) 

264 

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

266 

267 def generate_quick_error_summary( 

268 self, error_message: str 

269 ) -> Dict[str, str]: 

270 """ 

271 Generate a quick error summary for API responses 

272 

273 Args: 

274 error_message: The error message 

275 

276 Returns: 

277 dict: Quick error summary 

278 """ 

279 error_analysis = self.error_reporter.analyze_error(error_message) 

280 

281 return { 

282 "title": error_analysis["title"], 

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

284 "severity": error_analysis["severity"], 

285 "recoverable": error_analysis["recoverable"], 

286 } 

287 

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

289 """ 

290 Replace cryptic technical error messages with user-friendly versions 

291 

292 Args: 

293 error_message: The original technical error message 

294 

295 Returns: 

296 str: User-friendly error message, or original if no replacement found 

297 """ 

298 # Messages carrying a SPECIFIC "(Error type: <code>)" token have already 

299 # been classified and rewritten into user-friendly text by upstream code 

300 # (openai_compat_errors, the Ollama/status-code branches in 

301 # research_service). Return those unchanged -- the regex patterns below 

302 # are meant for raw, unrewritten exceptions only, and would otherwise 

303 # clobber the tailored upstream message (e.g. the openai_connection_refused 

304 # message getting overwritten by the generic "Connection refused" hint). 

305 # 

306 # "(Error type: unknown)", however, is attached to a RAW exception string 

307 # that upstream could NOT classify (research_service appends the token to 

308 # str(exc) verbatim). Let those fall through to the replacement table 

309 # below -- it acts as a second-chance classifier and can only add help to 

310 # an otherwise-cryptic message, never clobber friendly text. 

311 if ( 

312 re.search(r"\(Error type: \w+\)", error_message) 

313 and "(Error type: unknown)" not in error_message 

314 ): 

315 return error_message 

316 # Dictionary of technical errors to user-friendly messages 

317 error_replacements = { 

318 "max_workers must be greater than 0": ( 

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

320 "**Try this:**\n" 

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

322 "- Restart the LLM service\n" 

323 "- Try a different model" 

324 ), 

325 "POST predict.*EOF": ( 

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

327 "**Try this:**\n" 

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

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

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

331 ), 

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

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

334 "**Try this:**\n" 

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

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

337 "- Restart the LDR web interface" 

338 ), 

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

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

341 "**Try this:**\n" 

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

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

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

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

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

347 ), 

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

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

350 "**Try this:**\n" 

351 "- Shorten your research query\n" 

352 "- Use a different search engine (DuckDuckGo, Searx, etc.)\n" 

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

354 ), 

355 "No module named.*local_deep_research": ( 

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

357 "**Try this:**\n" 

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

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

360 "- For Docker users: rebuild the container" 

361 ), 

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

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

364 "**Try this:**\n" 

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

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

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

368 ), 

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

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

371 "**Try this:**\n" 

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

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

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

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

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

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

378 ), 

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

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

381 "**Try this:**\n" 

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

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

384 "- Try a different model" 

385 ), 

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

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

388 "**Try this:**\n" 

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

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

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

392 ), 

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

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

395 "**Try this:**\n" 

396 "- Set API key in the web UI settings (not in .env files)\n" 

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

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

399 ), 

400 "Attempt to write readonly database": ( 

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

402 "**Try this:**\n" 

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

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

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

406 ), 

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

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

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

410 "**Try this:**\n" 

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

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

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

414 ), 

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

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

417 "**Try this:**\n" 

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

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

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

421 ), 

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

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

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

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

426 "**Try this:**\n" 

427 "- **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" 

428 "- **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" 

429 "- **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" 

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

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

432 ), 

433 "Invalid value.*SearXNG": ( 

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

435 "**Try this:**\n" 

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

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

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

439 ), 

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

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

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

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

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

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

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

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

448 ), 

449 } 

450 

451 # Check each pattern and replace if found 

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

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

454 return f"{replacement}\n\nTechnical error: {error_message}" 

455 

456 # If no specific replacement found, return original message 

457 return error_message