github-agent 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,751 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ import json
4
+ import os
5
+ import argparse
6
+ import logging
7
+ import uvicorn
8
+ from typing import Optional, Any
9
+ from contextlib import asynccontextmanager
10
+
11
+ from pydantic_ai import Agent, ModelSettings, RunContext
12
+ from pydantic_ai.mcp import load_mcp_servers, MCPServerStreamableHTTP, MCPServerSSE
13
+ from pydantic_ai_skills import SkillsToolset
14
+ from fasta2a import Skill
15
+ from github_agent.utils import (
16
+ to_integer,
17
+ to_boolean,
18
+ to_float,
19
+ to_list,
20
+ to_dict,
21
+ get_mcp_config_path,
22
+ get_skills_path,
23
+ load_skills_from_directory,
24
+ create_model,
25
+ tool_in_tag,
26
+ prune_large_messages,
27
+ )
28
+
29
+ from fastapi import FastAPI, Request
30
+ from starlette.responses import Response, StreamingResponse
31
+ from pydantic import ValidationError
32
+ from pydantic_ai.ui import SSE_CONTENT_TYPE
33
+ from pydantic_ai.ui.ag_ui import AGUIAdapter
34
+
35
+ __version__ = "0.1.1"
36
+
37
+ logging.basicConfig(
38
+ level=logging.INFO,
39
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
40
+ handlers=[logging.StreamHandler()],
41
+ )
42
+ logging.getLogger("pydantic_ai").setLevel(logging.INFO)
43
+ logging.getLogger("fastmcp").setLevel(logging.INFO)
44
+ logging.getLogger("httpx").setLevel(logging.INFO)
45
+ logger = logging.getLogger(__name__)
46
+
47
+ DEFAULT_HOST = os.getenv("HOST", "0.0.0.0")
48
+ DEFAULT_PORT = to_integer(string=os.getenv("PORT", "9000"))
49
+ DEFAULT_DEBUG = to_boolean(string=os.getenv("DEBUG", "False"))
50
+ DEFAULT_PROVIDER = os.getenv("PROVIDER", "openai")
51
+ DEFAULT_MODEL_ID = os.getenv("MODEL_ID", "qwen/qwen3-4b-2507")
52
+ DEFAULT_OPENAI_BASE_URL = os.getenv(
53
+ "OPENAI_BASE_URL", "http://host.docker.internal:1234/v1"
54
+ )
55
+ DEFAULT_OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "ollama")
56
+ DEFAULT_MCP_URL = os.getenv("MCP_URL", None)
57
+ DEFAULT_MCP_CONFIG = os.getenv("MCP_CONFIG", get_mcp_config_path())
58
+ DEFAULT_SKILLS_DIRECTORY = os.getenv("SKILLS_DIRECTORY", get_skills_path())
59
+ DEFAULT_ENABLE_WEB_UI = to_boolean(os.getenv("ENABLE_WEB_UI", "False"))
60
+
61
+ # Model Settings
62
+ DEFAULT_MAX_TOKENS = to_integer(os.getenv("MAX_TOKENS", "8192"))
63
+ DEFAULT_TEMPERATURE = to_float(os.getenv("TEMPERATURE", "0.7"))
64
+ DEFAULT_TOP_P = to_float(os.getenv("TOP_P", "1.0"))
65
+ DEFAULT_TIMEOUT = to_float(os.getenv("TIMEOUT", "32400.0"))
66
+ DEFAULT_TOOL_TIMEOUT = to_float(os.getenv("TOOL_TIMEOUT", "32400.0"))
67
+ DEFAULT_PARALLEL_TOOL_CALLS = to_boolean(os.getenv("PARALLEL_TOOL_CALLS", "True"))
68
+ DEFAULT_SEED = to_integer(os.getenv("SEED", None))
69
+ DEFAULT_PRESENCE_PENALTY = to_float(os.getenv("PRESENCE_PENALTY", "0.0"))
70
+ DEFAULT_FREQUENCY_PENALTY = to_float(os.getenv("FREQUENCY_PENALTY", "0.0"))
71
+ DEFAULT_LOGIT_BIAS = to_dict(os.getenv("LOGIT_BIAS", None))
72
+ DEFAULT_STOP_SEQUENCES = to_list(os.getenv("STOP_SEQUENCES", None))
73
+ DEFAULT_EXTRA_HEADERS = to_dict(os.getenv("EXTRA_HEADERS", None))
74
+ DEFAULT_EXTRA_BODY = to_dict(os.getenv("EXTRA_BODY", None))
75
+
76
+ AGENT_NAME = "GitHubAgent"
77
+ AGENT_DESCRIPTION = (
78
+ "A multi-agent system for interacting with GitHub via delegated specialists."
79
+ )
80
+
81
+ # -------------------------------------------------------------------------
82
+ # 1. System Prompts
83
+ # -------------------------------------------------------------------------
84
+
85
+ SUPERVISOR_SYSTEM_PROMPT = os.environ.get(
86
+ "SUPERVISOR_SYSTEM_PROMPT",
87
+ default=(
88
+ "You are the GitHub Supervisor Agent.\n"
89
+ "Your goal is to assist the user by assigning tasks to specialized child agents through your available toolset.\n"
90
+ "Analyze the user's request and determine which domain(s) it falls into (e.g., issues, pull requests, repos, etc.).\n"
91
+ "Then, call the appropriate tool(s) to delegate the task.\n"
92
+ "Synthesize the results from the child agents into a final helpful response.\n"
93
+ "Always be warm, professional, and helpful.\n"
94
+ "Note: The final response should contain all the relevant information from the tool executions."
95
+ ),
96
+ )
97
+
98
+ CONTEXT_AGENT_PROMPT = os.environ.get(
99
+ "CONTEXT_AGENT_PROMPT",
100
+ default=(
101
+ "You are the GitHub Context Agent. "
102
+ "Your goal is to provide context about the current user and functionality within GitHub."
103
+ ),
104
+ )
105
+
106
+ ACTIONS_AGENT_PROMPT = os.environ.get(
107
+ "ACTIONS_AGENT_PROMPT",
108
+ default=(
109
+ "You are the GitHub Actions Agent. "
110
+ "Your goal is to manage GitHub Actions workflows and CI/CD operations."
111
+ ),
112
+ )
113
+
114
+ CODE_SECURITY_AGENT_PROMPT = os.environ.get(
115
+ "CODE_SECURITY_AGENT_PROMPT",
116
+ default=(
117
+ "You are the GitHub Code Security Agent. "
118
+ "Your goal is to manage code security tools and scans."
119
+ ),
120
+ )
121
+
122
+ DEPENDABOT_AGENT_PROMPT = os.environ.get(
123
+ "DEPENDABOT_AGENT_PROMPT",
124
+ default=(
125
+ "You are the GitHub Dependabot Agent. "
126
+ "Your goal is to manage Dependabot alerts and configurations."
127
+ ),
128
+ )
129
+
130
+ DISCUSSIONS_AGENT_PROMPT = os.environ.get(
131
+ "DISCUSSIONS_AGENT_PROMPT",
132
+ default=(
133
+ "You are the GitHub Discussions Agent. "
134
+ "Your goal is to manage GitHub Discussions."
135
+ ),
136
+ )
137
+
138
+ GISTS_AGENT_PROMPT = os.environ.get(
139
+ "GISTS_AGENT_PROMPT",
140
+ default=("You are the GitHub Gists Agent. " "Your goal is to manage GitHub Gists."),
141
+ )
142
+
143
+ GIT_AGENT_PROMPT = os.environ.get(
144
+ "GIT_AGENT_PROMPT",
145
+ default=(
146
+ "You are the GitHub Git Agent. "
147
+ "Your goal is to perform low-level Git operations via the GitHub API (e.g., refs, trees, blobs)."
148
+ ),
149
+ )
150
+
151
+ ISSUES_AGENT_PROMPT = os.environ.get(
152
+ "ISSUES_AGENT_PROMPT",
153
+ default=(
154
+ "You are the GitHub Issues Agent. "
155
+ "Your goal is to manage GitHub Issues (create, list, update, comment)."
156
+ ),
157
+ )
158
+
159
+ LABELS_AGENT_PROMPT = os.environ.get(
160
+ "LABELS_AGENT_PROMPT",
161
+ default=(
162
+ "You are the GitHub Labels Agent. " "Your goal is to manage repository labels."
163
+ ),
164
+ )
165
+
166
+ NOTIFICATIONS_AGENT_PROMPT = os.environ.get(
167
+ "NOTIFICATIONS_AGENT_PROMPT",
168
+ default=(
169
+ "You are the GitHub Notifications Agent. "
170
+ "Your goal is to manage and check GitHub notifications."
171
+ ),
172
+ )
173
+
174
+ ORGS_AGENT_PROMPT = os.environ.get(
175
+ "ORGS_AGENT_PROMPT",
176
+ default=(
177
+ "You are the GitHub Organizations Agent. "
178
+ "Your goal is to manage GitHub Organizations and memberships."
179
+ ),
180
+ )
181
+
182
+ PROJECTS_AGENT_PROMPT = os.environ.get(
183
+ "PROJECTS_AGENT_PROMPT",
184
+ default=(
185
+ "You are the GitHub Projects Agent. "
186
+ "Your goal is to manage GitHub Projects (V2/Beta)."
187
+ ),
188
+ )
189
+
190
+ PULL_REQUESTS_AGENT_PROMPT = os.environ.get(
191
+ "PULL_REQUESTS_AGENT_PROMPT",
192
+ default=(
193
+ "You are the GitHub Pull Requests Agent. "
194
+ "Your goal is to manage Pull Requests (list, create, review, merge)."
195
+ ),
196
+ )
197
+
198
+ REPOS_AGENT_PROMPT = os.environ.get(
199
+ "REPOS_AGENT_PROMPT",
200
+ default=(
201
+ "You are the GitHub Repositories Agent. "
202
+ "Your goal is to manage GitHub Repositories (create, list, delete, settings)."
203
+ ),
204
+ )
205
+
206
+ SECRET_PROTECTION_AGENT_PROMPT = os.environ.get(
207
+ "SECRET_PROTECTION_AGENT_PROMPT",
208
+ default=(
209
+ "You are the GitHub Secret Protection Agent. "
210
+ "Your goal is to manage secret scanning and protection features."
211
+ ),
212
+ )
213
+
214
+ SECURITY_ADVISORIES_AGENT_PROMPT = os.environ.get(
215
+ "SECURITY_ADVISORIES_AGENT_PROMPT",
216
+ default=(
217
+ "You are the GitHub Security Advisories Agent. "
218
+ "Your goal is to access and manage security advisories."
219
+ ),
220
+ )
221
+
222
+ STARGAZERS_AGENT_PROMPT = os.environ.get(
223
+ "STARGAZERS_AGENT_PROMPT",
224
+ default=(
225
+ "You are the GitHub Stargazers Agent. "
226
+ "Your goal is to manage and view repository stargazers."
227
+ ),
228
+ )
229
+
230
+ USERS_AGENT_PROMPT = os.environ.get(
231
+ "USERS_AGENT_PROMPT",
232
+ default=(
233
+ "You are the GitHub Users Agent. "
234
+ "Your goal is to access public user information and profile data."
235
+ ),
236
+ )
237
+
238
+ COPILOT_AGENT_PROMPT = os.environ.get(
239
+ "COPILOT_AGENT_PROMPT",
240
+ default=(
241
+ "You are the GitHub Copilot Agent. "
242
+ "Your goal is to assist with coding tasks using GitHub Copilot."
243
+ ),
244
+ )
245
+
246
+ COPILOT_SPACES_AGENT_PROMPT = os.environ.get(
247
+ "COPILOT_SPACES_AGENT_PROMPT",
248
+ default=(
249
+ "You are the GitHub Copilot Spaces Agent. "
250
+ "Your goal is to manage Copilot Spaces."
251
+ ),
252
+ )
253
+
254
+ SUPPORT_DOCS_AGENT_PROMPT = os.environ.get(
255
+ "SUPPORT_DOCS_AGENT_PROMPT",
256
+ default=(
257
+ "You are the GitHub Support Docs Agent. "
258
+ "Your goal is to search GitHub documentation to answer support questions."
259
+ ),
260
+ )
261
+
262
+
263
+ # -------------------------------------------------------------------------
264
+ # 2. Agent Creation Logic
265
+ # -------------------------------------------------------------------------
266
+
267
+
268
+ def create_agent(
269
+ provider: str = DEFAULT_PROVIDER,
270
+ model_id: str = DEFAULT_MODEL_ID,
271
+ base_url: Optional[str] = None,
272
+ api_key: Optional[str] = None,
273
+ mcp_url: str = DEFAULT_MCP_URL,
274
+ mcp_config: str = DEFAULT_MCP_CONFIG,
275
+ skills_directory: Optional[str] = DEFAULT_SKILLS_DIRECTORY,
276
+ ) -> Agent:
277
+ """
278
+ Creates the Supervisor Agent with sub-agents registered as tools.
279
+ """
280
+ logger.info("Initializing Multi-Agent System for GitHub...")
281
+
282
+ model = create_model(provider, model_id, base_url, api_key)
283
+ settings = ModelSettings(
284
+ max_tokens=DEFAULT_MAX_TOKENS,
285
+ temperature=DEFAULT_TEMPERATURE,
286
+ top_p=DEFAULT_TOP_P,
287
+ timeout=DEFAULT_TIMEOUT,
288
+ parallel_tool_calls=DEFAULT_PARALLEL_TOOL_CALLS,
289
+ seed=DEFAULT_SEED,
290
+ presence_penalty=DEFAULT_PRESENCE_PENALTY,
291
+ frequency_penalty=DEFAULT_FREQUENCY_PENALTY,
292
+ logit_bias=DEFAULT_LOGIT_BIAS,
293
+ stop_sequences=DEFAULT_STOP_SEQUENCES,
294
+ extra_headers=DEFAULT_EXTRA_HEADERS,
295
+ extra_body=DEFAULT_EXTRA_BODY,
296
+ )
297
+
298
+ # Load master toolsets
299
+ master_toolsets = []
300
+ if mcp_config:
301
+ mcp_toolset = load_mcp_servers(mcp_config)
302
+ master_toolsets.extend(mcp_toolset)
303
+ logger.info(f"Connected to MCP Config JSON: {mcp_toolset}")
304
+ elif mcp_url:
305
+ if "sse" in mcp_url.lower():
306
+ server = MCPServerSSE(mcp_url)
307
+ else:
308
+ server = MCPServerStreamableHTTP(mcp_url)
309
+ master_toolsets.append(server)
310
+ logger.info(f"Connected to MCP Server: {mcp_url}")
311
+
312
+ if skills_directory and os.path.exists(skills_directory):
313
+ master_toolsets.append(SkillsToolset(directories=[str(skills_directory)]))
314
+
315
+ # Define Tag -> Prompt map
316
+ # Key is the MCP Tool Tag (or set of tags), Value is (SystemPrompt, AgentName)
317
+ agent_defs = {
318
+ "person": (CONTEXT_AGENT_PROMPT, "GitHub_Context_Agent"),
319
+ "workflow": (ACTIONS_AGENT_PROMPT, "GitHub_Actions_Agent"),
320
+ "codescan": (CODE_SECURITY_AGENT_PROMPT, "GitHub_Code_Security_Agent"),
321
+ "dependabot": (DEPENDABOT_AGENT_PROMPT, "GitHub_Dependabot_Agent"),
322
+ "comment-discussion": (DISCUSSIONS_AGENT_PROMPT, "GitHub_Discussions_Agent"),
323
+ "logo-gist": (GISTS_AGENT_PROMPT, "GitHub_Gists_Agent"),
324
+ "git-branch": (GIT_AGENT_PROMPT, "GitHub_Git_Agent"),
325
+ "issue-opened": (ISSUES_AGENT_PROMPT, "GitHub_Issues_Agent"),
326
+ "tag": (LABELS_AGENT_PROMPT, "GitHub_Labels_Agent"),
327
+ "bell": (NOTIFICATIONS_AGENT_PROMPT, "GitHub_Notifications_Agent"),
328
+ "organization": (ORGS_AGENT_PROMPT, "GitHub_Organizations_Agent"),
329
+ "project": (PROJECTS_AGENT_PROMPT, "GitHub_Projects_Agent"),
330
+ "git-pull-request": (PULL_REQUESTS_AGENT_PROMPT, "GitHub_Pull_Requests_Agent"),
331
+ "repo": (REPOS_AGENT_PROMPT, "GitHub_Repos_Agent"),
332
+ "shield-lock": (
333
+ SECRET_PROTECTION_AGENT_PROMPT,
334
+ "GitHub_Secret_Protection_Agent",
335
+ ),
336
+ "shield": (
337
+ SECURITY_ADVISORIES_AGENT_PROMPT,
338
+ "GitHub_Security_Advisories_Agent",
339
+ ),
340
+ "star": (STARGAZERS_AGENT_PROMPT, "GitHub_Stargazers_Agent"),
341
+ "people": (USERS_AGENT_PROMPT, "GitHub_Users_Agent"),
342
+ "copilot": (COPILOT_AGENT_PROMPT, "GitHub_Copilot_Agent"),
343
+ "copilot_spaces": (COPILOT_SPACES_AGENT_PROMPT, "GitHub_Copilot_Spaces_Agent"),
344
+ "github_support_docs_search": (
345
+ SUPPORT_DOCS_AGENT_PROMPT,
346
+ "GitHub_Support_Docs_Agent",
347
+ ),
348
+ }
349
+
350
+ child_agents = {}
351
+
352
+ for tag, (system_prompt, agent_name) in agent_defs.items():
353
+ tag_toolsets = []
354
+ for ts in master_toolsets:
355
+
356
+ def filter_func(ctx, tool_def, t=tag):
357
+ return tool_in_tag(tool_def, t)
358
+
359
+ if hasattr(ts, "filtered"):
360
+ filtered_ts = ts.filtered(filter_func)
361
+ tag_toolsets.append(filtered_ts)
362
+ else:
363
+ pass
364
+
365
+ agent = Agent(
366
+ name=agent_name,
367
+ system_prompt=system_prompt,
368
+ model=model,
369
+ model_settings=settings,
370
+ toolsets=tag_toolsets,
371
+ tool_timeout=DEFAULT_TOOL_TIMEOUT,
372
+ )
373
+ child_agents[tag] = agent
374
+
375
+ # Create Supervisor
376
+ supervisor = Agent(
377
+ name=AGENT_NAME,
378
+ system_prompt=SUPERVISOR_SYSTEM_PROMPT,
379
+ model=model,
380
+ model_settings=settings,
381
+ deps_type=Any,
382
+ )
383
+
384
+ # Define delegation tools
385
+ # We define these explicitly to give the Supervisor clear, typed tools.
386
+
387
+ @supervisor.tool
388
+ async def assign_task_to_context_agent(ctx: RunContext[Any], task: str) -> str:
389
+ """Assign a task related to user context and general GitHub status to the Context Agent."""
390
+ return (
391
+ await child_agents["person"].run(task, usage=ctx.usage, deps=ctx.deps)
392
+ ).output
393
+
394
+ @supervisor.tool
395
+ async def assign_task_to_actions_agent(ctx: RunContext[Any], task: str) -> str:
396
+ """Assign a task related to GitHub Actions and Workflows to the Actions Agent."""
397
+ return (
398
+ await child_agents["workflow"].run(task, usage=ctx.usage, deps=ctx.deps)
399
+ ).output
400
+
401
+ @supervisor.tool
402
+ async def assign_task_to_code_security_agent(
403
+ ctx: RunContext[Any], task: str
404
+ ) -> str:
405
+ """Assign a task related to code security and scanning to the Code Security Agent."""
406
+ return (
407
+ await child_agents["codescan"].run(task, usage=ctx.usage, deps=ctx.deps)
408
+ ).output
409
+
410
+ @supervisor.tool
411
+ async def assign_task_to_dependabot_agent(ctx: RunContext[Any], task: str) -> str:
412
+ """Assign a task related to Dependabot to the Dependabot Agent."""
413
+ return (
414
+ await child_agents["dependabot"].run(task, usage=ctx.usage, deps=ctx.deps)
415
+ ).output
416
+
417
+ @supervisor.tool
418
+ async def assign_task_to_discussions_agent(ctx: RunContext[Any], task: str) -> str:
419
+ """Assign a task related to GitHub Discussions to the Discussions Agent."""
420
+ return (
421
+ await child_agents["comment-discussion"].run(
422
+ task, usage=ctx.usage, deps=ctx.deps
423
+ )
424
+ ).output
425
+
426
+ @supervisor.tool
427
+ async def assign_task_to_gists_agent(ctx: RunContext[Any], task: str) -> str:
428
+ """Assign a task related to Gists to the Gists Agent."""
429
+ return (
430
+ await child_agents["logo-gist"].run(task, usage=ctx.usage, deps=ctx.deps)
431
+ ).output
432
+
433
+ @supervisor.tool
434
+ async def assign_task_to_git_agent(ctx: RunContext[Any], task: str) -> str:
435
+ """Assign a task related to low-level Git operations (refs, blobs) to the Git Agent."""
436
+ return (
437
+ await child_agents["git-branch"].run(task, usage=ctx.usage, deps=ctx.deps)
438
+ ).output
439
+
440
+ @supervisor.tool
441
+ async def assign_task_to_issues_agent(ctx: RunContext[Any], task: str) -> str:
442
+ """Assign a task related to Issues (create, list, comment) to the Issues Agent."""
443
+ return (
444
+ await child_agents["issue-opened"].run(task, usage=ctx.usage, deps=ctx.deps)
445
+ ).output
446
+
447
+ @supervisor.tool
448
+ async def assign_task_to_labels_agent(ctx: RunContext[Any], task: str) -> str:
449
+ """Assign a task related to Labels to the Labels Agent."""
450
+ return (
451
+ await child_agents["tag"].run(task, usage=ctx.usage, deps=ctx.deps)
452
+ ).output
453
+
454
+ @supervisor.tool
455
+ async def assign_task_to_notifications_agent(
456
+ ctx: RunContext[Any], task: str
457
+ ) -> str:
458
+ """Assign a task related to Notifications to the Notifications Agent."""
459
+ return (
460
+ await child_agents["bell"].run(task, usage=ctx.usage, deps=ctx.deps)
461
+ ).output
462
+
463
+ @supervisor.tool
464
+ async def assign_task_to_organizations_agent(
465
+ ctx: RunContext[Any], task: str
466
+ ) -> str:
467
+ """Assign a task related to Organizations to the Organizations Agent."""
468
+ return (
469
+ await child_agents["organization"].run(task, usage=ctx.usage, deps=ctx.deps)
470
+ ).output
471
+
472
+ @supervisor.tool
473
+ async def assign_task_to_projects_agent(ctx: RunContext[Any], task: str) -> str:
474
+ """Assign a task related to GitHub Projects to the Projects Agent."""
475
+ return (
476
+ await child_agents["project"].run(task, usage=ctx.usage, deps=ctx.deps)
477
+ ).output
478
+
479
+ @supervisor.tool
480
+ async def assign_task_to_pull_requests_agent(
481
+ ctx: RunContext[Any], task: str
482
+ ) -> str:
483
+ """Assign a task related to Pull Requests to the Pull Requests Agent."""
484
+ return (
485
+ await child_agents["git-pull-request"].run(
486
+ task, usage=ctx.usage, deps=ctx.deps
487
+ )
488
+ ).output
489
+
490
+ @supervisor.tool
491
+ async def assign_task_to_repos_agent(ctx: RunContext[Any], task: str) -> str:
492
+ """Assign a task related to Repositories (list, settings, delete) to the Repositories Agent."""
493
+ return (
494
+ await child_agents["repo"].run(task, usage=ctx.usage, deps=ctx.deps)
495
+ ).output
496
+
497
+ @supervisor.tool
498
+ async def assign_task_to_secret_protection_agent(
499
+ ctx: RunContext[Any], task: str
500
+ ) -> str:
501
+ """Assign a task related to Secret Protection to the Secret Protection Agent."""
502
+ return (
503
+ await child_agents["shield-lock"].run(task, usage=ctx.usage, deps=ctx.deps)
504
+ ).output
505
+
506
+ @supervisor.tool
507
+ async def assign_task_to_security_advisories_agent(
508
+ ctx: RunContext[Any], task: str
509
+ ) -> str:
510
+ """Assign a task related to Security Advisories to the Security Advisories Agent."""
511
+ return (
512
+ await child_agents["shield"].run(task, usage=ctx.usage, deps=ctx.deps)
513
+ ).output
514
+
515
+ @supervisor.tool
516
+ async def assign_task_to_stargazers_agent(ctx: RunContext[Any], task: str) -> str:
517
+ """Assign a task related to Stargazers to the Stargazers Agent."""
518
+ return (
519
+ await child_agents["star"].run(task, usage=ctx.usage, deps=ctx.deps)
520
+ ).output
521
+
522
+ @supervisor.tool
523
+ async def assign_task_to_users_agent(ctx: RunContext[Any], task: str) -> str:
524
+ """Assign a task related to Users to the Users Agent."""
525
+ return (
526
+ await child_agents["people"].run(task, usage=ctx.usage, deps=ctx.deps)
527
+ ).output
528
+
529
+ @supervisor.tool
530
+ async def assign_task_to_copilot_agent(ctx: RunContext[Any], task: str) -> str:
531
+ """Assign a task related to GitHub Copilot coding tasks to the Copilot Agent."""
532
+ return (
533
+ await child_agents["copilot"].run(task, usage=ctx.usage, deps=ctx.deps)
534
+ ).output
535
+
536
+ @supervisor.tool
537
+ async def assign_task_to_copilot_spaces_agent(
538
+ ctx: RunContext[Any], task: str
539
+ ) -> str:
540
+ """Assign a task related to Copilot Spaces to the Copilot Spaces Agent."""
541
+ return (
542
+ await child_agents["copilot_spaces"].run(
543
+ task, usage=ctx.usage, deps=ctx.deps
544
+ )
545
+ ).output
546
+
547
+ @supervisor.tool
548
+ async def assign_task_to_support_docs_agent(ctx: RunContext[Any], task: str) -> str:
549
+ """Assign a task to search GitHub Support Docs to the Support Docs Agent."""
550
+ return (
551
+ await child_agents["github_support_docs_search"].run(
552
+ task, usage=ctx.usage, deps=ctx.deps
553
+ )
554
+ ).output
555
+
556
+ return supervisor
557
+
558
+
559
+ def create_agent_server(
560
+ provider: str = DEFAULT_PROVIDER,
561
+ model_id: str = DEFAULT_MODEL_ID,
562
+ base_url: Optional[str] = None,
563
+ api_key: Optional[str] = None,
564
+ mcp_url: str = DEFAULT_MCP_URL,
565
+ mcp_config: str = DEFAULT_MCP_CONFIG,
566
+ skills_directory: Optional[str] = DEFAULT_SKILLS_DIRECTORY,
567
+ debug: Optional[bool] = DEFAULT_DEBUG,
568
+ host: Optional[str] = DEFAULT_HOST,
569
+ port: Optional[int] = DEFAULT_PORT,
570
+ enable_web_ui: bool = DEFAULT_ENABLE_WEB_UI,
571
+ ):
572
+ print(
573
+ f"Starting {AGENT_NAME} with provider={provider}, model={model_id}, mcp={mcp_url} | {mcp_config}"
574
+ )
575
+ agent = create_agent(
576
+ provider=provider,
577
+ model_id=model_id,
578
+ base_url=base_url,
579
+ api_key=api_key,
580
+ mcp_url=mcp_url,
581
+ mcp_config=mcp_config,
582
+ skills_directory=skills_directory,
583
+ )
584
+
585
+ if skills_directory and os.path.exists(skills_directory):
586
+ skills = load_skills_from_directory(skills_directory)
587
+ logger.info(f"Loaded {len(skills)} skills from {skills_directory}")
588
+ else:
589
+ skills = [
590
+ Skill(
591
+ id="github_agent",
592
+ name="GitHub Agent",
593
+ description="General access to GitHub tools",
594
+ tags=["github"],
595
+ input_modes=["text"],
596
+ output_modes=["text"],
597
+ )
598
+ ]
599
+
600
+ a2a_app = agent.to_a2a(
601
+ name=AGENT_NAME,
602
+ description=AGENT_DESCRIPTION,
603
+ version=__version__,
604
+ skills=skills,
605
+ debug=debug,
606
+ )
607
+
608
+ @asynccontextmanager
609
+ async def lifespan(app: FastAPI):
610
+ if hasattr(a2a_app, "router"):
611
+ async with a2a_app.router.lifespan_context(a2a_app):
612
+ yield
613
+ else:
614
+ yield
615
+
616
+ app = FastAPI(
617
+ title=f"{AGENT_NAME} - A2A + AG-UI Server",
618
+ description=AGENT_DESCRIPTION,
619
+ debug=debug,
620
+ lifespan=lifespan,
621
+ )
622
+
623
+ @app.get("/health")
624
+ async def health_check():
625
+ return {"status": "OK"}
626
+
627
+ app.mount("/a2a", a2a_app)
628
+
629
+ @app.post("/ag-ui")
630
+ async def ag_ui_endpoint(request: Request) -> Response:
631
+ accept = request.headers.get("accept", SSE_CONTENT_TYPE)
632
+ try:
633
+ run_input = AGUIAdapter.build_run_input(await request.body())
634
+ except ValidationError as e:
635
+ return Response(
636
+ content=json.dumps(e.json()),
637
+ media_type="application/json",
638
+ status_code=422,
639
+ )
640
+
641
+ # Prune large messages from history
642
+ if hasattr(run_input, "messages"):
643
+ run_input.messages = prune_large_messages(run_input.messages)
644
+
645
+ adapter = AGUIAdapter(agent=agent, run_input=run_input, accept=accept)
646
+ event_stream = adapter.run_stream()
647
+ sse_stream = adapter.encode_stream(event_stream)
648
+
649
+ return StreamingResponse(
650
+ sse_stream,
651
+ media_type=accept,
652
+ )
653
+
654
+ if enable_web_ui:
655
+ web_ui = agent.to_web(instructions=SUPERVISOR_SYSTEM_PROMPT)
656
+ app.mount("/", web_ui)
657
+ logger.info(
658
+ "Starting server on %s:%s (A2A at /a2a, AG-UI at /ag-ui, Web UI: %s)",
659
+ host,
660
+ port,
661
+ "Enabled at /" if enable_web_ui else "Disabled",
662
+ )
663
+
664
+ uvicorn.run(
665
+ app,
666
+ host=host,
667
+ port=port,
668
+ timeout_keep_alive=1800,
669
+ timeout_graceful_shutdown=60,
670
+ log_level="debug" if debug else "info",
671
+ )
672
+
673
+
674
+ def agent_server():
675
+ print(f"github_agent v{__version__}")
676
+ parser = argparse.ArgumentParser(
677
+ description=f"Run the {AGENT_NAME} A2A + AG-UI Server"
678
+ )
679
+ parser.add_argument(
680
+ "--host", default=DEFAULT_HOST, help="Host to bind the server to"
681
+ )
682
+ parser.add_argument(
683
+ "--port", type=int, default=DEFAULT_PORT, help="Port to bind the server to"
684
+ )
685
+ parser.add_argument("--debug", type=bool, default=DEFAULT_DEBUG, help="Debug mode")
686
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
687
+
688
+ parser.add_argument(
689
+ "--provider",
690
+ default=DEFAULT_PROVIDER,
691
+ choices=["openai", "anthropic", "google", "huggingface"],
692
+ help="LLM Provider",
693
+ )
694
+ parser.add_argument("--model-id", default=DEFAULT_MODEL_ID, help="LLM Model ID")
695
+ parser.add_argument(
696
+ "--base-url",
697
+ default=DEFAULT_OPENAI_BASE_URL,
698
+ help="LLM Base URL (for OpenAI compatible providers)",
699
+ )
700
+ parser.add_argument("--api-key", default=DEFAULT_OPENAI_API_KEY, help="LLM API Key")
701
+ parser.add_argument("--mcp-url", default=DEFAULT_MCP_URL, help="MCP Server URL")
702
+ parser.add_argument(
703
+ "--mcp-config", default=DEFAULT_MCP_CONFIG, help="MCP Server Config"
704
+ )
705
+ parser.add_argument(
706
+ "--skills-directory",
707
+ default=DEFAULT_SKILLS_DIRECTORY,
708
+ help="Directory containing agent skills",
709
+ )
710
+ parser.add_argument(
711
+ "--web",
712
+ action="store_true",
713
+ default=DEFAULT_ENABLE_WEB_UI,
714
+ help="Enable Pydantic AI Web UI",
715
+ )
716
+ args = parser.parse_args()
717
+
718
+ if args.debug:
719
+ for handler in logging.root.handlers[:]:
720
+ logging.root.removeHandler(handler)
721
+
722
+ logging.basicConfig(
723
+ level=logging.DEBUG,
724
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
725
+ handlers=[logging.StreamHandler()],
726
+ force=True,
727
+ )
728
+ logging.getLogger("pydantic_ai").setLevel(logging.DEBUG)
729
+ logging.getLogger("fastmcp").setLevel(logging.DEBUG)
730
+ logging.getLogger("httpcore").setLevel(logging.DEBUG)
731
+ logging.getLogger("httpx").setLevel(logging.DEBUG)
732
+ logger.setLevel(logging.DEBUG)
733
+ logger.debug("Debug mode enabled")
734
+
735
+ create_agent_server(
736
+ provider=args.provider,
737
+ model_id=args.model_id,
738
+ base_url=args.base_url,
739
+ api_key=args.api_key,
740
+ mcp_url=args.mcp_url,
741
+ mcp_config=args.mcp_config,
742
+ skills_directory=args.skills_directory,
743
+ debug=args.debug,
744
+ host=args.host,
745
+ port=args.port,
746
+ enable_web_ui=args.web,
747
+ )
748
+
749
+
750
+ if __name__ == "__main__":
751
+ agent_server()
github_agent/utils.py ADDED
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+
4
+ import os
5
+ import pickle
6
+ import yaml
7
+ from pathlib import Path
8
+ from typing import Any, Union, List, Optional
9
+ import json
10
+ from importlib.resources import files, as_file
11
+ from pydantic_ai.models.openai import OpenAIChatModel
12
+ from pydantic_ai.models.anthropic import AnthropicModel
13
+ from pydantic_ai.models.google import GoogleModel
14
+ from pydantic_ai.models.huggingface import HuggingFaceModel
15
+ from pydantic_ai_skills import Skill
16
+
17
+
18
+ def to_integer(string: Union[str, int] = None) -> int:
19
+ if isinstance(string, int):
20
+ return string
21
+ if not string:
22
+ return 0
23
+ try:
24
+ return int(string.strip())
25
+ except ValueError:
26
+ raise ValueError(f"Cannot convert '{string}' to integer")
27
+
28
+
29
+ def to_boolean(string: Union[str, bool] = None) -> bool:
30
+ if isinstance(string, bool):
31
+ return string
32
+ if not string:
33
+ return False
34
+ normalized = str(string).strip().lower()
35
+ true_values = {"t", "true", "y", "yes", "1"}
36
+ false_values = {"f", "false", "n", "no", "0"}
37
+ if normalized in true_values:
38
+ return True
39
+ elif normalized in false_values:
40
+ return False
41
+ else:
42
+ raise ValueError(f"Cannot convert '{string}' to boolean")
43
+
44
+
45
+ def to_float(string: Union[str, float] = None) -> float:
46
+ if isinstance(string, float):
47
+ return string
48
+ if not string:
49
+ return 0.0
50
+ try:
51
+ return float(string.strip())
52
+ except ValueError:
53
+ raise ValueError(f"Cannot convert '{string}' to float")
54
+
55
+
56
+ def to_list(string: Union[str, list] = None) -> list:
57
+ if isinstance(string, list):
58
+ return string
59
+ if not string:
60
+ return []
61
+ try:
62
+ return json.loads(string)
63
+ except Exception:
64
+ return string.split(",")
65
+
66
+
67
+ def to_dict(string: Union[str, dict] = None) -> dict:
68
+ if isinstance(string, dict):
69
+ return string
70
+ if not string:
71
+ return {}
72
+ try:
73
+ return json.loads(string)
74
+ except Exception:
75
+ raise ValueError(f"Cannot convert '{string}' to dict")
76
+
77
+
78
+ def prune_large_messages(messages: list[Any], max_length: int = 5000) -> list[Any]:
79
+ """
80
+ Summarize large tool outputs in the message history to save context window.
81
+ Keeps the most recent tool outputs intact if they are the very last message,
82
+ but generally we want to prune history.
83
+ """
84
+ pruned_messages = []
85
+ for i, msg in enumerate(messages):
86
+ content = getattr(msg, "content", None)
87
+ if content is None and isinstance(msg, dict):
88
+ content = msg.get("content")
89
+
90
+ if isinstance(content, str) and len(content) > max_length:
91
+ summary = (
92
+ f"{content[:200]} ... "
93
+ f"[Output truncated, original length {len(content)} characters] "
94
+ f"... {content[-200:]}"
95
+ )
96
+
97
+ # Replace content
98
+ if isinstance(msg, dict):
99
+ msg["content"] = summary
100
+ pruned_messages.append(msg)
101
+ elif hasattr(msg, "content"):
102
+ # Try to create a copy or modify in place if mutable
103
+ # If it's a Pydantic model it might be immutable or require copy
104
+ try:
105
+ # Attempt shallow copy with update
106
+ from copy import copy
107
+
108
+ new_msg = copy(msg)
109
+ new_msg.content = summary
110
+ pruned_messages.append(new_msg)
111
+ except Exception:
112
+ # Fallback: keep original if we can't modify
113
+ pruned_messages.append(msg)
114
+ else:
115
+ pruned_messages.append(msg)
116
+ else:
117
+ pruned_messages.append(msg)
118
+
119
+ return pruned_messages
120
+
121
+
122
+ def save_model(model: Any, file_name: str = "model", file_path: str = ".") -> str:
123
+ pickle_file = os.path.join(file_path, f"{file_name}.pkl")
124
+ with open(pickle_file, "wb") as file:
125
+ pickle.dump(model, file)
126
+ return pickle_file
127
+
128
+
129
+ def load_model(file: str) -> Any:
130
+ with open(file, "rb") as model_file:
131
+ model = pickle.load(model_file)
132
+ return model
133
+
134
+
135
+ def retrieve_package_name() -> str:
136
+ """
137
+ Returns the top-level package name of the module that imported this utils.py.
138
+ """
139
+ if __package__:
140
+ top = __package__.partition(".")[0]
141
+ if top and top != "__main__":
142
+ return top
143
+
144
+ try:
145
+ file_path = Path(__file__).resolve()
146
+ for parent in file_path.parents:
147
+ if (
148
+ (parent / "pyproject.toml").is_file()
149
+ or (parent / "setup.py").is_file()
150
+ or (parent / "__init__.py").is_file()
151
+ ):
152
+ return parent.name
153
+ except Exception:
154
+ pass
155
+
156
+ return "unknown_package"
157
+
158
+
159
+ def get_skills_path() -> str:
160
+ skills_dir = files(retrieve_package_name()) / "skills"
161
+ with as_file(skills_dir) as path:
162
+ skills_path = str(path)
163
+ return skills_path
164
+
165
+
166
+ def get_mcp_config_path() -> str:
167
+ mcp_config_file = files(retrieve_package_name()) / "mcp_config.json"
168
+ with as_file(mcp_config_file) as path:
169
+ mcp_config_path = str(path)
170
+ return mcp_config_path
171
+
172
+
173
+ def load_skills_from_directory(directory: str) -> List[Skill]:
174
+ skills = []
175
+ base_path = Path(directory)
176
+
177
+ if not base_path.exists():
178
+ print(f"Skills directory not found: {directory}")
179
+ return skills
180
+
181
+ for item in base_path.iterdir():
182
+ if item.is_dir():
183
+ skill_file = item / "SKILL.md"
184
+ if skill_file.exists():
185
+ try:
186
+ with open(skill_file, "r") as f:
187
+ # Extract frontmatter
188
+ content = f.read()
189
+ if content.startswith("---"):
190
+ _, frontmatter, _ = content.split("---", 2)
191
+ data = yaml.safe_load(frontmatter)
192
+
193
+ skill_id = item.name
194
+ skill_name = data.get("name", skill_id)
195
+ skill_desc = data.get(
196
+ "description", f"Access to {skill_name} tools"
197
+ )
198
+ skills.append(
199
+ Skill(
200
+ id=skill_id,
201
+ name=skill_name,
202
+ description=skill_desc,
203
+ tags=[skill_id],
204
+ input_modes=["text"],
205
+ output_modes=["text"],
206
+ )
207
+ )
208
+ except Exception as e:
209
+ print(f"Error loading skill from {skill_file}: {e}")
210
+
211
+ return skills
212
+
213
+
214
+ def create_model(
215
+ provider: str,
216
+ model_id: str,
217
+ base_url: Optional[str],
218
+ api_key: Optional[str],
219
+ ):
220
+ if provider == "openai":
221
+ target_base_url = base_url
222
+ target_api_key = api_key
223
+ if target_base_url:
224
+ os.environ["OPENAI_BASE_URL"] = target_base_url
225
+ if target_api_key:
226
+ os.environ["OPENAI_API_KEY"] = target_api_key
227
+ return OpenAIChatModel(model_name=model_id, provider="openai")
228
+
229
+ elif provider == "anthropic":
230
+ if api_key:
231
+ os.environ["ANTHROPIC_API_KEY"] = api_key
232
+ return AnthropicModel(model_name=model_id)
233
+
234
+ elif provider == "google":
235
+ if api_key:
236
+ os.environ["GEMINI_API_KEY"] = api_key
237
+ os.environ["GOOGLE_API_KEY"] = api_key
238
+ return GoogleModel(model_name=model_id)
239
+
240
+ elif provider == "huggingface":
241
+ if api_key:
242
+ os.environ["HF_TOKEN"] = api_key
243
+ return HuggingFaceModel(model_name=model_id)
244
+ return OpenAIChatModel(model_name=model_id, provider="openai")
245
+
246
+
247
+ def extract_tool_tags(tool_def: Any) -> List[str]:
248
+ """
249
+ Extracts tags from a tool definition object.
250
+ """
251
+ tags_list = []
252
+
253
+ meta = getattr(tool_def, "meta", None)
254
+ if isinstance(meta, dict):
255
+ fastmcp = meta.get("fastmcp") or meta.get("_fastmcp") or {}
256
+ tags_list = fastmcp.get("tags", [])
257
+ if tags_list:
258
+ return tags_list
259
+
260
+ tags_list = meta.get("tags", [])
261
+ if tags_list:
262
+ return tags_list
263
+
264
+ metadata = getattr(tool_def, "metadata", None)
265
+ if isinstance(metadata, dict):
266
+ tags_list = metadata.get("tags", [])
267
+ if tags_list:
268
+ return tags_list
269
+
270
+ meta_nested = metadata.get("meta") or {}
271
+ fastmcp = meta_nested.get("fastmcp") or meta_nested.get("_fastmcp") or {}
272
+ tags_list = fastmcp.get("tags", [])
273
+ if tags_list:
274
+ return tags_list
275
+
276
+ tags_list = meta_nested.get("tags", [])
277
+ if tags_list:
278
+ return tags_list
279
+
280
+ tags_list = getattr(tool_def, "tags", [])
281
+ if isinstance(tags_list, list) and tags_list:
282
+ return tags_list
283
+
284
+ return []
285
+
286
+
287
+ def tool_in_tag(tool_def: Any, tag: str) -> bool:
288
+ """
289
+ Checks if a tool belongs to a specific tag.
290
+ """
291
+ tool_tags = extract_tool_tags(tool_def)
292
+ if tag in tool_tags:
293
+ return True
294
+ else:
295
+ return False
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: github-agent
3
+ Version: 0.1.1
4
+ Summary: GitHub Agent for MCP
5
+ Author-email: Audel Rouhi <knucklessg1@gmail.com>
6
+ License: MIT
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: License :: Public Domain
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: pydantic-ai-slim[a2a,ag-ui,anthropic,fastmcp,google,huggingface,openai,web]>=1.32.0
16
+ Requires-Dist: pydantic-ai-skills
17
+ Requires-Dist: fastapi>=0.128.0
18
+ Requires-Dist: fastmcp
19
+ Requires-Dist: uvicorn
20
+ Requires-Dist: fastapi
21
+ Dynamic: license-file
22
+
23
+ # GitHub Agent - A2A | AG-UI | MCP
24
+
25
+ ![PyPI - Version](https://img.shields.io/pypi/v/github-agent)
26
+ ![MCP Server](https://badge.mcpx.dev?type=server 'MCP Server')
27
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/github-agent)
28
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/github-agent)
29
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/github-agent)
30
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/github-agent)
31
+ ![PyPI - License](https://img.shields.io/pypi/l/github-agent)
32
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/github-agent)
33
+
34
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/github-agent)
35
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/github-agent)
36
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/github-agent)
37
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/github-agent)
38
+
39
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/github-agent)
40
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/github-agent)
41
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/github-agent)
42
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/github-agent)
43
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/github-agent)
44
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/github-agent)
45
+
46
+ *Version: 0.1.1*
47
+
48
+ ## Overview
49
+
50
+ **GitHub Agent** is a powerful **Model Context Protocol (MCP)** server and **Agent-to-Agent (A2A)** system designed to interact with GitHub.
51
+
52
+ It acts as a **Supervisor Agent**, delegating tasks to a suite of specialized **Child Agents**, each focused on a specific domain of the GitHub API (e.g., Issues, Pull Requests, Repositories, Actions). This architecture allows for precise and efficient handling of complex GitHub operations.
53
+
54
+ This repository is actively maintained - Contributions are welcome!
55
+
56
+ ### Capabilities:
57
+ - **Supervisor-Worker Architecture**: Orchestrates specialized agents for optimal task execution.
58
+ - **Comprehensive GitHub Coverage**: specialized agents for Issues, PRs, Repos, Actions, Organizations, and more.
59
+ - **MCP Support**: Fully compatible with the Model Context Protocol.
60
+ - **A2A Integration**: Ready for Agent-to-Agent communication.
61
+ - **Flexible Deployment**: Run via Docker, Docker Compose, or locally.
62
+
63
+ ## Architecture
64
+
65
+ ### System components
66
+
67
+ ```mermaid
68
+ ---
69
+ config:
70
+ layout: dagre
71
+ ---
72
+ flowchart TB
73
+ subgraph subGraph0["Agent Capabilities"]
74
+ Supervisor["Supervisor Agent"]
75
+ Server["A2A Server - Uvicorn/FastAPI"]
76
+ ChildAgents["Child Agents (Specialists)"]
77
+ MCP["GitHub MCP Tools"]
78
+ end
79
+ Supervisor --> ChildAgents
80
+ ChildAgents --> MCP
81
+ User["User Query"] --> Server
82
+ Server --> Supervisor
83
+ MCP --> GitHubAPI["GitHub API"]
84
+
85
+ Supervisor:::agent
86
+ ChildAgents:::agent
87
+ Server:::server
88
+ User:::server
89
+ classDef server fill:#f9f,stroke:#333
90
+ classDef agent fill:#bbf,stroke:#333,stroke-width:2px
91
+ style Server stroke:#000000,fill:#FFD600
92
+ style MCP stroke:#000000,fill:#BBDEFB
93
+ style GitHubAPI fill:#E6E6FA
94
+ style User fill:#C8E6C9
95
+ style subGraph0 fill:#FFF9C4
96
+ ```
97
+
98
+ ### Component Interaction
99
+
100
+ ```mermaid
101
+ sequenceDiagram
102
+ participant User
103
+ participant Server as A2A Server
104
+ participant Supervisor as Supervisor Agent
105
+ participant Child as Child Agent (e.g. Issues)
106
+ participant MCP as GitHub MCP Tools
107
+ participant GitHub as GitHub API
108
+
109
+ User->>Server: "Create an issue in repo X"
110
+ Server->>Supervisor: Invoke Supervisor
111
+ Supervisor->>Supervisor: Analyze Request & Select Specialist
112
+ Supervisor->>Child: Delegate to Issues Agent
113
+ Child->>MCP: Call create_issue Tool
114
+ MCP->>GitHub: POST /repos/user/repo/issues
115
+ GitHub-->>MCP: Issue Created JSON
116
+ MCP-->>Child: Tool Response
117
+ Child-->>Supervisor: Task Complete
118
+ Supervisor-->>Server: Final Response
119
+ Server-->>User: "Issue #123 created successfully"
120
+ ```
121
+
122
+ ## Specialized Agents
123
+
124
+ The Supervisor delegates tasks to these specialized agents:
125
+
126
+ | Agent Name | Description |
127
+ |:-----------|:------------|
128
+ | `GitHub_Context_Agent` | Provides context about the current user and GitHub status. |
129
+ | `GitHub_Actions_Agent` | Manages GitHub Actions workflows and runs. |
130
+ | `GitHub_Code_Security_Agent` | Handles code security scanning and alerts. |
131
+ | `GitHub_Dependabot_Agent` | Manages Dependabot alerts and configurations. |
132
+ | `GitHub_Discussions_Agent` | Manages repository discussions. |
133
+ | `GitHub_Gists_Agent` | Manages GitHub Gists. |
134
+ | `GitHub_Git_Agent` | Performs low-level Git operations (refs, trees, blobs). |
135
+ | `GitHub_Issues_Agent` | Manages Issues (create, list, update, comment). |
136
+ | `GitHub_Labels_Agent` | Manages repository labels. |
137
+ | `GitHub_Notifications_Agent` | Checks and manages notifications. |
138
+ | `GitHub_Organizations_Agent` | Manages Organization memberships and settings. |
139
+ | `GitHub_Projects_Agent` | Manages GitHub Projects (V2). |
140
+ | `GitHub_Pull_Requests_Agent` | Manages Pull Requests (create, review, merge). |
141
+ | `GitHub_Repos_Agent` | Manages Repositories (create, list, delete, settings). |
142
+ | `GitHub_Secret_Protection_Agent` | Manages secret scanning protection. |
143
+ | `GitHub_Security_Advisories_Agent` | Accesses security advisories. |
144
+ | `GitHub_Stargazers_Agent` | Views repository stargazers. |
145
+ | `GitHub_Users_Agent` | Accesses public user information. |
146
+ | `GitHub_Copilot_Agent` | Assists with coding tasks via Copilot. |
147
+ | `GitHub_Support_Docs_Agent` | Searches GitHub Support documentation. |
148
+
149
+ ## Usage
150
+
151
+ ### Prerequisites
152
+ - Python 3.10+
153
+ - A valid GitHub Personal Access Token (PAT) with appropriate permissions.
154
+
155
+ ### Installation
156
+
157
+ ```bash
158
+ pip install github-agent
159
+ ```
160
+ Or using UV:
161
+ ```bash
162
+ uv pip install github-agent
163
+ ```
164
+
165
+ ### CLI
166
+
167
+ The `github-agent` command starts the server.
168
+
169
+ | Argument | Description | Default |
170
+ |:---|:---|:---|
171
+ | `--host` | Host to bind the server to | `0.0.0.0` |
172
+ | `--port` | Port to bind the server to | `9000` |
173
+ | `--mcp-config` | Path to MCP configuration file | `mcp_config.json` |
174
+ | `--provider` | LLM Provider (openai, anthropic, google, etc.) | `openai` |
175
+ | `--model-id` | LLM Model ID | `qwen/qwen3-4b-2507` |
176
+
177
+ ### Running the Agent Server
178
+
179
+ ```bash
180
+ github-agent --provider openai --model-id gpt-4o --api-key sk-...
181
+ ```
182
+
183
+ ## Docker
184
+
185
+ ### Build
186
+
187
+ ```bash
188
+ docker build -t github-agent .
189
+ ```
190
+
191
+ ### Run using Docker
192
+
193
+ ```bash
194
+ docker run -d \
195
+ -p 9000:9000 \
196
+ -e OPENAI_API_KEY=sk-... \
197
+ -e MCP_CONFIG=/app/mcp_config.json \
198
+ knucklessg1/github-agent:latest
199
+ ```
200
+
201
+ ### Run using Docker Compose
202
+
203
+ Create a `docker-compose.yml`:
204
+
205
+ ```yaml
206
+ services:
207
+ github-agent:
208
+ image: knucklessg1/github-agent:latest
209
+ ports:
210
+ - "9000:9000"
211
+ environment:
212
+ - PROVIDER=openai
213
+ - MODEL_ID=gpt-4o
214
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
215
+ volumes:
216
+ - ./mcp_config.json:/app/mcp_config.json
217
+ ```
218
+
219
+ Then run:
220
+ ```bash
221
+ docker-compose up -d
222
+ ```
223
+
224
+ ## Repository Owners
225
+
226
+ <img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
227
+
228
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
229
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
@@ -0,0 +1,9 @@
1
+ github_agent/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ github_agent/github_agent.py,sha256=eb_j0zEtUMud7sXAh3uQNOU87Y5dRtJoejknnhUk4vg,26033
3
+ github_agent/utils.py,sha256=ruOgtdRLF2or1dsPNzl6Fe9iCVZncYyeIUsaePPOd0U,9135
4
+ github_agent-0.1.1.dist-info/licenses/LICENSE,sha256=5ALbh4fIALWVsUhO7q1nFT1bQb-CL9sWKf7p3GgLEG8,1070
5
+ github_agent-0.1.1.dist-info/METADATA,sha256=pETHU4C74fQm_u_E0gla9WhnlJm-YeyeLfG_arLExPU,8168
6
+ github_agent-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
7
+ github_agent-0.1.1.dist-info/entry_points.txt,sha256=PBPcmYlKzTvAtDxta6Vc681dQpFYFmoih1KZCwm_To4,72
8
+ github_agent-0.1.1.dist-info/top_level.txt,sha256=LpuUcrgMgA5o3phSpQjW0OJ7b2GpHuFQiCqxLfeu2c8,13
9
+ github_agent-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ github-agent = github_agent.github_agent:agent_server
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Knuckles Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ github_agent