Skip to content

Commit 345a68a

Browse files
Merge branch 'main' into FAQ-HANDLER
2 parents ef640d2 + a01d7ea commit 345a68a

File tree

119 files changed

+22248
-8807
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+22248
-8807
lines changed

README.md

Lines changed: 135 additions & 764 deletions
Large diffs are not rendered by default.

assets/images/DevrAI.svg

Lines changed: 3 additions & 0 deletions
Loading

backend/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ DISCORD_BOT_TOKEN=
99
# ENABLE_DISCORD_BOT=true
1010

1111
GITHUB_TOKEN=
12+
# Add Org Name here
13+
GITHUB_ORG=
1214

1315
# EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
1416
# EMBEDDING_MAX_BATCH_SIZE=32

backend/app/agents/devrel/github/github_toolkit.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import logging
2+
import json
3+
import re
4+
import config
25
from typing import Dict, Any
36
from langchain_google_genai import ChatGoogleGenerativeAI
47
from langchain_core.messages import HumanMessage
58
from app.core.config import settings
69
from .prompts.intent_analysis import GITHUB_INTENT_ANALYSIS_PROMPT
710
from .tools.search import handle_web_search
8-
# TODO: Implement all tools
11+
from .tools.github_support import handle_github_supp
912
from .tools.contributor_recommendation import handle_contributor_recommendation
10-
# from .tools.repository_query import handle_repo_query
11-
# from .tools.issue_creation import handle_issue_creation
12-
# from .tools.documentation_generation import handle_documentation_generation
1313
from .tools.general_github_help import handle_general_github_help
14+
from .tools.repo_support import handle_repo_support
15+
1416
logger = logging.getLogger(__name__)
1517

18+
DEFAULT_ORG = config.GITHUB_ORG
19+
20+
21+
def normalize_org(org_from_user: str = None) -> str:
22+
"""Fallback to env org if user does not specify one."""
23+
if org_from_user and org_from_user.strip():
24+
return org_from_user.strip()
25+
return DEFAULT_ORG
26+
1627

1728
class GitHubToolkit:
1829
"""
@@ -32,30 +43,37 @@ def __init__(self):
3243
"web_search",
3344
"contributor_recommendation",
3445
"repo_support",
46+
"github_support",
3547
"issue_creation",
3648
"documentation_generation",
3749
"find_good_first_issues",
3850
"general_github_help"
3951
]
4052

4153
async def classify_intent(self, user_query: str) -> Dict[str, Any]:
42-
"""
43-
Classify intent and return classification with reasoning.
44-
45-
Args:
46-
user_query: The user's request or question
47-
48-
Returns:
49-
Dictionary containing classification, reasoning, and confidence
50-
"""
54+
"""Classify intent and return classification with reasoning."""
5155
logger.info(f"Classifying intent for query: {user_query[:100]}")
5256

5357
try:
5458
prompt = GITHUB_INTENT_ANALYSIS_PROMPT.format(user_query=user_query)
5559
response = await self.llm.ainvoke([HumanMessage(content=prompt)])
5660

57-
import json
58-
result = json.loads(response.content.strip())
61+
content = response.content.strip()
62+
63+
try:
64+
result = json.loads(content)
65+
except json.JSONDecodeError:
66+
match = re.search(r"\{.*\}", content, re.DOTALL)
67+
if match:
68+
result = json.loads(match.group())
69+
else:
70+
logger.error(f"Invalid JSON in LLM response: {content}")
71+
return {
72+
"classification": "general_github_help",
73+
"reasoning": "Failed to parse LLM response as JSON",
74+
"confidence": "low",
75+
"query": user_query
76+
}
5977

6078
classification = result.get("classification")
6179
if classification not in self.tools:
@@ -65,21 +83,12 @@ async def classify_intent(self, user_query: str) -> Dict[str, Any]:
6583

6684
result["query"] = user_query
6785

68-
logger.info(f"Classified intent as for query: {user_query} is: {classification}")
86+
logger.info(f"Classified intent for query: {user_query} -> {classification}")
6987
logger.info(f"Reasoning: {result.get('reasoning', 'No reasoning provided')}")
7088
logger.info(f"Confidence: {result.get('confidence', 'unknown')}")
7189

7290
return result
7391

74-
except json.JSONDecodeError as e:
75-
logger.error(f"Error parsing JSON response from LLM: {str(e)}")
76-
logger.error(f"Raw response: {response.content}")
77-
return {
78-
"classification": "general_github_help",
79-
"reasoning": f"Failed to parse LLM response: {str(e)}",
80-
"confidence": "low",
81-
"query": user_query
82-
}
8392
except Exception as e:
8493
logger.error(f"Error in intent classification: {str(e)}")
8594
return {
@@ -90,9 +99,7 @@ async def classify_intent(self, user_query: str) -> Dict[str, Any]:
9099
}
91100

92101
async def execute(self, query: str) -> Dict[str, Any]:
93-
"""
94-
Main execution method - classifies intent and delegates to appropriate tools
95-
"""
102+
"""Main execution method - classifies intent and delegates to appropriate tools"""
96103
logger.info(f"Executing GitHub toolkit for query: {query[:100]}")
97104

98105
try:
@@ -103,15 +110,16 @@ async def execute(self, query: str) -> Dict[str, Any]:
103110

104111
if classification == "contributor_recommendation":
105112
result = await handle_contributor_recommendation(query)
113+
elif classification == "github_support":
114+
org = normalize_org()
115+
result = await handle_github_supp(query, org=org)
116+
result["org_used"] = org
106117
elif classification == "repo_support":
107-
result = "Not implemented"
108-
# result = await handle_repo_query(query)
118+
result = await handle_repo_support(query)
109119
elif classification == "issue_creation":
110120
result = "Not implemented"
111-
# result = await handle_issue_creation(query)
112121
elif classification == "documentation_generation":
113122
result = "Not implemented"
114-
# result = await handle_documentation_generation(query)
115123
elif classification == "web_search":
116124
result = await handle_web_search(query)
117125
else:

backend/app/agents/devrel/github/prompts/intent_analysis.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,52 @@
11
GITHUB_INTENT_ANALYSIS_PROMPT = """You are an expert GitHub DevRel AI assistant. Analyze the user query and classify the intent.
22
33
AVAILABLE FUNCTIONS:
4-
- web_search: Search the web for information
5-
- contributor_recommendation: Finding the right people to review PRs, assign issues, or collaborate (supports both issue URLs and general queries)
6-
- repo_support: Questions about codebase structure, dependencies, impact analysis, architecture
7-
- issue_creation: Creating bug reports, feature requests, or tracking items
8-
- documentation_generation: Generating docs, READMEs, API docs, guides, or explanations
9-
- find_good_first_issues: Finding beginner-friendly issues to work on across repositories
10-
- general_github_help: General GitHub-related assistance and guidance
4+
- github_support: Repository metadata (stars, forks, issues count, description)
5+
- repo_support: Code structure queries (WHERE code is, WHAT implements feature, HOW it works)
6+
* "Where is authentication in X repo?"
7+
* "Show me API endpoints"
8+
* "Find database models"
9+
* NOTE: Repo must be indexed first
10+
- contributor_recommendation: Find people for PRs/issues
11+
- web_search: External information
12+
- issue_creation: Create bugs/features
13+
- documentation_generation: Generate docs
14+
- find_good_first_issues: Beginner issues
15+
- general_github_help: General GitHub help
1116
1217
USER QUERY: {user_query}
1318
1419
Classification guidelines:
20+
- github_support:
21+
- ALWAYS classify as `github_support` if the query asks about:
22+
- repository information
23+
- stats (stars, forks, watchers, issues)
24+
- open issues, closed issues, or "what issues"
25+
- description, license, URL, metadata
26+
- any question containing "<repo> repo", "repository", "repo", "issues in", "stars in", "forks in"
27+
- Example queries:
28+
- "What all issues are in Dev.ai repo?" → github_support
29+
- "How many stars does Devr.AI repo have?" → github_support
30+
- "Show me forks of Aossie-org/Dev.ai" → github_support
1531
- contributor_recommendation:
1632
* "who should review this PR/issue?"
1733
* "find experts in React/Python/ML"
1834
* "recommend assignees for stripe integration"
1935
* "best people for database optimization"
2036
* URLs like github.com/owner/repo/issues/123
2137
* "I need help with RabbitMQ, can you suggest some people?"
22-
- repo_support: Code structure, dependencies, impact analysis, architecture
38+
- repo_support: Code structure, dependencies, impact analysis, architecture
2339
- issue_creation: Creating bugs, features, tracking items
2440
- documentation_generation: Docs, READMEs, guides, explanations
2541
- find_good_first_issues: Beginners, newcomers, "good first issue"
26-
- web_search: General information needing external search
42+
- web_search: Only for information that cannot be found through GitHub API (like news, articles, external documentation)
2743
- general_github_help: General GitHub questions not covered above
2844
45+
IMPORTANT:
46+
- Repository information queries (issues count, stars, forks, description) should ALWAYS use github_support, not web_search.
47+
- github_support: "How many stars does X have?" (metadata)
48+
- repo_support: "Where is auth in X?" (code structure)
49+
2950
CRITICAL: Return ONLY raw JSON. No markdown, no code blocks, no explanation text.
3051
3152
{{
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import logging
2+
from typing import Dict, Any, Optional, List, Union
3+
import aiohttp
4+
import asyncio
5+
import config
6+
7+
logger = logging.getLogger(__name__)
8+
9+
class GitHubMCPClient:
10+
"""Client for communicating with the GitHub MCP server."""
11+
12+
def __init__(self, mcp_server_url: str = "http://localhost:8001"):
13+
self.mcp_server_url = mcp_server_url
14+
self.session: Optional[aiohttp.ClientSession] = None
15+
# Default org pulled from environment
16+
self.org = config.GITHUB_ORG
17+
18+
async def __aenter__(self):
19+
# Async context manager entry
20+
self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15))
21+
return self
22+
23+
async def __aexit__(self, exc_type, exc_val, exc_tb):
24+
# Async context manager exit
25+
if self.session:
26+
await self.session.close()
27+
28+
async def get_github_supp(self, repo: str, owner: Optional[str] = None) -> Dict[str, Any]:
29+
"""
30+
Fetch metadata for a single repository.
31+
Owner defaults to org from environment if not provided.
32+
"""
33+
if not self.session:
34+
raise RuntimeError("Client not initialized. Use async context manager.")
35+
36+
owner = owner or self.org
37+
38+
try:
39+
payload = {"owner": owner, "repo": repo}
40+
41+
async with self.session.post(
42+
f"{self.mcp_server_url}/github_support",
43+
json=payload,
44+
headers={"Content-Type": "application/json"},
45+
) as response:
46+
if response.status == 200:
47+
result = await response.json()
48+
if result.get("status") == "success":
49+
return result.get("data", {})
50+
else:
51+
return {"error": result.get("error", "Unknown error")}
52+
else:
53+
logger.error(f"MCP server error: {response.status}")
54+
return {"error": f"MCP server error: {response.status}"}
55+
56+
except aiohttp.ClientError as e:
57+
logger.exception("Error communicating with MCP server: %s", e)
58+
return {"error": f"Communication error: {str(e)}"}
59+
except Exception as e:
60+
logger.exception("Unexpected error: %s", e)
61+
return {"error": f"Unexpected error: {str(e)}"}
62+
63+
async def list_org_repos(self, org: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
64+
if not self.session:
65+
raise RuntimeError("Client not initialized. Use async context manager.")
66+
67+
try:
68+
payload = {"org": org}
69+
async with self.session.post(
70+
f"{self.mcp_server_url}/list_org_repos",
71+
json=payload,
72+
headers={"Content-Type": "application/json"},
73+
) as response:
74+
if response.status == 200:
75+
result = await response.json()
76+
if result.get("status") == "success":
77+
return result.get("data", [])
78+
else:
79+
return {"error": result.get("error", "Unknown error")}
80+
else:
81+
logger.error(f"MCP server error: {response.status}")
82+
return {"error": f"MCP server error: {response.status}"}
83+
except aiohttp.ClientError as e:
84+
logger.error(f"Error communicating with MCP server: {e}")
85+
return {"error": f"Communication error: {str(e)}"}
86+
except Exception as e:
87+
logger.error(f"Unexpected error: {e}")
88+
return {"error": f"Unexpected error: {str(e)}"}
89+
90+
91+
async def is_server_available(self) -> bool:
92+
"""Health check for MCP server."""
93+
if not self.session:
94+
return False
95+
96+
try:
97+
async with self.session.get(f"{self.mcp_server_url}/health", timeout=5) as response:
98+
return response.status == 200
99+
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
100+
logger.debug(f"Health check failed: {e}")
101+
return False
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
import logging
3+
import asyncio
4+
import config
5+
from fastapi import FastAPI, HTTPException
6+
from pydantic import BaseModel
7+
from .github_mcp_service import GitHubMCPService
8+
from typing import Optional
9+
10+
11+
logging.basicConfig(level=logging.INFO)
12+
logger = logging.getLogger(__name__)
13+
14+
app = FastAPI(title="GitHub MCP Server", version="1.0.0")
15+
16+
# Load env vars
17+
GITHUB_ORG = config.GITHUB_ORG
18+
if not GITHUB_ORG:
19+
logger.warning("GITHUB_ORG not set in .env — defaulting to manual owner input")
20+
21+
github_service: Optional[GitHubMCPService] = None
22+
try:
23+
token = config.GITHUB_TOKEN
24+
if not token:
25+
logger.warning("GITHUB_TOKEN/GH_TOKEN not set; GitHub API calls may be rate-limited or fail.")
26+
github_service = GitHubMCPService(token=token)
27+
logger.info("GitHub service initialized successfully")
28+
except Exception as e:
29+
logger.exception("Failed to initialize GitHub service")
30+
github_service = None
31+
32+
class RepoInfoRequest(BaseModel):
33+
repo: str
34+
owner: Optional[str] = None
35+
36+
class RepoInfoResponse(BaseModel):
37+
status: str
38+
data: dict
39+
error: str = None
40+
41+
@app.get("/health")
42+
async def health_check():
43+
"""Health check endpoint"""
44+
return {"status": "healthy", "service": "github-mcp"}
45+
46+
class OrgInfoRequest(BaseModel):
47+
org: str
48+
49+
@app.post("/list_org_repos")
50+
async def list_org_repos(request: OrgInfoRequest):
51+
try:
52+
if not github_service:
53+
raise HTTPException(status_code=503, detail="GitHub service not available")
54+
55+
result = await asyncio.to_thread(github_service.list_org_repos, request.org)
56+
57+
if "error" in result:
58+
return {"status": "error", "data": {}, "error": result["error"]}
59+
60+
return {"status": "success", "data": result}
61+
62+
except Exception as e:
63+
logger.exception("Error listing org repos")
64+
raise HTTPException(status_code=500, detail=str(e))
65+
66+
@app.post("/github_support")
67+
async def get_github_supp(request: RepoInfoRequest):
68+
"""Get repo details, using fixed org from env"""
69+
if not github_service:
70+
raise HTTPException(status_code=503, detail="GitHub service not available")
71+
owner = request.owner or GITHUB_ORG
72+
if not owner:
73+
raise HTTPException(status_code=400, detail="Missing owner; provide 'owner' or set GITHUB_ORG")
74+
75+
try:
76+
result = await asyncio.to_thread(github_service.repo_query, owner, request.repo)
77+
if "error" in result:
78+
return RepoInfoResponse(status="error", data={}, error=result["error"])
79+
return RepoInfoResponse(status="success", data=result)
80+
except Exception as e:
81+
logger.exception("Error getting repo info")
82+
raise HTTPException(status_code=500, detail=str(e))
83+
84+
if __name__ == "__main__":
85+
import uvicorn
86+
uvicorn.run(app, host="0.0.0.0", port=8001)

0 commit comments

Comments
 (0)