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.
- holmes/.git_archival.json +7 -0
- holmes/__init__.py +76 -0
- holmes/__init__.py.bak +76 -0
- holmes/clients/robusta_client.py +24 -0
- holmes/common/env_vars.py +47 -0
- holmes/config.py +526 -0
- holmes/core/__init__.py +0 -0
- holmes/core/conversations.py +578 -0
- holmes/core/investigation.py +152 -0
- holmes/core/investigation_structured_output.py +264 -0
- holmes/core/issue.py +54 -0
- holmes/core/llm.py +250 -0
- holmes/core/models.py +157 -0
- holmes/core/openai_formatting.py +51 -0
- holmes/core/performance_timing.py +72 -0
- holmes/core/prompt.py +42 -0
- holmes/core/resource_instruction.py +17 -0
- holmes/core/runbooks.py +26 -0
- holmes/core/safeguards.py +120 -0
- holmes/core/supabase_dal.py +540 -0
- holmes/core/tool_calling_llm.py +798 -0
- holmes/core/tools.py +566 -0
- holmes/core/tools_utils/__init__.py +0 -0
- holmes/core/tools_utils/tool_executor.py +65 -0
- holmes/core/tools_utils/toolset_utils.py +52 -0
- holmes/core/toolset_manager.py +418 -0
- holmes/interactive.py +229 -0
- holmes/main.py +1041 -0
- holmes/plugins/__init__.py +0 -0
- holmes/plugins/destinations/__init__.py +6 -0
- holmes/plugins/destinations/slack/__init__.py +2 -0
- holmes/plugins/destinations/slack/plugin.py +163 -0
- holmes/plugins/interfaces.py +32 -0
- holmes/plugins/prompts/__init__.py +48 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
- holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
- holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
- holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
- holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
- holmes/plugins/prompts/generic_ask.jinja2 +36 -0
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
- holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
- holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
- holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
- holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
- holmes/plugins/runbooks/README.md +22 -0
- holmes/plugins/runbooks/__init__.py +100 -0
- holmes/plugins/runbooks/catalog.json +14 -0
- holmes/plugins/runbooks/jira.yaml +12 -0
- holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
- holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
- holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
- holmes/plugins/sources/github/__init__.py +77 -0
- holmes/plugins/sources/jira/__init__.py +123 -0
- holmes/plugins/sources/opsgenie/__init__.py +93 -0
- holmes/plugins/sources/pagerduty/__init__.py +147 -0
- holmes/plugins/sources/prometheus/__init__.py +0 -0
- holmes/plugins/sources/prometheus/models.py +104 -0
- holmes/plugins/sources/prometheus/plugin.py +154 -0
- holmes/plugins/toolsets/__init__.py +171 -0
- holmes/plugins/toolsets/aks-node-health.yaml +65 -0
- holmes/plugins/toolsets/aks.yaml +86 -0
- holmes/plugins/toolsets/argocd.yaml +70 -0
- holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
- holmes/plugins/toolsets/aws.yaml +76 -0
- holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
- holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
- holmes/plugins/toolsets/azure_sql/install.md +66 -0
- holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
- holmes/plugins/toolsets/azure_sql/utils.py +83 -0
- holmes/plugins/toolsets/bash/__init__.py +0 -0
- holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
- holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
- holmes/plugins/toolsets/bash/common/bash.py +52 -0
- holmes/plugins/toolsets/bash/common/config.py +14 -0
- holmes/plugins/toolsets/bash/common/stringify.py +25 -0
- holmes/plugins/toolsets/bash/common/validators.py +24 -0
- holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
- holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
- holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
- holmes/plugins/toolsets/bash/parse_command.py +103 -0
- holmes/plugins/toolsets/confluence.yaml +19 -0
- holmes/plugins/toolsets/consts.py +5 -0
- holmes/plugins/toolsets/coralogix/api.py +158 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
- holmes/plugins/toolsets/coralogix/utils.py +181 -0
- holmes/plugins/toolsets/datadog.py +153 -0
- holmes/plugins/toolsets/docker.yaml +46 -0
- holmes/plugins/toolsets/git.py +756 -0
- holmes/plugins/toolsets/grafana/__init__.py +0 -0
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
- holmes/plugins/toolsets/grafana/common.py +68 -0
- holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
- holmes/plugins/toolsets/grafana/loki_api.py +89 -0
- holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
- holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
- holmes/plugins/toolsets/helm.yaml +42 -0
- holmes/plugins/toolsets/internet/internet.py +275 -0
- holmes/plugins/toolsets/internet/notion.py +137 -0
- holmes/plugins/toolsets/kafka.py +638 -0
- holmes/plugins/toolsets/kubernetes.yaml +255 -0
- holmes/plugins/toolsets/kubernetes_logs.py +426 -0
- holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
- holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
- holmes/plugins/toolsets/logging_utils/types.py +0 -0
- holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
- holmes/plugins/toolsets/newrelic.py +222 -0
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
- holmes/plugins/toolsets/rabbitmq/api.py +398 -0
- holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
- holmes/plugins/toolsets/robusta/__init__.py +0 -0
- holmes/plugins/toolsets/robusta/robusta.py +235 -0
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
- holmes/plugins/toolsets/runbook/__init__.py +0 -0
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
- holmes/plugins/toolsets/service_discovery.py +92 -0
- holmes/plugins/toolsets/servicenow/install.md +37 -0
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
- holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
- holmes/plugins/toolsets/slab.yaml +20 -0
- holmes/plugins/toolsets/utils.py +137 -0
- holmes/plugins/utils.py +14 -0
- holmes/utils/__init__.py +0 -0
- holmes/utils/cache.py +84 -0
- holmes/utils/cert_utils.py +40 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
- holmes/utils/definitions.py +13 -0
- holmes/utils/env.py +53 -0
- holmes/utils/file_utils.py +56 -0
- holmes/utils/global_instructions.py +20 -0
- holmes/utils/holmes_status.py +22 -0
- holmes/utils/holmes_sync_toolsets.py +80 -0
- holmes/utils/markdown_utils.py +55 -0
- holmes/utils/pydantic_utils.py +54 -0
- holmes/utils/robusta.py +10 -0
- holmes/utils/tags.py +97 -0
- holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
- holmesgpt-0.11.5.dist-info/METADATA +400 -0
- holmesgpt-0.11.5.dist-info/RECORD +183 -0
- holmesgpt-0.11.5.dist-info/WHEEL +4 -0
- 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
|
+
)
|