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
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-20 01:24 +0000
1"""
2ErrorReportGenerator - Create user-friendly error reports
3"""
5import re
6from typing import Any, Dict, Optional
8from loguru import logger
10from .error_reporter import ErrorReporter
13class ErrorReportGenerator:
14 """
15 Generates comprehensive, user-friendly error reports
16 """
18 def __init__(self, llm=None):
19 """
20 Initialize error report generator
22 Args:
23 llm: Optional LLM instance (unused, kept for compatibility)
24 """
25 self.error_reporter = ErrorReporter()
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
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
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 }
57 if partial_results:
58 context.update(partial_results)
60 error_analysis = self.error_reporter.analyze_error(
61 error_message, context
62 )
64 # Build the simplified report
65 report_parts = []
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")
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 )
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 )
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 )
104 return "\n".join(report_parts)
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
111**What happened:** {error_message}
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!)*
119*Note: Error report generation failed - showing basic error information.*"""
121 def _format_partial_results(
122 self, partial_results: Optional[Dict[str, Any]]
123 ) -> str:
124 """
125 Format partial results for display
127 Args:
128 partial_results: Partial results data
130 Returns:
131 str: Formatted partial results
132 """
133 if not partial_results:
134 return ""
136 formatted_parts = []
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("")
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("")
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("")
180 if formatted_parts:
181 formatted_parts.append(
182 "*Note: The above results were successfully collected before the error occurred.*"
183 )
185 return "\n".join(formatted_parts) if formatted_parts else ""
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
195 Args:
196 error_analysis: Error analysis results
197 partial_results: Partial results if available
199 Returns:
200 str: Technical context information
201 """
202 context_parts = []
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 )
211 if "last_activity" in partial_results:
212 context_parts.append(
213 f"- **Last Activity:** {partial_results['last_activity']}"
214 )
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 )
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 )
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 )
242 if "error_code" in partial_results:
243 context_parts.append(
244 f"- **Error Code:** {partial_results['error_code']}"
245 )
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 )
265 return "\n".join(context_parts) if context_parts else ""
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
273 Args:
274 error_message: The error message
276 Returns:
277 dict: Quick error summary
278 """
279 error_analysis = self.error_reporter.analyze_error(error_message)
281 return {
282 "title": error_analysis["title"],
283 "category": error_analysis["category"].value,
284 "severity": error_analysis["severity"],
285 "recoverable": error_analysis["recoverable"],
286 }
288 def _make_error_user_friendly(self, error_message: str) -> str:
289 """
290 Replace cryptic technical error messages with user-friendly versions
292 Args:
293 error_message: The original technical error message
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 }
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}"
456 # If no specific replacement found, return original message
457 return error_message