github-haystack 1.0.0__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_haystack-1.0.0.dist-info/METADATA +45 -0
- github_haystack-1.0.0.dist-info/RECORD +26 -0
- github_haystack-1.0.0.dist-info/WHEEL +4 -0
- github_haystack-1.0.0.dist-info/licenses/LICENSE.txt +73 -0
- haystack_integrations/components/connectors/github/__init__.py +19 -0
- haystack_integrations/components/connectors/github/file_editor.py +303 -0
- haystack_integrations/components/connectors/github/issue_commenter.py +161 -0
- haystack_integrations/components/connectors/github/issue_viewer.py +216 -0
- haystack_integrations/components/connectors/github/pr_creator.py +277 -0
- haystack_integrations/components/connectors/github/repo_forker.py +306 -0
- haystack_integrations/components/connectors/github/repo_viewer.py +272 -0
- haystack_integrations/prompts/github/__init__.py +22 -0
- haystack_integrations/prompts/github/context_prompt.py +176 -0
- haystack_integrations/prompts/github/file_editor_prompt.py +122 -0
- haystack_integrations/prompts/github/issue_commenter_prompt.py +21 -0
- haystack_integrations/prompts/github/issue_viewer_prompt.py +25 -0
- haystack_integrations/prompts/github/pr_creator_prompt.py +83 -0
- haystack_integrations/prompts/github/repo_viewer_prompt.py +81 -0
- haystack_integrations/prompts/github/system_prompt.py +63 -0
- haystack_integrations/tools/github/__init__.py +16 -0
- haystack_integrations/tools/github/file_editor_tool.py +128 -0
- haystack_integrations/tools/github/issue_commenter_tool.py +123 -0
- haystack_integrations/tools/github/issue_viewer_tool.py +123 -0
- haystack_integrations/tools/github/pr_creator_tool.py +117 -0
- haystack_integrations/tools/github/repo_viewer_tool.py +138 -0
- haystack_integrations/tools/github/utils.py +73 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from haystack import component, default_from_dict, default_to_dict, logging
|
|
9
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@component
|
|
15
|
+
class GitHubRepoForker:
|
|
16
|
+
"""
|
|
17
|
+
Forks a GitHub repository from an issue URL.
|
|
18
|
+
|
|
19
|
+
The component takes a GitHub issue URL, extracts the repository information,
|
|
20
|
+
creates or syncs a fork of that repository, and optionally creates an issue-specific branch.
|
|
21
|
+
|
|
22
|
+
### Usage example
|
|
23
|
+
```python
|
|
24
|
+
from haystack_integrations.components.connectors.github import GitHubRepoForker
|
|
25
|
+
from haystack.utils import Secret
|
|
26
|
+
|
|
27
|
+
# Using direct token with auto-sync and branch creation
|
|
28
|
+
forker = GitHubRepoForker(
|
|
29
|
+
github_token=Secret.from_env_var("GITHUB_TOKEN"),
|
|
30
|
+
auto_sync=True,
|
|
31
|
+
create_branch=True
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
result = forker.run(url="https://github.com/owner/repo/issues/123")
|
|
35
|
+
print(result)
|
|
36
|
+
# Will create or sync fork and create branch "fix-123"
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
|
|
44
|
+
raise_on_failure: bool = True,
|
|
45
|
+
wait_for_completion: bool = False,
|
|
46
|
+
max_wait_seconds: int = 300,
|
|
47
|
+
poll_interval: int = 2,
|
|
48
|
+
auto_sync: bool = True,
|
|
49
|
+
create_branch: bool = True,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize the component.
|
|
53
|
+
|
|
54
|
+
:param github_token: GitHub personal access token for API authentication
|
|
55
|
+
:param raise_on_failure: If True, raises exceptions on API errors
|
|
56
|
+
:param wait_for_completion: If True, waits until fork is fully created
|
|
57
|
+
:param max_wait_seconds: Maximum time to wait for fork completion in seconds
|
|
58
|
+
:param poll_interval: Time between status checks in seconds
|
|
59
|
+
:param auto_sync: If True, syncs fork with original repository if it already exists
|
|
60
|
+
:param create_branch: If True, creates a fix branch based on the issue number
|
|
61
|
+
"""
|
|
62
|
+
error_message = "github_token must be a Secret"
|
|
63
|
+
if not isinstance(github_token, Secret):
|
|
64
|
+
raise TypeError(error_message)
|
|
65
|
+
|
|
66
|
+
self.github_token = github_token
|
|
67
|
+
self.raise_on_failure = raise_on_failure
|
|
68
|
+
self.wait_for_completion = wait_for_completion
|
|
69
|
+
self.max_wait_seconds = max_wait_seconds
|
|
70
|
+
self.poll_interval = poll_interval
|
|
71
|
+
self.auto_sync = auto_sync
|
|
72
|
+
self.create_branch = create_branch
|
|
73
|
+
|
|
74
|
+
self.base_headers = {
|
|
75
|
+
"Accept": "application/vnd.github.v3+json",
|
|
76
|
+
"User-Agent": "Haystack/GitHubRepoForker",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def _get_request_headers(self) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
Get headers with resolved token for the request.
|
|
82
|
+
|
|
83
|
+
:return: Dictionary of headers including authorization if token is present
|
|
84
|
+
"""
|
|
85
|
+
headers = self.base_headers.copy()
|
|
86
|
+
if self.github_token is not None:
|
|
87
|
+
headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
|
|
88
|
+
return headers
|
|
89
|
+
|
|
90
|
+
def _parse_github_url(self, url: str) -> tuple[str, str, str]:
|
|
91
|
+
"""
|
|
92
|
+
Parse GitHub URL into owner, repo, and issue number.
|
|
93
|
+
|
|
94
|
+
:param url: GitHub issue URL
|
|
95
|
+
:return: Tuple of (owner, repo, issue_number)
|
|
96
|
+
:raises ValueError: If URL format is invalid
|
|
97
|
+
"""
|
|
98
|
+
pattern = r"https?://github\.com/([^/]+)/([^/]+)/issues/(\d+)"
|
|
99
|
+
match = re.match(pattern, url)
|
|
100
|
+
if not match:
|
|
101
|
+
error_message = f"Invalid GitHub issue URL format: {url}"
|
|
102
|
+
raise ValueError(error_message)
|
|
103
|
+
|
|
104
|
+
owner, repo, issue_number = match.groups()
|
|
105
|
+
return owner, repo, issue_number
|
|
106
|
+
|
|
107
|
+
def _check_fork_status(self, fork_path: str) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if a forked repository exists and is ready.
|
|
110
|
+
|
|
111
|
+
:param fork_path: Repository path in owner/repo format
|
|
112
|
+
:return: True if fork exists and is ready, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
url = f"https://api.github.com/repos/{fork_path}"
|
|
115
|
+
try:
|
|
116
|
+
response = requests.get(
|
|
117
|
+
url,
|
|
118
|
+
headers=self._get_request_headers(),
|
|
119
|
+
timeout=10,
|
|
120
|
+
)
|
|
121
|
+
return response.status_code == 200 # noqa: PLR2004
|
|
122
|
+
except requests.RequestException:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def _get_authenticated_user(self) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Get the authenticated user's username.
|
|
128
|
+
|
|
129
|
+
:return: Username of the authenticated user
|
|
130
|
+
:raises requests.RequestException: If API call fails
|
|
131
|
+
"""
|
|
132
|
+
url = "https://api.github.com/user"
|
|
133
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
return response.json()["login"]
|
|
136
|
+
|
|
137
|
+
def _get_existing_repository(self, repo_name: str) -> Optional[str]:
|
|
138
|
+
"""
|
|
139
|
+
Check if a repository with the given name already exists in the authenticated user's account.
|
|
140
|
+
|
|
141
|
+
:param repo_name: Repository name to check
|
|
142
|
+
:return: Full repository name if it exists, None otherwise
|
|
143
|
+
"""
|
|
144
|
+
url = f"https://api.github.com/repos/{self._get_authenticated_user()}/{repo_name}"
|
|
145
|
+
try:
|
|
146
|
+
response = requests.get(
|
|
147
|
+
url,
|
|
148
|
+
headers=self._get_request_headers(),
|
|
149
|
+
timeout=10,
|
|
150
|
+
)
|
|
151
|
+
if response.status_code == 200: # noqa: PLR2004
|
|
152
|
+
return repo_name
|
|
153
|
+
return None
|
|
154
|
+
except requests.RequestException as e:
|
|
155
|
+
logger.warning(f"Failed to check repository existence: {e!s}")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _sync_fork(self, fork_path: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Sync a fork with its upstream repository.
|
|
161
|
+
|
|
162
|
+
:param fork_path: Fork path in owner/repo format
|
|
163
|
+
:raises requests.RequestException: If sync fails
|
|
164
|
+
"""
|
|
165
|
+
url = f"https://api.github.com/repos/{fork_path}/merge-upstream"
|
|
166
|
+
response = requests.post(
|
|
167
|
+
url,
|
|
168
|
+
headers=self._get_request_headers(),
|
|
169
|
+
json={"branch": "main"},
|
|
170
|
+
timeout=10,
|
|
171
|
+
)
|
|
172
|
+
response.raise_for_status()
|
|
173
|
+
|
|
174
|
+
def _create_issue_branch(self, fork_path: str, issue_number: str) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Create a new branch for the issue.
|
|
177
|
+
|
|
178
|
+
:param fork_path: Fork path in owner/repo format
|
|
179
|
+
:param issue_number: Issue number to use in branch name
|
|
180
|
+
:raises requests.RequestException: If branch creation fails
|
|
181
|
+
"""
|
|
182
|
+
# First, get the default branch SHA
|
|
183
|
+
url = f"https://api.github.com/repos/{fork_path}"
|
|
184
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
default_branch = response.json()["default_branch"]
|
|
187
|
+
|
|
188
|
+
# Get the SHA of the default branch
|
|
189
|
+
url = f"https://api.github.com/repos/{fork_path}/git/ref/heads/{default_branch}"
|
|
190
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
191
|
+
response.raise_for_status()
|
|
192
|
+
sha = response.json()["object"]["sha"]
|
|
193
|
+
|
|
194
|
+
# Create the new branch
|
|
195
|
+
branch_name = f"fix-{issue_number}"
|
|
196
|
+
url = f"https://api.github.com/repos/{fork_path}/git/refs"
|
|
197
|
+
response = requests.post(
|
|
198
|
+
url,
|
|
199
|
+
headers=self._get_request_headers(),
|
|
200
|
+
json={"ref": f"refs/heads/{branch_name}", "sha": sha},
|
|
201
|
+
timeout=10,
|
|
202
|
+
)
|
|
203
|
+
response.raise_for_status()
|
|
204
|
+
|
|
205
|
+
def _create_fork(self, owner: str, repo: str) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Create a fork of the repository.
|
|
208
|
+
|
|
209
|
+
:param owner: Original repository owner
|
|
210
|
+
:param repo: Repository name
|
|
211
|
+
:return: Fork path in owner/repo format
|
|
212
|
+
:raises requests.RequestException: If fork creation fails
|
|
213
|
+
"""
|
|
214
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/forks"
|
|
215
|
+
response = requests.post(url, headers=self._get_request_headers(), timeout=10)
|
|
216
|
+
response.raise_for_status()
|
|
217
|
+
|
|
218
|
+
fork_data = response.json()
|
|
219
|
+
return f"{fork_data['owner']['login']}/{fork_data['name']}"
|
|
220
|
+
|
|
221
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Serialize the component to a dictionary.
|
|
224
|
+
|
|
225
|
+
:returns: Dictionary with serialized data.
|
|
226
|
+
"""
|
|
227
|
+
return default_to_dict(
|
|
228
|
+
self,
|
|
229
|
+
github_token=self.github_token.to_dict() if self.github_token else None,
|
|
230
|
+
raise_on_failure=self.raise_on_failure,
|
|
231
|
+
wait_for_completion=self.wait_for_completion,
|
|
232
|
+
max_wait_seconds=self.max_wait_seconds,
|
|
233
|
+
poll_interval=self.poll_interval,
|
|
234
|
+
auto_sync=self.auto_sync,
|
|
235
|
+
create_branch=self.create_branch,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GitHubRepoForker":
|
|
240
|
+
"""
|
|
241
|
+
Deserialize the component from a dictionary.
|
|
242
|
+
|
|
243
|
+
:param data: Dictionary to deserialize from.
|
|
244
|
+
:returns: Deserialized component.
|
|
245
|
+
"""
|
|
246
|
+
init_params = data["init_parameters"]
|
|
247
|
+
deserialize_secrets_inplace(init_params, keys=["github_token"])
|
|
248
|
+
return default_from_dict(cls, data)
|
|
249
|
+
|
|
250
|
+
@component.output_types(repo=str, issue_branch=str)
|
|
251
|
+
def run(self, url: str) -> dict:
|
|
252
|
+
"""
|
|
253
|
+
Process a GitHub issue URL and create or sync a fork of the repository.
|
|
254
|
+
|
|
255
|
+
:param url: GitHub issue URL
|
|
256
|
+
:return: Dictionary containing repository path in owner/repo format
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
# Extract repository information
|
|
260
|
+
owner, repo, issue_number = self._parse_github_url(url)
|
|
261
|
+
|
|
262
|
+
# Check if fork already exists
|
|
263
|
+
user = self._get_authenticated_user()
|
|
264
|
+
existing_fork = self._get_existing_repository(repo)
|
|
265
|
+
|
|
266
|
+
if existing_fork and self.auto_sync:
|
|
267
|
+
# If fork exists and auto_sync is enabled, sync with upstream
|
|
268
|
+
fork_path = f"{user}/{repo}"
|
|
269
|
+
logger.info("Fork already exists, syncing with upstream repository")
|
|
270
|
+
self._sync_fork(fork_path)
|
|
271
|
+
else:
|
|
272
|
+
# Create new fork
|
|
273
|
+
fork_path = self._create_fork(owner, repo)
|
|
274
|
+
|
|
275
|
+
# Wait for fork completion if requested
|
|
276
|
+
if self.wait_for_completion:
|
|
277
|
+
import time
|
|
278
|
+
|
|
279
|
+
start_time = time.time()
|
|
280
|
+
|
|
281
|
+
while time.time() - start_time < self.max_wait_seconds:
|
|
282
|
+
if self._check_fork_status(fork_path):
|
|
283
|
+
logger.info("Fork creation completed successfully")
|
|
284
|
+
break
|
|
285
|
+
logger.debug("Waiting for fork creation to complete...")
|
|
286
|
+
time.sleep(self.poll_interval)
|
|
287
|
+
else:
|
|
288
|
+
msg = f"Fork creation timed out after {self.max_wait_seconds} seconds"
|
|
289
|
+
if self.raise_on_failure:
|
|
290
|
+
raise TimeoutError(msg)
|
|
291
|
+
logger.warning(msg)
|
|
292
|
+
|
|
293
|
+
# Create issue branch if enabled
|
|
294
|
+
issue_branch = None
|
|
295
|
+
if self.create_branch:
|
|
296
|
+
issue_branch = f"fix-{issue_number}"
|
|
297
|
+
logger.info(f"Creating branch for issue #{issue_number}")
|
|
298
|
+
self._create_issue_branch(fork_path, issue_number)
|
|
299
|
+
|
|
300
|
+
return {"repo": fork_path, "issue_branch": issue_branch}
|
|
301
|
+
|
|
302
|
+
except Exception as e:
|
|
303
|
+
if self.raise_on_failure:
|
|
304
|
+
raise
|
|
305
|
+
logger.warning("Error forking repository from {url}: {error}", url=url, error=str(e))
|
|
306
|
+
return {"repo": "", "issue_branch": None}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import base64
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from haystack import Document, component, default_from_dict, default_to_dict, logging
|
|
10
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class GitHubItem:
|
|
17
|
+
"""Represents an item (file or directory) in a GitHub repository"""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
type: str # "file" or "dir"
|
|
21
|
+
path: str
|
|
22
|
+
size: int
|
|
23
|
+
url: str
|
|
24
|
+
content: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@component
|
|
28
|
+
class GitHubRepoViewer:
|
|
29
|
+
"""
|
|
30
|
+
Navigates and fetches content from GitHub repositories.
|
|
31
|
+
|
|
32
|
+
For directories:
|
|
33
|
+
- Returns a list of Documents, one for each item
|
|
34
|
+
- Each Document's content is the item name
|
|
35
|
+
- Full path and metadata in Document.meta
|
|
36
|
+
|
|
37
|
+
For files:
|
|
38
|
+
- Returns a single Document
|
|
39
|
+
- Document's content is the file content
|
|
40
|
+
- Full path and metadata in Document.meta
|
|
41
|
+
|
|
42
|
+
For errors:
|
|
43
|
+
- Returns a single Document
|
|
44
|
+
- Document's content is the error message
|
|
45
|
+
- Document's meta contains type="error"
|
|
46
|
+
|
|
47
|
+
### Usage example
|
|
48
|
+
```python
|
|
49
|
+
from haystack_integrations.components.connectors.github import GitHubRepoViewer
|
|
50
|
+
|
|
51
|
+
viewer = GitHubRepoViewer()
|
|
52
|
+
|
|
53
|
+
# List directory contents - returns multiple documents
|
|
54
|
+
result = viewer.run(
|
|
55
|
+
repo="owner/repository",
|
|
56
|
+
path="docs/",
|
|
57
|
+
branch="main"
|
|
58
|
+
)
|
|
59
|
+
print(result)
|
|
60
|
+
|
|
61
|
+
# Get specific file - returns single document
|
|
62
|
+
result = viewer.run(
|
|
63
|
+
repo="owner/repository",
|
|
64
|
+
path="README.md",
|
|
65
|
+
branch="main"
|
|
66
|
+
)
|
|
67
|
+
print(result)
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
github_token: Optional[Secret] = None,
|
|
75
|
+
raise_on_failure: bool = True,
|
|
76
|
+
max_file_size: int = 1_000_000, # 1MB default limit
|
|
77
|
+
repo: Optional[str] = None,
|
|
78
|
+
branch: str = "main",
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Initialize the component.
|
|
82
|
+
|
|
83
|
+
:param github_token: GitHub personal access token for API authentication
|
|
84
|
+
:param raise_on_failure: If True, raises exceptions on API errors
|
|
85
|
+
:param max_file_size: Maximum file size in bytes to fetch (default: 1MB)
|
|
86
|
+
:param repo: Repository in format "owner/repo"
|
|
87
|
+
:param branch: Git reference (branch, tag, commit) to use
|
|
88
|
+
"""
|
|
89
|
+
if github_token is not None and not isinstance(github_token, Secret):
|
|
90
|
+
msg = "github_token must be a Secret"
|
|
91
|
+
raise TypeError(msg)
|
|
92
|
+
|
|
93
|
+
self.github_token = github_token
|
|
94
|
+
self.raise_on_failure = raise_on_failure
|
|
95
|
+
self.max_file_size = max_file_size
|
|
96
|
+
self.repo = repo
|
|
97
|
+
self.branch = branch
|
|
98
|
+
|
|
99
|
+
self.base_headers = {
|
|
100
|
+
"Accept": "application/vnd.github.v3+json",
|
|
101
|
+
"User-Agent": "Haystack/GitHubRepoViewer",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def _get_request_headers(self) -> dict:
|
|
105
|
+
"""
|
|
106
|
+
Get headers with resolved token for the request.
|
|
107
|
+
|
|
108
|
+
:return: Dictionary of headers including authorization if token is present
|
|
109
|
+
"""
|
|
110
|
+
headers = self.base_headers.copy()
|
|
111
|
+
if self.github_token is not None:
|
|
112
|
+
headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
|
|
113
|
+
return headers
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Serialize the component to a dictionary.
|
|
118
|
+
|
|
119
|
+
:returns: Dictionary with serialized data.
|
|
120
|
+
"""
|
|
121
|
+
return default_to_dict(
|
|
122
|
+
self,
|
|
123
|
+
github_token=self.github_token.to_dict() if self.github_token else None,
|
|
124
|
+
raise_on_failure=self.raise_on_failure,
|
|
125
|
+
max_file_size=self.max_file_size,
|
|
126
|
+
repo=self.repo,
|
|
127
|
+
branch=self.branch,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GitHubRepoViewer":
|
|
132
|
+
"""
|
|
133
|
+
Deserialize the component from a dictionary.
|
|
134
|
+
|
|
135
|
+
:param data: Dictionary to deserialize from.
|
|
136
|
+
:returns: Deserialized component.
|
|
137
|
+
"""
|
|
138
|
+
init_params = data["init_parameters"]
|
|
139
|
+
deserialize_secrets_inplace(init_params, keys=["github_token"])
|
|
140
|
+
return default_from_dict(cls, data)
|
|
141
|
+
|
|
142
|
+
def _parse_repo(self, repo: str) -> tuple[str, str]:
|
|
143
|
+
"""Parse owner/repo string"""
|
|
144
|
+
parts = repo.split("/")
|
|
145
|
+
if len(parts) != 2: # noqa: PLR2004
|
|
146
|
+
msg = f"Invalid repository format. Expected 'owner/repo', got '{repo}'"
|
|
147
|
+
raise ValueError(msg)
|
|
148
|
+
return parts[0], parts[1]
|
|
149
|
+
|
|
150
|
+
def _normalize_path(self, path: str) -> str:
|
|
151
|
+
"""Normalize repository path"""
|
|
152
|
+
return path.strip("/")
|
|
153
|
+
|
|
154
|
+
def _fetch_contents(self, owner: str, repo: str, path: str, ref: str) -> Any:
|
|
155
|
+
"""Fetch repository contents from GitHub API"""
|
|
156
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
|
|
157
|
+
if ref:
|
|
158
|
+
url += f"?ref={ref}"
|
|
159
|
+
|
|
160
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return response.json()
|
|
163
|
+
|
|
164
|
+
def _process_file_content(self, content: str, encoding: str) -> str:
|
|
165
|
+
"""Process file content based on encoding"""
|
|
166
|
+
if encoding == "base64":
|
|
167
|
+
return base64.b64decode(content).decode("utf-8")
|
|
168
|
+
return content
|
|
169
|
+
|
|
170
|
+
def _create_file_document(self, item: GitHubItem) -> Document:
|
|
171
|
+
"""Create a Document from a file"""
|
|
172
|
+
return Document(
|
|
173
|
+
content=item.content if item.content else item.name,
|
|
174
|
+
meta={
|
|
175
|
+
"path": item.path,
|
|
176
|
+
"type": "file_content",
|
|
177
|
+
"size": item.size,
|
|
178
|
+
"url": item.url,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _create_directory_documents(self, items: List[GitHubItem]) -> List[Document]:
|
|
183
|
+
"""Create a list of Documents from directory contents"""
|
|
184
|
+
return [
|
|
185
|
+
Document(
|
|
186
|
+
content=item.name,
|
|
187
|
+
meta={
|
|
188
|
+
"path": item.path,
|
|
189
|
+
"type": item.type,
|
|
190
|
+
"size": item.size,
|
|
191
|
+
"url": item.url,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
for item in sorted(items, key=lambda x: (x.type != "dir", x.name.lower()))
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
def _create_error_document(self, error: Exception, path: str) -> Document:
|
|
198
|
+
"""Create a Document from an error"""
|
|
199
|
+
return Document(
|
|
200
|
+
content=str(error),
|
|
201
|
+
meta={
|
|
202
|
+
"type": "error",
|
|
203
|
+
"path": path,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@component.output_types(documents=List[Document])
|
|
208
|
+
def run(self, path: str, repo: Optional[str] = None, branch: Optional[str] = None) -> Dict[str, List[Document]]:
|
|
209
|
+
"""
|
|
210
|
+
Process a GitHub repository path and return documents.
|
|
211
|
+
|
|
212
|
+
:param repo: Repository in format "owner/repo"
|
|
213
|
+
:param path: Path within repository (default: root)
|
|
214
|
+
:param branch: Git reference (branch, tag, commit) to use
|
|
215
|
+
:return: Dictionary containing list of documents
|
|
216
|
+
"""
|
|
217
|
+
if repo is None:
|
|
218
|
+
repo = self.repo
|
|
219
|
+
if repo is None:
|
|
220
|
+
msg = "Repository not provided in initialization or run() method"
|
|
221
|
+
raise ValueError(msg)
|
|
222
|
+
if branch is None:
|
|
223
|
+
branch = self.branch
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
owner, repo_name = self._parse_repo(repo)
|
|
227
|
+
normalized_path = self._normalize_path(path)
|
|
228
|
+
|
|
229
|
+
contents = self._fetch_contents(owner, repo_name, normalized_path, branch)
|
|
230
|
+
|
|
231
|
+
# Handle single file response
|
|
232
|
+
if not isinstance(contents, list):
|
|
233
|
+
if contents.get("size", 0) > self.max_file_size:
|
|
234
|
+
error_message = f"File size {contents['size']} exceeds limit of {self.max_file_size}"
|
|
235
|
+
raise ValueError(error_message)
|
|
236
|
+
|
|
237
|
+
item = GitHubItem(
|
|
238
|
+
name=contents["name"],
|
|
239
|
+
type="file",
|
|
240
|
+
path=contents["path"],
|
|
241
|
+
size=contents["size"],
|
|
242
|
+
url=contents["html_url"],
|
|
243
|
+
content=self._process_file_content(contents["content"], contents["encoding"]),
|
|
244
|
+
)
|
|
245
|
+
return {"documents": [self._create_file_document(item)]}
|
|
246
|
+
|
|
247
|
+
# Handle directory listing
|
|
248
|
+
items = [
|
|
249
|
+
GitHubItem(
|
|
250
|
+
name=item["name"],
|
|
251
|
+
type="dir" if item["type"] == "dir" else "file",
|
|
252
|
+
path=item["path"],
|
|
253
|
+
size=item.get("size", 0),
|
|
254
|
+
url=item["html_url"],
|
|
255
|
+
)
|
|
256
|
+
for item in contents
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
return {"documents": self._create_directory_documents(items)}
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
error_doc = self._create_error_document(
|
|
263
|
+
Exception(f"Error processing repository path {path}: {e!s}. Seems like the file does not exist."), path
|
|
264
|
+
)
|
|
265
|
+
if self.raise_on_failure:
|
|
266
|
+
raise
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Error processing repository path {path}: {error}",
|
|
269
|
+
path=path,
|
|
270
|
+
error=str(e),
|
|
271
|
+
)
|
|
272
|
+
return {"documents": [error_doc]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
from .context_prompt import CONTEXT_PROMPT
|
|
5
|
+
from .file_editor_prompt import FILE_EDITOR_PROMPT, FILE_EDITOR_SCHEMA
|
|
6
|
+
from .issue_commenter_prompt import ISSUE_COMMENTER_PROMPT, ISSUE_COMMENTER_SCHEMA
|
|
7
|
+
from .pr_creator_prompt import PR_CREATOR_PROMPT, PR_CREATOR_SCHEMA
|
|
8
|
+
from .repo_viewer_prompt import REPO_VIEWER_PROMPT, REPO_VIEWER_SCHEMA
|
|
9
|
+
from .system_prompt import SYSTEM_PROMPT
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CONTEXT_PROMPT",
|
|
13
|
+
"FILE_EDITOR_PROMPT",
|
|
14
|
+
"FILE_EDITOR_SCHEMA",
|
|
15
|
+
"ISSUE_COMMENTER_PROMPT",
|
|
16
|
+
"ISSUE_COMMENTER_SCHEMA",
|
|
17
|
+
"PR_CREATOR_PROMPT",
|
|
18
|
+
"PR_CREATOR_SCHEMA",
|
|
19
|
+
"REPO_VIEWER_PROMPT",
|
|
20
|
+
"REPO_VIEWER_SCHEMA",
|
|
21
|
+
"SYSTEM_PROMPT",
|
|
22
|
+
]
|