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.
Files changed (26) hide show
  1. github_haystack-1.0.0.dist-info/METADATA +45 -0
  2. github_haystack-1.0.0.dist-info/RECORD +26 -0
  3. github_haystack-1.0.0.dist-info/WHEEL +4 -0
  4. github_haystack-1.0.0.dist-info/licenses/LICENSE.txt +73 -0
  5. haystack_integrations/components/connectors/github/__init__.py +19 -0
  6. haystack_integrations/components/connectors/github/file_editor.py +303 -0
  7. haystack_integrations/components/connectors/github/issue_commenter.py +161 -0
  8. haystack_integrations/components/connectors/github/issue_viewer.py +216 -0
  9. haystack_integrations/components/connectors/github/pr_creator.py +277 -0
  10. haystack_integrations/components/connectors/github/repo_forker.py +306 -0
  11. haystack_integrations/components/connectors/github/repo_viewer.py +272 -0
  12. haystack_integrations/prompts/github/__init__.py +22 -0
  13. haystack_integrations/prompts/github/context_prompt.py +176 -0
  14. haystack_integrations/prompts/github/file_editor_prompt.py +122 -0
  15. haystack_integrations/prompts/github/issue_commenter_prompt.py +21 -0
  16. haystack_integrations/prompts/github/issue_viewer_prompt.py +25 -0
  17. haystack_integrations/prompts/github/pr_creator_prompt.py +83 -0
  18. haystack_integrations/prompts/github/repo_viewer_prompt.py +81 -0
  19. haystack_integrations/prompts/github/system_prompt.py +63 -0
  20. haystack_integrations/tools/github/__init__.py +16 -0
  21. haystack_integrations/tools/github/file_editor_tool.py +128 -0
  22. haystack_integrations/tools/github/issue_commenter_tool.py +123 -0
  23. haystack_integrations/tools/github/issue_viewer_tool.py +123 -0
  24. haystack_integrations/tools/github/pr_creator_tool.py +117 -0
  25. haystack_integrations/tools/github/repo_viewer_tool.py +138 -0
  26. haystack_integrations/tools/github/utils.py +73 -0
@@ -0,0 +1,216 @@
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, List, Optional
6
+
7
+ import requests
8
+ from haystack import Document, component, default_from_dict, default_to_dict, logging
9
+ from haystack.utils import deserialize_secrets_inplace
10
+ from haystack.utils.auth import Secret
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @component
16
+ class GitHubIssueViewer:
17
+ """
18
+ Fetches and parses GitHub issues into Haystack documents.
19
+
20
+ The component takes a GitHub issue URL and returns a list of documents where:
21
+ - First document contains the main issue content
22
+ - Subsequent documents contain the issue comments
23
+
24
+ ### Usage example
25
+ ```python
26
+ from haystack_integrations.components.connectors.github import GitHubIssueViewer
27
+
28
+ viewer = GitHubIssueViewer()
29
+ docs = viewer.run(
30
+ url="https://github.com/owner/repo/issues/123"
31
+ )["documents"]
32
+
33
+ print(docs)
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ *,
40
+ github_token: Optional[Secret] = None,
41
+ raise_on_failure: bool = True,
42
+ retry_attempts: int = 2,
43
+ ):
44
+ """
45
+ Initialize the component.
46
+
47
+ :param github_token: GitHub personal access token for API authentication as a Secret
48
+ :param raise_on_failure: If True, raises exceptions on API errors
49
+ :param retry_attempts: Number of retry attempts for failed requests
50
+ """
51
+ self.github_token = github_token
52
+ self.raise_on_failure = raise_on_failure
53
+ self.retry_attempts = retry_attempts
54
+
55
+ # Only set the basic headers during initialization
56
+ self.base_headers = {
57
+ "Accept": "application/vnd.github.v3+json",
58
+ "User-Agent": "Haystack/GitHubIssueViewer",
59
+ }
60
+
61
+ def _get_request_headers(self) -> dict:
62
+ """
63
+ Get headers with resolved token for the request.
64
+
65
+ :return: Dictionary of headers including authorization if token is present
66
+ """
67
+ headers = self.base_headers.copy()
68
+ if self.github_token:
69
+ headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
70
+ return headers
71
+
72
+ def _parse_github_url(self, url: str) -> tuple[str, str, int]:
73
+ """
74
+ Parse GitHub URL into owner, repo and issue number.
75
+
76
+ :param url: GitHub issue URL
77
+ :return: Tuple of (owner, repo, issue_number)
78
+ :raises ValueError: If URL format is invalid
79
+ """
80
+ pattern = r"https?://github\.com/([^/]+)/([^/]+)/issues/(\d+)"
81
+ match = re.match(pattern, url)
82
+ if not match:
83
+ msg = f"Invalid GitHub issue URL format: {url}"
84
+ raise ValueError(msg)
85
+
86
+ owner, repo, issue_number = match.groups()
87
+ return owner, repo, int(issue_number)
88
+
89
+ def _fetch_issue(self, owner: str, repo: str, issue_number: int) -> Any:
90
+ """
91
+ Fetch issue data from GitHub API.
92
+
93
+ :param owner: Repository owner
94
+ :param repo: Repository name
95
+ :param issue_number: Issue number
96
+ :return: Issue data dictionary
97
+ """
98
+ url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}"
99
+ response = requests.get(url, headers=self._get_request_headers(), timeout=10)
100
+ response.raise_for_status()
101
+ return response.json()
102
+
103
+ def _fetch_comments(self, comments_url: str) -> Any:
104
+ """
105
+ Fetch issue comments from GitHub API.
106
+
107
+ :param comments_url: URL for issue comments
108
+ :return: List of comment dictionaries
109
+ """
110
+ response = requests.get(comments_url, headers=self._get_request_headers(), timeout=10)
111
+ response.raise_for_status()
112
+ return response.json()
113
+
114
+ def _create_issue_document(self, issue_data: dict) -> Document:
115
+ """
116
+ Create a Document from issue data.
117
+
118
+ :param issue_data: Issue data from GitHub API
119
+ :return: Haystack Document
120
+ """
121
+ return Document( # type: ignore
122
+ content=issue_data["body"],
123
+ meta={
124
+ "type": "issue",
125
+ "title": issue_data["title"],
126
+ "number": issue_data["number"],
127
+ "state": issue_data["state"],
128
+ "created_at": issue_data["created_at"],
129
+ "updated_at": issue_data["updated_at"],
130
+ "author": issue_data["user"]["login"],
131
+ "url": issue_data["html_url"],
132
+ },
133
+ )
134
+
135
+ def _create_comment_document(self, comment_data: dict, issue_number: int) -> Document:
136
+ """
137
+ Create a Document from comment data.
138
+
139
+ :param comment_data: Comment data from GitHub API
140
+ :param issue_number: Parent issue number
141
+ :return: Haystack Document
142
+ """
143
+ return Document(
144
+ content=comment_data["body"],
145
+ meta={
146
+ "type": "comment",
147
+ "issue_number": issue_number,
148
+ "created_at": comment_data["created_at"],
149
+ "updated_at": comment_data["updated_at"],
150
+ "author": comment_data["user"]["login"],
151
+ "url": comment_data["html_url"],
152
+ },
153
+ )
154
+
155
+ def to_dict(self) -> Dict[str, Any]:
156
+ """
157
+ Serialize the component to a dictionary.
158
+
159
+ :returns: Dictionary with serialized data.
160
+ """
161
+ return default_to_dict(
162
+ self,
163
+ github_token=self.github_token.to_dict() if self.github_token else None,
164
+ raise_on_failure=self.raise_on_failure,
165
+ retry_attempts=self.retry_attempts,
166
+ )
167
+
168
+ @classmethod
169
+ def from_dict(cls, data: Dict[str, Any]) -> "GitHubIssueViewer":
170
+ """
171
+ Deserialize the component from a dictionary.
172
+
173
+ :param data: Dictionary to deserialize from.
174
+ :returns: Deserialized component.
175
+ """
176
+ init_params = data["init_parameters"]
177
+ deserialize_secrets_inplace(init_params, keys=["github_token"])
178
+ return default_from_dict(cls, data)
179
+
180
+ @component.output_types(documents=List[Document])
181
+ def run(self, url: str) -> dict:
182
+ """
183
+ Process a GitHub issue URL and return documents.
184
+
185
+ :param url: GitHub issue URL
186
+ :return: Dictionary containing list of documents
187
+ """
188
+ try:
189
+ owner, repo, issue_number = self._parse_github_url(url)
190
+
191
+ # Fetch issue data
192
+ issue_data = self._fetch_issue(owner, repo, issue_number)
193
+ documents = [self._create_issue_document(issue_data)]
194
+
195
+ # Fetch and process comments if they exist
196
+ if issue_data["comments"] > 0:
197
+ comments = self._fetch_comments(issue_data["comments_url"])
198
+ documents.extend(self._create_comment_document(comment, issue_number) for comment in comments)
199
+
200
+ return {"documents": documents}
201
+
202
+ except Exception as e:
203
+ if self.raise_on_failure:
204
+ raise
205
+
206
+ error_message = f"Error processing GitHub issue {url}: {e!s}"
207
+ logger.warning(error_message)
208
+ error_doc = Document(
209
+ content=error_message,
210
+ meta={
211
+ "error": True,
212
+ "type": "error",
213
+ "url": url,
214
+ },
215
+ )
216
+ return {"documents": [error_doc]}
@@ -0,0 +1,277 @@
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 GitHubPRCreator:
16
+ """
17
+ A Haystack component for creating pull requests from a fork back to the original repository.
18
+
19
+ Uses the authenticated user's fork to create the PR and links it to an existing issue.
20
+
21
+ ### Usage example
22
+ ```python
23
+ from haystack_integrations.components.connectors.github import GitHubPRCreator
24
+ from haystack.utils import Secret
25
+
26
+ pr_creator = GitHubPRCreator(
27
+ github_token=Secret.from_env_var("GITHUB_TOKEN") # Token from the fork owner
28
+ )
29
+
30
+ # Create a PR from your fork
31
+ result = pr_creator.run(
32
+ issue_url="https://github.com/owner/repo/issues/123",
33
+ title="Fix issue #123",
34
+ body="This PR addresses issue #123",
35
+ branch="feature-branch", # The branch in your fork with the changes
36
+ base="main" # The branch in the original repo to merge into
37
+ )
38
+ ```
39
+ """
40
+
41
+ def __init__(self, *, github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"), raise_on_failure: bool = True):
42
+ """
43
+ Initialize the component.
44
+
45
+ :param github_token: GitHub personal access token for authentication (from the fork owner)
46
+ :param raise_on_failure: If True, raises exceptions on API errors
47
+ """
48
+ if not isinstance(github_token, Secret):
49
+ msg = "github_token must be a Secret"
50
+ raise TypeError(msg)
51
+
52
+ self.github_token = github_token
53
+ self.raise_on_failure = raise_on_failure
54
+
55
+ self.base_headers = {
56
+ "Accept": "application/vnd.github.v3+json",
57
+ "User-Agent": "Haystack/GitHubPRCreator",
58
+ }
59
+
60
+ def _get_request_headers(self) -> dict:
61
+ """
62
+ Get headers with resolved token for the request.
63
+
64
+ :return: Dictionary of headers including authorization if token is present
65
+ """
66
+ headers = self.base_headers.copy()
67
+ if self.github_token is not None:
68
+ headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
69
+ return headers
70
+
71
+ def _parse_issue_url(self, issue_url: str) -> tuple[str, str, str]:
72
+ """
73
+ Parse owner, repo name, and issue number from GitHub issue URL.
74
+
75
+ :param issue_url: Full GitHub issue URL
76
+ :return: Tuple of (owner, repo_name, issue_number)
77
+ :raises ValueError: If URL format is invalid
78
+ """
79
+ pattern = r"https://github\.com/([^/]+)/([^/]+)/issues/(\d+)"
80
+ match = re.match(pattern, issue_url)
81
+ if not match:
82
+ msg = "Invalid GitHub issue URL format"
83
+ raise ValueError(msg)
84
+ return match.group(1), match.group(2), match.group(3)
85
+
86
+ def _get_authenticated_user(self) -> str:
87
+ """Get the username of the authenticated user (fork owner)."""
88
+ response = requests.get("https://api.github.com/user", headers=self._get_request_headers(), timeout=10)
89
+ response.raise_for_status()
90
+ return response.json()["login"]
91
+
92
+ def _check_fork_exists(self, repo: str, fork_owner: str) -> bool:
93
+ """Check if the fork exists."""
94
+ url = f"https://api.github.com/repos/{fork_owner}/{repo}"
95
+ try:
96
+ response = requests.get(url, headers=self._get_request_headers(), timeout=10)
97
+ response.raise_for_status()
98
+ fork_data = response.json()
99
+ return fork_data.get("fork", False)
100
+ except requests.RequestException:
101
+ return False
102
+
103
+ def _create_fork(self, owner: str, repo: str) -> Optional[str]:
104
+ """Create a fork of the repository."""
105
+ url = f"https://api.github.com/repos/{owner}/{repo}/forks"
106
+ try:
107
+ response = requests.post(url, headers=self._get_request_headers(), timeout=10)
108
+ response.raise_for_status()
109
+ fork_data = response.json()
110
+ return fork_data["owner"]["login"]
111
+ except requests.RequestException as e:
112
+ if self.raise_on_failure:
113
+ msg = f"Failed to create fork: {e!s}"
114
+ raise RuntimeError(msg) from e
115
+ return None
116
+
117
+ def _create_branch(self, owner: str, repo: str, branch_name: str, base_branch: str) -> bool:
118
+ """Create a new branch in the repository."""
119
+ # Get the SHA of the base branch
120
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{base_branch}"
121
+ try:
122
+ response = requests.get(url, headers=self._get_request_headers(), timeout=10)
123
+ response.raise_for_status()
124
+ base_sha = response.json()["object"]["sha"]
125
+
126
+ # Create the new branch
127
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/refs"
128
+ data = {"ref": f"refs/heads/{branch_name}", "sha": base_sha}
129
+ response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
130
+ response.raise_for_status()
131
+ return True
132
+ except requests.RequestException as e:
133
+ if self.raise_on_failure:
134
+ msg = f"Failed to create branch: {e!s}"
135
+ raise RuntimeError(msg) from e
136
+ return False
137
+
138
+ def _create_commit(
139
+ self,
140
+ owner: str,
141
+ repo: str,
142
+ branch_name: str,
143
+ file_path: str,
144
+ content: str,
145
+ message: str,
146
+ ) -> bool:
147
+ """Create a commit with the file changes."""
148
+ # Get the current commit SHA
149
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch_name}"
150
+ try:
151
+ response = requests.get(url, headers=self._get_request_headers(), timeout=10)
152
+ response.raise_for_status()
153
+ current_sha = response.json()["object"]["sha"]
154
+
155
+ # Create a blob with the file content
156
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/blobs"
157
+ data: dict[str, Any] = {"content": content, "encoding": "base64"}
158
+ response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
159
+ response.raise_for_status()
160
+ blob_sha = response.json()["sha"]
161
+
162
+ # Create a tree with the new file
163
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/trees"
164
+ data = {
165
+ "base_tree": current_sha,
166
+ "tree": [{"path": file_path, "mode": "100644", "type": "blob", "sha": blob_sha}],
167
+ }
168
+ response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
169
+ response.raise_for_status()
170
+ tree_sha = response.json()["sha"]
171
+
172
+ # Create the commit
173
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/commits"
174
+ data = {"message": message, "tree": tree_sha, "parents": [current_sha]}
175
+ response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
176
+ response.raise_for_status()
177
+ commit_sha = response.json()["sha"]
178
+
179
+ # Update the branch reference
180
+ url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch_name}"
181
+ data = {"sha": commit_sha}
182
+ response = requests.patch(url, headers=self._get_request_headers(), json=data, timeout=10)
183
+ response.raise_for_status()
184
+ return True
185
+ except requests.RequestException as e:
186
+ if self.raise_on_failure:
187
+ msg = f"Failed to create commit: {e!s}"
188
+ raise RuntimeError(msg) from e
189
+ return False
190
+
191
+ def _create_pull_request(
192
+ self,
193
+ owner: str,
194
+ repo: str,
195
+ branch_name: str,
196
+ base_branch: str,
197
+ title: str,
198
+ body: str,
199
+ ) -> bool:
200
+ """Create a pull request."""
201
+ url = f"https://api.github.com/repos/{owner}/{repo}/pulls"
202
+ data = {"title": title, "body": body, "head": branch_name, "base": base_branch}
203
+ try:
204
+ response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
205
+ response.raise_for_status()
206
+ return True
207
+ except requests.RequestException as e:
208
+ if self.raise_on_failure:
209
+ msg = f"Failed to create pull request: {e!s}"
210
+ raise RuntimeError(msg) from e
211
+ return False
212
+
213
+ @component.output_types(result=str)
214
+ def run(
215
+ self, issue_url: str, title: str, branch: str, base: str, body: str = "", draft: bool = False
216
+ ) -> Dict[str, str]:
217
+ """
218
+ Create a new pull request from your fork to the original repository, linked to the specified issue.
219
+
220
+ :param issue_url: URL of the GitHub issue to link the PR to
221
+ :param title: Title of the pull request
222
+ :param branch: Name of the branch in your fork where changes are implemented
223
+ :param base: Name of the branch in the original repo you want to merge into
224
+ :param body: Additional content for the pull request description
225
+ :param draft: Whether to create a draft pull request
226
+ :return: Dictionary containing operation result
227
+ """
228
+ try:
229
+ # Parse repository information from issue URL
230
+ owner, repo_name, issue_number = self._parse_issue_url(issue_url)
231
+
232
+ # Get the authenticated user (fork owner)
233
+ fork_owner = self._get_authenticated_user()
234
+
235
+ # Check if the fork exists
236
+ if not self._check_fork_exists(repo_name, fork_owner):
237
+ return {"result": f"Error: Fork not found at {fork_owner}/{repo_name}"}
238
+
239
+ url = f"https://api.github.com/repos/{owner}/{repo_name}/pulls"
240
+
241
+ # For cross-repository PRs, head must be in the format username:branch
242
+ head = f"{fork_owner}:{branch}"
243
+
244
+ pr_data = {
245
+ "title": title,
246
+ "body": body,
247
+ "head": head,
248
+ "base": base,
249
+ "draft": draft,
250
+ "maintainer_can_modify": True, # Allow maintainers to modify the PR
251
+ }
252
+
253
+ response = requests.post(url, headers=self._get_request_headers(), json=pr_data, timeout=10)
254
+ response.raise_for_status()
255
+ pr_number = response.json()["number"]
256
+
257
+ return {"result": f"Pull request #{pr_number} created successfully and linked to issue #{issue_number}"}
258
+
259
+ except (requests.RequestException, ValueError) as e:
260
+ if self.raise_on_failure:
261
+ raise
262
+ return {"result": f"Error: {e!s}"}
263
+
264
+ def to_dict(self) -> Dict[str, Any]:
265
+ """Serialize the component to a dictionary."""
266
+ return default_to_dict(
267
+ self,
268
+ github_token=self.github_token.to_dict() if self.github_token else None,
269
+ raise_on_failure=self.raise_on_failure,
270
+ )
271
+
272
+ @classmethod
273
+ def from_dict(cls, data: Dict[str, Any]) -> "GitHubPRCreator":
274
+ """Deserialize the component from a dictionary."""
275
+ init_params = data["init_parameters"]
276
+ deserialize_secrets_inplace(init_params, keys=["github_token"])
277
+ return default_from_dict(cls, data)