From 1e57ac9694e4c21e95eae56d121222c020f8758e Mon Sep 17 00:00:00 2001 From: Mahek Gupta Date: Wed, 18 Feb 2026 23:46:06 +0530 Subject: [PATCH 1/3] feat: add proactive contributor assistance with lightweight pattern detection --- .../classification/classification_router.py | 80 ++++++++++++- backend/integrations/discord/bot.py | 110 ++++++++++++++---- 2 files changed, 164 insertions(+), 26 deletions(-) diff --git a/backend/app/classification/classification_router.py b/backend/app/classification/classification_router.py index 1708dced..b1929220 100644 --- a/backend/app/classification/classification_router.py +++ b/backend/app/classification/classification_router.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) + class ClassificationRouter: """Simple DevRel triage - determines if message needs DevRel assistance""" @@ -17,20 +18,87 @@ def __init__(self, llm_client=None): google_api_key=settings.gemini_api_key ) - async def should_process_message(self, message: str, context: Dict[str, Any] = None) -> Dict[str, Any]: + # πŸ”₯ NEW: Proactive lightweight pattern detection + def _simple_pattern_match(self, message: str): + """ + Lightweight proactive detection before calling LLM. + Returns classification dict if matched, else None. + """ + + msg = message.lower().strip() + + greetings = ["hi", "hello", "hey"] + thanks = ["thanks", "thank you"] + onboarding_keywords = ["new here", "how to start", "beginner", "first time"] + issue_keywords = ["good first issue", "beginner issue", "start contributing"] + + if msg in greetings: + return { + "needs_devrel": True, + "priority": "low", + "reasoning": "Greeting detected - proactive onboarding opportunity", + "original_message": message, + "proactive_type": "greeting" + } + + if any(k in msg for k in onboarding_keywords): + return { + "needs_devrel": True, + "priority": "high", + "reasoning": "Onboarding request detected", + "original_message": message, + "proactive_type": "onboarding" + } + + if any(k in msg for k in issue_keywords): + return { + "needs_devrel": True, + "priority": "medium", + "reasoning": "Contributor looking for issues", + "original_message": message, + "proactive_type": "issue_suggestion" + } + + if any(t in msg for t in thanks): + return { + "needs_devrel": False, + "priority": "low", + "reasoning": "Acknowledgment message - no processing needed", + "original_message": message, + "proactive_type": "acknowledgment" + } + + return None + + async def should_process_message( + self, + message: str, + context: Dict[str, Any] = None + ) -> Dict[str, Any]: """Simple triage: Does this message need DevRel assistance?""" + try: + # πŸ”₯ Step 1: Lightweight proactive pattern check + pattern_result = self._simple_pattern_match(message) + if pattern_result: + logger.info("Pattern-based proactive classification triggered") + return pattern_result + + # πŸ”₯ Step 2: Fallback to LLM triage_prompt = DEVREL_TRIAGE_PROMPT.format( message=message, - context=context or 'No additional context' + context=context or "No additional context" ) - response = await self.llm.ainvoke([HumanMessage(content=triage_prompt)]) + response = await self.llm.ainvoke( + [HumanMessage(content=triage_prompt)] + ) response_text = response.content.strip() - if '{' in response_text: - json_start = response_text.find('{') - json_end = response_text.rfind('}') + 1 + + if "{" in response_text: + json_start = response_text.find("{") + json_end = response_text.rfind("}") + 1 json_str = response_text[json_start:json_end] import json diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index dbb7c3a4..441d7cb9 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -7,6 +7,7 @@ logger = logging.getLogger(__name__) + class DiscordBot(commands.Bot): """Discord bot with LangGraph agent integration""" @@ -31,10 +32,12 @@ def __init__(self, queue_manager: AsyncQueueManager, **kwargs): def _register_queue_handlers(self): """Register handlers for queue messages""" - self.queue_manager.register_handler("discord_response", self._handle_agent_response) + self.queue_manager.register_handler( + "discord_response", + self._handle_agent_response + ) async def on_ready(self): - """Bot ready event""" logger.info(f'Enhanced Discord bot logged in as {self.user}') print(f'Bot is ready! Logged in as {self.user}') try: @@ -44,7 +47,6 @@ async def on_ready(self): print(f"Failed to sync slash commands: {e}") async def on_message(self, message): - """Handles regular chat messages, but ignores slash commands.""" if message.author == self.user: return @@ -67,9 +69,50 @@ 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.""" + async def _handle_devrel_message( + self, + message, + triage_result: Dict[str, Any] + ): + """Handles both proactive responses and agent requests""" + try: + # πŸ”₯ PROACTIVE LAYER + if "proactive_type" in triage_result: + proactive_type = triage_result["proactive_type"] + + if proactive_type == "greeting": + await message.channel.send( + f"Hi {message.author.mention}! πŸ‘‹\n" + "Welcome to the community!\n" + "If you're new, I can guide you on how to start contributing πŸš€" + ) + return + + if proactive_type == "onboarding": + await message.channel.send( + f"Awesome {message.author.mention}! πŸŽ‰\n" + "Here’s how you can start:\n" + "1️⃣ Look for `good first issue`\n" + "2️⃣ Set up the project locally\n" + "3️⃣ Read CONTRIBUTING.md\n\n" + "Would you like me to suggest beginner-friendly issues?" + ) + return + + if proactive_type == "issue_suggestion": + await message.channel.send( + f"{message.author.mention} πŸ”\n" + "You can check open issues labeled `good first issue`.\n" + "Would you like me to fetch some right now?" + ) + return + + if proactive_type == "acknowledgment": + return + + # πŸ”₯ NORMAL AGENT FLOW (Fallback) + user_id = str(message.author.id) thread_id = await self._get_or_create_thread(message, user_id) @@ -88,27 +131,39 @@ async def _handle_devrel_message(self, message, triage_result: Dict[str, Any]): "author": { "username": message.author.name, "display_name": message.author.display_name, - "avatar_url": str(message.author.avatar.url) if message.author.avatar else None + "avatar_url": str(message.author.avatar.url) + if message.author.avatar else None } } - priority_map = {"high": QueuePriority.HIGH, - "medium": QueuePriority.MEDIUM, - "low": QueuePriority.LOW - } - priority = priority_map.get(triage_result.get("priority"), QueuePriority.MEDIUM) + + 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) - # --- "PROCESSING" MESSAGE RESTORED --- if thread_id: thread = self.get_channel(int(thread_id)) if thread: - await thread.send("I'm processing your request, please hold on...") - # ------------------------------------ + await thread.send( + "I'm processing your request, please hold on..." + ) except Exception as e: logger.error(f"Error handling DevRel message: {str(e)}") - async def _get_or_create_thread(self, message, user_id: str) -> Optional[str]: + async def _get_or_create_thread( + self, + message, + user_id: str + ) -> Optional[str]: try: if user_id in self.active_threads: thread_id = self.active_threads[user_id] @@ -118,28 +173,43 @@ async def _get_or_create_thread(self, message, user_id: str) -> Optional[str]: 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}" - thread = await message.create_thread(name=thread_name, auto_archive_duration=60) + thread = await message.create_thread( + name=thread_name, + auto_archive_duration=60 + ) self.active_threads[user_id] = str(thread.id) - await thread.send(f"Hello {message.author.mention}! I've created this thread to help you. How can I assist?") + await thread.send( + f"Hello {message.author.mention}! " + "I've created this thread to help you." + ) return str(thread.id) + except Exception as e: logger.error(f"Failed to create thread: {e}") + return str(message.channel.id) - async def _handle_agent_response(self, response_data: Dict[str, Any]): + async def _handle_agent_response( + self, + response_data: Dict[str, Any] + ): try: thread_id = response_data.get("thread_id") response_text = response_data.get("response", "") + if not thread_id or not response_text: return + thread = self.get_channel(int(thread_id)) if thread: for i in range(0, len(response_text), 2000): await thread.send(response_text[i:i+2000]) else: - logger.error(f"Thread {thread_id} not found for agent response") + logger.error( + f"Thread {thread_id} not found for agent response" + ) + except Exception as e: logger.error(f"Error handling agent response: {str(e)}") From 3709a9d5ba1f685192f8f4a613204f7f0da11d8c Mon Sep 17 00:00:00 2001 From: Mahek Gupta Date: Thu, 19 Feb 2026 14:36:15 +0530 Subject: [PATCH 2/3] fix: resolve CodeRabbit review issues (acknowledgment flow + ASCII apostrophe) --- backend/app/classification/classification_router.py | 8 ++++---- backend/integrations/discord/bot.py | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/app/classification/classification_router.py b/backend/app/classification/classification_router.py index b1929220..0333b74f 100644 --- a/backend/app/classification/classification_router.py +++ b/backend/app/classification/classification_router.py @@ -18,7 +18,7 @@ def __init__(self, llm_client=None): google_api_key=settings.gemini_api_key ) - # πŸ”₯ NEW: Proactive lightweight pattern detection + # NEW: Proactive lightweight pattern detection def _simple_pattern_match(self, message: str): """ Lightweight proactive detection before calling LLM. @@ -61,7 +61,7 @@ def _simple_pattern_match(self, message: str): if any(t in msg for t in thanks): return { - "needs_devrel": False, + "needs_devrel": True, "priority": "low", "reasoning": "Acknowledgment message - no processing needed", "original_message": message, @@ -78,13 +78,13 @@ async def should_process_message( """Simple triage: Does this message need DevRel assistance?""" try: - # πŸ”₯ Step 1: Lightweight proactive pattern check + # Step 1: Lightweight proactive pattern check pattern_result = self._simple_pattern_match(message) if pattern_result: logger.info("Pattern-based proactive classification triggered") return pattern_result - # πŸ”₯ Step 2: Fallback to LLM + # Step 2: Fallback to LLM triage_prompt = DEVREL_TRIAGE_PROMPT.format( message=message, context=context or "No additional context" diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index 441d7cb9..eec65c49 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -77,7 +77,7 @@ async def _handle_devrel_message( """Handles both proactive responses and agent requests""" try: - # πŸ”₯ PROACTIVE LAYER + # PROACTIVE LAYER if "proactive_type" in triage_result: proactive_type = triage_result["proactive_type"] @@ -92,7 +92,8 @@ async def _handle_devrel_message( if proactive_type == "onboarding": await message.channel.send( f"Awesome {message.author.mention}! πŸŽ‰\n" - "Here’s how you can start:\n" + "Here's how you can start:\n" + "1️⃣ Look for `good first issue`\n" "2️⃣ Set up the project locally\n" "3️⃣ Read CONTRIBUTING.md\n\n" @@ -111,7 +112,7 @@ async def _handle_devrel_message( if proactive_type == "acknowledgment": return - # πŸ”₯ NORMAL AGENT FLOW (Fallback) + # NORMAL AGENT FLOW (Fallback) user_id = str(message.author.id) thread_id = await self._get_or_create_thread(message, user_id) From 00d267908fb69164e3935a104d9d2ef4e45bb56a Mon Sep 17 00:00:00 2001 From: Mahek Gupta Date: Sat, 21 Feb 2026 14:53:07 +0530 Subject: [PATCH 3/3] Add global beginner issue search with rule-based classification and GitHub API integration --- .../agents/devrel/github/github_toolkit.py | 147 ++++++----- .../services/issue_suggestion_service.py | 63 +++++ backend/app/api/router.py | 8 + backend/app/api/v1/github.py | 34 +++ .../classification/classification_router.py | 2 +- backend/integrations/discord/bot.py | 164 ++---------- backend/main.py | 97 ++----- backend/requirements.txt | 243 ++---------------- backend/routes.py | 34 +++ .../github/issue_suggestion_service.py | 46 ++++ 10 files changed, 330 insertions(+), 508 deletions(-) create mode 100644 backend/app/agents/devrel/github/services/issue_suggestion_service.py create mode 100644 backend/app/api/v1/github.py create mode 100644 backend/services/github/issue_suggestion_service.py diff --git a/backend/app/agents/devrel/github/github_toolkit.py b/backend/app/agents/devrel/github/github_toolkit.py index 57f94fa7..c555dd01 100644 --- a/backend/app/agents/devrel/github/github_toolkit.py +++ b/backend/app/agents/devrel/github/github_toolkit.py @@ -1,12 +1,10 @@ +from .services.issue_suggestion_service import IssueSuggestionService + import logging -import json -import re import config from typing import Dict, Any -from langchain_google_genai import ChatGoogleGenerativeAI -from langchain_core.messages import HumanMessage from app.core.config import settings -from .prompts.intent_analysis import GITHUB_INTENT_ANALYSIS_PROMPT + from .tools.search import handle_web_search from .tools.github_support import handle_github_supp from .tools.contributor_recommendation import handle_contributor_recommendation @@ -19,7 +17,6 @@ def normalize_org(org_from_user: str = None) -> str: - """Fallback to env org if user does not specify one.""" if org_from_user and org_from_user.strip(): return org_from_user.strip() return DEFAULT_ORG @@ -27,18 +24,11 @@ def normalize_org(org_from_user: str = None) -> str: class GitHubToolkit: """ - GitHub Toolkit - Main entry point for GitHub operations - - This class serves as both the intent classifier and execution coordinator. - It thinks (classifies intent) and acts (delegates to appropriate tools). + GitHub Toolkit - Rule-based intent classifier + executor + (Gemini removed to avoid quota issues) """ def __init__(self): - self.llm = ChatGoogleGenerativeAI( - model=settings.github_agent_model, - temperature=0.1, - google_api_key=settings.gemini_api_key - ) self.tools = [ "web_search", "contributor_recommendation", @@ -50,80 +40,117 @@ def __init__(self): "general_github_help" ] + # -------------------------------------------------- + # RULE-BASED CLASSIFIER + # -------------------------------------------------- + async def classify_intent(self, user_query: str) -> Dict[str, Any]: - """Classify intent and return classification with reasoning.""" - logger.info(f"Classifying intent for query: {user_query[:100]}") - try: - prompt = GITHUB_INTENT_ANALYSIS_PROMPT.format(user_query=user_query) - response = await self.llm.ainvoke([HumanMessage(content=prompt)]) + query_lower = user_query.lower() - content = response.content.strip() + if "beginner" in query_lower or "good first issue" in query_lower: + classification = "find_good_first_issues" - try: - result = json.loads(content) - except json.JSONDecodeError: - match = re.search(r"\{.*\}", content, re.DOTALL) - if match: - result = json.loads(match.group()) - else: - logger.error(f"Invalid JSON in LLM response: {content}") - return { - "classification": "general_github_help", - "reasoning": "Failed to parse LLM response as JSON", - "confidence": "low", - "query": user_query - } + elif "contributor" in query_lower: + classification = "contributor_recommendation" - classification = result.get("classification") - if classification not in self.tools: - logger.warning(f"Returned invalid function: {classification}, defaulting to general_github_help") - classification = "general_github_help" - result["classification"] = classification + elif "repo" in query_lower: + classification = "repo_support" - result["query"] = user_query + elif "github support" in query_lower: + classification = "github_support" - logger.info(f"Classified intent for query: {user_query} -> {classification}") - logger.info(f"Reasoning: {result.get('reasoning', 'No reasoning provided')}") - logger.info(f"Confidence: {result.get('confidence', 'unknown')}") + elif "search" in query_lower: + classification = "web_search" - return result + else: + classification = "general_github_help" - except Exception as e: - logger.error(f"Error in intent classification: {str(e)}") - return { - "classification": "general_github_help", - "reasoning": f"Error occurred during classification: {str(e)}", - "confidence": "low", - "query": user_query - } + logger.info(f"Rule-based classification: {user_query} -> {classification}") + + return { + "classification": classification, + "reasoning": "Rule-based classification", + "confidence": "high", + "query": user_query + } + + # -------------------------------------------------- + # EXECUTION + # -------------------------------------------------- async def execute(self, query: str) -> Dict[str, Any]: - """Main execution method - classifies intent and delegates to appropriate tools""" logger.info(f"Executing GitHub toolkit for query: {query[:100]}") try: intent_result = await self.classify_intent(query) classification = intent_result["classification"] - logger.info(f"Executing {classification} for query") + logger.info(f"Executing action: {classification}") + + # ----------------------------------------- + # EXISTING HANDLERS + # ----------------------------------------- if classification == "contributor_recommendation": result = await handle_contributor_recommendation(query) + elif classification == "github_support": org = normalize_org() result = await handle_github_supp(query, org=org) result["org_used"] = org + elif classification == "repo_support": result = await handle_repo_support(query) + elif classification == "issue_creation": - result = "Not implemented" + result = { + "message": "Issue creation not implemented yet" + } + elif classification == "documentation_generation": - result = "Not implemented" + result = { + "message": "Documentation generation not implemented yet" + } + + # ----------------------------------------- + # BEGINNER ISSUE SEARCH (FIXED) + # ----------------------------------------- + + elif classification == "find_good_first_issues": + + service = IssueSuggestionService(settings.github_token) + + # βœ… FIXED β€” passing query argument + issues = await service.fetch_global_beginner_issues(query) + + if not issues: + result = { + "status": "success", + "message": "No beginner issues found globally right now.", + "issues": [] + } + else: + formatted = "\n\n".join( + f"πŸ”Ή [{i['repo']}] #{i['number']} - {i['title']}\n{i['url']}" + for i in issues + ) + + result = { + "status": "success", + "message": f"Here are beginner-friendly issues across GitHub:\n\n{formatted}", + "issues": issues + } + elif classification == "web_search": result = await handle_web_search(query) + + # ----------------------------------------- + # DEFAULT FALLBACK + # ----------------------------------------- + else: - result = await handle_general_github_help(query, self.llm) + result = await handle_general_github_help(query, None) result["intent_analysis"] = intent_result result["type"] = "github_toolkit" @@ -131,11 +158,11 @@ async def execute(self, query: str) -> Dict[str, Any]: return result except Exception as e: - logger.error(f"Error in GitHub toolkit execution: {str(e)}") + logger.error(f"GitHub toolkit execution error: {str(e)}") return { "status": "error", "type": "github_toolkit", "query": query, "error": str(e), "message": "Failed to execute GitHub operation" - } + } \ No newline at end of file diff --git a/backend/app/agents/devrel/github/services/issue_suggestion_service.py b/backend/app/agents/devrel/github/services/issue_suggestion_service.py new file mode 100644 index 00000000..3ef8ec1b --- /dev/null +++ b/backend/app/agents/devrel/github/services/issue_suggestion_service.py @@ -0,0 +1,63 @@ +import httpx +from typing import List, Dict + +GITHUB_API_BASE = "https://api.github.com" + + +class IssueSuggestionService: + + def __init__(self, token: str): + self.token = token + + async def fetch_global_beginner_issues( + self, + user_query: str, + limit: int = 5 + ) -> List[Dict]: + + headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json" + } + + # Base GitHub search query + search_query = 'label:"good first issue" is:issue state:open' + + query_lower = user_query.lower() + + # Language filter + if "python" in query_lower: + search_query += " language:python" + + # Org filter + if "django" in query_lower: + search_query += " org:django" + + url = f"{GITHUB_API_BASE}/search/issues?q={search_query}&per_page={limit}" + + print("πŸ” GitHub Search Query:", search_query) + print("πŸ”— GitHub URL:", url) + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + + if response.status_code != 200: + print("❌ GitHub API Error:", response.status_code) + print("❌ Response Body:", response.text) + return [] + + data = response.json() + + results = [] + + for item in data.get("items", []): + results.append({ + "repo": item["repository_url"].split("/")[-1], + "number": item["number"], + "title": item["title"], + "url": item["html_url"] + }) + + print(f"βœ… Found {len(results)} issues") + + return results \ No newline at end of file diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 67cd1e56..fde97770 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -1,3 +1,4 @@ +from .v1.github import router as github_router from fastapi import APIRouter from .v1.auth import router as auth_router from .v1.health import router as health_router @@ -5,6 +6,13 @@ api_router = APIRouter() +api_router.include_router( + github_router, + prefix="/v1/github", + tags=["GitHub"] +) + + api_router.include_router( auth_router, prefix="/v1/auth", diff --git a/backend/app/api/v1/github.py b/backend/app/api/v1/github.py new file mode 100644 index 00000000..d15b8c35 --- /dev/null +++ b/backend/app/api/v1/github.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter, HTTPException +from services.github.issue_suggestion_service import IssueSuggestionService +from config import GITHUB_TOKEN, GITHUB_ORG + +router = APIRouter() + +issue_service = IssueSuggestionService(GITHUB_TOKEN) + + +@router.get("/beginner-issues") +async def get_beginner_issues(repo: str): + if not GITHUB_TOKEN: + raise HTTPException( + status_code=500, + detail="GitHub token not configured" + ) + + try: + issues = await issue_service.fetch_beginner_issues( + owner=GITHUB_ORG, + repo=repo + ) + + return { + "repo": repo, + "count": len(issues), + "issues": issues + } + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to fetch issues: {str(e)}" + ) diff --git a/backend/app/classification/classification_router.py b/backend/app/classification/classification_router.py index 0333b74f..71e19903 100644 --- a/backend/app/classification/classification_router.py +++ b/backend/app/classification/classification_router.py @@ -80,7 +80,7 @@ async def should_process_message( try: # Step 1: Lightweight proactive pattern check pattern_result = self._simple_pattern_match(message) - if pattern_result: + if False and pattern_result: logger.info("Pattern-based proactive classification triggered") return pattern_result diff --git a/backend/integrations/discord/bot.py b/backend/integrations/discord/bot.py index eec65c49..df8b6c4a 100644 --- a/backend/integrations/discord/bot.py +++ b/backend/integrations/discord/bot.py @@ -1,17 +1,21 @@ import discord from discord.ext import commands import logging -from typing import Dict, Any, Optional -from app.core.orchestration.queue_manager import AsyncQueueManager, QueuePriority -from app.classification.classification_router import ClassificationRouter +from typing import Dict, Optional + +from app.agents.devrel.github.github_toolkit import GitHubToolkit logger = logging.getLogger(__name__) class DiscordBot(commands.Bot): - """Discord bot with LangGraph agent integration""" + """ + DEV MODE Discord Bot + Direct GitHubToolkit execution + No Queue, No Agent, No Gemini + """ - def __init__(self, queue_manager: AsyncQueueManager, **kwargs): + def __init__(self, **kwargs): intents = discord.Intents.default() intents.message_content = True intents.guilds = True @@ -25,20 +29,10 @@ def __init__(self, queue_manager: AsyncQueueManager, **kwargs): **kwargs ) - self.queue_manager = queue_manager - self.classifier = ClassificationRouter() self.active_threads: Dict[str, str] = {} - self._register_queue_handlers() - - def _register_queue_handlers(self): - """Register handlers for queue messages""" - self.queue_manager.register_handler( - "discord_response", - self._handle_agent_response - ) async def on_ready(self): - logger.info(f'Enhanced Discord bot logged in as {self.user}') + logger.info(f'Bot logged in as {self.user}') print(f'Bot is ready! Logged in as {self.user}') try: synced = await self.tree.sync() @@ -54,117 +48,32 @@ async def on_message(self, message): return try: - triage_result = await self.classifier.should_process_message( - message.content, - { - "channel_id": str(message.channel.id), - "user_id": str(message.author.id), - "guild_id": str(message.guild.id) if message.guild else None - } - ) - - if triage_result.get("needs_devrel", False): - await self._handle_devrel_message(message, triage_result) - - except Exception as e: - logger.error(f"Error processing message: {str(e)}") + user_id = str(message.author.id) + thread_id = await self._get_or_create_thread(message, user_id) - async def _handle_devrel_message( - self, - message, - triage_result: Dict[str, Any] - ): - """Handles both proactive responses and agent requests""" + thread = self.get_channel(int(thread_id)) + if thread: + await thread.send("Processing your request...") - try: - # PROACTIVE LAYER - if "proactive_type" in triage_result: - proactive_type = triage_result["proactive_type"] - - if proactive_type == "greeting": - await message.channel.send( - f"Hi {message.author.mention}! πŸ‘‹\n" - "Welcome to the community!\n" - "If you're new, I can guide you on how to start contributing πŸš€" - ) - return - - if proactive_type == "onboarding": - await message.channel.send( - f"Awesome {message.author.mention}! πŸŽ‰\n" - "Here's how you can start:\n" - - "1️⃣ Look for `good first issue`\n" - "2️⃣ Set up the project locally\n" - "3️⃣ Read CONTRIBUTING.md\n\n" - "Would you like me to suggest beginner-friendly issues?" - ) - return - - if proactive_type == "issue_suggestion": - await message.channel.send( - f"{message.author.mention} πŸ”\n" - "You can check open issues labeled `good first issue`.\n" - "Would you like me to fetch some right now?" - ) - return - - if proactive_type == "acknowledgment": - return - - # NORMAL AGENT FLOW (Fallback) + # πŸ”₯ Direct Toolkit Execution + toolkit = GitHubToolkit() + result = await toolkit.execute(message.content) - user_id = str(message.author.id) - thread_id = await self._get_or_create_thread(message, user_id) + response_text = result.get("message", "No response generated.") - agent_message = { - "type": "devrel_request", - "id": f"discord_{message.id}", - "user_id": user_id, - "channel_id": str(message.channel.id), - "thread_id": thread_id, - "memory_thread_id": user_id, - "content": message.content, - "triage": triage_result, - "classification": triage_result, - "platform": "discord", - "timestamp": message.created_at.isoformat(), - "author": { - "username": message.author.name, - "display_name": message.author.display_name, - "avatar_url": str(message.author.avatar.url) - if message.author.avatar else None - } - } - - 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) - - if thread_id: - thread = self.get_channel(int(thread_id)) - if thread: - await thread.send( - "I'm processing your request, please hold on..." - ) + if thread: + for i in range(0, len(response_text), 2000): + await thread.send(response_text[i:i+2000]) except Exception as e: - logger.error(f"Error handling DevRel message: {str(e)}") + logger.error(f"Error processing message: {str(e)}") async def _get_or_create_thread( self, message, user_id: str ) -> Optional[str]: + try: if user_id in self.active_threads: thread_id = self.active_threads[user_id] @@ -190,27 +99,4 @@ async def _get_or_create_thread( except Exception as e: logger.error(f"Failed to create thread: {e}") - return str(message.channel.id) - - async def _handle_agent_response( - self, - response_data: Dict[str, Any] - ): - try: - thread_id = response_data.get("thread_id") - response_text = response_data.get("response", "") - - if not thread_id or not response_text: - return - - thread = self.get_channel(int(thread_id)) - if thread: - for i in range(0, len(response_text), 2000): - await thread.send(response_text[i:i+2000]) - 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)}") + return str(message.channel.id) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index b7ad80a6..72f07c7e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,13 +9,7 @@ from app.api.router import api_router from app.core.config import settings -from app.core.orchestration.agent_coordinator import AgentCoordinator -from app.core.orchestration.queue_manager import AsyncQueueManager -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 logging.basicConfig( level=logging.INFO, @@ -26,77 +20,37 @@ class DevRAIApplication: """ - Manages the application's core components and background tasks. + DEV MODE - Only Discord bot. + No Queue. No Agent. No Gemini. No Weaviate. """ def __init__(self): - """Initializes all services required by the application.""" - self.weaviate_client = None - self.queue_manager = AsyncQueueManager() - self.agent_coordinator = AgentCoordinator(self.queue_manager) - self.discord_bot = DiscordBot(self.queue_manager) + self.discord_bot = DiscordBot() async def start_background_tasks(self): - """Starts the Discord bot and queue workers in the background.""" - try: - logger.info("Starting background tasks (Discord Bot & Queue Manager)...") - - await self.test_weaviate_connection() - - 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) + logger.info("Starting Discord bot (DEV MODE)...") - # Start the bot as a background task. - asyncio.create_task( - self.discord_bot.start(settings.discord_bot_token) - ) - logger.info("Background tasks started successfully!") - except Exception as e: - logger.error(f"Error during background task startup: {e}", exc_info=True) - await self.stop_background_tasks() - raise + asyncio.create_task( + self.discord_bot.start(settings.discord_bot_token) + ) - async def test_weaviate_connection(self): - """Test Weaviate connection during startup.""" - try: - async with get_weaviate_client() as client: - if await client.is_ready(): - logger.info("Weaviate connection successful and ready") - except Exception as e: - logger.error(f"Failed to connect to Weaviate: {e}") - raise + logger.info("Discord bot started successfully!") async def stop_background_tasks(self): - """Stops all background tasks and connections gracefully.""" - logger.info("Stopping background tasks and closing connections...") + logger.info("Stopping Discord bot...") try: if not self.discord_bot.is_closed(): await self.discord_bot.close() - logger.info("Discord bot has been closed.") - except Exception as e: - logger.error(f"Error closing Discord bot: {e}", exc_info=True) - try: - await self.queue_manager.stop() - logger.info("Queue manager has been stopped.") except Exception as e: - logger.error(f"Error stopping queue manager: {e}", exc_info=True) - logger.info("All background tasks and connections stopped.") + logger.error(f"Error closing Discord bot: {e}") -# --- FASTAPI LIFESPAN AND APP INITIALIZATION --- +# --- FASTAPI LIFESPAN --- app_instance = DevRAIApplication() + @asynccontextmanager async def lifespan(app: FastAPI): - """ - Lifespan manager for the FastAPI application. Handles startup and shutdown events. - """ - app.state.app_instance = app_instance await app_instance.start_background_tasks() yield await app_instance.stop_background_tasks() @@ -104,44 +58,27 @@ async def lifespan(app: FastAPI): api = FastAPI(title="Devr.AI API", version="1.0", lifespan=lifespan) -# Configure CORS api.add_middleware( CORSMiddleware, - allow_origins=[ - "http://localhost:5173", # Vite default dev server - "http://localhost:3000", # Alternative dev server - "http://127.0.0.1:5173", - "http://127.0.0.1:3000", - ], + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + @api.get("/favicon.ico") async def favicon(): - """Return empty favicon to prevent 404 logs""" return Response(status_code=204) + api.include_router(api_router) if __name__ == "__main__": - required_vars = [ - "DISCORD_BOT_TOKEN", "SUPABASE_URL", "SUPABASE_KEY", - "BACKEND_URL", "GEMINI_API_KEY", "TAVILY_API_KEY", "GITHUB_TOKEN" - ] - missing_vars = [var for var in required_vars if not getattr(settings, var.lower(), None)] - - if missing_vars: - logger.error(f"Missing required environment variables: {', '.join(missing_vars)}") - sys.exit(1) - uvicorn.run( "__main__:api", host="0.0.0.0", port=8000, - reload=True, - ws_ping_interval=20, - ws_ping_timeout=20 - ) + reload=True + ) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 59827539..a6c125a7 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,232 +1,19 @@ -aiohappyeyeballs==2.6.1 -aiohttp==3.12.12 -aio-pika==9.5.5 -aiosignal==1.3.2 +annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.9.0 -appdirs==1.4.4 -asgiref==3.8.1 -asttokens==3.0.0 -async-timeout==4.0.3 -attrs==25.3.0 -auth0-python==4.9.0 -Authlib==1.3.1 -autoflake==2.3.1 -autopep8==2.3.2 -backoff==2.2.1 -bcrypt==4.3.0 -blinker==1.9.0 -build==1.2.2.post1 -cachetools==5.5.2 -certifi==2025.4.26 -cffi==1.17.1 -charset-normalizer==3.4.2 -chroma-hnswlib==0.7.6 -chromadb==0.6.3 -click==8.1.8 -coloredlogs==15.0.1 -crewai==0.120.1 -cryptography==45.0.4 -decorator==5.2.1 -Deprecated==1.2.18 -deprecation==2.1.0 -docstring_parser==0.16 -durationpy==0.9 -et_xmlfile==2.0.0 -exceptiongroup==1.3.0 -executing==2.2.0 -fastapi==0.115.12 -filelock==3.18.0 -filetype==1.2.0 -flake8==7.2.0 -flatbuffers==25.2.10 -frozenlist==1.7.0 -fsspec==2025.5.1 -google-ai-generativelanguage==0.6.18 -google-api-core==2.25.1 -google-auth==2.40.3 -googleapis-common-protos==1.70.0 -gotrue==2.12.0 -greenlet==3.2.3 -grpcio==1.58.0 -grpcio-health-checking==1.58.0 -grpcio-status==1.58.0 -grpcio-tools==1.58.0 +anyio==4.12.1 +certifi==2026.1.4 +click==8.3.1 +colorama==0.4.6 +fastapi==0.129.0 h11==0.16.0 -h2==4.2.0 -hf-xet==1.1.3 -hpack==4.1.0 httpcore==1.0.9 -httptools==0.6.4 httpx==0.28.1 -huggingface-hub==0.33.0 -humanfriendly==10.0 -hyperframe==6.1.0 -idna==3.10 -importlib_metadata==8.6.1 -importlib_resources==6.5.2 -iniconfig==2.1.0 -instructor==1.8.2 -ipython==8.36.0 -isort==6.0.1 -jedi==0.19.2 -Jinja2==3.1.6 -jiter==0.8.2 -joblib==1.5.1 -json5==0.12.0 -json_repair==0.44.1 -jsonpatch==1.33 -jsonpickle==4.0.5 -jsonpointer==3.0.0 -jsonref==1.1.0 -jsonschema==4.23.0 -jsonschema-specifications==2025.4.1 -kubernetes==32.0.1 -langchain==0.3.26 -langchain-core==0.3.66 -langchain-google-genai==2.1.5 -langchain-tavily==0.2.2 -langchain-text-splitters==0.3.8 -langgraph==0.4.8 -langgraph-checkpoint==2.0.26 -langgraph-prebuilt==0.2.2 -langgraph-sdk==0.1.70 -langsmith==0.3.45 -litellm==1.68.0 -markdown-it-py==3.0.0 -MarkupSafe==3.0.2 -matplotlib-inline==0.1.7 -mccabe==0.7.0 -mdurl==0.1.2 -mmh3==5.1.0 -mpmath==1.3.0 -multidict==6.4.4 -mypy==1.16.0 -mypy_extensions==1.1.0 -networkx==3.2.1 -numpy==2.0.2 -nvidia-cublas-cu12==12.6.4.1 -nvidia-cuda-cupti-cu12==12.6.80 -nvidia-cuda-nvrtc-cu12==12.6.77 -nvidia-cuda-runtime-cu12==12.6.77 -nvidia-cudnn-cu12==9.5.1.17 -nvidia-cufft-cu12==11.3.0.4 -nvidia-cufile-cu12==1.11.1.6 -nvidia-curand-cu12==10.3.7.77 -nvidia-cusolver-cu12==11.7.1.2 -nvidia-cusparse-cu12==12.5.4.2 -nvidia-cusparselt-cu12==0.6.3 -nvidia-nccl-cu12==2.26.2 -nvidia-nvjitlink-cu12==12.6.85 -nvidia-nvtx-cu12==12.6.77 -oauthlib==3.2.2 -onnxruntime==1.22.0 -openai==1.75.0 -openpyxl==3.1.5 -opentelemetry-api==1.22.0 -opentelemetry-exporter-otlp-proto-common==1.22.0 -opentelemetry-exporter-otlp-proto-grpc==1.22.0 -opentelemetry-exporter-otlp-proto-http==1.22.0 -opentelemetry-instrumentation==0.42b0 -opentelemetry-instrumentation-asgi==0.42b0 -opentelemetry-instrumentation-fastapi==0.42b0 -opentelemetry-proto<1.31.0 -opentelemetry-sdk==1.22.0 -opentelemetry-semantic-conventions==0.42b0 -opentelemetry-util-http==0.42b0 -orjson==3.10.18 -ormsgpack==1.10.0 -overrides==7.7.0 -packaging==24.2 -parso==0.8.4 -pathspec==0.12.1 -pdfminer.six==20250327 -pdfplumber==0.11.6 -pexpect==4.9.0 -pillow==11.2.1 -pluggy==1.6.0 -postgrest==1.0.2 -posthog==4.0.1 -prompt_toolkit==3.0.51 -propcache==0.3.2 -proto-plus==1.26.1 -protobuf>=3.20.2,<4.0 -ptyprocess==0.7.0 -pure_eval==0.2.3 -py-cord==2.6.2 # Latest version to minimize deprecation warnings -pyasn1==0.6.1 -pyasn1_modules==0.4.2 -pycodestyle==2.13.0 -pycparser==2.22 -pydantic==2.11.6 -pydantic-settings==2.9.1 -pydantic_core==2.33.2 -pyflakes==3.3.2 -PyGithub==2.6.1 -Pygments==2.19.1 -PyJWT==2.10.1 -PyNaCl==1.5.0 -pypdfium2==4.30.1 -PyPika==0.48.9 -pyproject_hooks==1.2.0 -pytest==8.4.0 -pytest-mock==3.14.1 -python-dateutil==2.9.0.post0 -python-dotenv==1.1.1 -pyvis==0.3.2 -PyYAML==6.0.2 -realtime==2.4.3 -referencing==0.36.2 -regex==2024.11.6 -requests==2.32.4 -requests-oauthlib==2.0.0 -requests-toolbelt==1.0.0 -rich==13.9.4 -rpds-py==0.25.0 -rsa==4.9.1 -safetensors==0.5.3 -scikit-learn==1.7.0 -scipy==1.15.3 -sentence-transformers==3.4.1 -shellingham==1.5.4 -six==1.17.0 -slack_sdk==3.35.0 -sniffio==1.3.1 -SQLAlchemy==2.0.41 -stack-data==0.6.3 -starlette==0.46.2 -storage3==0.11.3 -StrEnum==0.4.15 -supabase==2.15.3 -supafunc==0.9.4 -sympy==1.14.0 -tavily-python==0.7.6 -tenacity==9.1.2 -threadpoolctl==3.6.0 -tiktoken==0.9.0 -tokenizers==0.21.1 -tomli==2.2.1 -tomli_w==1.2.0 -torch==2.7.1 -tqdm==4.67.1 -traitlets==5.14.3 -transformers==4.52.4 -triton==3.3.1 -typer==0.15.4 -typing-inspection==0.4.1 -typing_extensions==4.14.0 -urllib3==2.4.0 -uv==0.7.4 -uvicorn==0.34.2 -uvloop==0.21.0 -validators==0.34.0 -watchfiles==1.0.5 -wcwidth==0.2.13 -weaviate-client==4.15.4 -websocket-client==1.8.0 -websockets>=15.0.1,<16.0.0 -wrapt==1.17.2 -xxhash==3.5.0 -yarl==1.20.1 -zipp==3.21.0 -zstandard==0.23.0 +idna==3.11 +pydantic==2.12.5 +pydantic-settings==2.13.1 +pydantic_core==2.41.5 +python-dotenv==1.2.1 +starlette==0.52.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +uvicorn==0.41.0 diff --git a/backend/routes.py b/backend/routes.py index 7dbd6463..d03f2392 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,3 +1,7 @@ + +from services.github.issue_suggestion_service import IssueSuggestionService +from config import GITHUB_TOKEN, GITHUB_ORG + import asyncio import uuid import logging @@ -17,6 +21,7 @@ class RepoRequest(BaseModel): logging.basicConfig(level=logging.INFO) handler_registry = HandlerRegistry() event_bus = EventBus(handler_registry) +issue_service = IssueSuggestionService(GITHUB_TOKEN) # Sample handler function to process events async def sample_handler(event: BaseEvent): @@ -93,3 +98,32 @@ async def github_webhook(request: Request): logging.info(f"No matching event type for header: {event_header} with action: {payload.get('action')}") return {"status": "ok"} + +@router.get("/github/beginner-issues") +async def get_beginner_issues(repo: str): + if not GITHUB_TOKEN: + raise HTTPException( + status_code=500, + detail="GitHub token not configured" + ) + + try: + issues = await issue_service.fetch_beginner_issues( + owner=GITHUB_ORG, + repo=repo + ) + + return { + "repo": repo, + "count": len(issues), + "issues": issues + } + + except Exception as e: + logging.error(f"Error fetching beginner issues: {e}") + raise HTTPException( + status_code=500, + detail="Failed to fetch beginner issues" + ) + + diff --git a/backend/services/github/issue_suggestion_service.py b/backend/services/github/issue_suggestion_service.py new file mode 100644 index 00000000..37d75d96 --- /dev/null +++ b/backend/services/github/issue_suggestion_service.py @@ -0,0 +1,46 @@ +import httpx +from typing import List, Dict + +GITHUB_API_BASE = "https://api.github.com" + + +class IssueSuggestionService: + def __init__(self, token: str): + self.token = token + + async def fetch_global_beginner_issues( + self, + language: str = "python", + limit: int = 5 + ) -> List[Dict]: + + headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json" + } + + query = f'label:"good first issue" language:{language} state:open' + url = f"{GITHUB_API_BASE}/search/issues?q={query}&per_page={limit}" + + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + + if response.status_code != 200: + print("GitHub search failed:", response.text) + return [] + + data = response.json() + + items = data.get("items", []) + + results = [] + + for issue in items: + results.append({ + "number": issue["number"], + "title": issue["title"], + "url": issue["html_url"], + "repo": issue["repository_url"].split("/")[-1] + }) + + return results \ No newline at end of file