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.
- github_agent/__init__.py +1 -0
- github_agent/github_agent.py +751 -0
- github_agent/utils.py +295 -0
- github_agent-0.1.1.dist-info/METADATA +229 -0
- github_agent-0.1.1.dist-info/RECORD +9 -0
- github_agent-0.1.1.dist-info/WHEEL +5 -0
- github_agent-0.1.1.dist-info/entry_points.txt +2 -0
- github_agent-0.1.1.dist-info/licenses/LICENSE +21 -0
- github_agent-0.1.1.dist-info/top_level.txt +1 -0
github_agent/__init__.py
ADDED
|
@@ -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
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+

|
|
32
|
+

|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+
|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
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
|
+

|
|
229
|
+

|
|
@@ -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,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
|