agentr 0.1.6__py3-none-any.whl → 0.1.7__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.
- agentr/__init__.py +1 -1
- agentr/application.py +92 -56
- agentr/applications/__init__.py +27 -0
- agentr/applications/github/app.py +408 -56
- agentr/applications/google_calendar/app.py +548 -74
- agentr/applications/google_mail/app.py +565 -68
- agentr/applications/reddit/app.py +29 -29
- agentr/applications/resend/app.py +43 -43
- agentr/applications/tavily/app.py +57 -57
- agentr/applications/zenquotes/app.py +20 -20
- agentr/cli.py +76 -75
- agentr/config.py +15 -0
- agentr/exceptions.py +6 -5
- agentr/integration.py +85 -98
- agentr/server.py +113 -105
- agentr/store.py +70 -70
- agentr/test.py +39 -37
- agentr/utils/openapi.py +185 -184
- {agentr-0.1.6.dist-info → agentr-0.1.7.dist-info}/METADATA +4 -1
- agentr-0.1.7.dist-info/RECORD +25 -0
- {agentr-0.1.6.dist-info → agentr-0.1.7.dist-info}/licenses/LICENSE +21 -21
- agentr-0.1.6.dist-info/RECORD +0 -23
- {agentr-0.1.6.dist-info → agentr-0.1.7.dist-info}/WHEEL +0 -0
- {agentr-0.1.6.dist-info → agentr-0.1.7.dist-info}/entry_points.txt +0 -0
agentr/__init__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
def main():
|
1
|
+
def main():
|
2
2
|
print("hello")
|
agentr/application.py
CHANGED
@@ -1,57 +1,93 @@
|
|
1
|
-
from abc import ABC, abstractmethod
|
2
|
-
|
3
|
-
import
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
from agentr.exceptions import NotAuthorizedError
|
5
|
+
from agentr.integration import Integration
|
6
|
+
import httpx
|
7
|
+
|
8
|
+
class Application(ABC):
|
9
|
+
"""
|
10
|
+
Application is collection of tools that can be used by an agent.
|
11
|
+
"""
|
12
|
+
def __init__(self, name: str, **kwargs):
|
13
|
+
self.name = name
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def list_tools(self):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
class APIApplication(Application):
|
21
|
+
"""
|
22
|
+
APIApplication is an application that uses an API to interact with the world.
|
23
|
+
"""
|
24
|
+
def __init__(self, name: str, integration: Integration = None, **kwargs):
|
25
|
+
super().__init__(name, **kwargs)
|
26
|
+
self.integration = integration
|
27
|
+
|
28
|
+
def _get_headers(self):
|
29
|
+
return {}
|
30
|
+
|
31
|
+
def _get(self, url, params=None):
|
32
|
+
try:
|
33
|
+
headers = self._get_headers()
|
34
|
+
response = httpx.get(url, headers=headers, params=params)
|
35
|
+
response.raise_for_status()
|
36
|
+
return response
|
37
|
+
except NotAuthorizedError as e:
|
38
|
+
logger.warning(f"Reddit authorization needed: {e.message}")
|
39
|
+
return e.message
|
40
|
+
except Exception as e:
|
41
|
+
logger.error(f"Error getting {url}: {e}")
|
42
|
+
raise e
|
43
|
+
|
44
|
+
|
45
|
+
def _post(self, url, data):
|
46
|
+
try:
|
47
|
+
headers = self._get_headers()
|
48
|
+
response = httpx.post(url, headers=headers, json=data)
|
49
|
+
response.raise_for_status()
|
50
|
+
return response
|
51
|
+
except NotAuthorizedError as e:
|
52
|
+
logger.warning(f"Reddit authorization needed: {e.message}")
|
53
|
+
return e.message
|
54
|
+
except httpx.HTTPStatusError as e:
|
55
|
+
if e.response.status_code == 429:
|
56
|
+
return e.response.text or "Rate limit exceeded. Please try again later."
|
57
|
+
else:
|
58
|
+
raise e
|
59
|
+
except Exception as e:
|
60
|
+
logger.error(f"Error posting {url}: {e}")
|
61
|
+
raise e
|
62
|
+
|
63
|
+
def _put(self, url, data):
|
64
|
+
try:
|
65
|
+
headers = self._get_headers()
|
66
|
+
response = httpx.put(url, headers=headers, json=data)
|
67
|
+
response.raise_for_status()
|
68
|
+
return response
|
69
|
+
except NotAuthorizedError as e:
|
70
|
+
logger.warning(f"Reddit authorization needed: {e.message}")
|
71
|
+
return e.message
|
72
|
+
except Exception as e:
|
73
|
+
logger.error(f"Error posting {url}: {e}")
|
74
|
+
raise e
|
75
|
+
|
76
|
+
def _delete(self, url):
|
77
|
+
try:
|
78
|
+
headers = self._get_headers()
|
79
|
+
response = httpx.delete(url, headers=headers)
|
80
|
+
response.raise_for_status()
|
81
|
+
return response
|
82
|
+
except NotAuthorizedError as e:
|
83
|
+
logger.warning(f"Reddit authorization needed: {e.message}")
|
84
|
+
return e.message
|
85
|
+
except Exception as e:
|
86
|
+
logger.error(f"Error posting {url}: {e}")
|
87
|
+
|
88
|
+
def validate(self):
|
89
|
+
pass
|
90
|
+
|
91
|
+
@abstractmethod
|
92
|
+
def list_tools(self):
|
57
93
|
pass
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from agentr.applications.zenquotes.app import ZenQuoteApp
|
2
|
+
from agentr.applications.tavily.app import TavilyApp
|
3
|
+
from agentr.applications.github.app import GithubApp
|
4
|
+
from agentr.applications.google_calendar.app import GoogleCalendarApp
|
5
|
+
from agentr.applications.google_mail.app import GmailApp
|
6
|
+
from agentr.applications.resend.app import ResendApp
|
7
|
+
from agentr.applications.reddit.app import RedditApp
|
8
|
+
|
9
|
+
def app_from_name(name: str):
|
10
|
+
name = name.lower().strip()
|
11
|
+
name = name.replace(" ", "-")
|
12
|
+
if name == "zenquotes":
|
13
|
+
return ZenQuoteApp
|
14
|
+
elif name == "tavily":
|
15
|
+
return TavilyApp
|
16
|
+
elif name == "github":
|
17
|
+
return GithubApp
|
18
|
+
elif name == "google-calendar":
|
19
|
+
return GoogleCalendarApp
|
20
|
+
elif name == "google-mail":
|
21
|
+
return GmailApp
|
22
|
+
elif name == "resend":
|
23
|
+
return ResendApp
|
24
|
+
elif name == "reddit":
|
25
|
+
return RedditApp
|
26
|
+
else:
|
27
|
+
raise ValueError(f"App {name} not found")
|
@@ -1,56 +1,408 @@
|
|
1
|
-
from agentr.integration import Integration
|
2
|
-
from agentr.application import APIApplication
|
3
|
-
from loguru import logger
|
4
|
-
from agentr.exceptions import NotAuthorizedError
|
5
|
-
|
6
|
-
class GithubApp(APIApplication):
|
7
|
-
def __init__(self, integration: Integration) -> None:
|
8
|
-
super().__init__(name="github", integration=integration)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
"
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
1
|
+
from agentr.integration import Integration
|
2
|
+
from agentr.application import APIApplication
|
3
|
+
from loguru import logger
|
4
|
+
from agentr.exceptions import NotAuthorizedError
|
5
|
+
|
6
|
+
class GithubApp(APIApplication):
|
7
|
+
def __init__(self, integration: Integration) -> None:
|
8
|
+
super().__init__(name="github", integration=integration)
|
9
|
+
self.base_api_url = "https://api.github.com/repos"
|
10
|
+
|
11
|
+
def _get_headers(self):
|
12
|
+
if not self.integration:
|
13
|
+
raise ValueError("Integration not configured")
|
14
|
+
credentials = self.integration.get_credentials()
|
15
|
+
if not credentials:
|
16
|
+
logger.warning("No credentials found")
|
17
|
+
action = self.integration.authorize()
|
18
|
+
raise NotAuthorizedError(action)
|
19
|
+
if "headers" in credentials:
|
20
|
+
return credentials["headers"]
|
21
|
+
return {
|
22
|
+
"Authorization": f"Bearer {credentials['access_token']}",
|
23
|
+
"Accept": "application/vnd.github.v3+json"
|
24
|
+
}
|
25
|
+
|
26
|
+
|
27
|
+
def star_repository(self, repo_full_name: str) -> str:
|
28
|
+
"""Star a GitHub repository
|
29
|
+
|
30
|
+
Args:
|
31
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
|
35
|
+
A confirmation message
|
36
|
+
"""
|
37
|
+
url = f"https://api.github.com/user/starred/{repo_full_name}"
|
38
|
+
response = self._put(url, data={})
|
39
|
+
|
40
|
+
if response.status_code == 204:
|
41
|
+
return f"Successfully starred repository {repo_full_name}"
|
42
|
+
elif response.status_code == 404:
|
43
|
+
return f"Repository {repo_full_name} not found"
|
44
|
+
else:
|
45
|
+
logger.error(response.text)
|
46
|
+
return f"Error starring repository: {response.text}"
|
47
|
+
|
48
|
+
|
49
|
+
def list_commits(self, repo_full_name: str) -> str:
|
50
|
+
"""List recent commits for a GitHub repository
|
51
|
+
|
52
|
+
Args:
|
53
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
A formatted list of recent commits
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
repo_full_name = repo_full_name.strip()
|
60
|
+
url = f"{self.base_api_url}/{repo_full_name}/commits"
|
61
|
+
response = self._get(url)
|
62
|
+
|
63
|
+
if response.status_code == 200:
|
64
|
+
commits = response.json()
|
65
|
+
if not commits:
|
66
|
+
return f"No commits found for repository {repo_full_name}"
|
67
|
+
|
68
|
+
result = f"Recent commits for {repo_full_name}:\n\n"
|
69
|
+
for commit in commits[:12]: # Limit to 12 commits
|
70
|
+
sha = commit.get("sha", "")[:7]
|
71
|
+
message = commit.get("commit", {}).get("message", "").split('\n')[0]
|
72
|
+
author = commit.get("commit", {}).get("author", {}).get("name", "Unknown")
|
73
|
+
|
74
|
+
result += f"- {sha}: {message} (by {author})\n"
|
75
|
+
|
76
|
+
return result
|
77
|
+
elif response.status_code == 404:
|
78
|
+
return f"Repository {repo_full_name} not found"
|
79
|
+
else:
|
80
|
+
logger.error(response.text)
|
81
|
+
return f"Error retrieving commits: {response.status_code} - {response.text}"
|
82
|
+
except NotAuthorizedError as e:
|
83
|
+
return e.message
|
84
|
+
except Exception as e:
|
85
|
+
logger.error(e)
|
86
|
+
return f"Error retrieving commits: {str(e)}"
|
87
|
+
|
88
|
+
def list_branches(self, repo_full_name: str) -> str:
|
89
|
+
"""List branches for a GitHub repository
|
90
|
+
|
91
|
+
Args:
|
92
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
A formatted list of branches
|
96
|
+
"""
|
97
|
+
try:
|
98
|
+
repo_full_name = repo_full_name.strip()
|
99
|
+
url = f"{self.base_api_url}/{repo_full_name}/branches"
|
100
|
+
response = self._get(url)
|
101
|
+
|
102
|
+
if response.status_code == 200:
|
103
|
+
branches = response.json()
|
104
|
+
if not branches:
|
105
|
+
return f"No branches found for repository {repo_full_name}"
|
106
|
+
|
107
|
+
result = f"Branches for {repo_full_name}:\n\n"
|
108
|
+
for branch in branches:
|
109
|
+
branch_name = branch.get("name", "Unknown")
|
110
|
+
result += f"- {branch_name}\n"
|
111
|
+
|
112
|
+
return result
|
113
|
+
elif response.status_code == 404:
|
114
|
+
return f"Repository {repo_full_name} not found"
|
115
|
+
else:
|
116
|
+
logger.error(response.text)
|
117
|
+
return f"Error retrieving branches: {response.status_code} - {response.text}"
|
118
|
+
except NotAuthorizedError as e:
|
119
|
+
return e.message
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(e)
|
122
|
+
return f"Error retrieving branches: {str(e)}"
|
123
|
+
|
124
|
+
def list_pull_requests(self, repo_full_name: str, state: str = "open") -> str:
|
125
|
+
"""List pull requests for a GitHub repository
|
126
|
+
|
127
|
+
Args:
|
128
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
129
|
+
state: The state of the pull requests to filter by (open, closed, or all)
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
A formatted list of pull requests
|
133
|
+
"""
|
134
|
+
try:
|
135
|
+
repo_full_name = repo_full_name.strip()
|
136
|
+
url = f"{self.base_api_url}/{repo_full_name}/pulls"
|
137
|
+
params = {"state": state}
|
138
|
+
response = self._get(url, params=params)
|
139
|
+
|
140
|
+
if response.status_code == 200:
|
141
|
+
pull_requests = response.json()
|
142
|
+
if not pull_requests:
|
143
|
+
return f"No pull requests found for repository {repo_full_name} with state '{state}'"
|
144
|
+
|
145
|
+
result = f"Pull requests for {repo_full_name} (State: {state}):\n\n"
|
146
|
+
for pr in pull_requests:
|
147
|
+
pr_title = pr.get("title", "No Title")
|
148
|
+
pr_number = pr.get("number", "Unknown")
|
149
|
+
pr_state = pr.get("state", "Unknown")
|
150
|
+
pr_user = pr.get("user", {}).get("login", "Unknown")
|
151
|
+
|
152
|
+
result += f"- PR #{pr_number}: {pr_title} (by {pr_user}, Status: {pr_state})\n"
|
153
|
+
|
154
|
+
return result
|
155
|
+
elif response.status_code == 404:
|
156
|
+
return f"Repository {repo_full_name} not found"
|
157
|
+
else:
|
158
|
+
logger.error(response.text)
|
159
|
+
return f"Error retrieving pull requests: {response.status_code} - {response.text}"
|
160
|
+
except NotAuthorizedError as e:
|
161
|
+
return e.message
|
162
|
+
except Exception as e:
|
163
|
+
logger.error(e)
|
164
|
+
return f"Error retrieving pull requests: {str(e)}"
|
165
|
+
|
166
|
+
def list_issues(self, repo_full_name: str, per_page: int = 30, page: int = 1) -> str:
|
167
|
+
"""List issues for a GitHub repository
|
168
|
+
|
169
|
+
Args:
|
170
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
171
|
+
per_page: The number of results per page (max 100)
|
172
|
+
page: The page number of the results to fetch
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
A formatted list of issues
|
176
|
+
"""
|
177
|
+
try:
|
178
|
+
repo_full_name = repo_full_name.strip()
|
179
|
+
url = f"{self.base_api_url}/{repo_full_name}/issues/events"
|
180
|
+
params = {
|
181
|
+
"per_page": per_page,
|
182
|
+
"page": page
|
183
|
+
}
|
184
|
+
response = self._get(url, params=params)
|
185
|
+
|
186
|
+
if response.status_code == 200:
|
187
|
+
issues = response.json()
|
188
|
+
if not issues:
|
189
|
+
return f"No issues found for repository {repo_full_name}"
|
190
|
+
|
191
|
+
result = f"Issues for {repo_full_name} (Page {page}):\n\n"
|
192
|
+
for issue in issues:
|
193
|
+
issue_title = issue.get("issue", {}).get("title", "No Title")
|
194
|
+
issue_number = issue.get("issue", {}).get("number", "Unknown")
|
195
|
+
issue_state = issue.get("issue", {}).get("state", "Unknown")
|
196
|
+
issue_user = issue.get("issue", {}).get("user", {}).get("login", "Unknown")
|
197
|
+
|
198
|
+
result += f"- Issue #{issue_number}: {issue_title} (by {issue_user}, Status: {issue_state})\n"
|
199
|
+
|
200
|
+
return result
|
201
|
+
elif response.status_code == 404:
|
202
|
+
return f"Repository {repo_full_name} not found"
|
203
|
+
else:
|
204
|
+
logger.error(response.text)
|
205
|
+
return f"Error retrieving issues: {response.status_code} - {response.text}"
|
206
|
+
except NotAuthorizedError as e:
|
207
|
+
return e.message
|
208
|
+
except Exception as e:
|
209
|
+
logger.error(e)
|
210
|
+
return f"Error retrieving issues: {str(e)}"
|
211
|
+
|
212
|
+
def get_pull_request(self, repo_full_name: str, pull_number: int) -> str:
|
213
|
+
"""Get a specific pull request for a GitHub repository
|
214
|
+
|
215
|
+
Args:
|
216
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
217
|
+
pull_number: The number of the pull request to retrieve
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
A formatted string with pull request details
|
221
|
+
"""
|
222
|
+
try:
|
223
|
+
repo_full_name = repo_full_name.strip()
|
224
|
+
url = f"{self.base_api_url}/{repo_full_name}/pulls/{pull_number}"
|
225
|
+
response = self._get(url)
|
226
|
+
|
227
|
+
if response.status_code == 200:
|
228
|
+
pr = response.json()
|
229
|
+
pr_title = pr.get("title", "No Title")
|
230
|
+
pr_number = pr.get("number", "Unknown")
|
231
|
+
pr_state = pr.get("state", "Unknown")
|
232
|
+
pr_user = pr.get("user", {}).get("login", "Unknown")
|
233
|
+
pr_body = pr.get("body", "No description provided.")
|
234
|
+
|
235
|
+
result = (
|
236
|
+
f"Pull Request #{pr_number}: {pr_title}\n"
|
237
|
+
f"Created by: {pr_user}\n"
|
238
|
+
f"Status: {pr_state}\n"
|
239
|
+
f"Description: {pr_body}\n"
|
240
|
+
)
|
241
|
+
|
242
|
+
return result
|
243
|
+
elif response.status_code == 404:
|
244
|
+
return f"Pull request #{pull_number} not found in repository {repo_full_name}"
|
245
|
+
else:
|
246
|
+
logger.error(response.text)
|
247
|
+
return f"Error retrieving pull request: {response.status_code} - {response.text}"
|
248
|
+
except NotAuthorizedError as e:
|
249
|
+
return e.message
|
250
|
+
except Exception as e:
|
251
|
+
logger.error(e)
|
252
|
+
return f"Error retrieving pull request: {str(e)}"
|
253
|
+
|
254
|
+
def create_pull_request(self, repo_full_name: str, title: str, head: str, base: str, body: str = "",
|
255
|
+
maintainer_can_modify: bool = True, draft: bool = False) -> str:
|
256
|
+
"""Create a new pull request for a GitHub repository
|
257
|
+
|
258
|
+
Args:
|
259
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
260
|
+
title: The title of the new pull request
|
261
|
+
head: The name of the branch where your changes are implemented
|
262
|
+
base: The name of the branch you want the changes pulled into
|
263
|
+
body: The contents of the pull request
|
264
|
+
maintainer_can_modify: Indicates whether maintainers can modify the pull request
|
265
|
+
draft: Indicates whether the pull request is a draft
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
A confirmation message with the new pull request details
|
269
|
+
"""
|
270
|
+
try:
|
271
|
+
repo_full_name = repo_full_name.strip()
|
272
|
+
url = f"{self.base_api_url}/{repo_full_name}/pulls"
|
273
|
+
|
274
|
+
pull_request_data = {
|
275
|
+
"title": title,
|
276
|
+
"head": head,
|
277
|
+
"base": base,
|
278
|
+
"body": body,
|
279
|
+
"maintainer_can_modify": maintainer_can_modify,
|
280
|
+
"draft": draft
|
281
|
+
}
|
282
|
+
|
283
|
+
response = self._post(url, pull_request_data)
|
284
|
+
|
285
|
+
if response.status_code in [200, 201]:
|
286
|
+
pr = response.json()
|
287
|
+
pr_number = pr.get("number", "Unknown")
|
288
|
+
pr_url = pr.get("html_url", "")
|
289
|
+
|
290
|
+
return f"Successfully created pull request #{pr_number}:\n" \
|
291
|
+
f"Title: {title}\n" \
|
292
|
+
f"From: {head} → To: {base}\n" \
|
293
|
+
f"URL: {pr_url}"
|
294
|
+
else:
|
295
|
+
logger.error(response.text)
|
296
|
+
return f"Error creating pull request: {response.status_code} - {response.text}"
|
297
|
+
except NotAuthorizedError as e:
|
298
|
+
return e.message
|
299
|
+
except Exception as e:
|
300
|
+
logger.error(e)
|
301
|
+
return f"Error creating pull request: {str(e)}"
|
302
|
+
|
303
|
+
def create_issue(self, repo_full_name: str, title: str, body: str = "", labels = None) -> str:
|
304
|
+
"""Create a new issue in a GitHub repository
|
305
|
+
|
306
|
+
Args:
|
307
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
308
|
+
title: The title of the issue
|
309
|
+
body: The contents of the issue
|
310
|
+
labels: Labels to associate with this issue. Enter as a comma-separated string
|
311
|
+
(e.g. "bug,enhancement,documentation").
|
312
|
+
NOTE: Only users with push access can set labels for new issues.
|
313
|
+
Labels are silently dropped otherwise.
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
A confirmation message with the new issue details
|
317
|
+
"""
|
318
|
+
try:
|
319
|
+
repo_full_name = repo_full_name.strip()
|
320
|
+
url = f"{self.base_api_url}/{repo_full_name}/issues"
|
321
|
+
|
322
|
+
issue_data = {
|
323
|
+
"title": title,
|
324
|
+
"body": body
|
325
|
+
}
|
326
|
+
|
327
|
+
if labels:
|
328
|
+
if isinstance(labels, str):
|
329
|
+
labels_list = [label.strip() for label in labels.split(",") if label.strip()]
|
330
|
+
issue_data["labels"] = labels_list
|
331
|
+
else:
|
332
|
+
issue_data["labels"] = labels
|
333
|
+
|
334
|
+
response = self._post(url, issue_data)
|
335
|
+
|
336
|
+
if response.status_code in [200, 201]:
|
337
|
+
issue = response.json()
|
338
|
+
issue_number = issue.get("number", "Unknown")
|
339
|
+
issue_url = issue.get("html_url", "")
|
340
|
+
|
341
|
+
return f"Successfully created issue #{issue_number}:\n" \
|
342
|
+
f"Title: {title}\n" \
|
343
|
+
f"URL: {issue_url}"
|
344
|
+
else:
|
345
|
+
logger.error(response.text)
|
346
|
+
return f"Error creating issue: {response.status_code} - {response.text}"
|
347
|
+
except NotAuthorizedError as e:
|
348
|
+
return e.message
|
349
|
+
except Exception as e:
|
350
|
+
logger.error(e)
|
351
|
+
return f"Error creating issue: {str(e)}"
|
352
|
+
|
353
|
+
def list_repo_activities(self, repo_full_name: str, direction: str = "desc", per_page: int = 30) -> str:
|
354
|
+
"""List activities for a GitHub repository
|
355
|
+
|
356
|
+
Args:
|
357
|
+
repo_full_name: The full name of the repository (e.g. 'owner/repo')
|
358
|
+
direction: The direction to sort the results by (asc or desc). Default: desc
|
359
|
+
per_page: The number of results per page (max 100). Default: 30
|
360
|
+
|
361
|
+
Returns:
|
362
|
+
A formatted list of repository activities
|
363
|
+
"""
|
364
|
+
try:
|
365
|
+
repo_full_name = repo_full_name.strip()
|
366
|
+
url = f"{self.base_api_url}/{repo_full_name}/activity"
|
367
|
+
|
368
|
+
# Build query parameters
|
369
|
+
params = {
|
370
|
+
"direction": direction,
|
371
|
+
"per_page": per_page
|
372
|
+
}
|
373
|
+
|
374
|
+
response = self._get(url, params=params)
|
375
|
+
|
376
|
+
if response.status_code == 200:
|
377
|
+
activities = response.json()
|
378
|
+
if not activities:
|
379
|
+
return f"No activities found for repository {repo_full_name}"
|
380
|
+
|
381
|
+
result = f"Repository activities for {repo_full_name}:\n\n"
|
382
|
+
|
383
|
+
for activity in activities:
|
384
|
+
# Extract common fields
|
385
|
+
timestamp = activity.get("timestamp", "Unknown time")
|
386
|
+
actor_name = "Unknown user"
|
387
|
+
if "actor" in activity and activity["actor"]:
|
388
|
+
actor_name = activity["actor"].get("login", "Unknown user")
|
389
|
+
|
390
|
+
# Create a simple description of the activity
|
391
|
+
result += f"- {actor_name} performed an activity at {timestamp}\n"
|
392
|
+
|
393
|
+
return result
|
394
|
+
elif response.status_code == 404:
|
395
|
+
return f"Repository {repo_full_name} not found"
|
396
|
+
else:
|
397
|
+
logger.error(response.text)
|
398
|
+
return f"Error retrieving activities: {response.status_code} - {response.text}"
|
399
|
+
except NotAuthorizedError as e:
|
400
|
+
return e.message
|
401
|
+
except Exception as e:
|
402
|
+
logger.error(e)
|
403
|
+
return f"Error retrieving activities: {str(e)}"
|
404
|
+
|
405
|
+
def list_tools(self):
|
406
|
+
return [self.star_repository, self.list_commits, self.list_branches,
|
407
|
+
self.list_pull_requests, self.list_issues, self.get_pull_request,
|
408
|
+
self.create_pull_request, self.create_issue, self.list_repo_activities]
|