diff --git a/app/modules/intelligence/agents/agents/blast_radius_agent.py b/app/modules/intelligence/agents/agents/blast_radius_agent.py index 452d04fb..aaf09e1a 100644 --- a/app/modules/intelligence/agents/agents/blast_radius_agent.py +++ b/app/modules/intelligence/agents/agents/blast_radius_agent.py @@ -16,12 +16,17 @@ from app.modules.intelligence.tools.kg_based_tools.ask_knowledge_graph_queries_tool import ( get_ask_knowledge_graph_queries_tool, ) +from app.modules.intelligence.tools.kg_based_tools.get_code_from_multiple_node_ids_tool import ( + GetCodeFromMultipleNodeIdsTool, + get_code_from_multiple_node_ids_tool, +) from app.modules.intelligence.tools.kg_based_tools.get_nodes_from_tags_tool import ( get_nodes_from_tags_tool, ) from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class BlastRadiusAgent: def __init__(self, sql_db, user_id, llm): @@ -33,19 +38,24 @@ def __init__(self, sql_db, user_id, llm): self.ask_knowledge_graph_queries = get_ask_knowledge_graph_queries_tool( sql_db, user_id ) + self.get_code_from_multiple_node_ids = get_code_from_multiple_node_ids_tool(sql_db, user_id) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) async def create_agents(self): blast_radius_agent = Agent( - role="Blast Radius Agent", - goal="Explain the blast radius of the changes made in the code.", - backstory="You are an expert in understanding the impact of code changes on the codebase.", + role="Blast Radius Analyzer", + goal="Analyze the impact of code changes", + backstory="You are an AI expert in analyzing how code changes affect the rest of the codebase.", tools=[ + self.get_code_from_multiple_node_ids, get_change_detection_tool(self.user_id), self.get_nodes_from_tags, self.ask_knowledge_graph_queries, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, diff --git a/app/modules/intelligence/agents/agents/code_gen_agent.py b/app/modules/intelligence/agents/agents/code_gen_agent.py index d5888a72..88d4d88d 100644 --- a/app/modules/intelligence/agents/agents/code_gen_agent.py +++ b/app/modules/intelligence/agents/agents/code_gen_agent.py @@ -33,6 +33,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class CodeGenerationAgent: @@ -54,12 +55,13 @@ def __init__(self, sql_db, llm, mini_llm, user_id): self.get_file_structure = get_code_file_structure_tool(sql_db) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) self.llm = llm self.mini_llm = mini_llm self.user_id = user_id async def create_agents(self): - # [Previous create_agents code remains the same until the task description] code_generator = Agent( role="Code Generation Agent", goal="Generate precise, copy-paste ready code modifications that maintain project consistency and handle all dependencies", @@ -85,7 +87,8 @@ async def create_agents(self): self.query_knowledge_graph, self.get_nodes_from_tags, self.get_file_structure, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, diff --git a/app/modules/intelligence/agents/agents/debug_rag_agent.py b/app/modules/intelligence/agents/agents/debug_rag_agent.py index 86fe1e1a..8d329721 100644 --- a/app/modules/intelligence/agents/agents/debug_rag_agent.py +++ b/app/modules/intelligence/agents/agents/debug_rag_agent.py @@ -39,6 +39,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class NodeResponse(BaseModel): @@ -75,6 +76,8 @@ def __init__(self, sql_db, llm, mini_llm, user_id): ) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) self.get_code_file_structure = get_code_file_structure_tool(sql_db) self.llm = llm self.mini_llm = mini_llm @@ -104,7 +107,8 @@ async def create_agents(self): self.get_code_from_probable_node_name, self.get_node_neighbours_from_node_id, self.get_code_file_structure, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, diff --git a/app/modules/intelligence/agents/agents/integration_test_agent.py b/app/modules/intelligence/agents/agents/integration_test_agent.py index c7dba9d7..72b85564 100644 --- a/app/modules/intelligence/agents/agents/integration_test_agent.py +++ b/app/modules/intelligence/agents/agents/integration_test_agent.py @@ -22,6 +22,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class IntegrationTestAgent: @@ -37,10 +38,18 @@ def __init__(self, sql_db, llm, user_id): ) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) self.llm = llm self.max_iterations = os.getenv("MAX_ITER", 15) async def create_agents(self): + tools = [ + self.get_code_from_probable_node_name, + self.get_code_from_multiple_node_ids, + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) \ + + ([self.github_tool] if hasattr(self, 'github_tool') else []) + integration_test_agent = Agent( role="Integration Test Writer", goal="Create a comprehensive integration test suite for the provided codebase. Analyze the code, determine the appropriate testing language and framework, and write tests that cover all major integration points.", @@ -48,6 +57,7 @@ async def create_agents(self): allow_delegation=False, verbose=True, llm=self.llm, + tools=tools, ) return integration_test_agent @@ -151,10 +161,6 @@ async def create_tasks( expected_output=f"Write COMPLETE CODE for integration tests for each node based on the test plan. Ensure that your output ALWAYS follows the structure outlined in the following pydantic model:\n{self.TestAgentResponse.model_json_schema()}", agent=integration_test_agent, output_pydantic=self.TestAgentResponse, - tools=[ - self.get_code_from_probable_node_name, - self.get_code_from_multiple_node_ids, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), async_execution=True, ) diff --git a/app/modules/intelligence/agents/agents/low_level_design_agent.py b/app/modules/intelligence/agents/agents/low_level_design_agent.py index b9751987..a6815568 100644 --- a/app/modules/intelligence/agents/agents/low_level_design_agent.py +++ b/app/modules/intelligence/agents/agents/low_level_design_agent.py @@ -33,6 +33,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class DesignStep(BaseModel): @@ -82,6 +83,8 @@ def __init__(self, sql_db, llm, user_id): ) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) async def create_agents(self): codebase_analyst = Agent( @@ -96,10 +99,12 @@ async def create_agents(self): self.get_code_from_node_id, self.get_code_from_probable_node_name, self.get_code_file_structure, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, + max_iter=self.max_iter, ) design_planner = Agent( diff --git a/app/modules/intelligence/agents/agents/rag_agent.py b/app/modules/intelligence/agents/agents/rag_agent.py index 15c1dfba..f8d102d5 100644 --- a/app/modules/intelligence/agents/agents/rag_agent.py +++ b/app/modules/intelligence/agents/agents/rag_agent.py @@ -39,6 +39,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class NodeResponse(BaseModel): @@ -76,6 +77,8 @@ def __init__(self, sql_db, llm, mini_llm, user_id): self.get_code_file_structure = get_code_file_structure_tool(sql_db) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) self.llm = llm self.mini_llm = mini_llm self.user_id = user_id @@ -104,7 +107,8 @@ async def create_agents(self): self.get_code_from_probable_node_name, self.get_node_neighbours_from_node_id, self.get_code_file_structure, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, diff --git a/app/modules/intelligence/agents/agents/unit_test_agent.py b/app/modules/intelligence/agents/agents/unit_test_agent.py index a100d03a..97dfd208 100644 --- a/app/modules/intelligence/agents/agents/unit_test_agent.py +++ b/app/modules/intelligence/agents/agents/unit_test_agent.py @@ -18,6 +18,7 @@ from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import ( webpage_extractor_tool ) +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class UnitTestAgent: @@ -34,6 +35,8 @@ def __init__(self, sql_db, llm, user_id): ) if os.getenv("FIRECRAWL_API_KEY"): self.webpage_extractor_tool = webpage_extractor_tool(sql_db, user_id) + if os.getenv("GITHUB_APP_ID"): + self.github_tool = github_tool(sql_db, user_id) async def create_agents(self): unit_test_agent = Agent( @@ -41,13 +44,14 @@ async def create_agents(self): goal="Create test plans and write unit tests based on user requirements", backstory="You are a seasoned AI test engineer specializing in creating robust test plans and unit tests. You aim to assist users effectively in generating and refining test plans and unit tests, ensuring they are comprehensive and tailored to the user's project requirements.", tools=[ - self.get_code_from_probable_node_name, self.get_code_from_node_id, - ]+ ([self.webpage_extractor_tool] if os.getenv("FIRECRAWL_API_KEY") else []), + self.get_code_from_probable_node_name, + ] + ([self.webpage_extractor_tool] if hasattr(self, 'webpage_extractor_tool') else []) + + ([self.github_tool] if hasattr(self, 'github_tool') else []), allow_delegation=False, verbose=True, llm=self.llm, - max_iter=self.max_iterations, + max_iterations=self.max_iterations, ) return unit_test_agent diff --git a/app/modules/intelligence/tools/tool_service.py b/app/modules/intelligence/tools/tool_service.py index 00edaed7..c6d01e65 100644 --- a/app/modules/intelligence/tools/tool_service.py +++ b/app/modules/intelligence/tools/tool_service.py @@ -37,6 +37,7 @@ ) from app.modules.intelligence.tools.tool_schema import ToolInfo from app.modules.intelligence.tools.web_tools.webpage_extractor_tool import webpage_extractor_tool +from app.modules.intelligence.tools.web_tools.github_tool import github_tool class ToolService: @@ -44,6 +45,7 @@ def __init__(self, db: Session, user_id: str): self.db = db self.user_id = user_id self.webpage_extractor_tool = webpage_extractor_tool(db, user_id) + self.github_tool = github_tool(db, user_id) self.tools = self._initialize_tools() def _initialize_tools(self) -> Dict[str, Any]: @@ -71,6 +73,9 @@ def _initialize_tools(self) -> Dict[str, Any]: if self.webpage_extractor_tool: tools["webpage_extractor"] = self.webpage_extractor_tool + + if self.github_tool: + tools["github_tool"] = self.github_tool return tools diff --git a/app/modules/intelligence/tools/web_tools/github_tool.py b/app/modules/intelligence/tools/web_tools/github_tool.py new file mode 100644 index 00000000..7eb1e59b --- /dev/null +++ b/app/modules/intelligence/tools/web_tools/github_tool.py @@ -0,0 +1,190 @@ +import os +import asyncio +import logging +import requests +from typing import Dict, Any, Optional, List +from pydantic import BaseModel, Field +from sqlalchemy.orm import Session +from langchain_core.tools import StructuredTool, Tool +from github import Github +from github.Auth import AppAuth + +from app.core.config_provider import config_provider + + +class GithubToolInput(BaseModel): + repo_name: str = Field( + description="The full repository name in format 'owner/repo' WITHOUT any quotes" + ) + issue_number: Optional[int] = Field( + description="The issue or pull request number to fetch", + default=None + ) + is_pull_request: bool = Field( + description="Whether to fetch a pull request (True) or issue (False)", + default=False + ) + + +class GithubTool: + name = "GitHub Tool" + description = """Fetches GitHub issues and pull request information including diffs. + :param repo_name: string, the full repository name (owner/repo) + :param issue_number: optional int, the issue or PR number to fetch + :param is_pull_request: optional bool, whether to fetch a PR (True) or issue (False) + + example: + { + "repo_name": 'owner/repo', + "issue_number": 123, + "is_pull_request": true + } + + Returns dictionary containing the issue/PR content, metadata, and success status. + """ + + def __init__(self, sql_db: Session, user_id: str): + self.sql_db = sql_db + self.user_id = user_id + + async def arun(self, repo_name: str, issue_number: Optional[int] = None, is_pull_request: bool = False) -> Dict[str, Any]: + return await asyncio.to_thread(self.run, repo_name, issue_number, is_pull_request) + + def run(self, repo_name: str, issue_number: Optional[int] = None, is_pull_request: bool = False) -> Dict[str, Any]: + try: + repo_name = repo_name.strip('"') + content = self._fetch_github_content(repo_name, issue_number, is_pull_request) + if not content: + return { + "success": False, + "error": "Failed to fetch GitHub content", + "content": None + } + return content + except Exception as e: + logging.exception(f"An unexpected error occurred: {str(e)}") + return { + "success": False, + "error": f"An unexpected error occurred: {str(e)}", + "content": None + } + + def _get_github_client(self, repo_name: str) -> Github: + private_key = ( + "-----BEGIN RSA PRIVATE KEY-----\n" + + config_provider.get_github_key() + + "\n-----END RSA PRIVATE KEY-----\n" + ) + app_id = os.environ["GITHUB_APP_ID"] + auth = AppAuth(app_id=app_id, private_key=private_key) + jwt = auth.create_jwt() + + # Get installation ID + url = f"https://api.github.com/repos/{repo_name}/installation" + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {jwt}", + "X-GitHub-Api-Version": "2022-11-28", + } + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise Exception(f"Failed to get installation ID for {repo_name}") + + app_auth = auth.get_installation_auth(response.json()["id"]) + return Github(auth=app_auth) + + def _fetch_github_content(self, repo_name: str, issue_number: Optional[int], is_pull_request: bool) -> Optional[Dict[str, Any]]: + try: + github = self._get_github_client(repo_name) + repo = github.get_repo(repo_name) + + if issue_number is None: + # Fetch all issues/PRs + if is_pull_request: + items = list(repo.get_pulls(state='all')[:10]) # Limit to 10 most recent + else: + items = list(repo.get_issues(state='all')[:10]) # Limit to 10 most recent + + return { + "success": True, + "content": [{ + "number": item.number, + "title": item.title, + "state": item.state, + "created_at": item.created_at.isoformat(), + "updated_at": item.updated_at.isoformat(), + "body": item.body, + "url": item.html_url, + } for item in items], + "metadata": { + "repo": repo_name, + "type": "pull_requests" if is_pull_request else "issues", + "count": len(items) + } + } + else: + # Fetch specific issue/PR + if is_pull_request: + item = repo.get_pull(issue_number) + diff = item.get_files() + changes = [{ + "filename": file.filename, + "status": file.status, + "additions": file.additions, + "deletions": file.deletions, + "changes": file.changes, + "patch": file.patch if file.patch else None + } for file in diff] + else: + item = repo.get_issue(issue_number) + changes = None + + return { + "success": True, + "content": { + "number": item.number, + "title": item.title, + "state": item.state, + "created_at": item.created_at.isoformat(), + "updated_at": item.updated_at.isoformat(), + "body": item.body, + "url": item.html_url, + "changes": changes + }, + "metadata": { + "repo": repo_name, + "type": "pull_request" if is_pull_request else "issue", + "number": issue_number + } + } + + except Exception as e: + logging.error(f"Error fetching GitHub content: {str(e)}") + return None + + +def github_tool(sql_db: Session, user_id: str) -> Optional[Tool]: + if not os.getenv("GITHUB_APP_ID") or not config_provider.get_github_key(): + logging.warning("GitHub app credentials not set, GitHub tool will not be initialized") + return None + + tool_instance = GithubTool(sql_db, user_id) + return StructuredTool.from_function( + coroutine=tool_instance.arun, + func=tool_instance.run, + name="GitHub Content Fetcher", + description="""Fetches GitHub issues and pull request information including diffs. + :param repo_name: string, the full repository name (owner/repo) + :param issue_number: optional int, the issue or PR number to fetch + :param is_pull_request: optional bool, whether to fetch a PR (True) or issue (False) + + example: + { + "repo_name": "owner/repo", + "issue_number": 123, + "is_pull_request": true + } + + Returns dictionary containing the issue/PR content, metadata, and success status.""", + args_schema=GithubToolInput, + ) \ No newline at end of file