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
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)

# βœ… 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)
Comment on lines 152 to +153
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 | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and inspect the general_github_help.py file
find . -name "general_github_help.py" -type f

Repository: AOSSIE-Org/Devr.AI

Length of output: 191


🏁 Script executed:

# Also look at the github_toolkit.py file around lines 152-153 to see the full context
cat -n backend/app/agents/devrel/github/github_toolkit.py | sed -n '140,160p'

Repository: AOSSIE-Org/Devr.AI

Length of output: 919


🏁 Script executed:

# Search for handle_general_github_help function definition
rg "def handle_general_github_help|async def handle_general_github_help" -A 20

Repository: AOSSIE-Org/Devr.AI

Length of output: 2239


🏁 Script executed:

rg "_extract_search_query" -A 10 backend/app/agents/devrel/github/tools/general_github_help.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 1027


🏁 Script executed:

# Also check where llm is injected/initialized in github_toolkit.py
rg "self.llm|self\.llm" backend/app/agents/devrel/github/github_toolkit.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 44


🏁 Script executed:

rg "_extract_search_query" -B 5 -A 15 backend/app/agents/devrel/nodes/handlers/web_search.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 1628


🏁 Script executed:

# Also check all calls to handle_general_github_help to see if None is always passed
rg "handle_general_github_help" backend/app/agents/devrel/github/github_toolkit.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 198


handle_general_github_help(query, None) will crash β€” llm cannot be None.

The fallback path at line 153 passes None as the llm argument. The function immediately calls _extract_search_query(query, llm), which invokes await llm.ainvoke(...). This will raise AttributeError: 'NoneType' object has no attribute 'ainvoke'. Every unclassified query will hit this path and crash.

Either initialize an LLM instance in GitHubToolkit to pass here, or provide a non-LLM fallback for the general_github_help path.

πŸ€– 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 152 - 153,
The fallback call passing None to handle_general_github_help causes an
AttributeError because handle_general_github_help calls _extract_search_query
which uses llm. Fix by supplying a valid LLM instance instead of None: update
the fallback to pass the toolkit's LLM (e.g., self.llm or the LLM field on
GitHubToolkit) or ensure GitHubToolkit initializes a default LLM during
construction and then call handle_general_github_help(query, self.llm);
alternatively implement a safe non-LLM fallback in handle_general_github_help
that does not call _extract_search_query when llm is 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"
}
}
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 on lines +36 to +42
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:

cat -n backend/app/agents/devrel/github/services/issue_suggestion_service.py | head -70

Repository: AOSSIE-Org/Devr.AI

Length of output: 2196


🏁 Script executed:

# Check if there are any other similar files that might show patterns
find . -name "*issue_suggestion_service*" -type f

Repository: AOSSIE-Org/Devr.AI

Length of output: 187


🌐 Web query:

httpx AsyncClient URL encoding f-string vs params kwarg query string

πŸ’‘ Result:

In httpx (including AsyncClient), prefer params= (or httpx.URL(..., params=...)) over building a query string with an f-string:

  • params= is the supported API for query strings and accepts dict, str, or [(k, v), ...]. httpx will serialize/encode the parameters for you. [1][2]
  • httpx.QueryParams/params= encoding is form-style (e.g., spaces become +, @ becomes %40, ! becomes %21). [1]
  • Some characters may be treated as β€œsafe” and not percent-encoded (notably /), which can surprise people coming from requests; this is discussed as an intentional behavior change in httpx. [3]

Recommended

import httpx

async with httpx.AsyncClient() as client:
    r = await client.get(
        "https://api.example.com/search",
        params={"q": "How HTTP works!", "email": "user@example.com"},
    )

If you use an f-string/manual URL

You must encode yourself (and avoid double-encoding):

from urllib.parse import urlencode

q = urlencode({"q": "How HTTP works!", "email": "user@example.com"})
url = f"https://api.example.com/search?{q}"

If an API requires / to be encoded inside a parameter value

You may need to pre-quote that specific value before passing it (because httpx may leave / unescaped), e.g. for q="http://example.com"; this exact issue is reported/discussed. [3]

Sources: httpx URL/QueryParams docs [1], AsyncClient params API [2], httpx issue on query param encoding and / handling [3].


Search query is not URL-encoded β€” spaces and quotes will be sent raw to GitHub API

The search_query string (e.g. label:"good first issue" is:issue state:open language:python) contains spaces and double-quotes. Embedding it directly in an f-string URL bypasses httpx's query parameter encoding, sending unencoded characters to the GitHub API. This will result in malformed requests and unpredictable behavior.

Use the params= keyword argument instead, which httpx will properly URL-encode:

πŸ”§ Proposed fix
-        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)
+            response = await client.get(
+                f"{GITHUB_API_BASE}/search/issues",
+                headers=headers,
+                params={"q": search_query, "per_page": limit},
+            )

Additionally, remove debug print() statements (lines 38–39, 45–46, 61) from production code.

πŸ“ 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
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)
async with httpx.AsyncClient() as client:
response = await client.get(
f"{GITHUB_API_BASE}/search/issues",
headers=headers,
params={"q": search_query, "per_page": limit},
)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/agents/devrel/github/services/issue_suggestion_service.py` around
lines 36 - 42, The code builds the GitHub search URL by interpolating
search_query into an f-string (see the url variable and the client.get call in
issue_suggestion_service.py) which sends unencoded spaces/quotes; change the
request to call client.get(GITHUB_API_BASE + "/search/issues", params={"q":
search_query, "per_page": limit}, headers=headers) so httpx handles URL
encoding, remove the manual url f-string, and delete the debug print()
statements present around the search_query/url and response logging.


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"]
})
Comment on lines +53 to +59
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

Direct key access will raise KeyError on partial or unexpected GitHub API responses

All four field accesses on the item dict ("repository_url", "number", "title", "html_url") will raise a KeyError if a response item omits any field, silently aborting the entire result set instead of skipping the malformed entry.

πŸ”§ Proposed fix β€” use `.get()` and skip incomplete entries
         for item in data.get("items", []):
-            results.append({
-                "repo": item["repository_url"].split("/")[-1],
-                "number": item["number"],
-                "title": item["title"],
-                "url": item["html_url"]
-            })
+            repo_url = item.get("repository_url", "")
+            number = item.get("number")
+            title = item.get("title")
+            url = item.get("html_url")
+            if not all([repo_url, number, title, url]):
+                continue
+            results.append({
+                "repo": repo_url.split("/")[-1],
+                "number": number,
+                "title": title,
+                "url": url,
+            })
πŸ“ 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
for item in data.get("items", []):
results.append({
"repo": item["repository_url"].split("/")[-1],
"number": item["number"],
"title": item["title"],
"url": item["html_url"]
})
for item in data.get("items", []):
repo_url = item.get("repository_url", "")
number = item.get("number")
title = item.get("title")
url = item.get("html_url")
if not all([repo_url, number, title, url]):
continue
results.append({
"repo": repo_url.split("/")[-1],
"number": number,
"title": title,
"url": url,
})
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/agents/devrel/github/services/issue_suggestion_service.py` around
lines 53 - 59, The loop that builds results from data.get("items", []) uses
direct key access on item["repository_url"], item["number"], item["title"], and
item["html_url"], which can raise KeyError on partial GitHub API responses;
update the loop in issue_suggestion_service.py (where results is appended) to
safely read fields via item.get(...) and validate that required fields
(repository_url, number, title, html_url) are present before appending, skip
(and optionally log) any incomplete entries, and extract the repo name from
repository_url defensively (e.g., handle None or empty string before splitting).


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 +1 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
# Check if files exist
echo "=== Checking file existence ==="
ls -la backend/app/api/v1/github.py 2>&1 || echo "File not found"
ls -la backend/routes.py 2>&1 || echo "File not found"

echo ""
echo "=== Routes in backend/app/api/v1/github.py ==="
if [ -f backend/app/api/v1/github.py ]; then
  rg -n 'beginner|@router|def ' backend/app/api/v1/github.py | head -50
fi

echo ""
echo "=== Routes in backend/routes.py ==="
if [ -f backend/routes.py ]; then
  rg -n 'beginner|@router|def ' backend/routes.py | head -50
fi

Repository: AOSSIE-Org/Devr.AI

Length of output: 904


🏁 Script executed:

#!/bin/bash
echo "=== Full content of backend/app/api/v1/github.py ==="
cat -n backend/app/api/v1/github.py

echo ""
echo "=== Full content of backend/routes.py ==="
cat -n backend/routes.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 6730


Duplicate beginner-issues endpoints across two routers.

This wires github_router at /v1/github (exposing /v1/github/beginner-issues), while backend/routes.py also defines /github/beginner-issues on a separate router. Both endpoints are functionally identicalβ€”they call fetch_beginner_issues(owner=GITHUB_ORG, repo=repo) on separate IssueSuggestionService instances and return the same response structure. Consolidate to a single endpoint to avoid confusion and redundancy.

πŸ€– 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 1 - 13, The repo exposes the same
beginner-issues endpoint twice (api_router includes github_router at /v1/github
exposing /v1/github/beginner-issues while backend/routes.py separately registers
/github/beginner-issues), so remove the duplicate by consolidating to a single
router: choose the canonical implementation (either the endpoint in .v1.github's
router or the one in backend/routes.py), delete the other duplicate route
registration, and update imports/usages accordingly; ensure the remaining
endpoint still calls fetch_beginner_issues(owner=GITHUB_ORG, repo=repo) on
IssueSuggestionService and returns the same response shape, and remove or adjust
any tests/config that expect the removed path.



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
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 | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Search for fetch_beginner_issues definition
echo "=== Searching for fetch_beginner_issues definition ==="
rg -n "def fetch_beginner_issues" --type py

echo ""
echo "=== Methods on service classes (issue_suggestion_service) ==="
rg -n "def fetch" --type py -g "*issue_suggestion_service*" -A 2

echo ""
echo "=== Locating issue_suggestion_service.py ==="
fd "issue_suggestion_service.py"

echo ""
echo "=== Checking github.py lines 15-25 for actual method call ==="
fd "github.py" -g "*/api/v1/*" -x cat -n {} | sed -n '15,25p'

Repository: AOSSIE-Org/Devr.AI

Length of output: 1087


🏁 Script executed:

#!/bin/bash

# Find the github.py file in api/v1
find . -path "*/api/v1/github.py" -type f

echo ""
echo "=== Reading backend/app/api/v1/github.py ==="
cat -n backend/app/api/v1/github.py | head -30

Repository: AOSSIE-Org/Devr.AI

Length of output: 1084


🏁 Script executed:

#!/bin/bash
cat -n backend/services/github/issue_suggestion_service.py

Repository: AOSSIE-Org/Devr.AI

Length of output: 1619


fetch_beginner_issues does not exist on the imported service β€” AttributeError at runtime

Line 19 calls issue_service.fetch_beginner_issues(owner=GITHUB_ORG, repo=repo), but the imported IssueSuggestionService only defines fetch_global_beginner_issues(language: str, limit: int). This method does not exist and will raise AttributeError on every request to the /beginner-issues endpoint.

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

In `@backend/app/api/v1/github.py` at line 2, The handler is calling a
non-existent method issue_service.fetch_beginner_issues which will raise
AttributeError; update the call to use the existing
IssueSuggestionService.fetch_global_beginner_issues (or add a new method
fetch_beginner_issues to IssueSuggestionService) so signatures match: either
replace issue_service.fetch_beginner_issues(owner=GITHUB_ORG, repo=repo) with
issue_service.fetch_global_beginner_issues(language, limit) passing appropriate
language/limit values, or implement fetch_beginner_issues(owner, repo, ...) in
IssueSuggestionService that delegates to fetch_global_beginner_issues and
returns the expected shape.

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"
)
Comment on lines +7 to +16
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

Service instantiated with a potentially None token before the guard is checked

Line 7 creates IssueSuggestionService(GITHUB_TOKEN) at module load time, binding whatever value GITHUB_TOKEN holds (possibly None or empty) into self.token permanently. The if not GITHUB_TOKEN guard on Line 12 runs only per-request and long after the damage is done β€” outbound GitHub API calls will silently use an invalid token. Either lazily initialise the service, or fail fast at startup.

πŸ›‘οΈ Suggested fix β€” lazy initialisation with fail-fast guard
-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"
         )
+
+    issue_service = IssueSuggestionService(GITHUB_TOKEN)

     try:
πŸ€– 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 7 - 16, IssueSuggestionService is
instantiated at module import with GITHUB_TOKEN which can be None, so outbound
calls may silently use an invalid token; change to either validate GITHUB_TOKEN
at import and raise (fail-fast) or lazily create the service inside the request
handler (e.g., move creation of IssueSuggestionService into get_beginner_issues
and construct it after checking GITHUB_TOKEN), referencing
IssueSuggestionService and get_beginner_issues to locate the code and ensure the
token check precedes any service construction or API calls.


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)}"
)
Loading