github-agent 0.2.46__tar.gz → 0.2.48__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {github_agent-0.2.46 → github_agent-0.2.48}/PKG-INFO +2 -2
  2. {github_agent-0.2.46 → github_agent-0.2.48}/README.md +1 -1
  3. github_agent-0.2.48/github_agent/agent_data/mcp_config.json +10 -0
  4. github_agent-0.2.48/github_agent/agent_server.py +113 -0
  5. github_agent-0.2.48/github_agent/api_wrapper.py +278 -0
  6. github_agent-0.2.48/github_agent/auth.py +89 -0
  7. github_agent-0.2.48/github_agent/github_input_models.py +207 -0
  8. github_agent-0.2.48/github_agent/github_response_models.py +185 -0
  9. github_agent-0.2.48/github_agent/graph_config.py +25 -0
  10. github_agent-0.2.48/github_agent/mcp_server.py +254 -0
  11. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/PKG-INFO +2 -2
  12. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/SOURCES.txt +6 -0
  13. {github_agent-0.2.46 → github_agent-0.2.48}/pyproject.toml +1 -1
  14. github_agent-0.2.46/github_agent/agent_data/mcp_config.json +0 -3
  15. github_agent-0.2.46/github_agent/agent_server.py +0 -70
  16. {github_agent-0.2.46 → github_agent-0.2.48}/LICENSE +0 -0
  17. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/__init__.py +0 -0
  18. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/__main__.py +0 -0
  19. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/A2A_AGENTS.md +0 -0
  20. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/CRON.md +0 -0
  21. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/CRON_LOG.md +0 -0
  22. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/HEARTBEAT.md +0 -0
  23. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/IDENTITY.md +0 -0
  24. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/MEMORY.md +0 -0
  25. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/USER.md +0 -0
  26. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/chats +0 -0
  27. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/icon.png +0 -0
  28. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent/agent_data/templates.py +0 -0
  29. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/dependency_links.txt +0 -0
  30. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/entry_points.txt +0 -0
  31. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/requires.txt +0 -0
  32. {github_agent-0.2.46 → github_agent-0.2.48}/github_agent.egg-info/top_level.txt +0 -0
  33. {github_agent-0.2.46 → github_agent-0.2.48}/scripts/validate_a2a_agent.py +0 -0
  34. {github_agent-0.2.46 → github_agent-0.2.48}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-agent
3
- Version: 0.2.46
3
+ Version: 0.2.48
4
4
  Summary: GitHub Agent for MCP
5
5
  Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
6
  License: MIT
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
39
39
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
40
40
 
41
- *Version: 0.2.46*
41
+ *Version: 0.2.48*
42
42
 
43
43
  ## Overview
44
44
 
@@ -21,7 +21,7 @@
21
21
  ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
22
22
  ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
23
23
 
24
- *Version: 0.2.46*
24
+ *Version: 0.2.48*
25
25
 
26
26
  ## Overview
27
27
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "github": {
4
+ "serverUrl": "https://api.githubcopilot.com/mcp/",
5
+ "headers": {
6
+ "Authorization": "Bearer ${GITHUB_TOKEN}"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ import os
4
+ import logging
5
+
6
+ import sys
7
+ import warnings
8
+ from agent_utilities import (
9
+ build_system_prompt_from_workspace,
10
+ create_agent_parser,
11
+ create_graph_agent_server,
12
+ initialize_workspace,
13
+ load_identity,
14
+ )
15
+
16
+ __version__ = "0.2.48"
17
+
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
21
+ handlers=[logging.StreamHandler()],
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Load identity and system prompt from workspace
26
+ initialize_workspace()
27
+ meta = load_identity()
28
+ DEFAULT_AGENT_NAME = os.getenv("DEFAULT_AGENT_NAME", meta.get("name", "Github Agent"))
29
+ DEFAULT_AGENT_DESCRIPTION = os.getenv(
30
+ "AGENT_DESCRIPTION",
31
+ meta.get("description", "AI agent for GitHub Agent management."),
32
+ )
33
+ DEFAULT_AGENT_SYSTEM_PROMPT = os.getenv(
34
+ "AGENT_SYSTEM_PROMPT",
35
+ meta.get("content") or build_system_prompt_from_workspace(),
36
+ )
37
+
38
+
39
+ def agent_template(mcp_url: str = None, mcp_config: str = None, **kwargs):
40
+ """Factory function returning the fully initialized graph for execution."""
41
+ from agent_utilities import create_graph_agent
42
+ from github_agent.graph_config import TAG_PROMPTS, TAG_ENV_VARS
43
+
44
+ # In-process MCP loading: if no external URL/Config, load the local FastMCP instance
45
+ mcp_toolsets = []
46
+ effective_mcp_url = mcp_url or os.getenv("MCP_URL")
47
+ effective_mcp_config = mcp_config or os.getenv("MCP_CONFIG")
48
+
49
+ if not effective_mcp_url and not effective_mcp_config:
50
+ try:
51
+ from github_agent.mcp_server import get_mcp_instance
52
+
53
+ mcp, _, _, _ = get_mcp_instance()
54
+ mcp_toolsets.append(mcp)
55
+ logger.info("Github Agent: Using in-process MCP instance.")
56
+ except (ImportError, Exception) as e:
57
+ logger.warning(f"Github Agent: Could not load in-process MCP: {e}")
58
+
59
+ return create_graph_agent(
60
+ mcp_url=effective_mcp_url,
61
+ mcp_config=effective_mcp_config or "",
62
+ mcp_toolsets=mcp_toolsets,
63
+ name=f"{DEFAULT_AGENT_NAME} Graph Agent",
64
+ tag_prompts=TAG_PROMPTS,
65
+ tag_env_vars=TAG_ENV_VARS,
66
+ **kwargs,
67
+ )
68
+
69
+
70
+ def agent_server():
71
+
72
+ # Suppress RequestsDependencyWarning and FastMCP DeprecationWarnings
73
+ warnings.filterwarnings("ignore", message=".*urllib3.*or chardet.*")
74
+ warnings.filterwarnings("ignore", category=DeprecationWarning, module="fastmcp")
75
+
76
+ print(f"{DEFAULT_AGENT_NAME} v{__version__}", file=sys.stderr)
77
+ parser = create_agent_parser()
78
+
79
+ args = parser.parse_args()
80
+
81
+ if args.debug:
82
+ logging.getLogger().setLevel(logging.DEBUG)
83
+ logger.debug("Debug mode enabled")
84
+
85
+ # Create graph and config using standardized template
86
+ graph_bundle = agent_template(
87
+ provider=args.provider,
88
+ agent_model=args.model_id,
89
+ base_url=args.base_url,
90
+ api_key=args.api_key,
91
+ custom_skills_directory=args.custom_skills_directory,
92
+ debug=args.debug,
93
+ ssl_verify=not args.insecure,
94
+ )
95
+
96
+ # Start server using the pre-built graph bundle
97
+ create_graph_agent_server(
98
+ graph_bundle=graph_bundle,
99
+ host=args.host,
100
+ port=args.port,
101
+ enable_web_ui=args.web,
102
+ enable_otel=args.otel,
103
+ otel_endpoint=args.otel_endpoint,
104
+ otel_headers=args.otel_headers,
105
+ otel_public_key=args.otel_public_key,
106
+ otel_secret_key=args.otel_secret_key,
107
+ otel_protocol=args.otel_protocol,
108
+ debug=args.debug,
109
+ )
110
+
111
+
112
+ if __name__ == "__main__":
113
+ agent_server()
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ import re
4
+ import requests
5
+ import urllib3
6
+ import logging
7
+ from typing import List, TypeVar, Tuple
8
+ from pydantic import ValidationError
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ from agent_utilities.base_utilities import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ from github_agent.github_input_models import (
15
+ RepoModel,
16
+ IssueModel,
17
+ PullRequestModel,
18
+ ContentModel,
19
+ BranchModel,
20
+ CommitModel,
21
+ )
22
+ from github_agent.github_response_models import (
23
+ Repository,
24
+ Issue,
25
+ PullRequest,
26
+ Content,
27
+ Branch,
28
+ Commit,
29
+ Response,
30
+ )
31
+ from agent_utilities.decorators import require_auth
32
+ from agent_utilities.exceptions import (
33
+ AuthError,
34
+ UnauthorizedError,
35
+ ParameterError,
36
+ MissingParameterError,
37
+ )
38
+
39
+ T = TypeVar("T")
40
+
41
+
42
+ class Api(object):
43
+
44
+ def __init__(
45
+ self,
46
+ url: str = "https://api.github.com",
47
+ token: str = None,
48
+ proxies: dict = None,
49
+ verify: bool = True,
50
+ debug: bool = False,
51
+ ):
52
+ if debug:
53
+ logger.setLevel(logging.DEBUG)
54
+ else:
55
+ logger.setLevel(logging.ERROR)
56
+
57
+ if url is None:
58
+ raise MissingParameterError
59
+
60
+ self._session = requests.Session()
61
+ self.url = url.rstrip("/")
62
+ self.headers = {
63
+ "Accept": "application/vnd.github+json",
64
+ "X-GitHub-Api-Version": "2022-11-28",
65
+ }
66
+ self.verify = verify
67
+ self.proxies = proxies
68
+ self.debug = debug
69
+
70
+ if self.verify is False:
71
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
72
+
73
+ if token:
74
+ self.headers["Authorization"] = f"Bearer {token}"
75
+ else:
76
+ # GitHub typically requires a token for most operations
77
+ logger.warning("No token provided for GitHub API")
78
+
79
+ # Basic health check / auth test
80
+ try:
81
+ response = self._session.get(
82
+ url=f"{self.url}/user",
83
+ headers=self.headers,
84
+ verify=self.verify,
85
+ proxies=self.proxies,
86
+ )
87
+ if response.status_code in (401, 403):
88
+ logger.error(f"Authentication Error: {response.content}")
89
+ raise AuthError if response.status_code == 401 else UnauthorizedError
90
+ except requests.exceptions.RequestException as e:
91
+ logger.error(f"Connection Error: {str(e)}")
92
+ # Don't raise here, allow the client to be initialized but operations might fail
93
+
94
+ def _fetch_next_page(
95
+ self, endpoint: str, model: T, header: dict, page: int
96
+ ) -> List[dict]:
97
+ """Fetch a single page of data from the specified endpoint"""
98
+ model.page = page
99
+ model.model_post_init(None)
100
+ response = self._session.get(
101
+ url=f"{self.url}{endpoint}" if endpoint.startswith("/") else endpoint,
102
+ params=model.api_parameters,
103
+ headers=header,
104
+ verify=self.verify,
105
+ proxies=self.proxies,
106
+ )
107
+ response.raise_for_status()
108
+ page_data = response.json()
109
+ return page_data if isinstance(page_data, list) else []
110
+
111
+ def _get_total_pages(self, response: requests.Response) -> int:
112
+ """Extract total pages from GitHub Link header"""
113
+ link = response.headers.get("Link")
114
+ if not link:
115
+ return 1
116
+ # Example: <https://api.github.com/user/repos?page=2>; rel="next", <https://api.github.com/user/repos?page=10>; rel="last"
117
+ last_match = re.search(r'page=(\d+)>; rel="last"', link)
118
+ if last_match:
119
+ return int(last_match.group(1))
120
+ return 1
121
+
122
+ def _fetch_all_pages(
123
+ self, endpoint: str, model: T
124
+ ) -> Tuple[requests.Response, List[dict]]:
125
+ """Generic method to fetch all pages with parallelization if possible"""
126
+ all_data = []
127
+
128
+ initial_url = f"{self.url}{endpoint}" if endpoint.startswith("/") else endpoint
129
+
130
+ response = self._session.get(
131
+ url=initial_url,
132
+ params=model.api_parameters,
133
+ headers=self.headers,
134
+ verify=self.verify,
135
+ proxies=self.proxies,
136
+ )
137
+ response.raise_for_status()
138
+ initial_data = response.json()
139
+
140
+ if isinstance(initial_data, list):
141
+ all_data.extend(initial_data)
142
+ else:
143
+ return response, [initial_data]
144
+
145
+ total_pages = self._get_total_pages(response)
146
+
147
+ if not model.max_pages or model.max_pages == 0 or model.max_pages > total_pages:
148
+ model.max_pages = total_pages
149
+
150
+ if model.max_pages > 1:
151
+ # Parallel fetching if more than 1 page
152
+ with ThreadPoolExecutor(max_workers=5) as executor:
153
+ futures = []
154
+ for page in range(2, model.max_pages + 1):
155
+ futures.append(
156
+ executor.submit(
157
+ self._fetch_next_page,
158
+ initial_url,
159
+ model,
160
+ self.headers,
161
+ page,
162
+ )
163
+ )
164
+
165
+ for future in as_completed(futures):
166
+ try:
167
+ all_data.extend(future.result())
168
+ except Exception as e:
169
+ logger.error(f"Error fetching page: {str(e)}")
170
+
171
+ return response, all_data
172
+
173
+ @require_auth
174
+ def get_repositories(self, **kwargs) -> Response:
175
+ """List repositories for the authenticated user."""
176
+ model = RepoModel(**kwargs)
177
+ try:
178
+ response, data = self._fetch_all_pages("/user/repos", model)
179
+ parsed_data = [Repository(**item) for item in data]
180
+ return Response(response=response, data=parsed_data)
181
+ except ValidationError as e:
182
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
183
+
184
+ @require_auth
185
+ def get_repository(self, owner: str, repo: str) -> Response:
186
+ """Get a specific repository."""
187
+ try:
188
+ response = self._session.get(
189
+ url=f"{self.url}/repos/{owner}/{repo}",
190
+ headers=self.headers,
191
+ verify=self.verify,
192
+ proxies=self.proxies,
193
+ )
194
+ response.raise_for_status()
195
+ parsed_data = Repository(**response.json())
196
+ return Response(response=response, data=parsed_data)
197
+ except requests.exceptions.HTTPError as e:
198
+ if e.response.status_code == 404:
199
+ raise ParameterError(f"Repository {owner}/{repo} not found")
200
+ raise e
201
+
202
+ @require_auth
203
+ def get_issues(self, **kwargs) -> Response:
204
+ """List issues for a repository."""
205
+ model = IssueModel(**kwargs)
206
+ if not model.owner or not model.repo:
207
+ raise MissingParameterError("owner and repo are required")
208
+ try:
209
+ response, data = self._fetch_all_pages(
210
+ f"/repos/{model.owner}/{model.repo}/issues", model
211
+ )
212
+ parsed_data = [Issue(**item) for item in data]
213
+ return Response(response=response, data=parsed_data)
214
+ except ValidationError as e:
215
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
216
+
217
+ @require_auth
218
+ def get_pull_requests(self, **kwargs) -> Response:
219
+ """List pull requests for a repository."""
220
+ model = PullRequestModel(**kwargs)
221
+ if not model.owner or not model.repo:
222
+ raise MissingParameterError("owner and repo are required")
223
+ try:
224
+ response, data = self._fetch_all_pages(
225
+ f"/repos/{model.owner}/{model.repo}/pulls", model
226
+ )
227
+ parsed_data = [PullRequest(**item) for item in data]
228
+ return Response(response=response, data=parsed_data)
229
+ except ValidationError as e:
230
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
231
+
232
+ @require_auth
233
+ def get_contents(self, **kwargs) -> Response:
234
+ """Get contents of a file or directory in a repository."""
235
+ model = ContentModel(**kwargs)
236
+ try:
237
+ response = self._session.get(
238
+ url=f"{self.url}/repos/{model.owner}/{model.repo}/contents/{model.path}",
239
+ params=model.api_parameters,
240
+ headers=self.headers,
241
+ verify=self.verify,
242
+ proxies=self.proxies,
243
+ )
244
+ response.raise_for_status()
245
+ data = response.json()
246
+ if isinstance(data, list):
247
+ parsed_data = [Content(**item) for item in data]
248
+ else:
249
+ parsed_data = Content(**data)
250
+ return Response(response=response, data=parsed_data)
251
+ except ValidationError as e:
252
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
253
+
254
+ @require_auth
255
+ def get_branches(self, **kwargs) -> Response:
256
+ """List branches for a repository."""
257
+ model = BranchModel(**kwargs)
258
+ try:
259
+ response, data = self._fetch_all_pages(
260
+ f"/repos/{model.owner}/{model.repo}/branches", model
261
+ )
262
+ parsed_data = [Branch(**item) for item in data]
263
+ return Response(response=response, data=parsed_data)
264
+ except ValidationError as e:
265
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
266
+
267
+ @require_auth
268
+ def get_commits(self, **kwargs) -> Response:
269
+ """List commits for a repository."""
270
+ model = CommitModel(**kwargs)
271
+ try:
272
+ response, data = self._fetch_all_pages(
273
+ f"/repos/{model.owner}/{model.repo}/commits", model
274
+ )
275
+ parsed_data = [Commit(**item) for item in data]
276
+ return Response(response=response, data=parsed_data)
277
+ except ValidationError as e:
278
+ raise ParameterError(f"Invalid parameters: {e.errors()}")
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ import os
4
+ import threading
5
+ from typing import Optional
6
+
7
+ import requests
8
+ from agent_utilities.base_utilities import to_boolean, get_logger
9
+ from github_agent.api_wrapper import Api
10
+ from agent_utilities.exceptions import AuthError, UnauthorizedError
11
+
12
+ local = threading.local()
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ def get_client(
17
+ instance: str = os.getenv("GITHUB_URL", "https://api.github.com"),
18
+ token: Optional[str] = os.getenv("GITHUB_TOKEN", None),
19
+ verify: bool = to_boolean(string=os.getenv("GITHUB_VERIFY", "True")),
20
+ config: Optional[dict] = None,
21
+ ) -> Api:
22
+ """
23
+ Factory function to create the GitHub Api client.
24
+ Supports fixed credentials (token) and delegation (OAuth exchange).
25
+ """
26
+ if config is None:
27
+ from agent_utilities.mcp_utilities import config as default_config
28
+
29
+ config = default_config
30
+
31
+ if config.get("enable_delegation"):
32
+ user_token = getattr(local, "user_token", None)
33
+ if not user_token:
34
+ logger.error("No user token available for delegation")
35
+ raise ValueError("No user token available for delegation")
36
+
37
+ logger.info(
38
+ "Initiating OAuth token exchange for GitHub",
39
+ extra={
40
+ "audience": config["audience"],
41
+ "scopes": config["delegated_scopes"],
42
+ },
43
+ )
44
+
45
+ exchange_data = {
46
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
47
+ "subject_token": user_token,
48
+ "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
49
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
50
+ "audience": config["audience"],
51
+ "scope": config["delegated_scopes"],
52
+ }
53
+ auth = (config["oidc_client_id"], config["oidc_client_secret"])
54
+ try:
55
+ response = requests.post(
56
+ config["token_endpoint"], data=exchange_data, auth=auth
57
+ )
58
+ response.raise_for_status()
59
+ new_token = response.json()["access_token"]
60
+ logger.info("Token exchange successful")
61
+ except Exception as e:
62
+ logger.error(f"Token exchange failed: {str(e)}")
63
+ raise RuntimeError(f"Token exchange failed: {str(e)}")
64
+
65
+ try:
66
+ return Api(
67
+ url=instance,
68
+ token=new_token,
69
+ verify=verify,
70
+ )
71
+ except (AuthError, UnauthorizedError) as e:
72
+ raise RuntimeError(
73
+ f"AUTHENTICATION ERROR: The delegated GitHub credentials are not valid for '{instance}'."
74
+ f"Error details: {str(e)}"
75
+ ) from e
76
+ else:
77
+ logger.info("Using fixed credentials for GitHub API")
78
+ try:
79
+ return Api(
80
+ url=instance,
81
+ token=token,
82
+ verify=verify,
83
+ )
84
+ except (AuthError, UnauthorizedError) as e:
85
+ raise RuntimeError(
86
+ f"AUTHENTICATION ERROR: The GitHub credentials provided are not valid for '{instance}'. "
87
+ f"Please check your GITHUB_TOKEN and GITHUB_URL environment variables. "
88
+ f"Error details: {str(e)}"
89
+ ) from e