Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
147 changes: 87 additions & 60 deletions backend/app/agents/devrel/github/github_toolkit.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,26 +17,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 +40,129 @@ 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)

Copy link
Copy Markdown
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
# Check for github_token in app/core/config.py and GITHUB_TOKEN in config files
echo "=== Searching for github_token in app/core/config.py ==="
rg -n "github_token" backend/app/core/config.py

echo -e "\n=== Searching for GITHUB_TOKEN in config files ==="
rg -n "GITHUB_TOKEN" backend/config.py 2>/dev/null || echo "backend/config.py not found"

echo -e "\n=== Searching for all config references ==="
fd "config.py" backend/ --type f

echo -e "\n=== Check full app/core/config.py structure ==="
cat backend/app/core/config.py | head -50

Repository: AOSSIE-Org/Devr.AI

Length of output: 494


🏁 Script executed:

#!/bin/bash
# Search for IssueSuggestionService instantiation in routes.py and app/api/v1/github.py
echo "=== Searching for IssueSuggestionService in routes.py ==="
rg -n "IssueSuggestionService" backend/app/routes.py -A 1 -B 1

echo -e "\n=== Searching for IssueSuggestionService in app/api/v1/github.py ==="
rg -n "IssueSuggestionService" backend/app/api/v1/github.py -A 1 -B 1

echo -e "\n=== Searching for all IssueSuggestionService usages ==="
rg -n "IssueSuggestionService" backend/ -A 1 -B 1

Repository: AOSSIE-Org/Devr.AI

Length of output: 1891


🏁 Script executed:

#!/bin/bash
# Check full github_toolkit.py for settings import
echo "=== First 20 lines of github_toolkit.py ==="
head -20 backend/app/agents/devrel/github/github_toolkit.py

echo -e "\n=== Search for settings import in github_toolkit.py ==="
rg -n "^from|^import" backend/app/agents/devrel/github/github_toolkit.py | head -20

echo -e "\n=== Look for all settings references ==="
rg -n "settings\." backend/app/agents/devrel/github/github_toolkit.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 1446


🏁 Script executed:

#!/bin/bash
# Find where settings object is defined
echo "=== Search for Settings class definition ==="
rg -n "class Settings" backend/ -A 3

echo -e "\n=== Search for settings attribute assignments ==="
rg -n "github_token\s*=" backend/ -B 2 -A 1

echo -e "\n=== Check if settings module exists ==="
fd "settings" backend/ --type f

Repository: AOSSIE-Org/Devr.AI

Length of output: 473


🏁 Script executed:

#!/bin/bash
# Check the full Settings class definition
echo "=== Full Settings class in app/core/config/settings.py ==="
cat backend/app/core/config/settings.py

echo -e "\n=== Check app/core/config/__init__.py ==="
cat backend/app/core/config/__init__.py 2>/dev/null || echo "File not found"

Repository: AOSSIE-Org/Devr.AI

Length of output: 1654


🏁 Script executed:

#!/bin/bash
# Check config.py for full token configuration
echo "=== Full backend/config.py ==="
cat backend/config.py

echo -e "\n=== Check if github_token is in any config ==="
rg -n "github_token" backend/

Repository: AOSSIE-Org/Devr.AI

Length of output: 1017


Inconsistent token source β€” settings.github_token vs config.GITHUB_TOKEN.

routes.py and app/api/v1/github.py instantiate IssueSuggestionService(GITHUB_TOKEN) from config (direct os.getenv() with fallback to GH_TOKEN), while github_toolkit.py uses settings.github_token from app.core.config (Pydantic BaseSettings). Although both attributes exist, they read from environment variables through different mechanisms with different fallback behaviors:

  • config.GITHUB_TOKEN: os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN")
  • settings.github_token: Pydantic BaseSettings (defaults to empty string if not set)

This inconsistency could cause IssueSuggestionService to receive different token values or fail silently if one configuration path is properly set while the other isn't.

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

In `@backend/app/agents/devrel/github/github_toolkit.py` at line 121, The
instantiation of IssueSuggestionService in github_toolkit.py uses
settings.github_token which is inconsistent with other callers using
config.GITHUB_TOKEN; update the IssueSuggestionService construction to use the
same token source as the rest of the codebase (config.GITHUB_TOKEN) or modify
the Settings model so settings.github_token implements the same fallback
(os.getenv("GITHUB_TOKEN") or os.getenv("GH_TOKEN"))β€”ensure the change touches
the IssueSuggestionService(...) call in github_toolkit.py and aligns it with the
token used in routes.py and app/api/v1/github.py to avoid divergent values.


# βœ… FIXED β€” passing query argument
issues = await service.fetch_global_beginner_issues(query)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

fetch_global_beginner_issues does not exist β€” this will raise AttributeError at runtime.

IssueSuggestionService (imported from services.github.issue_suggestion_service) only defines fetch_beginner_issues(self, language, limit). There is no fetch_global_beginner_issues method, and passing a free-text query string to it wouldn't match the (language, limit) signature anyway.

πŸ› Proposed fix
-                # βœ… FIXED β€” passing query argument
-                issues = await service.fetch_global_beginner_issues(query)
+                # Extract language from query if possible, or default to "python"
+                issues = await service.fetch_beginner_issues(language="python", limit=5)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elif classification == "find_good_first_issues":
service = IssueSuggestionService(settings.github_token)
# βœ… FIXED β€” passing query argument
issues = await service.fetch_global_beginner_issues(query)
elif classification == "find_good_first_issues":
service = IssueSuggestionService(settings.github_token)
# Extract language from query if possible, or default to "python"
issues = await service.fetch_beginner_issues(language="python", limit=5)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/agents/devrel/github/github_toolkit.py` around lines 119 - 124,
In the "find_good_first_issues" branch replace the non-existent call to
service.fetch_global_beginner_issues(query) with the existing
IssueSuggestionService method: call service.fetch_beginner_issues(language,
limit) (e.g., pass the query as the language argument or map the query to a
language and choose a sensible default limit like 10), or alternatively add a
new wrapper method fetch_global_beginner_issues(query) to IssueSuggestionService
that adapts the free-text query into the (language, limit) signature; locate
IssueSuggestionService.fetch_beginner_issues and the classification branch where
service.fetch_global_beginner_issues is invoked to make the change.


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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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"
}
}
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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
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
Copy Markdown
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
34 changes: 34 additions & 0 deletions backend/app/api/v1/github.py
Original file line number Diff line number Diff line change
@@ -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
)
Comment on lines +27 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

fetch_beginner_issues does not exist β€” this will raise AttributeError at runtime.

The imported IssueSuggestionService (from services.github.issue_suggestion_service) only defines fetch_global_beginner_issues(self, language, limit). There is no fetch_beginner_issues method, and the call signature (owner=..., repo=...) doesn't match any method either. This endpoint is completely broken.

Proposed fix (adjust to the actual method signature)
         issues = await issue_service.fetch_beginner_issues(
-            owner=GITHUB_ORG,
-            repo=repo
+        issues = await issue_service.fetch_global_beginner_issues(
+            language="python",  # or derive from query param
+            limit=10
         )

You will also need to add language as a query parameter to the endpoint if you want it to be configurable.

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

In `@backend/app/api/v1/github.py` around lines 18 - 22, The endpoint currently
calls a non-existent method
issue_service.fetch_beginner_issues(owner=GITHUB_ORG, repo=repo); replace this
call with the correct service method fetch_global_beginner_issues and pass the
proper parameters (language and limit) matching
IssueSuggestionService.fetch_global_beginner_issues; update the API handler (in
backend/app/api/v1/github.py) to accept a language query parameter (and a
sensible default) and forward it to
issue_service.fetch_global_beginner_issues(language=language, limit=...) so the
call signature matches the service.


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)}"
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Loading