Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 81 additions & 24 deletions backend/app/agents/devrel/tools/search_tool/ddg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,94 @@
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:
return ddg.text(query, max_results=max_results)

@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 []
24 changes: 19 additions & 5 deletions backend/app/core/events/event_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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}")
30 changes: 22 additions & 8 deletions backend/integrations/discord/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import discord

from app.agents.devrel.onboarding.messages import (
Expand All @@ -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."""
Expand All @@ -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)}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except discord.Forbidden:
    logger.debug(f"Cannot send DM to user {user.id}")
except Exception:
    logger.exception("Failed to send handoff DM")


class OAuthView(discord.ui.View):
"""View with OAuth button."""
Expand Down Expand Up @@ -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)}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except discord.NotFound:
    logger.debug("Interaction message no longer exists")
except Exception:
    logger.exception("Failed to update interaction view")

else:
await interaction.followup.send(
"I still don't see a linked GitHub account. Run `/verify_github` and try again in a moment.",
Expand All @@ -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)}")