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