From 332fd0e89bf8ece9076d3038a3edfc3ebdfd435a Mon Sep 17 00:00:00 2001 From: Sainava Date: Sun, 4 Jan 2026 23:13:44 +0530 Subject: [PATCH 1/4] feat: replace empty pass statements with error handling and logging --- .../agents/devrel/tools/search_tool/ddg.py | 105 ++++++++++++++---- backend/app/core/events/event_bus.py | 24 +++- backend/integrations/discord/views.py | 30 +++-- 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/backend/app/agents/devrel/tools/search_tool/ddg.py b/backend/app/agents/devrel/tools/search_tool/ddg.py index 8790f4cf..059278c5 100644 --- a/backend/app/agents/devrel/tools/search_tool/ddg.py +++ b/backend/app/agents/devrel/tools/search_tool/ddg.py @@ -7,10 +7,20 @@ logger = logging.getLogger(__name__) class DuckDuckGoSearchTool: - """DDGS-based DuckDuckGo search integration""" + """DDGS-based DuckDuckGo search integration with configurable options. + + Args: + timeout: Timeout for search requests in seconds (default: 10) + max_retries: Maximum number of retry attempts on failure (default: 2) + cache_enabled: Enable caching of search results (default: False) + """ - def __init__(self): - pass + def __init__(self, timeout: int = 10, max_retries: int = 2, cache_enabled: bool = False): + self.timeout = timeout + self.max_retries = max_retries + self.cache_enabled = cache_enabled + self._cache: dict = {} if cache_enabled else None + logger.info(f"Initialized DuckDuckGoSearchTool (timeout={timeout}s, retries={max_retries}, cache={cache_enabled})") def _perform_search(self, query: str, max_results: int): with DDGS() as ddg: @@ -18,26 +28,73 @@ def _perform_search(self, query: str, max_results: int): @traceable(name="duckduckgo_search_tool", run_type="tool") async def search(self, query: str, max_results: int = 5) -> List[Dict[str, Any]]: - try: - response = await asyncio.to_thread( - self._perform_search, - query=query, - max_results=max_results - ) + """Perform a DuckDuckGo search with caching and retry logic. + + Args: + query: The search query string + max_results: Maximum number of results to return (default: 5) + + Returns: + List of search results with title, content, URL, and score + """ + # Check cache if enabled + cache_key = f"{query}:{max_results}" + if self.cache_enabled and cache_key in self._cache: + logger.debug(f"Returning cached results for query: {query}") + return self._cache[cache_key] + + # Retry logic + last_exception = None + for attempt in range(self.max_retries + 1): + try: + logger.debug(f"Search attempt {attempt + 1}/{self.max_retries + 1} for query: {query}") + response = await asyncio.wait_for( + asyncio.to_thread( + self._perform_search, + query=query, + max_results=max_results + ), + timeout=self.timeout + ) + + results = [] + for result in response or []: + results.append({ + "title": result.get("title", ""), + "content": result.get("body", ""), + "url": result.get("href", ""), + "score": 0 + }) + + # Cache results if enabled + if self.cache_enabled: + self._cache[cache_key] = results + logger.debug(f"Cached {len(results)} results for query: {query}") + + logger.info(f"Successfully retrieved {len(results)} results for query: {query}") + return results + + except asyncio.TimeoutError: + last_exception = TimeoutError(f"Search timed out after {self.timeout}s") + logger.warning(f"Search timeout (attempt {attempt + 1}/{self.max_retries + 1}): {query}") + if attempt < self.max_retries: + await asyncio.sleep(1) # Brief delay before retry + continue + + except (ConnectionError, TimeoutError) as e: + last_exception = e + logger.warning(f"Network issue (attempt {attempt + 1}/{self.max_retries + 1}): {e}") + if attempt < self.max_retries: + await asyncio.sleep(1) + continue - results = [] - for result in response or []: - results.append({ - "title": result.get("title", ""), - "content": result.get("body", ""), - "url": result.get("href", ""), - "score": 0 - }) - return results + except Exception as e: + last_exception = e + logger.error(f"Search error (attempt {attempt + 1}/{self.max_retries + 1}): {str(e)}") + if attempt < self.max_retries: + await asyncio.sleep(1) + continue - except (ConnectionError, TimeoutError) as e: - logger.warning("Network issue during DDG search: %s", e) - return [] - except Exception as e: - logger.error("DuckDuckGo search failed: %s", str(e)) - return [] + # All retries failed + logger.error(f"All {self.max_retries + 1} search attempts failed for query: {query}. Last error: {last_exception}") + return [] diff --git a/backend/app/core/events/event_bus.py b/backend/app/core/events/event_bus.py index ead6f297..6cba4446 100644 --- a/backend/app/core/events/event_bus.py +++ b/backend/app/core/events/event_bus.py @@ -20,21 +20,36 @@ def register_handler(self, event_type: Union[EventType, List[EventType]], handle if isinstance(event_type, list): for et in event_type: self._add_handler(et, handler_func) + logger.info(f"Registered handler '{handler_func.__name__}' for event types: {[et.value for et in event_type]}") else: self._add_handler(event_type, handler_func) - pass + logger.info(f"Registered handler '{handler_func.__name__}' for event type: {event_type.value}") + + if platform: + logger.debug(f"Handler registered with platform filter: {platform.value}") def _add_handler(self, event_type: EventType, handler_func: callable): if event_type not in self.handlers: self.handlers[event_type] = [] + logger.debug(f"Created new handler list for event type: {event_type.value}") + # Check for duplicate handler registration + if handler_func in self.handlers[event_type]: + logger.warning(f"Handler '{handler_func.__name__}' already registered for {event_type.value}. Skipping duplicate.") + return + self.handlers[event_type].append(handler_func) - pass + logger.debug(f"Added handler '{handler_func.__name__}' to {event_type.value} (total: {len(self.handlers[event_type])})") def register_global_handler(self, handler_func): """Register a handler that will receive all events""" + # Check for duplicate global handler registration + if handler_func in self.global_handlers: + logger.warning(f"Global handler '{handler_func.__name__}' already registered. Skipping duplicate.") + return + self.global_handlers.append(handler_func) - pass + logger.info(f"Registered global handler: {handler_func.__name__} (total global handlers: {len(self.global_handlers)})") async def dispatch(self, event: BaseEvent): """Dispatch an event to all registered handlers""" @@ -50,5 +65,4 @@ async def dispatch(self, event: BaseEvent): logger.info(f"Calling handler: {handler.__name__} for event type: {event.event_type}") asyncio.create_task(handler(event)) else: - logger.info(f"No handlers registered for event type {event.event_type}") - pass + logger.debug(f"No handlers registered for event type {event.event_type}") diff --git a/backend/integrations/discord/views.py b/backend/integrations/discord/views.py index 807d6dd7..d6df0276 100644 --- a/backend/integrations/discord/views.py +++ b/backend/integrations/discord/views.py @@ -1,3 +1,4 @@ +import logging import discord from app.agents.devrel.onboarding.messages import ( @@ -7,6 +8,8 @@ ) from app.services.auth.management import get_or_create_user_by_discord +logger = logging.getLogger(__name__) + def build_final_handoff_embed() -> discord.Embed: """Create the final hand-off embed describing capabilities.""" @@ -29,9 +32,13 @@ async def send_final_handoff_dm(user: discord.abc.User): try: embed = build_final_handoff_embed() await user.send(embed=embed) - except Exception: - # Fail silently to avoid crashing flows if DMs are closed or similar - pass + logger.info(f"Successfully sent handoff DM to user {user.id}") + except discord.Forbidden: + logger.warning(f"Cannot send DM to user {user.id} - DMs are disabled or bot is blocked") + except discord.HTTPException as e: + logger.error(f"Discord API error sending DM to user {user.id}: {e.status} - {e.text}") + except Exception as e: + logger.error(f"Unexpected error sending DM to user {user.id}: {type(e).__name__} - {str(e)}") class OAuthView(discord.ui.View): """View with OAuth button.""" @@ -99,8 +106,12 @@ async def check_verified( # type: ignore[override] await send_final_handoff_dm(interaction.user) try: await interaction.message.edit(view=self) - except Exception: - pass + except discord.NotFound: + logger.warning(f"Message not found when editing onboarding view for user {interaction.user.id}") + except discord.HTTPException as e: + logger.error(f"Failed to edit onboarding view: {e.status} - {e.text}") + except Exception as e: + logger.error(f"Unexpected error editing onboarding view: {type(e).__name__} - {str(e)}") else: await interaction.followup.send( "I still don't see a linked GitHub account. Run `/verify_github` and try again in a moment.", @@ -115,6 +126,9 @@ async def skip(self, interaction: discord.Interaction, button: discord.ui.Button item.disabled = True try: await interaction.response.edit_message(view=self) - except Exception: - # If edit fails (e.g., message deleted), ignore - pass + except discord.NotFound: + logger.warning(f"Message not found when editing skip view for user {interaction.user.id}") + except discord.HTTPException as e: + logger.error(f"Failed to edit skip view: {e.status} - {e.text}") + except Exception as e: + logger.error(f"Unexpected error editing skip view: {type(e).__name__} - {str(e)}") From 3b64e1606829d15c9576d7418cff92f3a8db9f00 Mon Sep 17 00:00:00 2001 From: Sainava Date: Sun, 4 Jan 2026 23:28:37 +0530 Subject: [PATCH 2/4] refactor: add exponential backoff with jitter to search retries --- .../agents/devrel/tools/search_tool/ddg.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/app/agents/devrel/tools/search_tool/ddg.py b/backend/app/agents/devrel/tools/search_tool/ddg.py index 059278c5..c7cb47ad 100644 --- a/backend/app/agents/devrel/tools/search_tool/ddg.py +++ b/backend/app/agents/devrel/tools/search_tool/ddg.py @@ -1,5 +1,6 @@ import asyncio import logging +import random from typing import List, Dict, Any from ddgs import DDGS from langsmith import traceable @@ -13,12 +14,17 @@ class DuckDuckGoSearchTool: timeout: Timeout for search requests in seconds (default: 10) max_retries: Maximum number of retry attempts on failure (default: 2) cache_enabled: Enable caching of search results (default: False) + base_delay: Base delay for exponential backoff in seconds (default: 1) + max_delay: Maximum delay between retries in seconds (default: 10) """ - def __init__(self, timeout: int = 10, max_retries: int = 2, cache_enabled: bool = False): + def __init__(self, timeout: int = 10, max_retries: int = 2, cache_enabled: bool = False, + base_delay: float = 1.0, max_delay: float = 10.0): self.timeout = timeout self.max_retries = max_retries self.cache_enabled = cache_enabled + self.base_delay = base_delay + self.max_delay = max_delay self._cache: dict = {} if cache_enabled else None logger.info(f"Initialized DuckDuckGoSearchTool (timeout={timeout}s, retries={max_retries}, cache={cache_enabled})") @@ -74,25 +80,28 @@ async def search(self, query: str, max_results: int = 5) -> List[Dict[str, Any]] logger.info(f"Successfully retrieved {len(results)} results for query: {query}") return results - except asyncio.TimeoutError: + except (asyncio.TimeoutError, TimeoutError): last_exception = TimeoutError(f"Search timed out after {self.timeout}s") logger.warning(f"Search timeout (attempt {attempt + 1}/{self.max_retries + 1}): {query}") if attempt < self.max_retries: - await asyncio.sleep(1) # Brief delay before retry + delay = min(self.base_delay * (2 ** attempt) + random.uniform(-0.1, 0.1), self.max_delay) + await asyncio.sleep(delay) continue - except (ConnectionError, TimeoutError) as e: + except ConnectionError as e: last_exception = e logger.warning(f"Network issue (attempt {attempt + 1}/{self.max_retries + 1}): {e}") if attempt < self.max_retries: - await asyncio.sleep(1) + delay = min(self.base_delay * (2 ** attempt) + random.uniform(-0.1, 0.1), self.max_delay) + await asyncio.sleep(delay) continue except Exception as e: last_exception = e logger.error(f"Search error (attempt {attempt + 1}/{self.max_retries + 1}): {str(e)}") if attempt < self.max_retries: - await asyncio.sleep(1) + delay = min(self.base_delay * (2 ** attempt) + random.uniform(-0.1, 0.1), self.max_delay) + await asyncio.sleep(delay) continue # All retries failed From b2042292b091050bf56e0f94d7d4d9794dbe5fa4 Mon Sep 17 00:00:00 2001 From: Sainava Date: Wed, 7 Jan 2026 22:56:39 +0530 Subject: [PATCH 3/4] refactor: simplify Discord exception handling pattern --- backend/integrations/discord/views.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/backend/integrations/discord/views.py b/backend/integrations/discord/views.py index d6df0276..056b0d15 100644 --- a/backend/integrations/discord/views.py +++ b/backend/integrations/discord/views.py @@ -34,11 +34,9 @@ async def send_final_handoff_dm(user: discord.abc.User): await user.send(embed=embed) logger.info(f"Successfully sent handoff DM to user {user.id}") except discord.Forbidden: - logger.warning(f"Cannot send DM to user {user.id} - DMs are disabled or bot is blocked") - except discord.HTTPException as e: - logger.error(f"Discord API error sending DM to user {user.id}: {e.status} - {e.text}") - except Exception as e: - logger.error(f"Unexpected error sending DM to user {user.id}: {type(e).__name__} - {str(e)}") + logger.debug(f"Cannot send DM to user {user.id} - DMs are disabled or bot is blocked") + except Exception: + logger.exception(f"Unexpected error sending DM to user {user.id}") class OAuthView(discord.ui.View): """View with OAuth button.""" @@ -107,11 +105,9 @@ async def check_verified( # type: ignore[override] try: await interaction.message.edit(view=self) except discord.NotFound: - logger.warning(f"Message not found when editing onboarding view for user {interaction.user.id}") - except discord.HTTPException as e: - logger.error(f"Failed to edit onboarding view: {e.status} - {e.text}") - except Exception as e: - logger.error(f"Unexpected error editing onboarding view: {type(e).__name__} - {str(e)}") + logger.debug(f"Message not found when editing onboarding view for user {interaction.user.id}") + except Exception: + logger.exception(f"Unexpected error editing onboarding view") else: await interaction.followup.send( "I still don't see a linked GitHub account. Run `/verify_github` and try again in a moment.", @@ -127,8 +123,6 @@ async def skip(self, interaction: discord.Interaction, button: discord.ui.Button try: await interaction.response.edit_message(view=self) except discord.NotFound: - logger.warning(f"Message not found when editing skip view for user {interaction.user.id}") - except discord.HTTPException as e: - logger.error(f"Failed to edit skip view: {e.status} - {e.text}") - except Exception as e: - logger.error(f"Unexpected error editing skip view: {type(e).__name__} - {str(e)}") + logger.debug(f"Message not found when editing skip view for user {interaction.user.id}") + except Exception: + logger.exception(f"Unexpected error editing skip view") From 4d99091d4cb1766783655bfe869b28ee54509231 Mon Sep 17 00:00:00 2001 From: Sainava Date: Wed, 7 Jan 2026 23:05:15 +0530 Subject: [PATCH 4/4] fix: remove f-string prefix without placeholders --- backend/integrations/discord/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/integrations/discord/views.py b/backend/integrations/discord/views.py index 056b0d15..b9da36b3 100644 --- a/backend/integrations/discord/views.py +++ b/backend/integrations/discord/views.py @@ -107,7 +107,7 @@ async def check_verified( # type: ignore[override] except discord.NotFound: logger.debug(f"Message not found when editing onboarding view for user {interaction.user.id}") except Exception: - logger.exception(f"Unexpected error editing onboarding view") + logger.exception("Unexpected error editing onboarding view") else: await interaction.followup.send( "I still don't see a linked GitHub account. Run `/verify_github` and try again in a moment.", @@ -125,4 +125,4 @@ async def skip(self, interaction: discord.Interaction, button: discord.ui.Button except discord.NotFound: logger.debug(f"Message not found when editing skip view for user {interaction.user.id}") except Exception: - logger.exception(f"Unexpected error editing skip view") + logger.exception("Unexpected error editing skip view")