From ee03d4bed896ba842d227d5770d2bc48daaa0390 Mon Sep 17 00:00:00 2001 From: Geo Date: Mon, 22 Jun 2026 17:09:59 +0530 Subject: [PATCH] Add GitHub Repository Analyzer agent --- ai-agents/github_repo_analyzer/__init__.py | 3 + ai-agents/github_repo_analyzer/entrypoint.py | 34 +++ .../github_repo_analyzer_agent_tool.py | 207 ++++++++++++++++++ ai-agents/github_repo_analyzer/plugin.json | 9 + 4 files changed, 253 insertions(+) create mode 100644 ai-agents/github_repo_analyzer/__init__.py create mode 100644 ai-agents/github_repo_analyzer/entrypoint.py create mode 100644 ai-agents/github_repo_analyzer/github_repo_analyzer_agent_tool.py create mode 100644 ai-agents/github_repo_analyzer/plugin.json diff --git a/ai-agents/github_repo_analyzer/__init__.py b/ai-agents/github_repo_analyzer/__init__.py new file mode 100644 index 0000000..805d5b1 --- /dev/null +++ b/ai-agents/github_repo_analyzer/__init__.py @@ -0,0 +1,3 @@ +""" +GitHub Repository Analyzer tool plugin package. +""" \ No newline at end of file diff --git a/ai-agents/github_repo_analyzer/entrypoint.py b/ai-agents/github_repo_analyzer/entrypoint.py new file mode 100644 index 0000000..e6420d5 --- /dev/null +++ b/ai-agents/github_repo_analyzer/entrypoint.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Any, Dict, Optional + +from .github_repo_analyzer_agent_tool import GitHubRepoAnalyzerAgent + +async def create_tool_agent( + *, + app_code: str, + app_config: Dict[str, Any], + llm_provider: Any, + token: str, + user_id: int, + company_id: Optional[str] = None, + agent_id: Optional[str] = None, +): + """ + Plugin entrypoint for the github repo analyser tool. + Loads GitHubRepoAnalyzerAgent from this plugin folder (self-contained plugin). + """ + + agent = GitHubRepoAnalyzerAgent( + llm_provider=llm_provider, + agent_id=agent_id or "", + token=token, + company_id=company_id, + user_id=user_id, + app_config=app_config or {}, + ) + + if hasattr(agent, "initialize"): + await agent.initialize() + + return agent diff --git a/ai-agents/github_repo_analyzer/github_repo_analyzer_agent_tool.py b/ai-agents/github_repo_analyzer/github_repo_analyzer_agent_tool.py new file mode 100644 index 0000000..fd18302 --- /dev/null +++ b/ai-agents/github_repo_analyzer/github_repo_analyzer_agent_tool.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +""" +GitHub Repository Analyzer Agent Tool (app_code: github_repo_analyzer) +Built-in tool that analyzes public GitHub repositories. + +Input: +- GitHub repository URL + +Output: +- Repository metadata +- Project summary +- Technologies used +- Contributor insights +- Learning recommendations for developers +""" + +import json +import logging +import re +import uuid +from datetime import datetime +from typing import Any, AsyncGenerator, Dict, Optional + +import httpx + +from backend.services.ai_agents.base_tool_agent import BaseToolAgent + +logger = logging.getLogger(__name__) + +GITHUB_ANALYSIS_SYSTEM_PROMPT = """You are analyzing a GitHub repository based on its metadata. + +Provide a concise analysis covering: +1. Project purpose — what problem it solves +2. Likely target users — who would use or contribute to it +3. Contributor difficulty — how approachable the project is for new contributors + +Keep the response clear and factual. Base your analysis only on the provided metadata.""" + + +def event(event_type: str, content: Any) -> Dict[str, Any]: + return { + "id": str(uuid.uuid4()), + "type": event_type, + "timestamp": datetime.now(datetime.UTC).isoformat() + "Z", + "content": content, + } + + +class GitHubRepoAnalyzerAgent(BaseToolAgent): + TOOL_DESCRIPTION = """ +"Fetches and analyzes any public GitHub repository. " +"Expects tool_args={'url': ''} or a plain GitHub URL in user_query. " +"Returns repository metadata (name, owner, stars, forks, language, open issues) " +"and an LLM-generated summary of the project's purpose, target users, and contributor difficulty." + """ + TOOL_NAME = "GitHub Repository Analyzer" + + APP_CODE = "github_rep_analyzer" + + # Only valid GitHub owner/repo characters; avoids capturing trailing punctuation. + _REPO_PATTERN = re.compile(r"github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)") + + @classmethod + def get_tool_responsibility(cls) -> str: + return cls.TOOL_DESCRIPTION + + def get_description(self) -> str: + return self.get_tool_responsibility() + + def __init__( + self, + llm_provider: Any, + agent_id: Optional[str] = None, + token: Optional[str] = None, + company_id: Optional[str] = None, + user_id: Optional[int] = None, + app_config: Optional[Dict[str, Any]] = None, + ): + self.llm_provider = llm_provider + self.agent_id = agent_id or "" + self.token = token + self.company_id = company_id + self.user_id = user_id + self.app_config = app_config or {} + + def _parse_owner_repo(self, text: str) -> tuple[str, str] | tuple[None, None]: + """Return (owner, repo) parsed from a GitHub URL, or (None, None) if not found.""" + match = self._REPO_PATTERN.search(text) + if not match: + return None, None + owner = match.group(1) + repo = match.group(2).removesuffix(".git") + return owner, repo + + async def run( + self, + user_query: str, + provided_data: Optional[Any] = None, + session_id: Optional[str] = None, + tool_args: Optional[Dict[str, Any]] = None, + ) -> AsyncGenerator[Dict, None]: + tool_args = tool_args or {} + + # Accept URL from tool_args, a JSON blob in user_query, or plain user_query. + raw = (tool_args.get("url") or "").strip() + if not raw: + s = (user_query or "").strip() + if s.startswith("{") and "}" in s: + try: + obj = json.loads(s[:2000]) + raw = str(obj.get("url", "")).strip() + except json.JSONDecodeError: + pass + if not raw: + raw = s[:500] + + owner, repo = self._parse_owner_repo(raw) + if not owner: + out = {"success": False, "error": "Please provide a valid GitHub repository URL.", "result": None} + yield event("error", out) + yield event("final", out) + return + + yield event("thought", f"Fetching metadata for {owner}/{repo}...") + + try: + headers = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"} + + async with httpx.AsyncClient(timeout=15, headers=headers) as client: + response = await client.get(f"https://api.github.com/repos/{owner}/{repo}") + + if response.status_code == 403: + out = { + "success": False, + "error": "GitHub API rate limit exceeded. Try again later.", + "result": None, + } + + yield event("error", out) + yield event("final", out) + return + + if response.status_code == 404: + out = { + "success": False, + "error": f"Repository not found: {owner}/{repo}", + "result": None + } + yield event("error", out) + yield event("final", out) + return + + response.raise_for_status() + data = response.json() + + except httpx.HTTPStatusError as exc: + logger.exception("GitHub API returned an unexpected status") + out = {"success": False, + "error": f"GitHub API error {exc.response.status_code}: {exc.response.text}", + "result": None + } + yield event("error", out) + yield event("final", out) + return + + repo_info = { + "name": data.get("name"), + "owner": data.get("owner", {}).get("login"), + "description": data.get("description"), + "language": data.get("language"), + "stars": data.get("stargazers_count"), + "forks": data.get("forks_count"), + "open_issues": data.get("open_issues_count"), + "url": data.get("html_url"), + } + + summary = None + if self.llm_provider and getattr(self.llm_provider, "get_response", None): + yield event("thought", f"Generating analysis for {owner}/{repo}...") + try: + summary = await self.llm_provider.get_response( + transcript=json.dumps(repo_info), + system_prompt=GITHUB_ANALYSIS_SYSTEM_PROMPT, + max_tokens=300, + temperature=0.3, + ) + except Exception as exc: + logger.warning("LLM summarization failed for %s/%s: %s", owner, repo, exc) + summary = None + + out = { + "success": True, + "response": summary, + "result": { + "repository": repo_info, + "analysis": summary, + }, + } + yield event("result", out) + yield event("final", out) + + async def initialize(self) -> None: + logger.debug(" Search agent initialized") + + async def cleanup(self) -> None: + logger.debug("Web Search agent cleaned up") \ No newline at end of file diff --git a/ai-agents/github_repo_analyzer/plugin.json b/ai-agents/github_repo_analyzer/plugin.json new file mode 100644 index 0000000..9b48d01 --- /dev/null +++ b/ai-agents/github_repo_analyzer/plugin.json @@ -0,0 +1,9 @@ +{ +"plugin_id": "github_repo_analyzer", +"app_codes": ["github_repo_analyzer"], +"type": "python_tool_agent", +"display_name": "GitHub Repository Analyzer", +"description": "Analyze GitHub repositories and provide project insights.", +"entrypoint": "entrypoint.py", +"auto_attach_to_all_agents": true +}