diff --git a/backend/app/agents/devrel/tools/search_tool/ddg.py b/backend/app/agents/devrel/tools/search_tool/ddg.py index 8790f4cf..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 @@ -7,10 +8,25 @@ 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) + base_delay: Base delay for exponential backoff in seconds (default: 1) + max_delay: Maximum delay between retries in seconds (default: 10) + """ - def __init__(self): - pass + 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})") def _perform_search(self, query: str, max_results: int): with DDGS() as ddg: @@ -18,26 +34,76 @@ 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, 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: + delay = min(self.base_delay * (2 ** attempt) + random.uniform(-0.1, 0.1), self.max_delay) + await asyncio.sleep(delay) + continue + + 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: + delay = min(self.base_delay * (2 ** attempt) + random.uniform(-0.1, 0.1), self.max_delay) + await asyncio.sleep(delay) + 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: + 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: - 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..b9da36b3 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,11 @@ async def send_final_handoff_dm(user: discord.abc.User): try: embed = build_final_handoff_embed() await user.send(embed=embed) + logger.info(f"Successfully sent handoff DM to user {user.id}") + except discord.Forbidden: + logger.debug(f"Cannot send DM to user {user.id} - DMs are disabled or bot is blocked") except Exception: - # Fail silently to avoid crashing flows if DMs are closed or similar - pass + logger.exception(f"Unexpected error sending DM to user {user.id}") class OAuthView(discord.ui.View): """View with OAuth button.""" @@ -99,8 +104,10 @@ async def check_verified( # type: ignore[override] await send_final_handoff_dm(interaction.user) try: await interaction.message.edit(view=self) + except discord.NotFound: + logger.debug(f"Message not found when editing onboarding view for user {interaction.user.id}") except Exception: - pass + 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.", @@ -115,6 +122,7 @@ async def skip(self, interaction: discord.Interaction, button: discord.ui.Button item.disabled = True try: await interaction.response.edit_message(view=self) + except discord.NotFound: + logger.debug(f"Message not found when editing skip view for user {interaction.user.id}") except Exception: - # If edit fails (e.g., message deleted), ignore - pass + logger.exception("Unexpected error editing skip view")