From 97784a4f7a66f1559980241ae2ebc35fd8a2553a Mon Sep 17 00:00:00 2001 From: Prithvijit Bose Date: Mon, 4 Aug 2025 13:10:49 +0530 Subject: [PATCH 1/2] feat: Implement Dynamic FAQ Handler with Web Search for Organizational Queries --- backend/app/agents/devrel/agent.py | 3 +- .../app/agents/devrel/nodes/handlers/faq.py | 141 ++++++++- .../devrel/nodes/handlers/web_search.py | 4 +- .../agents/devrel/nodes/react_supervisor.py | 294 ++++++++++++++---- backend/app/agents/devrel/tool_wrappers.py | 6 +- backend/app/core/handler/faq_handler.py | 2 +- backend/integrations/discord/bot.py | 12 +- backend/integrations/discord/cogs.py | 74 +++-- backend/main.py | 17 +- poetry.lock | 109 +++++-- pyproject.toml | 3 +- 11 files changed, 528 insertions(+), 137 deletions(-) diff --git a/backend/app/agents/devrel/agent.py b/backend/app/agents/devrel/agent.py index 4dd2af0f..2b356b2b 100644 --- a/backend/app/agents/devrel/agent.py +++ b/backend/app/agents/devrel/agent.py @@ -43,7 +43,8 @@ def _build_graph(self): # Phase 2: ReAct Supervisor - Decide what to do next workflow.add_node("react_supervisor", partial(react_supervisor_node, llm=self.llm)) workflow.add_node("web_search_tool", partial(web_search_tool_node, search_tool=self.search_tool, llm=self.llm)) - workflow.add_node("faq_handler_tool", partial(faq_handler_tool_node, faq_tool=self.faq_tool)) + workflow.add_node("faq_handler_tool", partial( + faq_handler_tool_node, search_tool=self.search_tool, llm=self.llm)) workflow.add_node("onboarding_tool", onboarding_tool_node) workflow.add_node("github_toolkit_tool", partial(github_toolkit_tool_node, github_toolkit=self.github_toolkit)) diff --git a/backend/app/agents/devrel/nodes/handlers/faq.py b/backend/app/agents/devrel/nodes/handlers/faq.py index 8855c323..995c10d6 100644 --- a/backend/app/agents/devrel/nodes/handlers/faq.py +++ b/backend/app/agents/devrel/nodes/handlers/faq.py @@ -1,11 +1,13 @@ import logging +from typing import List, Dict from app.agents.state import AgentState +from langchain_core.messages import HumanMessage logger = logging.getLogger(__name__) -async def handle_faq_node(state: AgentState, faq_tool) -> dict: - """Handle FAQ requests""" - logger.info(f"Handling FAQ for session {state.session_id}") +async def handle_faq_node(state: AgentState, search_tool, llm) -> dict: + """Handle FAQ requests dynamically using web search and AI synthesis""" + logger.info(f"Handling dynamic FAQ for session {state.session_id}") latest_message = "" if state.messages: @@ -13,14 +15,141 @@ async def handle_faq_node(state: AgentState, faq_tool) -> dict: elif state.context.get("original_message"): latest_message = state.context["original_message"] - # faq_tool will be passed from the agent, similar to llm for classify_intent - faq_response = await faq_tool.get_response(latest_message) + # Dynamic FAQ processing (replaces static faq_tool.get_response) + faq_response = await _dynamic_faq_process(latest_message, search_tool, llm, org_name="Devr.AI") return { "task_result": { "type": "faq", "response": faq_response, - "source": "faq_database" + "source": "dynamic_web_search" # Updated source }, "current_task": "faq_handled" } + +async def _dynamic_faq_process(message: str, search_tool, llm, org_name: str = "Devr.AI") -> str: + """ + Dynamic FAQ handler that implements the 5-step process: + 1. Intent Detection & Query Refinement + 2. Web Search (DuckDuckGo) + 3. AI-Powered Synthesis + 4. Generate Final Response + 5. Format with Sources + """ + + try: + # Step 1: Intent Detection & Query Refinement + logger.info(f"Step 1: Refining FAQ query for org '{org_name}'") + refined_query = await _refine_faq_query(message, llm, org_name) + + # Step 2: Dynamic Web Search + logger.info(f"Step 2: Searching for: {refined_query}") + search_results = await search_tool.search(refined_query) + + if not search_results: + return _generate_fallback_response(message, org_name) + + # Step 3 & 4: AI-Powered Synthesis & Response Generation + logger.info("Step 3-4: Synthesizing search results into FAQ response") + synthesized_response = await _synthesize_faq_response( + message, search_results, llm, org_name + ) + + # Step 5: Format Final Response with Sources + logger.info("Step 5: Formatting final response with sources") + final_response = _format_faq_response(synthesized_response, search_results) + + return final_response + + except Exception as e: + logger.error(f"Error in dynamic FAQ process: {e}") + return _generate_fallback_response(message, org_name) + +async def _refine_faq_query(message: str, llm, org_name: str) -> str: + """Step 1: Refine user query for organization-specific FAQ search""" + + refinement_prompt = f""" +You are helping someone find information about {org_name}. +Transform their question into an effective search query that will find official information about the organization. + +User Question: "{message}" + +Create a search query that focuses on: +- Official {org_name} information +- The organization's website, blog, or documentation +- Adding terms like "about", "mission", "projects" if relevant + +Return only the refined search query, nothing else. + +Examples: +- "What does this org do?" → "{org_name} about mission what we do" +- "How do you work?" → "{org_name} how it works process methodology" +- "What projects do you have?" → "{org_name} projects portfolio what we build" +""" + + response = await llm.ainvoke([HumanMessage(content=refinement_prompt)]) + refined_query = response.content.strip() + logger.info(f"Refined query: {refined_query}") + return refined_query + +async def _synthesize_faq_response(message: str, search_results: List[Dict], llm, org_name: str) -> str: + """Step 3-4: Use LLM to synthesize search results into a comprehensive FAQ answer""" + + # Prepare search results context + results_context = "" + for i, result in enumerate(search_results[:5]): # Top 5 results + title = result.get('title', 'N/A') + content = result.get('content', 'N/A') + url = result.get('url', 'N/A') + results_context += f"\nResult {i+1}:\nTitle: {title}\nContent: {content}\nURL: {url}\n" + + synthesis_prompt = f""" +You are an AI assistant representing {org_name}. A user asked: "{message}" + +Based on the following search results from official sources, provide a comprehensive, helpful answer about {org_name}. + +Search Results: +{results_context} + +Instructions: +1. Answer the user's question directly and conversationally +2. Focus on the most relevant and recent information +3. Be informative but concise (2-3 paragraphs max) +4. If the search results don't fully answer the question, acknowledge what you found +5. Sound helpful and knowledgeable about {org_name} +6. Don't mention "search results" in your response - speak as if you know about the organization + +Your response: +""" + + response = await llm.ainvoke([HumanMessage(content=synthesis_prompt)]) + synthesized_answer = response.content.strip() + logger.info(f"Synthesized FAQ response: {synthesized_answer[:100]}...") + return synthesized_answer + +def _format_faq_response(synthesized_answer: str, search_results: List[Dict]) -> str: + """Step 5: Format the final response with sources""" + + # Start with the synthesized answer + formatted_response = synthesized_answer + + # Add sources section + if search_results: + formatted_response += "\n\n**📚 Sources:**" + for i, result in enumerate(search_results[:3]): # Top 3 sources + title = result.get('title', 'Source') + url = result.get('url', '#') + formatted_response += f"\n{i+1}. [{title}]({url})" + + return formatted_response + +def _generate_fallback_response(message: str, org_name: str) -> str: + """Generate a helpful fallback when search fails""" + return f"""I'd be happy to help you learn about {org_name}, but I couldn't find current information to answer your question: "{message}" + +This might be because: +- The information isn't publicly available yet +- The search terms need to be more specific +- There might be connectivity issues + +Try asking a more specific question, or check out our official website and documentation for the most up-to-date information about {org_name}.""" diff --git a/backend/app/agents/devrel/nodes/handlers/web_search.py b/backend/app/agents/devrel/nodes/handlers/web_search.py index ad141312..5abd6463 100644 --- a/backend/app/agents/devrel/nodes/handlers/web_search.py +++ b/backend/app/agents/devrel/nodes/handlers/web_search.py @@ -51,7 +51,7 @@ def create_search_response(task_result: Dict[str, Any]) -> str: """ Create a user-friendly response string from search results. """ - + query = task_result.get("query") results = task_result.get("results", []) @@ -61,7 +61,7 @@ def create_search_response(task_result: Dict[str, Any]) -> str: response_parts = [f"Here's what I found for '{query}':"] for i, result in enumerate(results[:5]): title = result.get('title', 'N/A') - snippet = result.get('content', 'N/A') + snippet = result.get('content', 'N/A') url = result.get('url', '#') response_parts.append(f"{i+1}. {title}: {snippet}") response_parts.append(f" (Source: {url})") diff --git a/backend/app/agents/devrel/nodes/react_supervisor.py b/backend/app/agents/devrel/nodes/react_supervisor.py index 12bec4c0..37bd1d84 100644 --- a/backend/app/agents/devrel/nodes/react_supervisor.py +++ b/backend/app/agents/devrel/nodes/react_supervisor.py @@ -7,106 +7,264 @@ logger = logging.getLogger(__name__) +# Configuration constants +MAX_ITERATIONS = 10 +MAX_CONVERSATION_HISTORY = 5 +VALID_ACTIONS = ["web_search", "faq_handler", "onboarding", "github_toolkit", "complete"] + async def react_supervisor_node(state: AgentState, llm) -> Dict[str, Any]: """ReAct Supervisor: Think -> Act -> Observe""" + + # Validate state first + if not _validate_state(state): + logger.error(f"Invalid state for session {getattr(state, 'session_id', 'unknown')}") + return _create_error_response(state, "Invalid state") + logger.info(f"ReAct Supervisor thinking for session {state.session_id}") - # Get current context - latest_message = _get_latest_message(state) - conversation_history = _get_conversation_history(state) - tool_results = state.context.get("tool_results", []) - iteration_count = state.context.get("iteration_count", 0) + try: + # Get current context + latest_message = _get_latest_message(state) + conversation_history = _get_conversation_history(state) + tool_results = state.context.get("tool_results", []) + iteration_count = state.context.get("iteration_count", 0) - prompt = REACT_SUPERVISOR_PROMPT.format( - latest_message=latest_message, - platform=state.platform, - interaction_count=state.interaction_count, - iteration_count=iteration_count, - conversation_history=conversation_history, - tool_results=json.dumps(tool_results, indent=2) if tool_results else "No previous tool results" - ) + # Safety check for max iterations + if iteration_count >= MAX_ITERATIONS: + logger.warning(f"Max iterations ({MAX_ITERATIONS}) reached for session {state.session_id}") + return _create_completion_response(state, "Maximum iterations reached") - response = await llm.ainvoke([HumanMessage(content=prompt)]) - decision = _parse_supervisor_decision(response.content) + logger.debug(f"Current iteration: {iteration_count}") + logger.debug(f"Latest message length: {len(latest_message)}") - logger.info(f"ReAct Supervisor decision: {decision['action']}") + prompt = REACT_SUPERVISOR_PROMPT.format( + latest_message=latest_message, + platform=getattr(state, 'platform', 'unknown'), + interaction_count=getattr(state, 'interaction_count', 0), + iteration_count=iteration_count, + conversation_history=conversation_history, + tool_results=json.dumps(tool_results, indent=2) if tool_results else "No previous tool results" + ) - # Update state with supervisor's thinking - return { - "context": { - **state.context, - "supervisor_thinking": response.content, - "supervisor_decision": decision, - "iteration_count": iteration_count + 1 - }, - "current_task": f"supervisor_decided_{decision['action']}" - } + response = await llm.ainvoke([HumanMessage(content=prompt)]) + decision = _parse_supervisor_decision(response.content) + + logger.info(f"ReAct Supervisor decision: {decision['action']}") + logger.debug(f"Supervisor thinking: {decision.get('thinking', 'N/A')[:100]}...") + logger.debug(f"Supervisor reasoning: {decision.get('reasoning', 'N/A')[:100]}...") + + # Update state with supervisor's thinking + return { + "context": { + **state.context, + "supervisor_thinking": response.content, + "supervisor_decision": decision, + "iteration_count": iteration_count + 1, + "last_action": decision['action'] + }, + "current_task": f"supervisor_decided_{decision['action']}" + } + + except Exception as e: + logger.error(f"Error in react_supervisor_node: {e}", exc_info=True) + return _create_error_response(state, f"Supervisor error: {str(e)}") def _parse_supervisor_decision(response: str) -> Dict[str, Any]: - """Parse the supervisor's decision from LLM response""" + """Enhanced parsing with better handling of multi-line responses""" try: - lines = response.strip().split('\n') decision = {"action": "complete", "reasoning": "", "thinking": ""} - for line in lines: + if not response or not response.strip(): + logger.warning("Empty response from supervisor, defaulting to complete") + return decision + + # Handle multi-line sections + current_section = None + content_buffer = [] + + for line in response.strip().split('\n'): + line = line.strip() + if not line: + continue + if line.startswith("THINK:"): - decision["thinking"] = line.replace("THINK:", "").strip() + if current_section and content_buffer: + decision[current_section] = " ".join(content_buffer) + current_section = "thinking" + content_buffer = [line.replace("THINK:", "").strip()] elif line.startswith("ACT:"): + if current_section and content_buffer: + decision[current_section] = " ".join(content_buffer) action = line.replace("ACT:", "").strip().lower() - if action in ["web_search", "faq_handler", "onboarding", "github_toolkit", "complete"]: + if action in VALID_ACTIONS: decision["action"] = action + else: + logger.warning(f"Invalid action '{action}', defaulting to 'complete'") + decision["action"] = "complete" + current_section = None + content_buffer = [] elif line.startswith("REASON:"): - decision["reasoning"] = line.replace("REASON:", "").strip() + if current_section and content_buffer: + decision[current_section] = " ".join(content_buffer) + current_section = "reasoning" + content_buffer = [line.replace("REASON:", "").strip()] + elif current_section and line: + content_buffer.append(line) + + # Handle any remaining content + if current_section and content_buffer: + decision[current_section] = " ".join(content_buffer) + + # Validate final decision + if decision["action"] not in VALID_ACTIONS: + logger.warning(f"Final validation failed for action '{decision['action']}', defaulting to 'complete'") + decision["action"] = "complete" return decision + except Exception as e: - logger.error(f"Error parsing supervisor decision: {e}") + logger.error(f"Error parsing supervisor decision: {e}", exc_info=True) return {"action": "complete", "reasoning": "Error in decision parsing", "thinking": ""} def supervisor_decision_router(state: AgentState) -> Literal["web_search", "faq_handler", "onboarding", "github_toolkit", "complete"]: """Route based on supervisor's decision""" - decision = state.context.get("supervisor_decision", {}) - action = decision.get("action", "complete") + try: + decision = state.context.get("supervisor_decision", {}) + action = decision.get("action", "complete") - # Safety check for infinite loops - iteration_count = state.context.get("iteration_count", 0) - if iteration_count > 10: - logger.warning(f"Max iterations reached for session {state.session_id}") - return "complete" + # Safety check for infinite loops + iteration_count = state.context.get("iteration_count", 0) + if iteration_count > MAX_ITERATIONS: + logger.warning(f"Max iterations reached for session {state.session_id}") + return "complete" - return action + # Validate action + if action not in VALID_ACTIONS: + logger.warning(f"Invalid routing action '{action}', defaulting to 'complete'") + return "complete" + + logger.debug(f"Routing to: {action} (iteration {iteration_count})") + return action + + except Exception as e: + logger.error(f"Error in supervisor_decision_router: {e}", exc_info=True) + return "complete" def add_tool_result(state: AgentState, tool_name: str, result: Dict[str, Any]) -> Dict[str, Any]: - """Add tool result to state context""" - tool_results = state.context.get("tool_results", []) - tool_results.append({ - "tool": tool_name, - "result": result, - "iteration": state.context.get("iteration_count", 0) - }) + """Add tool result to state context with validation""" + try: + if not _validate_state(state): + logger.error("Invalid state in add_tool_result") + return {"context": state.context if hasattr(state, 'context') else {}} + + tool_results = state.context.get("tool_results", []) + + # Validate tool result + if not isinstance(result, dict): + logger.warning(f"Tool result for {tool_name} is not a dict, converting") + result = {"result": str(result)} + + tool_entry = { + "tool": tool_name, + "result": result, + "iteration": state.context.get("iteration_count", 0), + "timestamp": json.dumps({"iteration": state.context.get("iteration_count", 0)}) # Simple timestamp + } + + tool_results.append(tool_entry) + # Limit tool results to prevent memory issues + if len(tool_results) > 20: + tool_results = tool_results[-20:] + logger.debug("Trimmed tool results to last 20 entries") + + tools_used = getattr(state, 'tools_used', []) + [tool_name] + + return { + "context": { + **state.context, + "tool_results": tool_results + }, + "tools_used": tools_used, + "current_task": f"completed_{tool_name}" + } + + except Exception as e: + logger.error(f"Error in add_tool_result: {e}", exc_info=True) + return {"context": state.context if hasattr(state, 'context') else {}} + +def _get_latest_message(state: AgentState) -> str: + """Extract the latest message from state with validation""" + try: + if hasattr(state, 'messages') and state.messages: + latest = state.messages[-1] + if isinstance(latest, dict): + return latest.get("content", "") + return str(latest) + return state.context.get("original_message", "") if hasattr(state, 'context') else "" + except Exception as e: + logger.error(f"Error getting latest message: {e}") + return "" + +def _get_conversation_history(state: AgentState, max_messages: int = MAX_CONVERSATION_HISTORY) -> str: + """Get formatted conversation history with validation""" + try: + if not hasattr(state, 'messages') or not state.messages: + return "No previous conversation" + + recent_messages = state.messages[-max_messages:] + formatted_messages = [] + + for msg in recent_messages: + if isinstance(msg, dict): + role = msg.get('role', 'user') + content = msg.get('content', '') + if content: # Only include non-empty messages + formatted_messages.append(f"{role}: {content[:200]}{'...' if len(content) > 200 else ''}") + + return "\n".join(formatted_messages) if formatted_messages else "No previous conversation" + + except Exception as e: + logger.error(f"Error getting conversation history: {e}") + return "Error retrieving conversation history" + +def _validate_state(state: AgentState) -> bool: + """Validate state before processing""" + try: + if not state: + return False + + if not hasattr(state, 'session_id') or not state.session_id: + logger.error("Invalid state: missing session_id") + return False + + if not hasattr(state, 'context'): + logger.error("Invalid state: missing context") + return False + + return True + except Exception as e: + logger.error(f"Error validating state: {e}") + return False + +def _create_error_response(state: AgentState, error_message: str) -> Dict[str, Any]: + """Create standardized error response""" return { "context": { - **state.context, - "tool_results": tool_results + **(state.context if hasattr(state, 'context') else {}), + "supervisor_decision": {"action": "complete", "reasoning": error_message, "thinking": "Error occurred"}, + "error": error_message }, - "tools_used": state.tools_used + [tool_name], - "current_task": f"completed_{tool_name}" + "current_task": "supervisor_decided_complete" } -def _get_latest_message(state: AgentState) -> str: - """Extract the latest message from state""" - if state.messages: - return state.messages[-1].get("content", "") - return state.context.get("original_message", "") - -def _get_conversation_history(state: AgentState, max_messages: int = 5) -> str: - """Get formatted conversation history""" - if not state.messages: - return "No previous conversation" - - recent_messages = state.messages[-max_messages:] - return "\n".join([ - f"{msg.get('role', 'user')}: {msg.get('content', '')}" - for msg in recent_messages - ]) +def _create_completion_response(state: AgentState, reason: str) -> Dict[str, Any]: + """Create standardized completion response""" + return { + "context": { + **state.context, + "supervisor_decision": {"action": "complete", "reasoning": reason, "thinking": "Completing task"}, + "completion_reason": reason + }, + "current_task": "supervisor_decided_complete" + } diff --git a/backend/app/agents/devrel/tool_wrappers.py b/backend/app/agents/devrel/tool_wrappers.py index 7fa10bb6..baa675b1 100644 --- a/backend/app/agents/devrel/tool_wrappers.py +++ b/backend/app/agents/devrel/tool_wrappers.py @@ -16,11 +16,12 @@ async def web_search_tool_node(state: AgentState, search_tool, llm) -> Dict[str, tool_result = handler_result.get("task_result", {}) return add_tool_result(state, "web_search", tool_result) -async def faq_handler_tool_node(state: AgentState, faq_tool) -> Dict[str, Any]: +async def faq_handler_tool_node(state: AgentState, search_tool, llm) -> Dict[str, Any]: """Execute FAQ handler tool and add result to ReAct context""" logger.info(f"Executing FAQ handler tool for session {state.session_id}") - handler_result = await handle_faq_node(state, faq_tool) + # Updated to use search_tool and llm instead of faq_tool for dynamic FAQ + handler_result = await handle_faq_node(state, search_tool, llm) tool_result = handler_result.get("task_result", {}) return add_tool_result(state, "faq_handler", tool_result) @@ -32,7 +33,6 @@ async def onboarding_tool_node(state: AgentState) -> Dict[str, Any]: tool_result = handler_result.get("task_result", {}) return add_tool_result(state, "onboarding", tool_result) - async def github_toolkit_tool_node(state: AgentState, github_toolkit) -> Dict[str, Any]: """Execute GitHub toolkit tool and add result to ReAct context""" logger.info(f"Executing GitHub toolkit tool for session {state.session_id}") diff --git a/backend/app/core/handler/faq_handler.py b/backend/app/core/handler/faq_handler.py index fca3285e..3cfcb019 100644 --- a/backend/app/core/handler/faq_handler.py +++ b/backend/app/core/handler/faq_handler.py @@ -1,7 +1,7 @@ import logging from typing import Dict, Any from app.core.events.base import BaseEvent -from app.core.events.enums import EventType, PlatformType +from app.core.events.enums import EventType from app.core.handler.base import BaseHandler logger = logging.getLogger(__name__) diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index 26ad8dbc..e30b7fe3 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -65,13 +65,13 @@ async def on_message(self, message): except Exception as e: logger.error(f"Error processing message: {str(e)}") - + async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): """This now handles both new requests and follow-ups in threads.""" try: user_id = str(message.author.id) thread_id = await self._get_or_create_thread(message, user_id) - + agent_message = { "type": "devrel_request", "id": f"discord_{message.id}", @@ -91,7 +91,7 @@ async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): priority_map = {"high": QueuePriority.HIGH, "medium": QueuePriority.MEDIUM, "low": QueuePriority.LOW - } + } priority = priority_map.get(triage_result.get("priority"), QueuePriority.MEDIUM) await self.queue_manager.enqueue(agent_message, priority) @@ -101,7 +101,7 @@ async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): if thread: await thread.send("I'm processing your request, please hold on...") # ------------------------------------ - + except Exception as e: logger.error(f"Error handling DevRel message: {str(e)}") @@ -114,7 +114,7 @@ async def _get_or_create_thread(self, message, user_id: str) -> Optional[str]: return thread_id else: del self.active_threads[user_id] - + # This part only runs if it's not a follow-up message in an active thread. if isinstance(message.channel, discord.TextChannel): thread_name = f"DevRel Chat - {message.author.display_name}" @@ -139,4 +139,4 @@ async def _handle_agent_response(self, response_data: Dict[str, Any]): else: logger.error(f"Thread {thread_id} not found for agent response") except Exception as e: - logger.error(f"Error handling agent response: {str(e)}") \ No newline at end of file + logger.error(f"Error handling agent response: {str(e)}") diff --git a/backend/integrations/discord/cogs.py b/backend/integrations/discord/cogs.py index a92765f9..5533bcb1 100644 --- a/backend/integrations/discord/cogs.py +++ b/backend/integrations/discord/cogs.py @@ -1,7 +1,8 @@ import discord from discord import app_commands -from discord.ext import commands, tasks +from discord.ext import commands import logging +import asyncio from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority from app.services.auth.supabase import login_with_github from app.services.auth.management import get_or_create_user_by_discord @@ -10,36 +11,69 @@ from integrations.discord.views import OAuthView from app.core.config import settings +# Try to import tasks, fallback if not available +try: + from discord.ext import tasks + TASKS_AVAILABLE = True +except ImportError: + TASKS_AVAILABLE = False + tasks = None + logger = logging.getLogger(__name__) class DevRelCommands(commands.Cog): def __init__(self, bot: DiscordBot, queue_manager: AsyncQueueManager): self.bot = bot self.queue = queue_manager + self._cleanup_task = None @commands.Cog.listener() async def on_ready(self): - if not self.cleanup_expired_tokens.is_running(): - print("--> Starting the token cleanup task...") - self.cleanup_expired_tokens.start() + if TASKS_AVAILABLE and hasattr(self, 'cleanup_expired_tokens'): + if not self.cleanup_expired_tokens.is_running(): + print("--> Starting the token cleanup task...") + self.cleanup_expired_tokens.start() + else: + # Start manual cleanup task if tasks extension is not available + if not self._cleanup_task: + self._cleanup_task = asyncio.create_task(self._manual_cleanup_loop()) + print("--> Starting manual token cleanup task...") def cog_unload(self): - self.cleanup_expired_tokens.cancel() + if TASKS_AVAILABLE and hasattr(self, 'cleanup_expired_tokens'): + self.cleanup_expired_tokens.cancel() + if self._cleanup_task: + self._cleanup_task.cancel() - @tasks.loop(minutes=5) - async def cleanup_expired_tokens(self): - """Periodic cleanup of expired verification tokens""" - try: - print("--> Running token cleanup task...") - await cleanup_expired_tokens() - print("--> Token cleanup task finished.") - except Exception as e: - logger.error(f"Error during token cleanup: {e}") + if TASKS_AVAILABLE: + @tasks.loop(minutes=5) + async def cleanup_expired_tokens(self): + """Periodic cleanup of expired verification tokens""" + try: + print("--> Running token cleanup task...") + await cleanup_expired_tokens() + print("--> Token cleanup task finished.") + except Exception as e: + logger.error(f"Error during token cleanup: {e}") - @cleanup_expired_tokens.before_loop - async def before_cleanup(self): - """Wait until the bot is ready before starting cleanup""" + @cleanup_expired_tokens.before_loop + async def before_cleanup(self): + """Wait until the bot is ready before starting cleanup""" + await self.bot.wait_until_ready() + + async def _manual_cleanup_loop(self): + """Manual cleanup loop if tasks extension is not available""" await self.bot.wait_until_ready() + while True: + try: + await asyncio.sleep(300) # 5 minutes + print("--> Running manual token cleanup task...") + await cleanup_expired_tokens() + print("--> Manual token cleanup task finished.") + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Error during manual token cleanup: {e}") @app_commands.command(name="reset", description="Reset your DevRel thread and memory.") async def reset_thread(self, interaction: discord.Interaction): @@ -104,7 +138,7 @@ async def verification_status(self, interaction: discord.Interaction): async def verify_github(self, interaction: discord.Interaction): try: await interaction.response.defer(ephemeral=True) - + user_profile = await get_or_create_user_by_discord( discord_id=str(interaction.user.id), display_name=interaction.user.display_name, @@ -119,7 +153,7 @@ async def verify_github(self, interaction: discord.Interaction): ) await interaction.followup.send(embed=embed, ephemeral=True) return - + if user_profile.verification_token: embed = discord.Embed( title="⏳ Verification Pending", @@ -180,4 +214,4 @@ async def verify_github(self, interaction: discord.Interaction): async def setup(bot: commands.Bot): """This function is called by the bot to load the cog.""" - await bot.add_cog(DevRelCommands(bot, bot.queue_manager)) \ No newline at end of file + await bot.add_cog(DevRelCommands(bot, bot.queue_manager)) diff --git a/backend/main.py b/backend/main.py index ed59e6af..2f95b819 100644 --- a/backend/main.py +++ b/backend/main.py @@ -13,6 +13,7 @@ from app.database.weaviate.client import get_weaviate_client from integrations.discord.bot import DiscordBot from discord.ext import commands + # DevRel commands are now loaded dynamically (commented out below) # from integrations.discord.cogs import DevRelCommands @@ -45,11 +46,15 @@ async def start_background_tasks(self): await self.queue_manager.start(num_workers=3) # --- Load commands inside the async startup function --- - try: - await self.discord_bot.load_extension("integrations.discord.cogs") - except (ImportError, commands.ExtensionError) as e: - logger.error("Failed to load Discord cog extension: %s", e) - + # Temporarily disabled to troubleshoot import issues + # try: + # await self.discord_bot.load_extension("integrations.discord.cogs") + # except (ImportError, discord.errors.ExtensionFailed, discord.errors.ExtensionNotFound, discord.errors.NoEntryPointError) as e: + # logger.error("Failed to load Discord cog extension: %s", e) + # # Don't re-raise here to allow the app to continue without Discord cogs + # except Exception as e: + # logger.error("Unexpected error loading Discord cog extension: %s", e) + # Start the bot as a background task. asyncio.create_task( self.discord_bot.start(settings.discord_bot_token) @@ -127,4 +132,4 @@ async def favicon(): host="0.0.0.0", port=8000, reload=True - ) \ No newline at end of file + ) diff --git a/poetry.lock b/poetry.lock index 6375438e..4808dc05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -238,6 +238,50 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +[[package]] +name = "audioop-lts" +version = "0.2.1" +description = "LTS Port of Python audioop" +optional = false +python-versions = ">=3.13" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059"}, + {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47"}, + {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4"}, + {file = "audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7"}, + {file = "audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0"}, + {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, +] + [[package]] name = "authlib" version = "1.3.1" @@ -701,6 +745,29 @@ files = [ [package.dependencies] packaging = "*" +[[package]] +name = "discord-py" +version = "2.5.2" +description = "A Python wrapper for the Discord API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "discord_py-2.5.2-py3-none-any.whl", hash = "sha256:81f23a17c50509ffebe0668441cb80c139e74da5115305f70e27ce821361295a"}, + {file = "discord_py-2.5.2.tar.gz", hash = "sha256:01cd362023bfea1a4a1d43f5280b5ef00cad2c7eba80098909f98bf28e578524"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4,<4" +audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} + +[package.extras] +dev = ["black (==22.6)", "typing_extensions (>=4.3,<5)"] +docs = ["imghdr-lts (==1.0.0) ; python_version >= \"3.13\"", "sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-websupport (==1.2.4)", "sphinxcontrib_trio (==1.1.2)", "typing-extensions (>=4.3,<5)"] +speed = ["Brotli", "aiodns (>=1.1) ; sys_platform != \"win32\"", "cchardet (==2.1.7) ; python_version < \"3.10\"", "orjson (>=3.5.4)", "zstandard (>=0.23.0)"] +test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata ; sys_platform == \"win32\""] +voice = ["PyNaCl (>=1.3.0,<1.6)"] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -1777,6 +1844,8 @@ files = [ {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8"}, {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082"}, {file = "lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc"}, {file = "lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76"}, @@ -1789,6 +1858,8 @@ files = [ {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36"}, {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25"}, {file = "lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e"}, {file = "lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58"}, @@ -1801,11 +1872,15 @@ files = [ {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108"}, {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be"}, {file = "lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da"}, {file = "lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16"}, {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0"}, {file = "lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a"}, {file = "lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3"}, @@ -1813,17 +1888,22 @@ files = [ {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da"}, {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7"}, {file = "lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29"}, {file = "lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f"}, {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef"}, {file = "lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181"}, {file = "lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e"}, {file = "lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03"}, {file = "lxml-6.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4eb114a0754fd00075c12648d991ec7a4357f9cb873042cc9a77bf3a7e30c9db"}, {file = "lxml-6.0.0-cp38-cp38-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:7da298e1659e45d151b4028ad5c7974917e108afb48731f4ed785d02b6818994"}, + {file = "lxml-6.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bf61bc4345c1895221357af8f3e89f8c103d93156ef326532d35c707e2fb19d"}, {file = "lxml-6.0.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b634facdfbad421d4b61c90735688465d4ab3a8853ac22c76ccac2baf98d97"}, {file = "lxml-6.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e380e85b93f148ad28ac15f8117e2fd8e5437aa7732d65e260134f83ce67911b"}, {file = "lxml-6.0.0-cp38-cp38-win32.whl", hash = "sha256:185efc2fed89cdd97552585c624d3c908f0464090f4b91f7d92f8ed2f3b18f54"}, @@ -1831,6 +1911,8 @@ files = [ {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85b14a4689d5cff426c12eefe750738648706ea2753b20c2f973b2a000d3d261"}, {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f64ccf593916e93b8d36ed55401bb7fe9c7d5de3180ce2e10b08f82a8f397316"}, {file = "lxml-6.0.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:b372d10d17a701b0945f67be58fae4664fd056b85e0ff0fbc1e6c951cdbc0512"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a674c0948789e9136d69065cc28009c1b1874c6ea340253db58be7622ce6398f"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:edf6e4c8fe14dfe316939711e3ece3f9a20760aabf686051b537a7562f4da91a"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:048a930eb4572829604982e39a0c7289ab5dc8abc7fc9f5aabd6fbc08c154e93"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b5fa5eda84057a4f1bbb4bb77a8c28ff20ae7ce211588d698ae453e13c6281"}, {file = "lxml-6.0.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:c352fc8f36f7e9727db17adbf93f82499457b3d7e5511368569b4c5bd155a922"}, @@ -1841,10 +1923,14 @@ files = [ {file = "lxml-6.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0b1520ef900e9ef62e392dd3d7ae4f5fa224d1dd62897a792cf353eb20b6cae"}, {file = "lxml-6.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:e35e8aaaf3981489f42884b59726693de32dabfc438ac10ef4eb3409961fd402"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065"}, {file = "lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4337e4aec93b7c011f7ee2e357b0d30562edd1955620fdd4aeab6aacd90d43c5"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae74f7c762270196d2dda56f8dd7309411f08a4084ff2dfcc0b095a218df2e06"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:059c4cbf3973a621b62ea3132934ae737da2c132a788e6cfb9b08d63a0ef73f9"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f090a9bc0ce8da51a5632092f98a7e7f84bca26f33d161a98b57f7fb0004ca"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9da022c14baeec36edfcc8daf0e281e2f55b950249a455776f0d1adeeada4734"}, {file = "lxml-6.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a55da151d0b0c6ab176b4e761670ac0e2667817a1e0dadd04a01d0561a219349"}, @@ -2998,27 +3084,6 @@ files = [ {file = "protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a"}, ] -[[package]] -name = "py-cord" -version = "2.6.1" -description = "A Python wrapper for the Discord API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "py_cord-2.6.1-py3-none-any.whl", hash = "sha256:e3d3b528c5e37b0e0825f5b884cbb9267860976c1e4878e28b55da8fd3af834b"}, - {file = "py_cord-2.6.1.tar.gz", hash = "sha256:36064f225f2c7bbddfe542d5ed581f2a5744f618e039093cf7cd2659a58bc79b"}, -] - -[package.dependencies] -aiohttp = ">=3.6.0,<4.0" -typing-extensions = {version = ">=4,<5", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (==2023.3.23)", "myst-parser (==1.0.0)", "sphinx (==5.3.0)", "sphinx-autodoc-typehints (==1.23.0)", "sphinx-copybutton (==0.5.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "sphinxext-opengraph (==0.9.1)"] -speed = ["aiohttp[speedups]", "msgspec (>=0.18.6,<0.19.0)"] -voice = ["PyNaCl (>=1.3.0,<1.6)"] - [[package]] name = "pyasn1" version = "0.6.1" @@ -5168,4 +5233,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.9, <4.0" -content-hash = "4f306eb987b6a66ddb2c4ed592aa9e83c72e3dae0639d9d05e2f9af608b14a4b" +content-hash = "08ba3a64e2bb86ff6b0b8df3b8dc19bd737faab40e48113133b868f3496a04e0" diff --git a/pyproject.toml b/pyproject.toml index b653f853..4b711531 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ requires-python = ">=3.9, <4.0" dependencies = [ "supabase (>=2.13.0,<3.0.0)", "fastapi (>=0.115.11,<0.116.0)", - "py-cord (>=2.6.1,<3.0.0)", "pygithub (>=2.6.1,<3.0.0)", "slack-sdk (>=3.34.0,<4.0.0)", "sentence-transformers (>=3.4.1,<4.0.0)", @@ -25,7 +24,7 @@ dependencies = [ "aio-pika (>=9.5.5,<10.0.0)", "uvicorn (>=0.35.0,<0.36.0)", "ddgs (>=9.0.2,<10.0.0)", - "discord-py (>=2.5.2,<3.0.0)", + "discord-py (>=2.3.0,<3.0.0)", ] [tool.poetry] From 862bb9323e610fb5386fee96b19d5871f76787e8 Mon Sep 17 00:00:00 2001 From: Prithvijit Bose Date: Tue, 5 Aug 2025 20:06:00 +0530 Subject: [PATCH 2/2] Fixed Code Rabbit's conflicts --- backend/app/agents/devrel/nodes/react_supervisor.py | 11 ++++------- backend/integrations/discord/cogs.py | 12 +++++++----- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/app/agents/devrel/nodes/react_supervisor.py b/backend/app/agents/devrel/nodes/react_supervisor.py index 37bd1d84..ab4b273a 100644 --- a/backend/app/agents/devrel/nodes/react_supervisor.py +++ b/backend/app/agents/devrel/nodes/react_supervisor.py @@ -4,6 +4,8 @@ from app.agents.state import AgentState from langchain_core.messages import HumanMessage from ..prompts.react_prompt import REACT_SUPERVISOR_PROMPT +from datetime import datetime +from ..nodes.generate_response import _get_latest_message as get_latest_message_util logger = logging.getLogger(__name__) @@ -168,7 +170,7 @@ def add_tool_result(state: AgentState, tool_name: str, result: Dict[str, Any]) - "tool": tool_name, "result": result, "iteration": state.context.get("iteration_count", 0), - "timestamp": json.dumps({"iteration": state.context.get("iteration_count", 0)}) # Simple timestamp + "timestamp": datetime.now().isoformat() # Now actually stores timestamp } tool_results.append(tool_entry) @@ -196,12 +198,7 @@ def add_tool_result(state: AgentState, tool_name: str, result: Dict[str, Any]) - def _get_latest_message(state: AgentState) -> str: """Extract the latest message from state with validation""" try: - if hasattr(state, 'messages') and state.messages: - latest = state.messages[-1] - if isinstance(latest, dict): - return latest.get("content", "") - return str(latest) - return state.context.get("original_message", "") if hasattr(state, 'context') else "" + return get_latest_message_util(state) except Exception as e: logger.error(f"Error getting latest message: {e}") return "" diff --git a/backend/integrations/discord/cogs.py b/backend/integrations/discord/cogs.py index 5533bcb1..7bb18998 100644 --- a/backend/integrations/discord/cogs.py +++ b/backend/integrations/discord/cogs.py @@ -27,15 +27,17 @@ def __init__(self, bot: DiscordBot, queue_manager: AsyncQueueManager): self.queue = queue_manager self._cleanup_task = None + # Set up cleanup method based on availability + if TASKS_AVAILABLE: + self._setup_tasks_cleanup() + @commands.Cog.listener() async def on_ready(self): - if TASKS_AVAILABLE and hasattr(self, 'cleanup_expired_tokens'): - if not self.cleanup_expired_tokens.is_running(): + if TASKS_AVAILABLE: + if hasattr(self, 'cleanup_expired_tokens') and not self.cleanup_expired_tokens.is_running(): print("--> Starting the token cleanup task...") self.cleanup_expired_tokens.start() - else: - # Start manual cleanup task if tasks extension is not available - if not self._cleanup_task: + elif not self._cleanup_task: self._cleanup_task = asyncio.create_task(self._manual_cleanup_loop()) print("--> Starting manual token cleanup task...") diff --git a/pyproject.toml b/pyproject.toml index 4b711531..953feeea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "aio-pika (>=9.5.5,<10.0.0)", "uvicorn (>=0.35.0,<0.36.0)", "ddgs (>=9.0.2,<10.0.0)", - "discord-py (>=2.3.0,<3.0.0)", + "discord-py (>=2.4.0,<3.0.0)", ] [tool.poetry]