Spaces:
Paused
Paused
| """GitHub tool implementation.""" | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any | |
| import httpx | |
| from hermes.tools.base.tool import BaseTool, ToolSchema | |
| logger = logging.getLogger(__name__) | |
| class GitHubTool(BaseTool): | |
| """Tool for interacting with GitHub.""" | |
| def __init__(self, token: str | None = None) -> None: | |
| super().__init__() | |
| self.token = token | |
| self.base_url = "https://api.github.com" | |
| def _define_schema(self) -> ToolSchema: | |
| return ToolSchema( | |
| name="github_repo_reader", | |
| description="Read GitHub repository information, files, and analysis", | |
| parameters={ | |
| "action": { | |
| "type": "string", | |
| "description": "Action to perform: get_repo, list_files, read_file, get_readme, search_code", | |
| }, | |
| "owner": { | |
| "type": "string", | |
| "description": "Repository owner", | |
| }, | |
| "repo": { | |
| "type": "string", | |
| "description": "Repository name", | |
| }, | |
| "path": { | |
| "type": "string", | |
| "description": "File path (for read_file action)", | |
| }, | |
| "query": { | |
| "type": "string", | |
| "description": "Search query (for search_code action)", | |
| }, | |
| "branch": { | |
| "type": "string", | |
| "description": "Branch name", | |
| "default": "main", | |
| }, | |
| }, | |
| required=["action", "owner", "repo"], | |
| category="research", | |
| tags=["github", "code", "research"], | |
| ) | |
| def _get_headers(self) -> dict[str, str]: | |
| """Get request headers.""" | |
| headers = {"Accept": "application/vnd.github.v3+json"} | |
| if self.token: | |
| headers["Authorization"] = f"Bearer {self.token}" | |
| return headers | |
| async def execute(self, **kwargs: Any) -> dict[str, Any]: | |
| """Execute GitHub API call.""" | |
| action = kwargs["action"] | |
| owner = kwargs["owner"] | |
| repo = kwargs["repo"] | |
| try: | |
| if action == "get_repo": | |
| return await self._get_repo(owner, repo) | |
| elif action == "list_files": | |
| return await self._list_files(owner, repo, kwargs.get("path", ""), kwargs.get("branch", "main")) | |
| elif action == "read_file": | |
| return await self._read_file(owner, repo, kwargs["path"], kwargs.get("branch", "main")) | |
| elif action == "get_readme": | |
| return await self._get_readme(owner, repo) | |
| elif action == "search_code": | |
| return await self._search_code(owner, repo, kwargs.get("query", "")) | |
| else: | |
| return {"error": f"Unknown action: {action}"} | |
| except Exception as e: | |
| logger.error(f"GitHub API error: {e}") | |
| return {"error": str(e)} | |
| async def _get_repo(self, owner: str, repo: str) -> dict[str, Any]: | |
| """Get repository information.""" | |
| async with httpx.AsyncClient() as client: | |
| response = await client.get( | |
| f"{self.base_url}/repos/{owner}/{repo}", | |
| headers=self._get_headers(), | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| return { | |
| "name": data.get("name", ""), | |
| "full_name": data.get("full_name", ""), | |
| "description": data.get("description", ""), | |
| "language": data.get("language", ""), | |
| "stars": data.get("stargazers_count", 0), | |
| "forks": data.get("forks_count", 0), | |
| "open_issues": data.get("open_issues_count", 0), | |
| "created_at": data.get("created_at", ""), | |
| "updated_at": data.get("updated_at", ""), | |
| "topics": data.get("topics", []), | |
| "license": data.get("license", {}).get("name", "") if data.get("license") else "", | |
| "default_branch": data.get("default_branch", "main"), | |
| } | |
| async def _list_files( | |
| self, owner: str, repo: str, path: str, branch: str | |
| ) -> dict[str, Any]: | |
| """List files in a directory.""" | |
| async with httpx.AsyncClient() as client: | |
| url = f"{self.base_url}/repos/{owner}/{repo}/contents/{path}" | |
| response = await client.get( | |
| url, | |
| headers=self._get_headers(), | |
| params={"ref": branch}, | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| files = [] | |
| for item in data if isinstance(data, list) else [data]: | |
| files.append( | |
| { | |
| "name": item.get("name", ""), | |
| "path": item.get("path", ""), | |
| "type": item.get("type", ""), | |
| "size": item.get("size", 0), | |
| "url": item.get("html_url", ""), | |
| } | |
| ) | |
| return {"path": path, "files": files, "count": len(files)} | |
| async def _read_file( | |
| self, owner: str, repo: str, path: str, branch: str | |
| ) -> dict[str, Any]: | |
| """Read a file's contents.""" | |
| import base64 | |
| async with httpx.AsyncClient() as client: | |
| url = f"{self.base_url}/repos/{owner}/{repo}/contents/{path}" | |
| response = await client.get( | |
| url, | |
| headers=self._get_headers(), | |
| params={"ref": branch}, | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| content = "" | |
| if data.get("encoding") == "base64" and data.get("content"): | |
| content = base64.b64decode(data["content"]).decode("utf-8", errors="replace") | |
| return { | |
| "path": path, | |
| "content": content, | |
| "size": data.get("size", 0), | |
| "sha": data.get("sha", ""), | |
| } | |
| async def _get_readme(self, owner: str, repo: str) -> dict[str, Any]: | |
| """Get repository README.""" | |
| import base64 | |
| async with httpx.AsyncClient() as client: | |
| url = f"{self.base_url}/repos/{owner}/{repo}/readme" | |
| response = await client.get( | |
| url, | |
| headers=self._get_headers(), | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| content = "" | |
| if data.get("encoding") == "base64" and data.get("content"): | |
| content = base64.b64decode(data["content"]).decode("utf-8", errors="replace") | |
| return { | |
| "name": data.get("name", ""), | |
| "content": content, | |
| "size": data.get("size", 0), | |
| } | |
| async def _search_code( | |
| self, owner: str, repo: str, query: str | |
| ) -> dict[str, Any]: | |
| """Search code in repository.""" | |
| async with httpx.AsyncClient() as client: | |
| response = await client.get( | |
| f"{self.base_url}/search/code", | |
| headers=self._get_headers(), | |
| params={"q": f"{query} repo:{owner}/{repo}"}, | |
| timeout=30.0, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| results = [] | |
| for item in data.get("items", []): | |
| results.append( | |
| { | |
| "name": item.get("name", ""), | |
| "path": item.get("path", ""), | |
| "url": item.get("html_url", ""), | |
| "score": item.get("score", 0), | |
| } | |
| ) | |
| return {"query": query, "results": results, "total": data.get("total_count", 0)} | |