Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 88 additions & 60 deletions backend/app/agents/devrel/github/github_toolkit.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from app.services.github.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
Expand All @@ -19,26 +16,18 @@


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


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",
Expand All @@ -50,92 +39,131 @@ 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
# -----------------------------------------

elif classification == "find_good_first_issues":

service = IssueSuggestionService(settings.github_token_resolved)

issues = await service.fetch_beginner_issues(
language="python",
limit=10
)

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"

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"
}
}
53 changes: 33 additions & 20 deletions backend/app/agents/devrel/github/tools/general_github_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,48 @@
logger = logging.getLogger(__name__)


async def handle_general_github_help(query: str, llm) -> Dict[str, Any]:
"""Execute general GitHub help with web search and LLM knowledge"""
logger.info("Providing general GitHub help")
async def handle_general_github_help(query: str, llm=None) -> Dict[str, Any]:
"""
Execute general GitHub help using web search only (LLM removed)
"""

logger.info("Providing general GitHub help (LLM-free mode)")

try:
query = await _extract_search_query(query, llm)
# Extract search query safely (without LLM)
search_result = await handle_web_search(query)

if search_result.get("status") == "success":
search_context = "SEARCH RESULTS:\n"
for result in search_result.get("results", []):
search_context += f"- {result.get('title', 'No title')}: {result.get('content', 'No content')}\n"
else:
search_context = "No search results available."

help_prompt = GENERAL_GITHUB_HELP_PROMPT.format(
query=query,
search_context=search_context
)

response = await llm.ainvoke([HumanMessage(content=help_prompt)])
results = search_result.get("results", [])

if not results:
return {
"status": "success",
"sub_function": "general_github_help",
"query": query,
"response": "No relevant information found on GitHub.",
"message": "Provided GitHub help using web search only"
}

formatted = "\n\n".join(
f"{i+1}. {r.get('title', 'No title')}\n{r.get('content', 'No content')}"
for i, r in enumerate(results)
)

return {
"status": "success",
"sub_function": "general_github_help",
"query": query,
"response": f"Here are helpful GitHub resources:\n\n{formatted}",
"message": "Provided GitHub help using web search only"
}

return {
"status": "success",
"sub_function": "general_github_help",
"query": query,
"response": response.content.strip(),
"search_context": search_context,
"message": "Provided GitHub help using LLM expertise and web search"
"response": "No search results available.",
"message": "Provided GitHub help (no results found)"
}

except Exception as e:
Expand All @@ -46,5 +59,5 @@ async def handle_general_github_help(query: str, llm) -> Dict[str, Any]:
"sub_function": "general_github_help",
"query": query,
"error": str(e),
"message": "Failed to provide general GitHub help"
"message": "Failed to provide GitHub help"
}
8 changes: 8 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
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
from .v1.integrations import router as integrations_router

api_router = APIRouter()

api_router.include_router(
github_router,
prefix="/v1/github",
tags=["GitHub"]
)
Comment on lines +9 to +13
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all routes that serve beginner-issues
echo "=== Searching for beginner-issues registrations ==="
rg -n 'beginner.issues' --type=py -B2 -A2

echo -e "\n=== Checking backend/routes.py existence ==="
find . -name "routes.py" -type f | head -20

echo -e "\n=== Checking for github router in app/api ==="
find . -path "*/app/api*" -name "*.py" -type f | head -20

Repository: AOSSIE-Org/Devr.AI

Length of output: 3781


🏁 Script executed:

#!/bin/bash
echo "=== backend/routes.py: beginner-issues endpoint ==="
sed -n '102,127p' backend/routes.py

echo -e "\n=== backend/app/api/v1/github.py: beginner-issues endpoint ==="
sed -n '10,35p' backend/app/api/v1/github.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 1476


Remove or consolidate the duplicate beginner-issues endpoint.

Two endpoints serve identical functionality: /github/beginner-issues (from backend/routes.py) and /v1/github/beginner-issues (from backend/app/api/v1/github.py included in backend/app/api/router.py). Both call the same service method with identical parameters and nearly identical implementations. This duplication creates maintenance burden and inconsistency risk. Consolidate to a single endpoint.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/router.py` around lines 9 - 13, There are two duplicate
endpoints providing identical behavior at /github/beginner-issues
(backend/routes.py) and /v1/github/beginner-issues
(backend/app/api/v1/github.py) β€” remove the duplication by choosing one
canonical implementation (recommend keeping the versioned /v1/github endpoint in
backend/app/api/v1/github.py) and deleting the other handler in
backend/routes.py; update backend/app/api/router.py (api_router.include_router)
to ensure the chosen router (github_router) is the one exposing the retained
endpoint, remove any duplicate import or registration of the removed handler,
and run/update any tests or docs referencing the old path to point to the single
kept endpoint.



api_router.include_router(
auth_router,
prefix="/v1/auth",
Expand Down
43 changes: 43 additions & 0 deletions backend/app/api/v1/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from fastapi import APIRouter, HTTPException
from app.services.github.issue_suggestion_service import IssueSuggestionService
from app.core.config import settings

router = APIRouter()


@router.get("/beginner-issues")
async def get_beginner_issues(
language: str = "python",
limit: int = 5
):
"""
Fetch global beginner-friendly GitHub issues.
"""

token = settings.github_token_resolved

if not token:
raise HTTPException(
status_code=500,
detail="GitHub token not configured"
)

issue_service = IssueSuggestionService(token)

try:
issues = await issue_service.fetch_beginner_issues(
language=language,
limit=limit
)

return {
"language": language,
"count": len(issues),
"issues": issues
}

except Exception as e:
raise HTTPException(
status_code=500,
detail="Failed to fetch beginner issues"
) from e
Loading