Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
114 changes: 90 additions & 24 deletions backend/app/agents/devrel/tools/search_tool/ddg.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,109 @@
import asyncio
import logging
import random
from typing import List, Dict, Any
from ddgs import DDGS
from langsmith import traceable

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:
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, 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 []
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}")
18 changes: 13 additions & 5 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,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."""
Expand Down Expand Up @@ -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.",
Expand All @@ -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")