holmesgpt 0.11.5__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.

Potentially problematic release.


This version of holmesgpt might be problematic. Click here for more details.

Files changed (183) hide show
  1. holmes/.git_archival.json +7 -0
  2. holmes/__init__.py +76 -0
  3. holmes/__init__.py.bak +76 -0
  4. holmes/clients/robusta_client.py +24 -0
  5. holmes/common/env_vars.py +47 -0
  6. holmes/config.py +526 -0
  7. holmes/core/__init__.py +0 -0
  8. holmes/core/conversations.py +578 -0
  9. holmes/core/investigation.py +152 -0
  10. holmes/core/investigation_structured_output.py +264 -0
  11. holmes/core/issue.py +54 -0
  12. holmes/core/llm.py +250 -0
  13. holmes/core/models.py +157 -0
  14. holmes/core/openai_formatting.py +51 -0
  15. holmes/core/performance_timing.py +72 -0
  16. holmes/core/prompt.py +42 -0
  17. holmes/core/resource_instruction.py +17 -0
  18. holmes/core/runbooks.py +26 -0
  19. holmes/core/safeguards.py +120 -0
  20. holmes/core/supabase_dal.py +540 -0
  21. holmes/core/tool_calling_llm.py +798 -0
  22. holmes/core/tools.py +566 -0
  23. holmes/core/tools_utils/__init__.py +0 -0
  24. holmes/core/tools_utils/tool_executor.py +65 -0
  25. holmes/core/tools_utils/toolset_utils.py +52 -0
  26. holmes/core/toolset_manager.py +418 -0
  27. holmes/interactive.py +229 -0
  28. holmes/main.py +1041 -0
  29. holmes/plugins/__init__.py +0 -0
  30. holmes/plugins/destinations/__init__.py +6 -0
  31. holmes/plugins/destinations/slack/__init__.py +2 -0
  32. holmes/plugins/destinations/slack/plugin.py +163 -0
  33. holmes/plugins/interfaces.py +32 -0
  34. holmes/plugins/prompts/__init__.py +48 -0
  35. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  36. holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
  37. holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
  38. holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
  39. holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
  41. holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
  42. holmes/plugins/prompts/generic_ask.jinja2 +36 -0
  43. holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
  44. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
  45. holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
  46. holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
  47. holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
  48. holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
  49. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
  50. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
  51. holmes/plugins/runbooks/README.md +22 -0
  52. holmes/plugins/runbooks/__init__.py +100 -0
  53. holmes/plugins/runbooks/catalog.json +14 -0
  54. holmes/plugins/runbooks/jira.yaml +12 -0
  55. holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
  56. holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
  57. holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
  58. holmes/plugins/sources/github/__init__.py +77 -0
  59. holmes/plugins/sources/jira/__init__.py +123 -0
  60. holmes/plugins/sources/opsgenie/__init__.py +93 -0
  61. holmes/plugins/sources/pagerduty/__init__.py +147 -0
  62. holmes/plugins/sources/prometheus/__init__.py +0 -0
  63. holmes/plugins/sources/prometheus/models.py +104 -0
  64. holmes/plugins/sources/prometheus/plugin.py +154 -0
  65. holmes/plugins/toolsets/__init__.py +171 -0
  66. holmes/plugins/toolsets/aks-node-health.yaml +65 -0
  67. holmes/plugins/toolsets/aks.yaml +86 -0
  68. holmes/plugins/toolsets/argocd.yaml +70 -0
  69. holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
  70. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
  71. holmes/plugins/toolsets/aws.yaml +76 -0
  72. holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
  73. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
  74. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
  75. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
  76. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
  77. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
  78. holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
  79. holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
  80. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
  81. holmes/plugins/toolsets/azure_sql/install.md +66 -0
  82. holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
  83. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
  84. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
  85. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
  86. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
  87. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
  88. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
  89. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
  90. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
  91. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
  92. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
  93. holmes/plugins/toolsets/azure_sql/utils.py +83 -0
  94. holmes/plugins/toolsets/bash/__init__.py +0 -0
  95. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
  96. holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
  97. holmes/plugins/toolsets/bash/common/bash.py +52 -0
  98. holmes/plugins/toolsets/bash/common/config.py +14 -0
  99. holmes/plugins/toolsets/bash/common/stringify.py +25 -0
  100. holmes/plugins/toolsets/bash/common/validators.py +24 -0
  101. holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
  102. holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
  103. holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
  104. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
  105. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
  106. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
  107. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
  108. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
  109. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
  110. holmes/plugins/toolsets/bash/parse_command.py +103 -0
  111. holmes/plugins/toolsets/confluence.yaml +19 -0
  112. holmes/plugins/toolsets/consts.py +5 -0
  113. holmes/plugins/toolsets/coralogix/api.py +158 -0
  114. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
  115. holmes/plugins/toolsets/coralogix/utils.py +181 -0
  116. holmes/plugins/toolsets/datadog.py +153 -0
  117. holmes/plugins/toolsets/docker.yaml +46 -0
  118. holmes/plugins/toolsets/git.py +756 -0
  119. holmes/plugins/toolsets/grafana/__init__.py +0 -0
  120. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
  121. holmes/plugins/toolsets/grafana/common.py +68 -0
  122. holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
  123. holmes/plugins/toolsets/grafana/loki_api.py +89 -0
  124. holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
  125. holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
  126. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
  127. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
  128. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
  129. holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
  130. holmes/plugins/toolsets/helm.yaml +42 -0
  131. holmes/plugins/toolsets/internet/internet.py +275 -0
  132. holmes/plugins/toolsets/internet/notion.py +137 -0
  133. holmes/plugins/toolsets/kafka.py +638 -0
  134. holmes/plugins/toolsets/kubernetes.yaml +255 -0
  135. holmes/plugins/toolsets/kubernetes_logs.py +426 -0
  136. holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
  137. holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
  138. holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
  139. holmes/plugins/toolsets/logging_utils/types.py +0 -0
  140. holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
  141. holmes/plugins/toolsets/newrelic.py +222 -0
  142. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  143. holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
  144. holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
  145. holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
  146. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
  147. holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
  148. holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
  149. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
  150. holmes/plugins/toolsets/rabbitmq/api.py +398 -0
  151. holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
  152. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
  153. holmes/plugins/toolsets/robusta/__init__.py +0 -0
  154. holmes/plugins/toolsets/robusta/robusta.py +235 -0
  155. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
  156. holmes/plugins/toolsets/runbook/__init__.py +0 -0
  157. holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
  158. holmes/plugins/toolsets/service_discovery.py +92 -0
  159. holmes/plugins/toolsets/servicenow/install.md +37 -0
  160. holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
  161. holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
  162. holmes/plugins/toolsets/slab.yaml +20 -0
  163. holmes/plugins/toolsets/utils.py +137 -0
  164. holmes/plugins/utils.py +14 -0
  165. holmes/utils/__init__.py +0 -0
  166. holmes/utils/cache.py +84 -0
  167. holmes/utils/cert_utils.py +40 -0
  168. holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
  169. holmes/utils/definitions.py +13 -0
  170. holmes/utils/env.py +53 -0
  171. holmes/utils/file_utils.py +56 -0
  172. holmes/utils/global_instructions.py +20 -0
  173. holmes/utils/holmes_status.py +22 -0
  174. holmes/utils/holmes_sync_toolsets.py +80 -0
  175. holmes/utils/markdown_utils.py +55 -0
  176. holmes/utils/pydantic_utils.py +54 -0
  177. holmes/utils/robusta.py +10 -0
  178. holmes/utils/tags.py +97 -0
  179. holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
  180. holmesgpt-0.11.5.dist-info/METADATA +400 -0
  181. holmesgpt-0.11.5.dist-info/RECORD +183 -0
  182. holmesgpt-0.11.5.dist-info/WHEEL +4 -0
  183. holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,756 @@
1
+ import base64
2
+ import logging
3
+ import requests # type: ignore
4
+ import os
5
+ from typing import Any, Optional, Dict, List, Tuple
6
+ from pydantic import BaseModel
7
+ from holmes.core.tools import StructuredToolResult, ToolResultStatus
8
+
9
+ from holmes.core.tools import (
10
+ Toolset,
11
+ Tool,
12
+ ToolParameter,
13
+ ToolsetTag,
14
+ CallablePrerequisite,
15
+ )
16
+
17
+
18
+ class GitHubConfig(BaseModel):
19
+ git_repo: str
20
+ git_credentials: str
21
+ git_branch: str = "main"
22
+
23
+
24
+ class GitToolset(Toolset):
25
+ git_repo: Optional[str] = None
26
+ git_credentials: Optional[str] = None
27
+ git_branch: Optional[str] = None
28
+ _created_branches: set[str] = set() # Track branches created by the tool
29
+ _created_prs: set[int] = set() # Track PRs created by the tool
30
+
31
+ def __init__(self):
32
+ super().__init__(
33
+ name="git",
34
+ description="Runs git commands to read repos and create PRs",
35
+ docs_url="https://docs.github.com/en/rest",
36
+ icon_url="https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg",
37
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
38
+ tools=[
39
+ GitReadFileWithLineNumbers(self),
40
+ GitListFiles(self),
41
+ GitListOpenPRs(self),
42
+ GitExecuteChanges(self),
43
+ GitUpdatePR(self),
44
+ ],
45
+ experimental=True,
46
+ tags=[ToolsetTag.CORE],
47
+ )
48
+
49
+ def add_created_branch(self, branch_name: str) -> None:
50
+ """Add a branch to the list of branches created by the tool."""
51
+ self._created_branches.add(branch_name)
52
+
53
+ def is_created_branch(self, branch_name: str) -> bool:
54
+ """Check if a branch was created by the tool."""
55
+ return branch_name in self._created_branches
56
+
57
+ def add_created_pr(self, pr_number: int) -> None:
58
+ """Add a PR to the list of PRs created by the tool."""
59
+ self._created_prs.add(pr_number)
60
+
61
+ def is_created_pr(self, pr_number: int) -> bool:
62
+ """Check if a PR was created by the tool."""
63
+ return pr_number in self._created_prs
64
+
65
+ def _sanitize_error(self, error_msg: str) -> str:
66
+ """Sanitize error messages by removing sensitive information."""
67
+ if not self.git_credentials:
68
+ return error_msg
69
+ return error_msg.replace(self.git_credentials, "[REDACTED]")
70
+
71
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
72
+ if not config and not (os.getenv("GIT_REPO") and os.getenv("GIT_CREDENTIALS")):
73
+ return False, "Missing one or more required Git configuration values."
74
+
75
+ try:
76
+ self.git_repo = os.getenv("GIT_REPO") or config.get("git_repo")
77
+ self.git_credentials = os.getenv("GIT_CREDENTIALS") or config.get(
78
+ "git_credentials"
79
+ )
80
+ self.git_branch = os.getenv("GIT_BRANCH") or config.get(
81
+ "git_branch", "main"
82
+ )
83
+
84
+ if not all([self.git_repo, self.git_credentials, self.git_branch]):
85
+ logging.error("Missing one or more required Git configuration values.")
86
+ return False, "Missing one or more required Git configuration values."
87
+ return True, ""
88
+ except Exception:
89
+ logging.exception("GitHub prerequisites failed.")
90
+ return False, ""
91
+
92
+ def get_example_config(self) -> Dict[str, Any]:
93
+ return {}
94
+
95
+ def list_open_prs(self) -> List[Dict[str, Any]]:
96
+ """Helper method to list all open PRs in the repository."""
97
+ headers = {"Authorization": f"token {self.git_credentials}"}
98
+ url = f"https://api.github.com/repos/{self.git_repo}/pulls?state=open"
99
+ resp = requests.get(url, headers=headers)
100
+ if resp.status_code != 200:
101
+ raise Exception(self._sanitize_error(f"Error listing PRs: {resp.text}"))
102
+ return resp.json()
103
+
104
+ def get_branch_ref(self, branch_name: str) -> Optional[str]:
105
+ """Get the SHA of a branch reference."""
106
+ headers = {"Authorization": f"token {self.git_credentials}"}
107
+ url = (
108
+ f"https://api.github.com/repos/{self.git_repo}/git/refs/heads/{branch_name}"
109
+ )
110
+ resp = requests.get(url, headers=headers)
111
+ if resp.status_code == 404:
112
+ return None
113
+ if resp.status_code != 200:
114
+ raise Exception(
115
+ self._sanitize_error(f"Error getting branch reference: {resp.text}")
116
+ )
117
+ return resp.json()["object"]["sha"]
118
+
119
+ def create_branch(self, branch_name: str, base_sha: str) -> None:
120
+ """Create a new branch from a base SHA."""
121
+ headers = {"Authorization": f"token {self.git_credentials}"}
122
+ url = f"https://api.github.com/repos/{self.git_repo}/git/refs"
123
+ resp = requests.post(
124
+ url,
125
+ headers=headers,
126
+ json={
127
+ "ref": f"refs/heads/{branch_name}",
128
+ "sha": base_sha,
129
+ },
130
+ )
131
+ if resp.status_code not in (200, 201):
132
+ raise Exception(self._sanitize_error(f"Error creating branch: {resp.text}"))
133
+ self.add_created_branch(branch_name) # Track the created branch
134
+
135
+ def get_file_content(self, filepath: str, branch: str) -> tuple[str, str]:
136
+ """Get file content and SHA from a specific branch."""
137
+ headers = {"Authorization": f"token {self.git_credentials}"}
138
+ url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}?ref={branch}"
139
+ resp = requests.get(url, headers=headers)
140
+ if resp.status_code == 404:
141
+ raise Exception(f"File not found: {filepath}")
142
+ if resp.status_code != 200:
143
+ raise Exception(self._sanitize_error(f"Error fetching file: {resp.text}"))
144
+ file_json = resp.json()
145
+ return file_json["sha"], base64.b64decode(file_json["content"]).decode()
146
+
147
+ def update_file(
148
+ self, filepath: str, branch: str, content: str, sha: str, message: str
149
+ ) -> None:
150
+ """Update a file in a specific branch."""
151
+ headers = {"Authorization": f"token {self.git_credentials}"}
152
+ url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}"
153
+ encoded_content = base64.b64encode(content.encode()).decode()
154
+ resp = requests.put(
155
+ url,
156
+ headers=headers,
157
+ json={
158
+ "message": message,
159
+ "content": encoded_content,
160
+ "branch": branch,
161
+ "sha": sha,
162
+ },
163
+ )
164
+ if resp.status_code not in (200, 201):
165
+ raise Exception(self._sanitize_error(f"Error updating file: {resp.text}"))
166
+
167
+ def create_pr(self, title: str, head: str, base: str, body: str) -> str:
168
+ """Create a new pull request."""
169
+ headers = {"Authorization": f"token {self.git_credentials}"}
170
+ url = f"https://api.github.com/repos/{self.git_repo}/pulls"
171
+ resp = requests.post(
172
+ url,
173
+ headers=headers,
174
+ json={
175
+ "title": title,
176
+ "body": body,
177
+ "head": head,
178
+ "base": base,
179
+ },
180
+ )
181
+ if resp.status_code not in (200, 201):
182
+ raise Exception(self._sanitize_error(f"Error creating PR: {resp.text}"))
183
+ pr_number = resp.json()["number"]
184
+ self.add_created_pr(pr_number) # Track the created PR
185
+ return resp.json()["html_url"]
186
+
187
+ def get_pr_details(self, pr_number: int) -> Dict[str, Any]:
188
+ """Get details of a specific PR."""
189
+ headers = {"Authorization": f"token {self.git_credentials}"}
190
+ url = f"https://api.github.com/repos/{self.git_repo}/pulls/{pr_number}"
191
+ resp = requests.get(url, headers=headers)
192
+ if resp.status_code != 200:
193
+ raise Exception(
194
+ self._sanitize_error(f"Error getting PR details: {resp.text}")
195
+ )
196
+ return resp.json()
197
+
198
+ def get_pr_branch(self, pr_number: int) -> str:
199
+ """Get the branch name for a specific PR."""
200
+ pr_details = self.get_pr_details(pr_number)
201
+ return pr_details["head"]["ref"]
202
+
203
+ def add_commit_to_pr(
204
+ self, pr_number: int, filepath: str, content: str, message: str
205
+ ) -> None:
206
+ """Add a commit to an existing PR's branch."""
207
+ branch = self.get_pr_branch(pr_number)
208
+ try:
209
+ # Get current file content and SHA
210
+ sha, _ = self.get_file_content(filepath, branch)
211
+ except Exception:
212
+ # File might not exist yet, that's okay
213
+ sha = None
214
+
215
+ # Update file
216
+ headers = {"Authorization": f"token {self.git_credentials}"}
217
+ url = f"https://api.github.com/repos/{self.git_repo}/contents/{filepath}"
218
+ encoded_content = base64.b64encode(content.encode()).decode()
219
+ data = {
220
+ "message": message,
221
+ "content": encoded_content,
222
+ "branch": branch,
223
+ }
224
+ if sha:
225
+ data["sha"] = sha
226
+
227
+ resp = requests.put(url, headers=headers, json=data)
228
+ if resp.status_code not in (200, 201):
229
+ raise Exception(
230
+ self._sanitize_error(f"Error adding commit to PR: {resp.text}")
231
+ )
232
+
233
+
234
+ class GitReadFileWithLineNumbers(Tool):
235
+ toolset: GitToolset
236
+
237
+ def __init__(self, toolset: GitToolset):
238
+ super().__init__(
239
+ name="git_read_file_with_line_numbers",
240
+ description="Reads a file from the Git repo and prints each line with line numbers",
241
+ parameters={
242
+ "filepath": ToolParameter(
243
+ description="The path of the file in the repository to read.",
244
+ type="string",
245
+ required=True,
246
+ ),
247
+ },
248
+ toolset=toolset, # type: ignore
249
+ )
250
+
251
+ def _invoke(self, params: Any) -> StructuredToolResult:
252
+ filepath = params["filepath"]
253
+ try:
254
+ headers = {"Authorization": f"token {self.toolset.git_credentials}"}
255
+ url = f"https://api.github.com/repos/{self.toolset.git_repo}/contents/{filepath}"
256
+ resp = requests.get(url, headers=headers)
257
+ if resp.status_code != 200:
258
+ return StructuredToolResult(
259
+ status=ToolResultStatus.ERROR,
260
+ data=self.toolset._sanitize_error(
261
+ f"Error fetching file: {resp.text}"
262
+ ),
263
+ params=params,
264
+ )
265
+ content = base64.b64decode(resp.json()["content"]).decode().splitlines()
266
+ numbered = "\n".join(f"{i+1}: {line}" for i, line in enumerate(content))
267
+ return StructuredToolResult(
268
+ status=ToolResultStatus.SUCCESS,
269
+ data=numbered,
270
+ params=params,
271
+ )
272
+ except Exception as e:
273
+ return StructuredToolResult(
274
+ status=ToolResultStatus.ERROR,
275
+ data=self.toolset._sanitize_error(str(e)),
276
+ params=params,
277
+ )
278
+
279
+ def get_parameterized_one_liner(self, params) -> str:
280
+ return "Reading git files"
281
+
282
+
283
+ class GitListFiles(Tool):
284
+ toolset: GitToolset
285
+
286
+ def __init__(self, toolset: GitToolset):
287
+ super().__init__(
288
+ name="git_list_files",
289
+ description="Lists all files and directories in the remote Git repository.",
290
+ parameters={},
291
+ toolset=toolset, # type: ignore
292
+ )
293
+
294
+ def _invoke(self, params: Any) -> StructuredToolResult:
295
+ try:
296
+ headers = {"Authorization": f"token {self.toolset.git_credentials}"}
297
+ url = f"https://api.github.com/repos/{self.toolset.git_repo}/git/trees/{self.toolset.git_branch}?recursive=1"
298
+ resp = requests.get(url, headers=headers)
299
+ if resp.status_code != 200:
300
+ return StructuredToolResult(
301
+ status=ToolResultStatus.ERROR,
302
+ data=self.toolset._sanitize_error(
303
+ f"Error listing files: {resp.text}"
304
+ ),
305
+ params=params,
306
+ )
307
+ paths = [entry["path"] for entry in resp.json()["tree"]]
308
+ return StructuredToolResult(
309
+ status=ToolResultStatus.SUCCESS,
310
+ data=paths,
311
+ params=params,
312
+ )
313
+ except Exception as e:
314
+ return StructuredToolResult(
315
+ status=ToolResultStatus.ERROR,
316
+ data=self.toolset._sanitize_error(str(e)),
317
+ params=params,
318
+ )
319
+
320
+ def get_parameterized_one_liner(self, params) -> str:
321
+ return "listing git files"
322
+
323
+
324
+ class GitListOpenPRs(Tool):
325
+ toolset: GitToolset
326
+
327
+ def __init__(self, toolset: GitToolset):
328
+ super().__init__(
329
+ name="git_list_open_prs",
330
+ description="Lists all open pull requests (PRs) in the remote Git repository.",
331
+ parameters={},
332
+ toolset=toolset, # type: ignore
333
+ )
334
+
335
+ def _invoke(self, params: Any) -> StructuredToolResult:
336
+ try:
337
+ prs = self.toolset.list_open_prs()
338
+ formatted = [
339
+ {
340
+ "number": pr["number"],
341
+ "title": pr["title"],
342
+ "branch": pr["head"]["ref"],
343
+ "url": pr["html_url"],
344
+ }
345
+ for pr in prs
346
+ ]
347
+ return StructuredToolResult(
348
+ status=ToolResultStatus.SUCCESS,
349
+ data=formatted,
350
+ params=params,
351
+ )
352
+ except Exception as e:
353
+ return StructuredToolResult(
354
+ status=ToolResultStatus.ERROR,
355
+ data=self.toolset._sanitize_error(str(e)),
356
+ params=params,
357
+ )
358
+
359
+ def get_parameterized_one_liner(self, params) -> str:
360
+ return "Listing PR's"
361
+
362
+
363
+ class GitExecuteChanges(Tool):
364
+ toolset: GitToolset
365
+
366
+ def __init__(self, toolset: GitToolset):
367
+ super().__init__(
368
+ name="git_execute_changes",
369
+ description="Make changes to a GitHub file and optionally open a PR or add to existing PR. This tool requires two steps: first run with dry_run=true to preview changes, then run again with dry_run=false to commit the changes.",
370
+ parameters={
371
+ "line": ToolParameter(
372
+ description="Line number to change", type="integer", required=True
373
+ ),
374
+ "filename": ToolParameter(
375
+ description="Filename (relative path)", type="string", required=True
376
+ ),
377
+ "command": ToolParameter(
378
+ description="insert/update/remove", type="string", required=True
379
+ ),
380
+ "code": ToolParameter(
381
+ description="The entire line of code to insert or update",
382
+ type="string",
383
+ required=False,
384
+ ),
385
+ "open_pr": ToolParameter(
386
+ description="Whether to open PR", type="boolean", required=True
387
+ ),
388
+ "commit_pr": ToolParameter(
389
+ description="PR title or PR number to add commit to",
390
+ type="string",
391
+ required=True,
392
+ ),
393
+ "dry_run": ToolParameter(
394
+ description="Dry-run mode", type="boolean", required=True
395
+ ),
396
+ "commit_message": ToolParameter(
397
+ description="Commit message", type="string", required=True
398
+ ),
399
+ },
400
+ toolset=toolset, # type: ignore
401
+ )
402
+
403
+ def _invoke(self, params: Any) -> StructuredToolResult:
404
+ def error(msg: str) -> StructuredToolResult:
405
+ return StructuredToolResult(
406
+ status=ToolResultStatus.ERROR,
407
+ data=self.toolset._sanitize_error(msg),
408
+ params=params,
409
+ )
410
+
411
+ def success(msg: Any) -> StructuredToolResult:
412
+ return StructuredToolResult(
413
+ status=ToolResultStatus.SUCCESS, data=msg, params=params
414
+ )
415
+
416
+ def modify_lines(lines: List[str]) -> List[str]:
417
+ nonlocal command, line, code # type: ignore
418
+ if command == "insert":
419
+ prev_line = lines[line - 2] if line > 1 else ""
420
+ prev_indent = len(prev_line) - len(prev_line.lstrip())
421
+ indent = (
422
+ prev_indent + 2 if prev_line.rstrip().endswith(":") else prev_indent
423
+ )
424
+ for i in range(line - 1, len(lines)):
425
+ if lines[i].strip():
426
+ next_indent = len(lines[i]) - len(lines[i].lstrip())
427
+ if next_indent > prev_indent:
428
+ indent = next_indent
429
+ break
430
+ lines.insert(line - 1, " " * indent + code.lstrip())
431
+ elif command == "update":
432
+ indent = len(lines[line - 1]) - len(lines[line - 1].lstrip())
433
+ lines[line - 1] = " " * indent + code.lstrip()
434
+ elif command == "remove":
435
+ del lines[line - 1]
436
+ else:
437
+ raise ValueError(f"Invalid command: {command}")
438
+ return lines
439
+
440
+ try:
441
+ line = params["line"]
442
+ filename = params["filename"]
443
+ command = params["command"]
444
+ code = params.get("code", "")
445
+ open_pr = params["open_pr"]
446
+ commit_pr = params["commit_pr"]
447
+ dry_run = params["dry_run"]
448
+ commit_message = params["commit_message"]
449
+ branch = self.toolset.git_branch
450
+
451
+ if not commit_message.strip():
452
+ return error("Commit message cannot be empty")
453
+ if not filename.strip():
454
+ return error("Filename cannot be empty")
455
+ if line < 1:
456
+ return error("Line number must be positive")
457
+
458
+ # Handle updating an existing PR
459
+ if commit_pr.startswith("#") or commit_pr.isdigit():
460
+ try:
461
+ pr_number = int(commit_pr.lstrip("#"))
462
+ branch = self.toolset.get_pr_branch(pr_number)
463
+ sha, content = self.toolset.get_file_content(filename, branch)
464
+ updated_lines = modify_lines(content.splitlines())
465
+ updated_content = "\n".join(updated_lines) + "\n"
466
+ if dry_run:
467
+ return success(
468
+ f"DRY RUN: Updated content for PR #{pr_number}:\n\n{updated_content}"
469
+ )
470
+ self.toolset.add_commit_to_pr(
471
+ pr_number, filename, updated_content, commit_message
472
+ )
473
+ return success(f"Added commit to PR #{pr_number} successfully")
474
+ except Exception as e:
475
+ return error(f"Error updating PR: {e}")
476
+
477
+ # Handle creating a new PR
478
+ pr_name = commit_pr.replace(" ", "_").replace("'", "")
479
+ branch_name = f"feature/{pr_name}"
480
+ if not commit_pr.strip():
481
+ return error("PR title cannot be empty")
482
+
483
+ if self.toolset.get_branch_ref(
484
+ branch_name
485
+ ) and not self.toolset.is_created_branch(branch_name):
486
+ return error(
487
+ f"Branch {branch_name} already exists. Please use a different PR title or manually delete it."
488
+ )
489
+
490
+ # Reuse existing PR if matched
491
+ if open_pr:
492
+ try:
493
+ for pr in self.toolset.list_open_prs():
494
+ if (
495
+ pr["title"].lower() == commit_pr.lower()
496
+ and pr["head"]["ref"] == branch_name
497
+ ):
498
+ if not self.toolset.is_created_pr(pr["number"]):
499
+ return error(
500
+ f"PR #{pr['number']} was not created by this tool."
501
+ )
502
+ branch = self.toolset.get_pr_branch(pr["number"])
503
+ sha, content = self.toolset.get_file_content(
504
+ filename, branch
505
+ )
506
+ updated_lines = modify_lines(content.splitlines())
507
+ updated_content = "\n".join(updated_lines) + "\n"
508
+ if dry_run:
509
+ return success(
510
+ f"DRY RUN: Updated content for PR #{pr['number']}:\n\n{updated_content}"
511
+ )
512
+ self.toolset.add_commit_to_pr(
513
+ pr["number"], filename, updated_content, commit_message
514
+ )
515
+ return success(
516
+ f"Added commit to PR #{pr['number']} successfully"
517
+ )
518
+ except Exception as e:
519
+ return error(f"Error checking existing PRs: {e}")
520
+
521
+ try:
522
+ base_sha = self.toolset.get_branch_ref(branch) # type: ignore
523
+ if not base_sha:
524
+ return error(f"Base branch {branch} not found")
525
+ except Exception as e:
526
+ return error(f"Error getting base branch reference: {e}")
527
+
528
+ try:
529
+ sha, content = self.toolset.get_file_content(filename, branch) # type: ignore
530
+ lines = content.splitlines()
531
+ except Exception as e:
532
+ return error(f"Error getting file content: {e}")
533
+
534
+ if line > len(lines) + 1:
535
+ return error(
536
+ f"Line number {line} is out of range. File has {len(lines)} lines."
537
+ )
538
+
539
+ updated_lines = modify_lines(lines)
540
+ updated_content = "\n".join(updated_lines) + "\n"
541
+
542
+ if dry_run:
543
+ return success(f"DRY RUN: Updated content:\n\n{updated_content}")
544
+
545
+ try:
546
+ self.toolset.create_branch(branch_name, base_sha)
547
+ self.toolset.update_file(
548
+ filename, branch_name, updated_content, sha, commit_message
549
+ )
550
+ except Exception as e:
551
+ return error(f"Error during branch creation or file update: {e}")
552
+
553
+ if open_pr:
554
+ try:
555
+ pr_url = self.toolset.create_pr(
556
+ commit_pr,
557
+ branch_name,
558
+ branch, # type: ignore
559
+ commit_message, # type: ignore
560
+ )
561
+ return success(f"PR opened successfully: {pr_url}")
562
+ except Exception as e:
563
+ return error(
564
+ f"PR creation failed. Branch created and committed successfully. Error: {e}"
565
+ )
566
+
567
+ return success("Change committed successfully, no PR opened.")
568
+ except Exception as e:
569
+ return error(f"Unexpected error: {e}")
570
+
571
+ def get_parameterized_one_liner(self, params) -> str:
572
+ return (
573
+ f"git execute_changes(line={params['line']}, filename='{params['filename']}', "
574
+ f"command='{params['command']}', code='{params.get('code', '')}', "
575
+ f"open_pr={params['open_pr']}, commit_pr='{params['commit_pr']}', "
576
+ f"dry_run={params['dry_run']}, commit_message='{params['commit_message']}')"
577
+ )
578
+
579
+
580
+ class GitUpdatePR(Tool):
581
+ """A tool specifically for updating existing PRs that were created by this tool.
582
+ This tool can only update PRs that were created using the GitExecuteChanges tool,
583
+ as it relies on the specific branch naming convention used by that tool.
584
+ The tool requires two steps: first run with dry_run=true to preview changes,
585
+ then run again with dry_run=false to commit the changes to the PR.
586
+ """
587
+
588
+ toolset: GitToolset
589
+
590
+ def __init__(self, toolset: GitToolset):
591
+ super().__init__(
592
+ name="git_update_pr",
593
+ description="Update an existing PR that was created by this tool. Can only update PRs created using git_execute_changes.",
594
+ parameters={
595
+ "line": ToolParameter(
596
+ description="Line number to change", type="integer", required=True
597
+ ),
598
+ "filename": ToolParameter(
599
+ description="Filename (relative path)", type="string", required=True
600
+ ),
601
+ "command": ToolParameter(
602
+ description="insert/update/remove", type="string", required=True
603
+ ),
604
+ "code": ToolParameter(
605
+ description="The entire line of code to insert or update",
606
+ type="string",
607
+ required=False,
608
+ ),
609
+ "pr_number": ToolParameter(
610
+ description="PR number to update", type="integer", required=True
611
+ ),
612
+ "dry_run": ToolParameter(
613
+ description="Dry-run mode", type="boolean", required=True
614
+ ),
615
+ "commit_message": ToolParameter(
616
+ description="Commit message", type="string", required=True
617
+ ),
618
+ },
619
+ toolset=toolset, # type: ignore
620
+ )
621
+
622
+ def _invoke(self, params: Any) -> StructuredToolResult:
623
+ try:
624
+ line = params["line"]
625
+ filename = params["filename"]
626
+ command = params["command"]
627
+ code = params.get("code", "")
628
+ pr_number = params["pr_number"]
629
+ dry_run = params["dry_run"]
630
+ commit_message = params["commit_message"]
631
+
632
+ # Validate inputs
633
+ if not commit_message.strip():
634
+ return StructuredToolResult(
635
+ status=ToolResultStatus.ERROR,
636
+ error="Tool call failed to run: Commit message cannot be empty",
637
+ )
638
+ if not filename.strip():
639
+ return StructuredToolResult(
640
+ status=ToolResultStatus.ERROR,
641
+ error="Tool call failed to run: Filename cannot be empty",
642
+ )
643
+ if line < 1:
644
+ return StructuredToolResult(
645
+ status=ToolResultStatus.ERROR,
646
+ error="Tool call failed to run: Line number must be positive",
647
+ )
648
+
649
+ # Verify this is a PR created by our tool
650
+ if not self.toolset.is_created_pr(pr_number):
651
+ return StructuredToolResult(
652
+ status=ToolResultStatus.ERROR,
653
+ error=f"Tool call failed to run: PR #{pr_number} was not created by this tool. Only PRs created using git_execute_changes can be updated.",
654
+ )
655
+
656
+ # Get PR details
657
+ try:
658
+ pr_details = self.toolset.get_pr_details(pr_number)
659
+ branch = pr_details["head"]["ref"]
660
+
661
+ # Get current file content from PR branch
662
+ sha, content = self.toolset.get_file_content(filename, branch)
663
+ content_lines = content.splitlines()
664
+
665
+ # Update content
666
+ if command == "insert":
667
+ # Get the previous line's indentation
668
+ prev_line = content_lines[line - 2] if line > 1 else ""
669
+ prev_indent = len(prev_line) - len(prev_line.lstrip())
670
+
671
+ # If previous line ends with colon, add extra indentation
672
+ if prev_line.rstrip().endswith(":"):
673
+ # Find the next non-empty line to determine proper indentation
674
+ next_line_idx = line - 1
675
+ while (
676
+ next_line_idx < len(content_lines)
677
+ and not content_lines[next_line_idx].strip()
678
+ ):
679
+ next_line_idx += 1
680
+
681
+ if next_line_idx < len(content_lines):
682
+ next_line = content_lines[next_line_idx]
683
+ next_indent = len(next_line) - len(next_line.lstrip())
684
+ # Use the next line's indentation if it's more indented than current
685
+ if next_indent > prev_indent:
686
+ indent = next_indent
687
+ else:
688
+ indent = prev_indent + 2
689
+ else:
690
+ indent = prev_indent + 2
691
+ else:
692
+ indent = prev_indent
693
+
694
+ # Apply indentation to the new line
695
+ indented_code = " " * indent + code.lstrip()
696
+ content_lines.insert(line - 1, indented_code)
697
+ elif command == "update":
698
+ indent = len(content_lines[line - 1]) - len(
699
+ content_lines[line - 1].lstrip()
700
+ )
701
+ content_lines[line - 1] = " " * indent + code.lstrip()
702
+ elif command == "remove":
703
+ del content_lines[line - 1]
704
+ else:
705
+ return StructuredToolResult(
706
+ status=ToolResultStatus.ERROR,
707
+ error=f"Tool call failed to run: Invalid command: {command}",
708
+ )
709
+
710
+ updated_content = "\n".join(content_lines) + "\n"
711
+
712
+ if dry_run:
713
+ return StructuredToolResult(
714
+ status=ToolResultStatus.SUCCESS,
715
+ data=f"DRY RUN: Updated content for PR #{pr_number}:\n\n{updated_content}",
716
+ )
717
+
718
+ # Add commit to PR
719
+ self.toolset.add_commit_to_pr(
720
+ pr_number, filename, updated_content, commit_message
721
+ )
722
+ return StructuredToolResult(
723
+ status=ToolResultStatus.SUCCESS,
724
+ data=f"Added commit to PR #{pr_number} successfully",
725
+ )
726
+
727
+ except Exception as e:
728
+ return StructuredToolResult(
729
+ status=ToolResultStatus.ERROR,
730
+ error=self.toolset._sanitize_error(
731
+ f"Tool call failed to run: Error updating PR: {str(e)}"
732
+ ),
733
+ )
734
+
735
+ except requests.exceptions.RequestException as e:
736
+ return StructuredToolResult(
737
+ status=ToolResultStatus.ERROR,
738
+ error=self.toolset._sanitize_error(
739
+ f"Tool call failed to run: Network error: {str(e)}"
740
+ ),
741
+ )
742
+ except Exception as e:
743
+ return StructuredToolResult(
744
+ status=ToolResultStatus.ERROR,
745
+ error=self.toolset._sanitize_error(
746
+ f"Tool call failed to run: Unexpected error: {str(e)}"
747
+ ),
748
+ )
749
+
750
+ def get_parameterized_one_liner(self, params) -> str:
751
+ return (
752
+ f"git update_pr(line={params['line']}, filename='{params['filename']}', "
753
+ f"command='{params['command']}', code='{params.get('code', '')}', "
754
+ f"pr_number={params['pr_number']}, dry_run={params['dry_run']}, "
755
+ f"commit_message='{params['commit_message']}')"
756
+ )