Chris commited on
Commit
a248c93
·
1 Parent(s): d57fa7d
src/__pycache__/app.cpython-310.pyc CHANGED
Binary files a/src/__pycache__/app.cpython-310.pyc and b/src/__pycache__/app.cpython-310.pyc differ
 
src/agents/__pycache__/reasoning_agent.cpython-310.pyc CHANGED
Binary files a/src/agents/__pycache__/reasoning_agent.cpython-310.pyc and b/src/agents/__pycache__/reasoning_agent.cpython-310.pyc differ
 
src/agents/__pycache__/router.cpython-310.pyc CHANGED
Binary files a/src/agents/__pycache__/router.cpython-310.pyc and b/src/agents/__pycache__/router.cpython-310.pyc differ
 
src/agents/__pycache__/synthesizer.cpython-310.pyc CHANGED
Binary files a/src/agents/__pycache__/synthesizer.cpython-310.pyc and b/src/agents/__pycache__/synthesizer.cpython-310.pyc differ
 
src/agents/__pycache__/web_researcher.cpython-310.pyc CHANGED
Binary files a/src/agents/__pycache__/web_researcher.cpython-310.pyc and b/src/agents/__pycache__/web_researcher.cpython-310.pyc differ
 
src/agents/reasoning_agent.py CHANGED
@@ -36,21 +36,37 @@ class ReasoningAgent:
36
  strategy = self._determine_reasoning_strategy(state.question)
37
  state.add_processing_step(f"Reasoning Agent: Strategy = {strategy}")
38
 
39
- # Execute reasoning based on strategy
40
- if strategy == "mathematical":
41
- result = self._process_mathematical(state)
42
- elif strategy == "statistical":
43
- result = self._process_statistical(state)
44
- elif strategy == "unit_conversion":
45
- result = self._process_unit_conversion(state)
46
- elif strategy == "logical_deduction":
47
- result = self._process_logical_deduction(state)
48
- elif strategy == "pattern_analysis":
49
- result = self._process_pattern_analysis(state)
50
- elif strategy == "step_by_step":
51
- result = self._process_step_by_step(state)
52
- else:
53
- result = self._process_general_reasoning(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  # Add result to state
56
  state.add_agent_result(result)
@@ -63,8 +79,18 @@ class ReasoningAgent:
63
  state.add_error(error_msg)
64
  logger.error(error_msg)
65
 
66
- # Create failure result
67
- failure_result = self._create_failure_result(error_msg)
 
 
 
 
 
 
 
 
 
 
68
  state.add_agent_result(failure_result)
69
  return state
70
 
@@ -630,4 +656,62 @@ class ReasoningAgent:
630
  model_used="error",
631
  processing_time=0.0,
632
  cost_estimate=0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  )
 
36
  strategy = self._determine_reasoning_strategy(state.question)
37
  state.add_processing_step(f"Reasoning Agent: Strategy = {strategy}")
38
 
39
+ # Execute reasoning with enhanced error handling
40
+ result = None
41
+ try:
42
+ # Execute reasoning based on strategy
43
+ if strategy == "mathematical":
44
+ result = self._process_mathematical(state)
45
+ elif strategy == "statistical":
46
+ result = self._process_statistical(state)
47
+ elif strategy == "unit_conversion":
48
+ result = self._process_unit_conversion(state)
49
+ elif strategy == "logical_deduction":
50
+ result = self._process_logical_deduction(state)
51
+ elif strategy == "pattern_analysis":
52
+ result = self._process_pattern_analysis(state)
53
+ elif strategy == "step_by_step":
54
+ result = self._process_step_by_step(state)
55
+ else:
56
+ result = self._process_general_reasoning(state)
57
+
58
+ except Exception as strategy_error:
59
+ logger.warning(f"Strategy {strategy} failed: {strategy_error}, trying fallback")
60
+ # Try fallback reasoning
61
+ try:
62
+ result = self._process_fallback_reasoning(state, strategy, str(strategy_error))
63
+ except Exception as fallback_error:
64
+ logger.error(f"Fallback reasoning also failed: {fallback_error}")
65
+ result = self._create_graceful_failure_result(state, f"Reasoning failed: {fallback_error}")
66
+
67
+ # Ensure we always have a valid result
68
+ if not result or not isinstance(result, AgentResult):
69
+ result = self._create_graceful_failure_result(state, "No reasoning results available")
70
 
71
  # Add result to state
72
  state.add_agent_result(result)
 
79
  state.add_error(error_msg)
80
  logger.error(error_msg)
81
 
82
+ # Create failure result but ensure system continues
83
+ failure_result = AgentResult(
84
+ agent_role=AgentRole.REASONING_AGENT,
85
+ success=False,
86
+ result=f"Processing encountered difficulties: Reasoning failed",
87
+ confidence=0.1, # Very low but not zero to allow synthesis
88
+ reasoning=f"Exception during reasoning: {str(e)}",
89
+ tools_used=[],
90
+ model_used="error",
91
+ processing_time=0.0,
92
+ cost_estimate=0.0
93
+ )
94
  state.add_agent_result(failure_result)
95
  return state
96
 
 
656
  model_used="error",
657
  processing_time=0.0,
658
  cost_estimate=0.0
659
+ )
660
+
661
+ def _process_fallback_reasoning(self, state: GAIAAgentState, original_strategy: str, error_msg: str) -> AgentResult:
662
+ """Enhanced fallback reasoning when primary strategy fails"""
663
+
664
+ logger.info(f"Executing fallback reasoning after {original_strategy} failure")
665
+
666
+ # Try simple general reasoning as fallback
667
+ try:
668
+ fallback_prompt = f"""
669
+ Please answer this question using basic reasoning:
670
+
671
+ Question: {state.question}
672
+
673
+ Note: Original strategy '{original_strategy}' failed with: {error_msg}
674
+
675
+ Please provide the best answer you can using simple analysis and reasoning.
676
+ Focus on extracting key information from the question and providing a helpful response.
677
+ """
678
+
679
+ # Use main model for fallback
680
+ llm_result = self.llm_client.generate(fallback_prompt, tier=ModelTier.MAIN, max_tokens=400)
681
+
682
+ if llm_result.success:
683
+ return AgentResult(
684
+ agent_role=AgentRole.REASONING_AGENT,
685
+ success=True,
686
+ result=llm_result.response,
687
+ confidence=0.3, # Lower confidence for fallback
688
+ reasoning=f"Fallback reasoning after {original_strategy} failed: {error_msg}",
689
+ tools_used=[],
690
+ model_used=llm_result.model_used,
691
+ processing_time=llm_result.response_time,
692
+ cost_estimate=llm_result.cost_estimate
693
+ )
694
+ else:
695
+ raise Exception(f"Fallback LLM reasoning failed: {llm_result.error}")
696
+
697
+ except Exception as fallback_error:
698
+ logger.error(f"Fallback reasoning failed: {fallback_error}")
699
+ return self._create_graceful_failure_result(state, f"All reasoning methods failed: {fallback_error}")
700
+
701
+ def _create_graceful_failure_result(self, state: GAIAAgentState, error_context: str) -> AgentResult:
702
+ """Create a graceful failure result that allows the system to continue"""
703
+
704
+ # Try to extract any useful information from the question itself
705
+ question_analysis = f"Question analysis: {state.question[:200]}"
706
+
707
+ return AgentResult(
708
+ agent_role=AgentRole.REASONING_AGENT,
709
+ success=False,
710
+ result=f"Processing encountered difficulties: {error_context}",
711
+ confidence=0.1,
712
+ reasoning=f"Reasoning failed: {error_context}",
713
+ tools_used=[],
714
+ model_used="none",
715
+ processing_time=0.0,
716
+ cost_estimate=0.0
717
  )
src/agents/router.py CHANGED
@@ -102,40 +102,75 @@ class RouterAgent:
102
  # Content-based classification
103
  classification_patterns = {
104
  QuestionType.MATHEMATICAL: [
105
- r'calculate', r'compute', r'solve', r'equation', r'formula',
106
- r'sum', r'total', r'average', r'percentage', r'ratio',
107
- r'how many', r'how much', r'\d+.*\d+', r'math'
 
108
  ],
109
  QuestionType.CODE_EXECUTION: [
110
- r'code', r'program', r'script', r'function', r'algorithm',
111
- r'execute', r'run.*code', r'python', r'javascript'
112
  ],
113
  QuestionType.TEXT_MANIPULATION: [
114
- r'reverse', r'encode', r'decode', r'transform', r'convert',
115
- r'uppercase', r'lowercase', r'replace', r'extract'
116
  ],
117
  QuestionType.REASONING: [
118
- r'why', r'explain', r'analyze', r'reasoning', r'logic',
119
- r'relationship', r'compare', r'contrast', r'conclusion'
120
  ],
121
  QuestionType.WEB_RESEARCH: [
122
- r'search', r'find.*information', r'research', r'look up',
123
- r'website', r'online', r'internet'
 
 
124
  ]
125
  }
126
 
127
- # Score each category
128
  type_scores = {}
129
  for question_type, patterns in classification_patterns.items():
130
- score = sum(1 for pattern in patterns if re.search(pattern, question_lower))
 
 
 
131
  if score > 0:
132
  type_scores[question_type] = score
133
 
134
- # Return highest scoring type, or UNKNOWN if no clear match
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  if type_scores:
136
- return max(type_scores.keys(), key=lambda t: type_scores[t])
 
 
 
 
 
 
 
 
 
 
137
 
138
- return QuestionType.UNKNOWN
 
139
 
140
  def _assess_complexity(self, question: str) -> str:
141
  """Assess question complexity"""
 
102
  # Content-based classification
103
  classification_patterns = {
104
  QuestionType.MATHEMATICAL: [
105
+ r'\bcalculate\b', r'\bcompute\b', r'\bsolve\b', r'\bequation\b', r'\bformula\b',
106
+ r'\bsum\b', r'\btotal\b', r'\baverage\b', r'\bpercentage\b', r'\bratio\b',
107
+ r'\bhow many\b', r'\bhow much\b', r'\d+\s*[\+\-\*/]\s*\d+', r'\bmath\b',
108
+ r'\bsquare root\b', r'\bfactorial\b', r'\bdivided by\b', r'\bmultiply\b'
109
  ],
110
  QuestionType.CODE_EXECUTION: [
111
+ r'\bcode\b', r'\bprogram\b', r'\bscript\b', r'\bfunction\b', r'\balgorithm\b',
112
+ r'\bexecute\b', r'\brun.*code\b', r'\bpython\b', r'\bjavascript\b'
113
  ],
114
  QuestionType.TEXT_MANIPULATION: [
115
+ r'\breverse\b', r'\bencode\b', r'\bdecode\b', r'\btransform\b', r'\bconvert\b',
116
+ r'\buppercase\b', r'\blowercase\b', r'\breplace\b', r'\bextract\b'
117
  ],
118
  QuestionType.REASONING: [
119
+ r'\bwhy\b', r'\bexplain\b', r'\banalyze\b', r'\breasoning\b', r'\blogic\b',
120
+ r'\brelationship\b', r'\bcompare\b', r'\bcontrast\b', r'\bconclusion\b'
121
  ],
122
  QuestionType.WEB_RESEARCH: [
123
+ r'\bsearch\b', r'\bfind.*information\b', r'\bresearch\b', r'\blook up\b',
124
+ r'\bwebsite\b', r'\bonline\b', r'\binternet\b', r'\bwho\s+(?:is|was|are|were)\b',
125
+ r'\bwhat\s+(?:is|was|are|were)\b', r'\bwhen\s+(?:is|was|did|does)\b',
126
+ r'\bwhere\s+(?:is|was|are|were)\b'
127
  ]
128
  }
129
 
130
+ # Score each category with refined scoring
131
  type_scores = {}
132
  for question_type, patterns in classification_patterns.items():
133
+ score = 0
134
+ for pattern in patterns:
135
+ matches = re.findall(pattern, question_lower)
136
+ score += len(matches)
137
  if score > 0:
138
  type_scores[question_type] = score
139
 
140
+ # Special handling for specific question patterns
141
+
142
+ # Check for fictional/non-existent content (should be WEB_RESEARCH)
143
+ if any(term in question_lower for term in ['fictional', 'imaginary', 'non-existent', 'nonexistent']):
144
+ type_scores[QuestionType.WEB_RESEARCH] = type_scores.get(QuestionType.WEB_RESEARCH, 0) + 2
145
+
146
+ # Check for research questions about people, places, things
147
+ if re.search(r'\bwho\s+(?:is|was|are|were|did|does)\b', question_lower):
148
+ type_scores[QuestionType.WEB_RESEARCH] = type_scores.get(QuestionType.WEB_RESEARCH, 0) + 2
149
+
150
+ # Check for historical or factual queries
151
+ if any(term in question_lower for term in ['history', 'historical', 'century', 'year', 'published', 'author']):
152
+ type_scores[QuestionType.WEB_RESEARCH] = type_scores.get(QuestionType.WEB_RESEARCH, 0) + 1
153
+
154
+ # Check for specific mathematical operations (boost mathematical score)
155
+ if re.search(r'\d+\s*[\+\-\*/]\s*\d+', question_lower):
156
+ type_scores[QuestionType.MATHEMATICAL] = type_scores.get(QuestionType.MATHEMATICAL, 0) + 3
157
+
158
+ # Return highest scoring type, or WEB_RESEARCH as default for informational questions
159
  if type_scores:
160
+ best_type = max(type_scores.keys(), key=lambda t: type_scores[t])
161
+
162
+ # If it's a tie or low score, check for general informational patterns
163
+ max_score = type_scores[best_type]
164
+ if max_score <= 1:
165
+ # Check if it's a general informational question
166
+ info_patterns = [r'\bwhat\b', r'\bwho\b', r'\bwhen\b', r'\bwhere\b', r'\bhow\b']
167
+ if any(re.search(pattern, question_lower) for pattern in info_patterns):
168
+ return QuestionType.WEB_RESEARCH
169
+
170
+ return best_type
171
 
172
+ # Default to WEB_RESEARCH for unknown informational questions
173
+ return QuestionType.WEB_RESEARCH
174
 
175
  def _assess_complexity(self, question: str) -> str:
176
  """Assess question complexity"""
src/agents/synthesizer.py CHANGED
@@ -52,6 +52,8 @@ class SynthesizerAgent:
52
  final_result = self._synthesize_confidence_weighted(state)
53
  elif synthesis_strategy == "llm_synthesis":
54
  final_result = self._synthesize_with_llm(state)
 
 
55
  else:
56
  final_result = self._synthesize_fallback(state)
57
 
@@ -96,9 +98,11 @@ class SynthesizerAgent:
96
  """Determine the best synthesis strategy based on available results"""
97
 
98
  successful_results = [r for r in state.agent_results.values() if r.success]
 
99
 
100
- if len(successful_results) == 0:
101
- return "fallback"
 
102
  elif len(successful_results) == 1:
103
  return "single_agent"
104
  elif len(successful_results) == 2:
@@ -245,32 +249,91 @@ Focus on accuracy and be direct in your response.
245
  return self._synthesize_confidence_weighted(state)
246
 
247
  def _synthesize_fallback(self, state: GAIAAgentState) -> Dict[str, Any]:
248
- """Fallback synthesis when other strategies fail"""
249
 
250
  # Try to get any result, even if not successful
251
  all_results = list(state.agent_results.values())
252
 
253
  if all_results:
254
- # Use the result with highest confidence, even if failed
255
- best_attempt = max(all_results, key=lambda r: r.confidence if r.success else 0.0)
256
-
257
- if best_attempt.success:
258
  return {
259
  "answer": best_attempt.result,
260
  "confidence": max(0.3, best_attempt.confidence * 0.8), # Reduce confidence for fallback
261
  "reasoning": f"Fallback result from {best_attempt.agent_role.value}: {best_attempt.reasoning}",
262
  "source": f"fallback_{best_attempt.agent_role.value}"
263
  }
264
- else:
265
- return {
266
- "answer": f"Processing encountered difficulties: {best_attempt.result}",
267
- "confidence": 0.2,
268
- "reasoning": f"Fallback from failed attempt by {best_attempt.agent_role.value}",
269
- "source": "failed_fallback"
270
- }
271
  else:
272
  return self._create_fallback_result("No agent results available")
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  def _create_fallback_result(self, reason: str) -> Dict[str, Any]:
275
  """Create a fallback result when synthesis is impossible"""
276
  return {
 
52
  final_result = self._synthesize_confidence_weighted(state)
53
  elif synthesis_strategy == "llm_synthesis":
54
  final_result = self._synthesize_with_llm(state)
55
+ elif synthesis_strategy == "failure_analysis":
56
+ final_result = self._synthesize_failure_analysis(state)
57
  else:
58
  final_result = self._synthesize_fallback(state)
59
 
 
98
  """Determine the best synthesis strategy based on available results"""
99
 
100
  successful_results = [r for r in state.agent_results.values() if r.success]
101
+ failed_results = [r for r in state.agent_results.values() if not r.success]
102
 
103
+ # If we have some results but they're mostly failures, try to extract useful info
104
+ if len(successful_results) == 0 and len(failed_results) > 0:
105
+ return "failure_analysis"
106
  elif len(successful_results) == 1:
107
  return "single_agent"
108
  elif len(successful_results) == 2:
 
249
  return self._synthesize_confidence_weighted(state)
250
 
251
  def _synthesize_fallback(self, state: GAIAAgentState) -> Dict[str, Any]:
252
+ """Enhanced fallback synthesis when other strategies fail"""
253
 
254
  # Try to get any result, even if not successful
255
  all_results = list(state.agent_results.values())
256
 
257
  if all_results:
258
+ # First try successful results
259
+ successful_results = [r for r in all_results if r.success]
260
+ if successful_results:
261
+ best_attempt = max(successful_results, key=lambda r: r.confidence)
262
  return {
263
  "answer": best_attempt.result,
264
  "confidence": max(0.3, best_attempt.confidence * 0.8), # Reduce confidence for fallback
265
  "reasoning": f"Fallback result from {best_attempt.agent_role.value}: {best_attempt.reasoning}",
266
  "source": f"fallback_{best_attempt.agent_role.value}"
267
  }
268
+
269
+ # If no successful results, try to extract useful info from failures
270
+ return self._synthesize_failure_analysis(state)
 
 
 
 
271
  else:
272
  return self._create_fallback_result("No agent results available")
273
 
274
+ def _synthesize_failure_analysis(self, state: GAIAAgentState) -> Dict[str, Any]:
275
+ """Analyze failed results to provide some useful response"""
276
+
277
+ failed_results = [r for r in state.agent_results.values() if not r.success]
278
+
279
+ if not failed_results:
280
+ return self._create_fallback_result("No results to analyze")
281
+
282
+ # Look for patterns in failures
283
+ error_patterns = []
284
+ attempted_agents = []
285
+
286
+ for result in failed_results:
287
+ attempted_agents.append(result.agent_role.value)
288
+
289
+ # Extract meaningful error information
290
+ result_text = result.result.lower()
291
+ if "research sources failed" in result_text:
292
+ error_patterns.append("external_research_unavailable")
293
+ elif "reasoning failed" in result_text:
294
+ error_patterns.append("complex_reasoning_required")
295
+ elif "conversion" in result_text:
296
+ error_patterns.append("conversion_difficulty")
297
+ elif "mathematical" in result_text:
298
+ error_patterns.append("mathematical_complexity")
299
+
300
+ # Try to provide a helpful response based on the question type and failures
301
+ try:
302
+ analysis_prompt = f"""
303
+ Question: {state.question}
304
+
305
+ Multiple specialized agents attempted to answer this question but encountered difficulties:
306
+ - Agents tried: {', '.join(attempted_agents)}
307
+ - Common issues: {', '.join(set(error_patterns)) if error_patterns else 'processing difficulties'}
308
+
309
+ Based on the question itself, please provide the best answer you can using basic reasoning and knowledge.
310
+ Even if external resources failed, try to answer based on general knowledge.
311
+
312
+ Be honest about limitations but try to be helpful.
313
+ """
314
+
315
+ # Use main model for analysis
316
+ llm_result = self.llm_client.generate(analysis_prompt, tier=ModelTier.MAIN, max_tokens=300)
317
+
318
+ if llm_result.success:
319
+ return {
320
+ "answer": llm_result.response,
321
+ "confidence": 0.25, # Low confidence but still attempting
322
+ "reasoning": f"Generated from failure analysis. Agents tried: {', '.join(attempted_agents)}",
323
+ "source": "failure_analysis"
324
+ }
325
+
326
+ except Exception as analysis_error:
327
+ logger.warning(f"Failure analysis also failed: {analysis_error}")
328
+
329
+ # Final fallback - provide structured error message
330
+ return {
331
+ "answer": f"Processing encountered difficulties: All research sources failed",
332
+ "confidence": 0.1,
333
+ "reasoning": f"Multiple agents failed: {', '.join(attempted_agents)}. {', '.join(set(error_patterns)) if error_patterns else 'Various processing issues encountered'}",
334
+ "source": "structured_failure"
335
+ }
336
+
337
  def _create_fallback_result(self, reason: str) -> Dict[str, Any]:
338
  """Create a fallback result when synthesis is impossible"""
339
  return {
src/agents/web_researcher.py CHANGED
@@ -39,19 +39,35 @@ class WebResearchAgent:
39
  strategy = self._determine_research_strategy(state.question, state.file_name)
40
  state.add_processing_step(f"Web Researcher: Strategy = {strategy}")
41
 
42
- # Execute research based on strategy
43
- if strategy == "wikipedia_direct":
44
- result = self._research_wikipedia_direct(state)
45
- elif strategy == "wikipedia_search":
46
- result = self._research_wikipedia_search(state)
47
- elif strategy == "youtube_analysis":
48
- result = self._research_youtube(state)
49
- elif strategy == "web_search":
50
- result = self._research_web_general(state)
51
- elif strategy == "url_extraction":
52
- result = self._research_url_content(state)
53
- else:
54
- result = self._research_multi_source(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  # Add result to state
57
  state.add_agent_result(result)
@@ -64,13 +80,14 @@ class WebResearchAgent:
64
  state.add_error(error_msg)
65
  logger.error(error_msg)
66
 
67
- # Create failure result
68
  failure_result = AgentResult(
69
  agent_role=AgentRole.WEB_RESEARCHER,
70
  success=False,
71
- result=f"Research failed: {str(e)}",
72
- confidence=0.0,
73
  reasoning=f"Exception during web research: {str(e)}",
 
74
  model_used="error",
75
  processing_time=0.0,
76
  cost_estimate=0.0
@@ -309,6 +326,64 @@ class WebResearchAgent:
309
  else:
310
  return self._create_failure_result(f"Fallback failed: {reason}")
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  def _extract_wikipedia_topic(self, question: str) -> str:
313
  """Extract Wikipedia topic from question"""
314
 
 
39
  strategy = self._determine_research_strategy(state.question, state.file_name)
40
  state.add_processing_step(f"Web Researcher: Strategy = {strategy}")
41
 
42
+ # Execute research with enhanced error handling
43
+ result = None
44
+ try:
45
+ # Execute research based on strategy
46
+ if strategy == "wikipedia_direct":
47
+ result = self._research_wikipedia_direct(state)
48
+ elif strategy == "wikipedia_search":
49
+ result = self._research_wikipedia_search(state)
50
+ elif strategy == "youtube_analysis":
51
+ result = self._research_youtube(state)
52
+ elif strategy == "web_search":
53
+ result = self._research_web_general(state)
54
+ elif strategy == "url_extraction":
55
+ result = self._research_url_content(state)
56
+ else:
57
+ result = self._research_multi_source(state)
58
+
59
+ except Exception as strategy_error:
60
+ logger.warning(f"Strategy {strategy} failed: {strategy_error}, trying fallback")
61
+ # Try fallback strategy
62
+ try:
63
+ result = self._research_fallback_strategy(state, str(strategy_error))
64
+ except Exception as fallback_error:
65
+ logger.error(f"Fallback strategy also failed: {fallback_error}")
66
+ result = self._create_basic_response(state, f"Research failed: {fallback_error}")
67
+
68
+ # Ensure we always have a valid result
69
+ if not result or not isinstance(result, AgentResult):
70
+ result = self._create_basic_response(state, "No research results available")
71
 
72
  # Add result to state
73
  state.add_agent_result(result)
 
80
  state.add_error(error_msg)
81
  logger.error(error_msg)
82
 
83
+ # Create failure result but ensure system continues
84
  failure_result = AgentResult(
85
  agent_role=AgentRole.WEB_RESEARCHER,
86
  success=False,
87
+ result=f"Research encountered difficulties: {str(e)}",
88
+ confidence=0.1, # Very low but not zero to allow synthesis
89
  reasoning=f"Exception during web research: {str(e)}",
90
+ tools_used=[],
91
  model_used="error",
92
  processing_time=0.0,
93
  cost_estimate=0.0
 
326
  else:
327
  return self._create_failure_result(f"Fallback failed: {reason}")
328
 
329
+ def _research_fallback_strategy(self, state: GAIAAgentState, original_error: str) -> AgentResult:
330
+ """Enhanced fallback strategy when primary research fails"""
331
+
332
+ logger.info("Executing fallback research strategy")
333
+
334
+ # Try simple web search as universal fallback
335
+ try:
336
+ search_terms = self._extract_search_terms(state.question)
337
+ web_result = self.web_search_tool.execute(search_terms)
338
+
339
+ if web_result.success and web_result.result.get('found'):
340
+ # Analyze results with basic processing
341
+ search_results = web_result.result.get('results', [])
342
+ if search_results:
343
+ first_result = search_results[0]
344
+ fallback_answer = f"Based on web search: {first_result.get('snippet', 'Limited information available')}"
345
+
346
+ return AgentResult(
347
+ agent_role=AgentRole.WEB_RESEARCHER,
348
+ success=True,
349
+ result=fallback_answer,
350
+ confidence=0.4, # Lower confidence for fallback
351
+ reasoning=f"Fallback web search after: {original_error}",
352
+ tools_used=[ToolResult(
353
+ tool_name="web_search_fallback",
354
+ success=True,
355
+ result={"summary": "Fallback search completed"},
356
+ execution_time=web_result.execution_time
357
+ )],
358
+ model_used="fallback",
359
+ processing_time=web_result.execution_time,
360
+ cost_estimate=0.0
361
+ )
362
+
363
+ except Exception as fallback_error:
364
+ logger.warning(f"Web search fallback failed: {fallback_error}")
365
+
366
+ # If all else fails, try basic text processing
367
+ return self._create_basic_response(state, f"Fallback failed: {original_error}")
368
+
369
+ def _create_basic_response(self, state: GAIAAgentState, error_context: str) -> AgentResult:
370
+ """Create a basic response when all research methods fail"""
371
+
372
+ # Try to extract any useful information from the question itself
373
+ basic_analysis = f"Unable to conduct external research. Question analysis: {state.question[:100]}"
374
+
375
+ return AgentResult(
376
+ agent_role=AgentRole.WEB_RESEARCHER,
377
+ success=False,
378
+ result=f"Processing encountered difficulties: {error_context}",
379
+ confidence=0.1,
380
+ reasoning=f"All research sources failed: {error_context}",
381
+ tools_used=[],
382
+ model_used="none",
383
+ processing_time=0.0,
384
+ cost_estimate=0.0
385
+ )
386
+
387
  def _extract_wikipedia_topic(self, question: str) -> str:
388
  """Extract Wikipedia topic from question"""
389
 
src/app.py CHANGED
@@ -345,26 +345,182 @@ def create_interface():
345
 
346
  # Custom CSS for better styling
347
  css = """
348
- .container {max-width: 1200px; margin: auto; padding: 20px;}
349
- .output-markdown {font-size: 16px; line-height: 1.6; color: #333;}
350
- .details-box {background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0; color: #333;}
351
- .reasoning-box {background-color: #fff; padding: 20px; border: 1px solid #dee2e6; border-radius: 8px; color: #333;}
352
- .unit4-section {background-color: #e3f2fd; padding: 20px; border-radius: 8px; margin: 20px 0; color: #1565c0;}
353
-
354
- /* Fix white text on white background issues */
355
- .gradio-container {color: #333 !important;}
356
- .gradio-container .gr-button-primary {background: #007bff; color: white;}
357
- .gradio-container .gr-button-secondary {background: #6c757d; color: white;}
358
- .gradio-container .gr-textbox textarea {color: #333; background: white;}
359
- .gradio-container .gr-markdown {color: #333;}
360
-
361
- /* Ensure login section has proper contrast */
362
- .oauth-login {background: #f8f9fa; padding: 10px; border-radius: 5px; margin: 10px 0;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
  /* Fix any remaining text contrast issues */
365
- .gradio-container * {color: inherit;}
366
- .gradio-container h1, .gradio-container h2, .gradio-container h3 {color: #333 !important;}
367
- .gradio-container p {color: #555 !important;}
 
 
 
 
 
 
 
 
 
 
368
  """
369
 
370
  with gr.Blocks(css=css, title="GAIA Agent System", theme=gr.themes.Soft()) as interface:
 
345
 
346
  # Custom CSS for better styling
347
  css = """
348
+ /* Base styling for proper contrast */
349
+ .gradio-container {
350
+ color: #333 !important;
351
+ background-color: #ffffff !important;
352
+ }
353
+
354
+ /* Fix all text elements */
355
+ .gradio-container *,
356
+ .gradio-container *::before,
357
+ .gradio-container *::after {
358
+ color: #333 !important;
359
+ }
360
+
361
+ /* Headers */
362
+ .gradio-container h1,
363
+ .gradio-container h2,
364
+ .gradio-container h3,
365
+ .gradio-container h4,
366
+ .gradio-container h5,
367
+ .gradio-container h6 {
368
+ color: #1a1a1a !important;
369
+ font-weight: 600 !important;
370
+ }
371
+
372
+ /* Paragraphs and text content */
373
+ .gradio-container p,
374
+ .gradio-container div,
375
+ .gradio-container span,
376
+ .gradio-container label {
377
+ color: #333 !important;
378
+ }
379
+
380
+ /* Input fields */
381
+ .gradio-container input,
382
+ .gradio-container textarea {
383
+ color: #333 !important;
384
+ background-color: #ffffff !important;
385
+ border: 1px solid #ccc !important;
386
+ }
387
+
388
+ /* Buttons */
389
+ .gradio-container .gr-button-primary {
390
+ background: #007bff !important;
391
+ color: white !important;
392
+ border: none !important;
393
+ }
394
+
395
+ .gradio-container .gr-button-secondary {
396
+ background: #6c757d !important;
397
+ color: white !important;
398
+ border: none !important;
399
+ }
400
+
401
+ .gradio-container button {
402
+ color: white !important;
403
+ }
404
+
405
+ /* Markdown content */
406
+ .gradio-container .gr-markdown,
407
+ .gradio-container .markdown,
408
+ .gradio-container .prose {
409
+ color: #333 !important;
410
+ background-color: transparent !important;
411
+ }
412
+
413
+ /* Special content boxes */
414
+ .container {
415
+ max-width: 1200px;
416
+ margin: auto;
417
+ padding: 20px;
418
+ background-color: #ffffff !important;
419
+ color: #333 !important;
420
+ }
421
+
422
+ .output-markdown {
423
+ font-size: 16px;
424
+ line-height: 1.6;
425
+ color: #333 !important;
426
+ background-color: #ffffff !important;
427
+ }
428
+
429
+ .details-box {
430
+ background-color: #f8f9fa !important;
431
+ padding: 15px;
432
+ border-radius: 8px;
433
+ margin: 10px 0;
434
+ color: #333 !important;
435
+ border: 1px solid #dee2e6 !important;
436
+ }
437
+
438
+ .reasoning-box {
439
+ background-color: #fff !important;
440
+ padding: 20px;
441
+ border: 1px solid #dee2e6 !important;
442
+ border-radius: 8px;
443
+ color: #333 !important;
444
+ }
445
+
446
+ .unit4-section {
447
+ background-color: #e3f2fd !important;
448
+ padding: 20px;
449
+ border-radius: 8px;
450
+ margin: 20px 0;
451
+ color: #1565c0 !important;
452
+ border: 1px solid #90caf9 !important;
453
+ }
454
+
455
+ .unit4-section h1,
456
+ .unit4-section h2,
457
+ .unit4-section h3,
458
+ .unit4-section p,
459
+ .unit4-section div {
460
+ color: #1565c0 !important;
461
+ }
462
+
463
+ /* Login section */
464
+ .oauth-login {
465
+ background: #f8f9fa !important;
466
+ padding: 10px;
467
+ border-radius: 5px;
468
+ margin: 10px 0;
469
+ color: #333 !important;
470
+ border: 1px solid #dee2e6 !important;
471
+ }
472
+
473
+ /* Tables */
474
+ .gradio-container table,
475
+ .gradio-container th,
476
+ .gradio-container td {
477
+ color: #333 !important;
478
+ background-color: #ffffff !important;
479
+ border: 1px solid #dee2e6 !important;
480
+ }
481
+
482
+ .gradio-container th {
483
+ background-color: #f8f9fa !important;
484
+ font-weight: 600 !important;
485
+ }
486
+
487
+ /* Override any white text */
488
+ .gradio-container [style*="color: white"],
489
+ .gradio-container [style*="color: #fff"],
490
+ .gradio-container [style*="color: #ffffff"] {
491
+ color: #333 !important;
492
+ }
493
+
494
+ /* Ensure buttons keep white text */
495
+ .gradio-container button,
496
+ .gradio-container .gr-button-primary,
497
+ .gradio-container .gr-button-secondary {
498
+ color: white !important;
499
+ }
500
+
501
+ /* Examples and other interactive elements */
502
+ .gradio-container .gr-examples,
503
+ .gradio-container .gr-file,
504
+ .gradio-container .gr-textbox,
505
+ .gradio-container .gr-checkbox {
506
+ color: #333 !important;
507
+ background-color: #ffffff !important;
508
+ }
509
 
510
  /* Fix any remaining text contrast issues */
511
+ .gradio-container .gr-form,
512
+ .gradio-container .gr-panel,
513
+ .gradio-container .gr-block {
514
+ color: #333 !important;
515
+ background-color: transparent !important;
516
+ }
517
+
518
+ /* Ensure dark text on light backgrounds for all content */
519
+ .gradio-container .light,
520
+ .gradio-container [data-theme="light"] {
521
+ color: #333 !important;
522
+ background-color: #ffffff !important;
523
+ }
524
  """
525
 
526
  with gr.Blocks(css=css, title="GAIA Agent System", theme=gr.themes.Soft()) as interface:
src/test_production_fixes.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test Production Fixes for GAIA Agent System
4
+ Quick validation that error handling improvements are working
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from typing import List, Dict, Any
10
+
11
+ from models.qwen_client import QwenClient
12
+ from workflow.gaia_workflow import SimpleGAIAWorkflow
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class ProductionFixTester:
19
+ """Test the production fixes for error handling and robustness"""
20
+
21
+ def __init__(self):
22
+ try:
23
+ self.llm_client = QwenClient()
24
+ self.workflow = SimpleGAIAWorkflow(self.llm_client)
25
+ logger.info("✅ Test environment initialized")
26
+ except Exception as e:
27
+ logger.error(f"❌ Failed to initialize test environment: {e}")
28
+ raise
29
+
30
+ def test_error_handling_scenarios(self) -> Dict[str, Any]:
31
+ """Test various error scenarios that were causing production failures"""
32
+
33
+ test_scenarios = [
34
+ {
35
+ "name": "Wikipedia Research Failure Simulation",
36
+ "question": "What is the most obscure fictional character from the imaginary book 'Zzzzz12345NonExistent'?",
37
+ "expected_behavior": "Should fail gracefully and provide fallback response"
38
+ },
39
+ {
40
+ "name": "Mathematical Reasoning with Complex Data",
41
+ "question": "Calculate the square root of negative infinity divided by zero plus the factorial of pi",
42
+ "expected_behavior": "Should handle impossible math gracefully"
43
+ },
44
+ {
45
+ "name": "Conversion with Invalid Units",
46
+ "question": "Convert 50 zorkples to flibbers using the international zorkple standard",
47
+ "expected_behavior": "Should recognize invalid units and respond appropriately"
48
+ },
49
+ {
50
+ "name": "Web Research with Rate Limiting Simulation",
51
+ "question": "What are the current stock prices for all Fortune 500 companies as of this exact moment?",
52
+ "expected_behavior": "Should handle external API limitations gracefully"
53
+ },
54
+ {
55
+ "name": "Complex Multi-Agent Question",
56
+ "question": "Analyze the correlation between quantum entanglement and the price of tea in 17th century Mongolia while also calculating the fibonacci sequence backwards from infinity",
57
+ "expected_behavior": "Should route to multiple agents and synthesize results"
58
+ }
59
+ ]
60
+
61
+ results = {
62
+ "test_summary": {
63
+ "total_tests": len(test_scenarios),
64
+ "passed": 0,
65
+ "failed": 0,
66
+ "errors": []
67
+ },
68
+ "detailed_results": []
69
+ }
70
+
71
+ for i, scenario in enumerate(test_scenarios, 1):
72
+ logger.info(f"\n🧪 Test {i}/{len(test_scenarios)}: {scenario['name']}")
73
+ logger.info(f"Question: {scenario['question']}")
74
+
75
+ start_time = time.time()
76
+
77
+ try:
78
+ # Process the question
79
+ result_state = self.workflow.process_question(
80
+ question=scenario['question'],
81
+ task_id=f"fix_test_{i}"
82
+ )
83
+
84
+ processing_time = time.time() - start_time
85
+
86
+ # Analyze the result
87
+ test_result = self._analyze_test_result(scenario, result_state, processing_time)
88
+ results["detailed_results"].append(test_result)
89
+
90
+ if test_result["passed"]:
91
+ results["test_summary"]["passed"] += 1
92
+ logger.info(f"✅ PASSED: {test_result['reason']}")
93
+ else:
94
+ results["test_summary"]["failed"] += 1
95
+ logger.warning(f"❌ FAILED: {test_result['reason']}")
96
+
97
+ # Log key metrics
98
+ logger.info(f" 📊 Confidence: {result_state.final_confidence:.2f}")
99
+ logger.info(f" ⏱️ Time: {processing_time:.2f}s")
100
+ logger.info(f" 💰 Cost: ${result_state.total_cost:.4f}")
101
+ logger.info(f" 🎯 Answer: {result_state.final_answer[:100]}...")
102
+
103
+ except Exception as e:
104
+ error_msg = f"Exception in test {i}: {str(e)}"
105
+ logger.error(f"❌ ERROR: {error_msg}")
106
+ results["test_summary"]["errors"].append(error_msg)
107
+ results["test_summary"]["failed"] += 1
108
+
109
+ results["detailed_results"].append({
110
+ "test_name": scenario['name'],
111
+ "passed": False,
112
+ "reason": f"Test exception: {str(e)}",
113
+ "processing_time": time.time() - start_time,
114
+ "confidence": 0.0,
115
+ "answer": "Test failed with exception"
116
+ })
117
+
118
+ return results
119
+
120
+ def _analyze_test_result(self, scenario: Dict[str, Any], result_state, processing_time: float) -> Dict[str, Any]:
121
+ """Analyze if a test result meets expectations for error handling"""
122
+
123
+ test_result = {
124
+ "test_name": scenario['name'],
125
+ "passed": False,
126
+ "reason": "",
127
+ "processing_time": processing_time,
128
+ "confidence": result_state.final_confidence,
129
+ "answer": result_state.final_answer,
130
+ "agents_used": [role.value for role in result_state.agent_results.keys()],
131
+ "error_count": len(result_state.error_messages)
132
+ }
133
+
134
+ # Check for catastrophic failures
135
+ if result_state.final_answer is None or result_state.final_answer == "":
136
+ test_result["reason"] = "Critical failure: No answer generated"
137
+ return test_result
138
+
139
+ # Check for system crash indicators
140
+ crash_indicators = [
141
+ "system not initialized",
142
+ "workflow execution failed",
143
+ "unable to process question - no agent results available"
144
+ ]
145
+
146
+ answer_lower = result_state.final_answer.lower()
147
+ if any(indicator in answer_lower for indicator in crash_indicators):
148
+ test_result["reason"] = "System crash detected in response"
149
+ return test_result
150
+
151
+ # Check for graceful error handling
152
+ graceful_indicators = [
153
+ "processing encountered difficulties",
154
+ "research sources failed",
155
+ "reasoning failed",
156
+ "conversion failed",
157
+ "mathematical complexity",
158
+ "limited information available"
159
+ ]
160
+
161
+ has_graceful_handling = any(indicator in answer_lower for indicator in graceful_indicators)
162
+
163
+ # Evaluate based on scenario expectations
164
+ if has_graceful_handling and result_state.final_confidence >= 0.1:
165
+ test_result["passed"] = True
166
+ test_result["reason"] = "Graceful error handling with reasonable confidence"
167
+ elif not has_graceful_handling and result_state.final_confidence >= 0.3:
168
+ test_result["passed"] = True
169
+ test_result["reason"] = "Provided meaningful answer with acceptable confidence"
170
+ elif result_state.final_confidence > 0.0 and len(result_state.agent_results) > 0:
171
+ test_result["passed"] = True
172
+ test_result["reason"] = "System remained stable and attempted processing"
173
+ else:
174
+ test_result["reason"] = f"Insufficient error handling or system instability (confidence: {result_state.final_confidence:.2f})"
175
+
176
+ return test_result
177
+
178
+ def run_comprehensive_test(self) -> None:
179
+ """Run comprehensive test and report results"""
180
+
181
+ logger.info("🚀 Starting Production Fix Validation Tests")
182
+ logger.info("=" * 60)
183
+
184
+ start_time = time.time()
185
+
186
+ try:
187
+ results = self.test_error_handling_scenarios()
188
+ total_time = time.time() - start_time
189
+
190
+ # Print summary
191
+ summary = results["test_summary"]
192
+ logger.info("\n" + "=" * 60)
193
+ logger.info("📋 TEST SUMMARY")
194
+ logger.info("=" * 60)
195
+ logger.info(f"Total Tests: {summary['total_tests']}")
196
+ logger.info(f"✅ Passed: {summary['passed']}")
197
+ logger.info(f"❌ Failed: {summary['failed']}")
198
+ logger.info(f"⚠️ Errors: {len(summary['errors'])}")
199
+ logger.info(f"📊 Success Rate: {summary['passed']/summary['total_tests']*100:.1f}%")
200
+ logger.info(f"⏱️ Total Time: {total_time:.2f}s")
201
+
202
+ # Success threshold
203
+ success_rate = summary['passed'] / summary['total_tests']
204
+ if success_rate >= 0.8: # 80% success rate for error handling
205
+ logger.info("🎉 PRODUCTION FIXES VALIDATION: PASSED")
206
+ logger.info("System demonstrates robust error handling and graceful degradation")
207
+ else:
208
+ logger.warning("⚠️ PRODUCTION FIXES VALIDATION: NEEDS IMPROVEMENT")
209
+ logger.warning(f"Success rate {success_rate*100:.1f}% below 80% threshold")
210
+
211
+ # Print any errors
212
+ if summary['errors']:
213
+ logger.error("\n🔥 ERRORS ENCOUNTERED:")
214
+ for error in summary['errors']:
215
+ logger.error(f" - {error}")
216
+
217
+ except Exception as e:
218
+ logger.error(f"❌ Comprehensive test failed: {str(e)}")
219
+ raise
220
+
221
+ def main():
222
+ """Main test execution"""
223
+ try:
224
+ tester = ProductionFixTester()
225
+ tester.run_comprehensive_test()
226
+ except Exception as e:
227
+ logger.error(f"Test execution failed: {e}")
228
+ exit(1)
229
+
230
+ if __name__ == "__main__":
231
+ main()
src/tools/__pycache__/web_search_tool.cpython-310.pyc CHANGED
Binary files a/src/tools/__pycache__/web_search_tool.cpython-310.pyc and b/src/tools/__pycache__/web_search_tool.cpython-310.pyc differ
 
src/tools/web_search_tool.py CHANGED
@@ -88,57 +88,97 @@ class WebSearchTool(BaseTool):
88
 
89
  def _search_web(self, query: str, limit: int = 5, extract_content: bool = False) -> Dict[str, Any]:
90
  """
91
- Search the web using DuckDuckGo
92
  """
93
- try:
94
- logger.info(f"Searching web for: {query}")
95
-
96
- # Perform DuckDuckGo search
97
- with DDGS() as ddgs:
98
- search_results = list(ddgs.text(
99
- keywords=query,
100
- max_results=limit,
101
- region='us-en',
102
- safesearch='moderate'
103
- ))
104
-
105
- if not search_results:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  return {
107
  "query": query,
108
- "found": False,
109
- "message": "No web search results found",
110
- "results": []
 
111
  }
112
-
113
- results = []
114
- for result in search_results:
115
- web_result = WebSearchResult(
116
- title=result.get('title', 'No title'),
117
- url=result.get('href', ''),
118
- snippet=result.get('body', 'No description')
119
- )
120
-
121
- # Optionally extract full content from each URL
122
- if extract_content and web_result.url:
123
- try:
124
- content_result = self._extract_content_from_url(web_result.url)
125
- if content_result.get('found'):
126
- web_result.content = content_result['content'][:1000] # Limit content size
127
- except Exception as e:
128
- logger.warning(f"Failed to extract content from {web_result.url}: {e}")
129
 
130
- results.append(web_result.to_dict())
131
-
132
- return {
133
- "query": query,
134
- "found": True,
135
- "results": results,
136
- "total_results": len(results),
137
- "message": f"Found {len(results)} web search results"
138
- }
139
-
140
- except Exception as e:
141
- raise Exception(f"Web search failed: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  def _extract_content_from_url(self, url: str) -> Dict[str, Any]:
144
  """
 
88
 
89
  def _search_web(self, query: str, limit: int = 5, extract_content: bool = False) -> Dict[str, Any]:
90
  """
91
+ Search the web using DuckDuckGo with retry mechanisms
92
  """
93
+ max_retries = 3
94
+ retry_delay = 1.0
95
+
96
+ for attempt in range(max_retries):
97
+ try:
98
+ logger.info(f"Searching web for: {query} (attempt {attempt + 1}/{max_retries})")
99
+
100
+ # Perform DuckDuckGo search with timeout
101
+ with DDGS() as ddgs:
102
+ search_results = list(ddgs.text(
103
+ keywords=query,
104
+ max_results=limit,
105
+ region='us-en',
106
+ safesearch='moderate'
107
+ ))
108
+
109
+ if not search_results:
110
+ if attempt < max_retries - 1:
111
+ logger.warning(f"No results on attempt {attempt + 1}, retrying...")
112
+ time.sleep(retry_delay)
113
+ retry_delay *= 2 # Exponential backoff
114
+ continue
115
+ else:
116
+ return {
117
+ "query": query,
118
+ "found": False,
119
+ "message": "No web search results found after retries",
120
+ "results": []
121
+ }
122
+
123
+ results = []
124
+ for result in search_results:
125
+ try:
126
+ web_result = WebSearchResult(
127
+ title=result.get('title', 'No title'),
128
+ url=result.get('href', ''),
129
+ snippet=result.get('body', 'No description')
130
+ )
131
+
132
+ # Optionally extract full content from each URL
133
+ if extract_content and web_result.url:
134
+ try:
135
+ content_result = self._extract_content_from_url(web_result.url)
136
+ if content_result.get('found'):
137
+ web_result.content = content_result['content'][:1000] # Limit content size
138
+ except Exception as e:
139
+ logger.warning(f"Failed to extract content from {web_result.url}: {e}")
140
+ # Continue without content extraction rather than failing
141
+
142
+ results.append(web_result.to_dict())
143
+
144
+ except Exception as result_error:
145
+ logger.warning(f"Error processing search result: {result_error}")
146
+ # Continue with other results rather than failing entire search
147
+ continue
148
+
149
+ # Return successful results even if some individual results failed
150
  return {
151
  "query": query,
152
+ "found": len(results) > 0,
153
+ "results": results,
154
+ "total_results": len(results),
155
+ "message": f"Found {len(results)} web search results"
156
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ except Exception as e:
159
+ logger.warning(f"Web search attempt {attempt + 1} failed: {str(e)}")
160
+ if attempt < max_retries - 1:
161
+ time.sleep(retry_delay)
162
+ retry_delay *= 2 # Exponential backoff
163
+ continue
164
+ else:
165
+ # Final attempt failed, but don't raise exception
166
+ logger.error(f"Web search failed after {max_retries} attempts: {str(e)}")
167
+ return {
168
+ "query": query,
169
+ "found": False,
170
+ "message": f"Web search failed after retries: {str(e)}",
171
+ "results": [],
172
+ "error_type": "search_failure"
173
+ }
174
+
175
+ # Should not reach here, but just in case
176
+ return {
177
+ "query": query,
178
+ "found": False,
179
+ "message": "Unexpected search failure",
180
+ "results": []
181
+ }
182
 
183
  def _extract_content_from_url(self, url: str) -> Dict[str, Any]:
184
  """