gitarsenal-cli 1.9.21 โ 1.9.24
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.
- package/.venv_status.json +1 -1
- package/package.json +1 -1
- package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/command_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/fetch_modal_tokens.cpython-313.pyc +0 -0
- package/python/__pycache__/llm_debugging.cpython-313.pyc +0 -0
- package/python/__pycache__/modal_container.cpython-313.pyc +0 -0
- package/python/__pycache__/shell.cpython-313.pyc +0 -0
- package/python/api_integration.py +0 -0
- package/python/command_manager.py +613 -0
- package/python/credentials_manager.py +0 -0
- package/python/fetch_modal_tokens.py +0 -0
- package/python/fix_modal_token.py +0 -0
- package/python/fix_modal_token_advanced.py +0 -0
- package/python/gitarsenal.py +0 -0
- package/python/gitarsenal_proxy_client.py +0 -0
- package/python/llm_debugging.py +1369 -0
- package/python/modal_container.py +626 -0
- package/python/setup.py +15 -0
- package/python/setup_modal_token.py +0 -39
- package/python/shell.py +627 -0
- package/python/test_modalSandboxScript.py +75 -2639
- package/scripts/postinstall.js +22 -23
- package/python/__pycache__/credentials_manager.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript.cpython-313.pyc +0 -0
- package/python/__pycache__/test_modalSandboxScript_stable.cpython-313.pyc +0 -0
- package/python/debug_delete.py +0 -167
- package/python/documentation.py +0 -76
- package/python/fix_setup_commands.py +0 -116
- package/python/modal_auth_patch.py +0 -178
- package/python/modal_proxy_service.py +0 -665
- package/python/modal_token_solution.py +0 -293
- package/python/test_dynamic_commands.py +0 -147
- package/test_modalSandboxScript.py +0 -5004
|
@@ -42,2653 +42,51 @@ if args.proxy_url:
|
|
|
42
42
|
if args.proxy_api_key:
|
|
43
43
|
os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self.working_dir = working_dir
|
|
50
|
-
self.timeout = timeout
|
|
51
|
-
self.process = None
|
|
52
|
-
self.stdout_lines = [] # Use list instead of queue
|
|
53
|
-
self.stderr_lines = [] # Use list instead of queue
|
|
54
|
-
self.stdout_lock = threading.Lock()
|
|
55
|
-
self.stderr_lock = threading.Lock()
|
|
56
|
-
self.stdout_thread = None
|
|
57
|
-
self.stderr_thread = None
|
|
58
|
-
self.command_counter = 0
|
|
59
|
-
self.is_running = False
|
|
60
|
-
self.virtual_env_path = None # Track activated virtual environment
|
|
61
|
-
self.suggested_alternative = None # Store suggested alternative commands
|
|
62
|
-
self.should_remove_command = False # Flag to indicate if a command should be removed
|
|
63
|
-
self.removal_reason = None # Reason for removing a command
|
|
64
|
-
|
|
65
|
-
def start(self):
|
|
66
|
-
"""Start the persistent bash shell."""
|
|
67
|
-
if self.is_running:
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
print(f"๐ Starting persistent bash shell in {self.working_dir}")
|
|
71
|
-
|
|
72
|
-
# Start bash with unbuffered output
|
|
73
|
-
self.process = subprocess.Popen(
|
|
74
|
-
['bash', '-i'], # Interactive bash
|
|
75
|
-
stdin=subprocess.PIPE,
|
|
76
|
-
stdout=subprocess.PIPE,
|
|
77
|
-
stderr=subprocess.PIPE,
|
|
78
|
-
text=True,
|
|
79
|
-
bufsize=0, # Unbuffered
|
|
80
|
-
cwd=self.working_dir,
|
|
81
|
-
preexec_fn=os.setsid # Create new process group
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
# Start threads to read stdout and stderr
|
|
85
|
-
self.stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
|
|
86
|
-
self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
|
|
87
|
-
|
|
88
|
-
self.stdout_thread.start()
|
|
89
|
-
self.stderr_thread.start()
|
|
90
|
-
|
|
91
|
-
self.is_running = True
|
|
92
|
-
|
|
93
|
-
# Initial setup commands
|
|
94
|
-
self._send_command_raw("set +h") # Disable hash table for commands
|
|
95
|
-
self._send_command_raw("export PS1='$ '") # Simpler prompt
|
|
96
|
-
self._send_command_raw("cd " + self.working_dir) # Change to working directory
|
|
97
|
-
time.sleep(0.5) # Let initial commands settle
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _read_stdout(self):
|
|
101
|
-
"""Read stdout in a separate thread."""
|
|
102
|
-
while self.process and self.process.poll() is None:
|
|
103
|
-
try:
|
|
104
|
-
line = self.process.stdout.readline()
|
|
105
|
-
if line:
|
|
106
|
-
with self.stdout_lock:
|
|
107
|
-
self.stdout_lines.append(line.rstrip('\n'))
|
|
108
|
-
else:
|
|
109
|
-
time.sleep(0.01)
|
|
110
|
-
except Exception as e:
|
|
111
|
-
print(f"Error reading stdout: {e}")
|
|
112
|
-
break
|
|
113
|
-
|
|
114
|
-
def _read_stderr(self):
|
|
115
|
-
"""Read stderr in a separate thread."""
|
|
116
|
-
while self.process and self.process.poll() is None:
|
|
117
|
-
try:
|
|
118
|
-
line = self.process.stderr.readline()
|
|
119
|
-
if line:
|
|
120
|
-
with self.stderr_lock:
|
|
121
|
-
self.stderr_lines.append(line.rstrip('\n'))
|
|
122
|
-
else:
|
|
123
|
-
time.sleep(0.01)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
print(f"Error reading stderr: {e}")
|
|
126
|
-
break
|
|
127
|
-
|
|
128
|
-
def _send_command_raw(self, command):
|
|
129
|
-
"""Send a raw command to the shell without waiting for completion."""
|
|
130
|
-
if not self.is_running or not self.process:
|
|
131
|
-
raise RuntimeError("Shell is not running")
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
self.process.stdin.write(command + '\n')
|
|
135
|
-
self.process.stdin.flush()
|
|
136
|
-
except Exception as e:
|
|
137
|
-
print(f"Error sending command: {e}")
|
|
138
|
-
raise
|
|
139
|
-
|
|
140
|
-
def _preprocess_command(self, command):
|
|
141
|
-
"""Preprocess commands to handle special cases like virtual environment activation."""
|
|
142
|
-
# Handle virtual environment creation and activation
|
|
143
|
-
if "uv venv" in command and "&&" in command and "source" in command:
|
|
144
|
-
# Split the compound command into separate parts
|
|
145
|
-
parts = [part.strip() for part in command.split("&&")]
|
|
146
|
-
return parts
|
|
147
|
-
elif command.strip().startswith("source ") and "/bin/activate" in command:
|
|
148
|
-
# Handle standalone source command
|
|
149
|
-
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
|
150
|
-
self.virtual_env_path = venv_path
|
|
151
|
-
return [command]
|
|
152
|
-
elif "source" in command and "activate" in command:
|
|
153
|
-
# Handle any other source activation pattern
|
|
154
|
-
return [command]
|
|
155
|
-
elif "uv pip install" in command and self.is_in_venv():
|
|
156
|
-
# If we're in a virtual environment, ensure we use the right pip
|
|
157
|
-
return [command]
|
|
158
|
-
else:
|
|
159
|
-
return [command]
|
|
160
|
-
|
|
161
|
-
def execute(self, command, timeout=None):
|
|
162
|
-
"""Execute a command and return (success, stdout, stderr)."""
|
|
163
|
-
if not self.is_running:
|
|
164
|
-
self.start()
|
|
165
|
-
|
|
166
|
-
if timeout is None:
|
|
167
|
-
timeout = self.timeout
|
|
168
|
-
|
|
169
|
-
# Preprocess the command to handle special cases
|
|
170
|
-
command_parts = self._preprocess_command(command)
|
|
171
|
-
|
|
172
|
-
# If we have multiple parts, execute them sequentially
|
|
173
|
-
if len(command_parts) > 1:
|
|
174
|
-
print(f"๐ง Executing compound command in {len(command_parts)} parts")
|
|
175
|
-
all_stdout = []
|
|
176
|
-
all_stderr = []
|
|
177
|
-
|
|
178
|
-
for i, part in enumerate(command_parts):
|
|
179
|
-
print(f" Part {i+1}/{len(command_parts)}: {part}")
|
|
180
|
-
success, stdout, stderr = self._execute_single(part, timeout)
|
|
181
|
-
|
|
182
|
-
if stdout:
|
|
183
|
-
all_stdout.append(stdout)
|
|
184
|
-
if stderr:
|
|
185
|
-
all_stderr.append(stderr)
|
|
186
|
-
|
|
187
|
-
if not success:
|
|
188
|
-
# If any part fails, return the failure
|
|
189
|
-
return False, '\n'.join(all_stdout), '\n'.join(all_stderr)
|
|
190
|
-
|
|
191
|
-
# Small delay between parts to let environment changes take effect
|
|
192
|
-
time.sleep(0.1)
|
|
193
|
-
|
|
194
|
-
return True, '\n'.join(all_stdout), '\n'.join(all_stderr)
|
|
195
|
-
else:
|
|
196
|
-
return self._execute_single(command_parts[0], timeout)
|
|
197
|
-
|
|
198
|
-
def _execute_single(self, command, timeout):
|
|
199
|
-
"""Execute a single command and return (success, stdout, stderr)."""
|
|
200
|
-
self.command_counter += 1
|
|
201
|
-
marker = f"CMD_DONE_{self.command_counter}_{uuid.uuid4().hex[:8]}"
|
|
202
|
-
|
|
203
|
-
print(f"๐ง Executing: {command}")
|
|
204
|
-
|
|
205
|
-
# Clear any existing output
|
|
206
|
-
self._clear_lines()
|
|
207
|
-
|
|
208
|
-
# Wait for shell to be ready (prompt should be visible)
|
|
209
|
-
if not self.wait_for_prompt(timeout=2):
|
|
210
|
-
# print("โ ๏ธ Shell not ready, waiting...")
|
|
211
|
-
time.sleep(0.5)
|
|
212
|
-
|
|
213
|
-
# For source commands, we need special handling
|
|
214
|
-
if command.strip().startswith("source "):
|
|
215
|
-
# Send the source command in a way that preserves the environment
|
|
216
|
-
try:
|
|
217
|
-
# Extract the virtual environment path
|
|
218
|
-
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
|
219
|
-
|
|
220
|
-
# Use a more robust approach that actually activates the environment
|
|
221
|
-
activation_script = f"""
|
|
222
|
-
if [ -f "{venv_path}/bin/activate" ]; then
|
|
223
|
-
source "{venv_path}/bin/activate"
|
|
224
|
-
echo "VIRTUAL_ENV=$VIRTUAL_ENV"
|
|
225
|
-
echo "PATH=$PATH"
|
|
226
|
-
echo 'SOURCE_SUCCESS'
|
|
227
|
-
else
|
|
228
|
-
echo 'SOURCE_FAILED - activation script not found'
|
|
229
|
-
fi
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
self._send_command_raw(activation_script)
|
|
233
|
-
time.sleep(0.3) # Give more time for environment changes
|
|
234
|
-
self._send_command_raw(f'echo "EXIT_CODE:$?"')
|
|
235
|
-
self._send_command_raw(f'echo "{marker}"')
|
|
236
|
-
except Exception as e:
|
|
237
|
-
return False, "", f"Failed to send source command: {e}"
|
|
238
|
-
else:
|
|
239
|
-
# Send the command followed by markers
|
|
240
|
-
try:
|
|
241
|
-
self._send_command_raw(command)
|
|
242
|
-
# Wait a moment for the command to start
|
|
243
|
-
time.sleep(0.1)
|
|
244
|
-
self._send_command_raw(f'echo "EXIT_CODE:$?"')
|
|
245
|
-
self._send_command_raw(f'echo "{marker}"')
|
|
246
|
-
except Exception as e:
|
|
247
|
-
return False, "", f"Failed to send command: {e}"
|
|
248
|
-
|
|
249
|
-
# Collect output until we see the marker
|
|
250
|
-
command_stdout = []
|
|
251
|
-
command_stderr = []
|
|
252
|
-
start_time = time.time()
|
|
253
|
-
found_marker = False
|
|
254
|
-
exit_code = None
|
|
255
|
-
last_stdout_index = 0
|
|
256
|
-
last_stderr_index = 0
|
|
257
|
-
source_success = None
|
|
258
|
-
|
|
259
|
-
while time.time() - start_time < timeout:
|
|
260
|
-
# Check for new stdout lines
|
|
261
|
-
with self.stdout_lock:
|
|
262
|
-
current_stdout = self.stdout_lines[last_stdout_index:]
|
|
263
|
-
last_stdout_index = len(self.stdout_lines)
|
|
264
|
-
|
|
265
|
-
for line in current_stdout:
|
|
266
|
-
if line == marker:
|
|
267
|
-
found_marker = True
|
|
268
|
-
break
|
|
269
|
-
elif line.startswith("EXIT_CODE:"):
|
|
270
|
-
try:
|
|
271
|
-
exit_code = int(line.split(":", 1)[1])
|
|
272
|
-
except (ValueError, IndexError):
|
|
273
|
-
exit_code = 1
|
|
274
|
-
elif line == "SOURCE_SUCCESS":
|
|
275
|
-
source_success = True
|
|
276
|
-
elif line.startswith("SOURCE_FAILED"):
|
|
277
|
-
source_success = False
|
|
278
|
-
command_stderr.append(line)
|
|
279
|
-
elif line.startswith("VIRTUAL_ENV="):
|
|
280
|
-
# Extract and store the virtual environment path
|
|
281
|
-
venv_path = line.split("=", 1)[1]
|
|
282
|
-
self.virtual_env_path = venv_path
|
|
283
|
-
command_stdout.append(line)
|
|
284
|
-
elif line.startswith("PATH="):
|
|
285
|
-
# Store the updated PATH
|
|
286
|
-
command_stdout.append(line)
|
|
287
|
-
elif line.strip() and not line.startswith("$"): # Skip empty lines and prompt lines
|
|
288
|
-
command_stdout.append(line)
|
|
289
|
-
|
|
290
|
-
if found_marker:
|
|
291
|
-
break
|
|
292
|
-
|
|
293
|
-
# Check for new stderr lines
|
|
294
|
-
with self.stderr_lock:
|
|
295
|
-
current_stderr = self.stderr_lines[last_stderr_index:]
|
|
296
|
-
last_stderr_index = len(self.stderr_lines)
|
|
297
|
-
|
|
298
|
-
for line in current_stderr:
|
|
299
|
-
if line.strip(): # Skip empty lines
|
|
300
|
-
command_stderr.append(line)
|
|
301
|
-
|
|
302
|
-
# Check if command is waiting for user input
|
|
303
|
-
if not found_marker and time.time() - start_time > 5: # Wait at least 5 seconds before checking
|
|
304
|
-
if self._is_waiting_for_input(command_stdout, command_stderr):
|
|
305
|
-
print("โ ๏ธ Command appears to be waiting for user input")
|
|
306
|
-
# Try to handle the input requirement
|
|
307
|
-
input_handled = self._handle_input_requirement(command, command_stdout, command_stderr)
|
|
308
|
-
|
|
309
|
-
if input_handled is True and self.should_remove_command:
|
|
310
|
-
# If LLM suggested to remove the command
|
|
311
|
-
self._send_command_raw("\x03") # Send Ctrl+C
|
|
312
|
-
time.sleep(0.5)
|
|
313
|
-
return False, '\n'.join(command_stdout), f"Command removed - {self.removal_reason}"
|
|
314
|
-
elif not input_handled:
|
|
315
|
-
# If we couldn't handle the input, abort the command
|
|
316
|
-
self._send_command_raw("\x03") # Send Ctrl+C
|
|
317
|
-
time.sleep(0.5)
|
|
318
|
-
return False, '\n'.join(command_stdout), "Command aborted - requires user input"
|
|
319
|
-
|
|
320
|
-
time.sleep(0.1)
|
|
321
|
-
|
|
322
|
-
if not found_marker:
|
|
323
|
-
print(f"โ ๏ธ Command timed out after {timeout} seconds")
|
|
324
|
-
return False, '\n'.join(command_stdout), f"Command timed out after {timeout} seconds"
|
|
325
|
-
|
|
326
|
-
stdout_text = '\n'.join(command_stdout)
|
|
327
|
-
stderr_text = '\n'.join(command_stderr)
|
|
328
|
-
|
|
329
|
-
# Determine success based on multiple factors
|
|
330
|
-
if source_success is not None:
|
|
331
|
-
success = source_success
|
|
332
|
-
else:
|
|
333
|
-
success = exit_code == 0 if exit_code is not None else len(command_stderr) == 0
|
|
334
|
-
|
|
335
|
-
if success:
|
|
336
|
-
if stdout_text:
|
|
337
|
-
print(f"โ
Output: {stdout_text}")
|
|
338
|
-
# Track virtual environment activation
|
|
339
|
-
if command.strip().startswith("source ") and "/bin/activate" in command:
|
|
340
|
-
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
|
341
|
-
self.virtual_env_path = venv_path
|
|
342
|
-
print(f"โ
Virtual environment activated: {venv_path}")
|
|
343
|
-
else:
|
|
344
|
-
print(f"โ Command failed with exit code: {exit_code}")
|
|
345
|
-
if stderr_text:
|
|
346
|
-
print(f"โ Error: {stderr_text}")
|
|
347
|
-
|
|
348
|
-
# Wait a moment for the shell to be ready for the next command
|
|
349
|
-
time.sleep(0.2)
|
|
350
|
-
|
|
351
|
-
return success, stdout_text, stderr_text
|
|
352
|
-
|
|
353
|
-
def _is_waiting_for_input(self, stdout_lines, stderr_lines):
|
|
354
|
-
"""Detect if a command is waiting for user input."""
|
|
355
|
-
# Common patterns that indicate waiting for user input
|
|
356
|
-
input_patterns = [
|
|
357
|
-
r'(?i)(y/n|yes/no)\??\s*$', # Yes/No prompts
|
|
358
|
-
r'(?i)password:?\s*$', # Password prompts
|
|
359
|
-
r'(?i)continue\??\s*$', # Continue prompts
|
|
360
|
-
r'(?i)proceed\??\s*$', # Proceed prompts
|
|
361
|
-
r'\[\s*[Yy]/[Nn]\s*\]\s*$', # [Y/n] style prompts
|
|
362
|
-
r'(?i)username:?\s*$', # Username prompts
|
|
363
|
-
r'(?i)token:?\s*$', # Token prompts
|
|
364
|
-
r'(?i)api key:?\s*$', # API key prompts
|
|
365
|
-
r'(?i)press enter to continue', # Press enter prompts
|
|
366
|
-
r'(?i)select an option:?\s*$', # Selection prompts
|
|
367
|
-
r'(?i)choose an option:?\s*$', # Choice prompts
|
|
368
|
-
]
|
|
369
|
-
|
|
370
|
-
# Check the last few lines of stdout and stderr for input patterns
|
|
371
|
-
last_lines = []
|
|
372
|
-
if stdout_lines:
|
|
373
|
-
last_lines.extend(stdout_lines[-3:]) # Check last 3 lines of stdout
|
|
374
|
-
if stderr_lines:
|
|
375
|
-
last_lines.extend(stderr_lines[-3:]) # Check last 3 lines of stderr
|
|
376
|
-
|
|
377
|
-
for line in last_lines:
|
|
378
|
-
for pattern in input_patterns:
|
|
379
|
-
if re.search(pattern, line):
|
|
380
|
-
print(f"๐ Detected input prompt: {line}")
|
|
381
|
-
return True
|
|
382
|
-
|
|
383
|
-
# Check if there's no output for a while but the command is still running
|
|
384
|
-
if len(stdout_lines) == 0 and len(stderr_lines) == 0:
|
|
385
|
-
# This might be a command waiting for input without a prompt
|
|
386
|
-
# We'll be cautious and only return True if we're sure
|
|
387
|
-
return False
|
|
388
|
-
|
|
389
|
-
return False
|
|
390
|
-
|
|
391
|
-
def _handle_input_requirement(self, command, stdout_lines, stderr_lines):
|
|
392
|
-
"""Attempt to handle commands that require input."""
|
|
393
|
-
# Extract the last few lines to analyze what kind of input is needed
|
|
394
|
-
last_lines = []
|
|
395
|
-
if stdout_lines:
|
|
396
|
-
last_lines.extend(stdout_lines[-3:])
|
|
397
|
-
if stderr_lines:
|
|
398
|
-
last_lines.extend(stderr_lines[-3:])
|
|
399
|
-
|
|
400
|
-
last_line = last_lines[-1] if last_lines else ""
|
|
401
|
-
|
|
402
|
-
# Try to determine what kind of input is needed
|
|
403
|
-
if re.search(r'(?i)(y/n|yes/no|\[y/n\])', last_line):
|
|
404
|
-
# For yes/no prompts, usually 'yes' is safer
|
|
405
|
-
print("๐ง Auto-responding with 'y' to yes/no prompt")
|
|
406
|
-
self._send_command_raw("y")
|
|
407
|
-
return True
|
|
408
|
-
|
|
409
|
-
elif re.search(r'(?i)password', last_line):
|
|
410
|
-
# For password prompts, check if we have stored credentials
|
|
411
|
-
stored_creds = get_stored_credentials()
|
|
412
|
-
if stored_creds and 'ssh_password' in stored_creds:
|
|
413
|
-
print("๐ง Auto-responding with stored SSH password")
|
|
414
|
-
self._send_command_raw(stored_creds['ssh_password'])
|
|
415
|
-
return True
|
|
416
|
-
else:
|
|
417
|
-
print("โ ๏ธ Password prompt detected but no stored password available")
|
|
418
|
-
return False
|
|
419
|
-
|
|
420
|
-
elif re.search(r'(?i)token|api.key', last_line):
|
|
421
|
-
# For token/API key prompts
|
|
422
|
-
stored_creds = get_stored_credentials()
|
|
423
|
-
if stored_creds:
|
|
424
|
-
if 'openai_api_key' in stored_creds and re.search(r'(?i)openai|api.key', last_line):
|
|
425
|
-
print("๐ง Auto-responding with stored OpenAI API key")
|
|
426
|
-
self._send_command_raw(stored_creds['openai_api_key'])
|
|
427
|
-
return True
|
|
428
|
-
elif 'hf_token' in stored_creds and re.search(r'(?i)hugg|hf|token', last_line):
|
|
429
|
-
print("๐ง Auto-responding with stored Hugging Face token")
|
|
430
|
-
self._send_command_raw(stored_creds['hf_token'])
|
|
431
|
-
return True
|
|
432
|
-
|
|
433
|
-
print("โ ๏ธ Token/API key prompt detected but no matching stored credentials")
|
|
434
|
-
return False
|
|
435
|
-
|
|
436
|
-
elif re.search(r'(?i)press enter|continue|proceed', last_line):
|
|
437
|
-
# For "press enter to continue" prompts
|
|
438
|
-
print("๐ง Auto-responding with Enter to continue")
|
|
439
|
-
self._send_command_raw("") # Empty string sends just Enter
|
|
440
|
-
return True
|
|
441
|
-
|
|
442
|
-
# If we can't determine the type of input needed
|
|
443
|
-
print("โ ๏ธ Couldn't determine the type of input needed")
|
|
444
|
-
|
|
445
|
-
# Try to use LLM to suggest an alternative command
|
|
446
|
-
try:
|
|
447
|
-
# Get current working directory for context
|
|
448
|
-
cwd = self.get_cwd()
|
|
449
|
-
|
|
450
|
-
# Reset command removal flags
|
|
451
|
-
self.should_remove_command = False
|
|
452
|
-
self.removal_reason = None
|
|
453
|
-
|
|
454
|
-
# Call LLM to suggest an alternative
|
|
455
|
-
alternative = self._suggest_alternative_command(command, stdout_lines, stderr_lines, cwd)
|
|
456
|
-
|
|
457
|
-
# Check if LLM suggested to remove the command
|
|
458
|
-
if self.should_remove_command:
|
|
459
|
-
print(f"๐ซ Command will be removed: {self.removal_reason}")
|
|
460
|
-
return True # Return True to indicate the command has been handled (by removing it)
|
|
461
|
-
|
|
462
|
-
if alternative:
|
|
463
|
-
print(f"๐ง LLM suggested alternative command: {alternative}")
|
|
464
|
-
# We don't execute the alternative here, but return False so the calling code
|
|
465
|
-
# can handle it (e.g., by adding it to the command list)
|
|
466
|
-
|
|
467
|
-
# Store the suggested alternative for later use
|
|
468
|
-
self.suggested_alternative = alternative
|
|
469
|
-
return False
|
|
470
|
-
except Exception as e:
|
|
471
|
-
print(f"โ ๏ธ Error getting LLM suggestion: {e}")
|
|
472
|
-
|
|
473
|
-
return False
|
|
474
|
-
|
|
475
|
-
def _suggest_alternative_command(self, command, stdout_lines, stderr_lines, current_dir):
|
|
476
|
-
"""Use LLM to suggest an alternative command that doesn't require user input."""
|
|
477
|
-
try:
|
|
478
|
-
# Get API key
|
|
479
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
480
|
-
if not api_key:
|
|
481
|
-
# Try to load from saved file
|
|
482
|
-
key_file = os.path.expanduser("~/.gitarsenal/openai_key")
|
|
483
|
-
if os.path.exists(key_file):
|
|
484
|
-
with open(key_file, "r") as f:
|
|
485
|
-
api_key = f.read().strip()
|
|
486
|
-
|
|
487
|
-
if not api_key:
|
|
488
|
-
print("โ ๏ธ No OpenAI API key available for suggesting alternative command")
|
|
489
|
-
return None
|
|
490
|
-
|
|
491
|
-
# Prepare the prompt
|
|
492
|
-
stdout_text = '\n'.join(stdout_lines[-10:]) if stdout_lines else ""
|
|
493
|
-
stderr_text = '\n'.join(stderr_lines[-10:]) if stderr_lines else ""
|
|
494
|
-
|
|
495
|
-
prompt = f"""
|
|
496
|
-
The command '{command}' appears to be waiting for user input.
|
|
497
|
-
|
|
498
|
-
Current directory: {current_dir}
|
|
499
|
-
|
|
500
|
-
Last stdout output:
|
|
501
|
-
{stdout_text}
|
|
502
|
-
|
|
503
|
-
Last stderr output:
|
|
504
|
-
{stderr_text}
|
|
505
|
-
|
|
506
|
-
Please analyze this command and determine if it's useful to continue with it.
|
|
507
|
-
If it's useful, suggest an alternative command that achieves the same goal but doesn't require user input.
|
|
508
|
-
For example, add flags like -y, --yes, --no-input, etc., or provide the required input in the command.
|
|
509
|
-
|
|
510
|
-
If the command is not useful or cannot be executed non-interactively, respond with "REMOVE_COMMAND" and explain why.
|
|
511
|
-
|
|
512
|
-
Format your response as:
|
|
513
|
-
ALTERNATIVE: <alternative command>
|
|
514
|
-
or
|
|
515
|
-
REMOVE_COMMAND: <reason>
|
|
516
|
-
"""
|
|
517
|
-
|
|
518
|
-
# Call OpenAI API
|
|
519
|
-
import openai
|
|
520
|
-
client = openai.OpenAI(api_key=api_key)
|
|
521
|
-
|
|
522
|
-
response = client.chat.completions.create(
|
|
523
|
-
model="gpt-4o-mini",
|
|
524
|
-
messages=[
|
|
525
|
-
{"role": "system", "content": "You are a helpful assistant that suggests alternative commands that don't require user input."},
|
|
526
|
-
{"role": "user", "content": prompt}
|
|
527
|
-
],
|
|
528
|
-
max_tokens=150,
|
|
529
|
-
temperature=0.7
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
response_text = response.choices[0].message.content.strip()
|
|
533
|
-
|
|
534
|
-
# Check if the response suggests removing the command
|
|
535
|
-
if response_text.startswith("REMOVE_COMMAND:"):
|
|
536
|
-
reason = response_text.replace("REMOVE_COMMAND:", "").strip()
|
|
537
|
-
print(f"๐ซ LLM suggests removing command: {reason}")
|
|
538
|
-
self.should_remove_command = True
|
|
539
|
-
self.removal_reason = reason
|
|
540
|
-
return None
|
|
541
|
-
|
|
542
|
-
# Extract the alternative command
|
|
543
|
-
if response_text.startswith("ALTERNATIVE:"):
|
|
544
|
-
alternative_command = response_text.replace("ALTERNATIVE:", "").strip()
|
|
545
|
-
else:
|
|
546
|
-
# Try to extract the command from a free-form response
|
|
547
|
-
lines = response_text.split('\n')
|
|
548
|
-
for line in lines:
|
|
549
|
-
line = line.strip()
|
|
550
|
-
if line and not line.startswith(('Here', 'I', 'You', 'The', 'This', 'Use', 'Try')):
|
|
551
|
-
alternative_command = line
|
|
552
|
-
break
|
|
553
|
-
else:
|
|
554
|
-
alternative_command = lines[0].strip()
|
|
555
|
-
|
|
556
|
-
return alternative_command
|
|
557
|
-
|
|
558
|
-
except Exception as e:
|
|
559
|
-
print(f"โ ๏ธ Error suggesting alternative command: {e}")
|
|
560
|
-
return None
|
|
561
|
-
|
|
562
|
-
def _clear_lines(self):
|
|
563
|
-
"""Clear both output line lists."""
|
|
564
|
-
with self.stdout_lock:
|
|
565
|
-
self.stdout_lines.clear()
|
|
566
|
-
with self.stderr_lock:
|
|
567
|
-
self.stderr_lines.clear()
|
|
568
|
-
|
|
569
|
-
def get_cwd(self):
|
|
570
|
-
"""Get current working directory."""
|
|
571
|
-
success, output, _ = self._execute_single("pwd", 10)
|
|
572
|
-
if success:
|
|
573
|
-
return output.strip()
|
|
574
|
-
return self.working_dir
|
|
575
|
-
|
|
576
|
-
def get_virtual_env(self):
|
|
577
|
-
"""Get the currently activated virtual environment path."""
|
|
578
|
-
return self.virtual_env_path
|
|
579
|
-
|
|
580
|
-
def is_in_venv(self):
|
|
581
|
-
"""Check if we're currently in a virtual environment."""
|
|
582
|
-
return self.virtual_env_path is not None and self.virtual_env_path != ""
|
|
583
|
-
|
|
584
|
-
def get_venv_name(self):
|
|
585
|
-
"""Get the name of the current virtual environment if active."""
|
|
586
|
-
if self.is_in_venv():
|
|
587
|
-
return os.path.basename(self.virtual_env_path)
|
|
588
|
-
return None
|
|
589
|
-
|
|
590
|
-
def exec(self, *args, **kwargs):
|
|
591
|
-
"""Compatibility method to make PersistentShell work with call_openai_for_debug."""
|
|
592
|
-
# Convert exec call to execute method
|
|
593
|
-
if len(args) >= 2 and args[0] == "bash" and args[1] == "-c":
|
|
594
|
-
command = args[2]
|
|
595
|
-
success, stdout, stderr = self.execute(command)
|
|
596
|
-
|
|
597
|
-
# Create a mock result object that mimics the expected interface
|
|
598
|
-
class MockResult:
|
|
599
|
-
def __init__(self, stdout, stderr, returncode):
|
|
600
|
-
self.stdout = [stdout] if stdout else []
|
|
601
|
-
self.stderr = [stderr] if stderr else []
|
|
602
|
-
self.returncode = 0 if returncode else 1
|
|
603
|
-
|
|
604
|
-
def wait(self):
|
|
605
|
-
pass
|
|
606
|
-
|
|
607
|
-
return MockResult(stdout, stderr, success)
|
|
608
|
-
else:
|
|
609
|
-
raise NotImplementedError("exec method only supports bash -c commands")
|
|
610
|
-
|
|
611
|
-
def wait_for_prompt(self, timeout=5):
|
|
612
|
-
"""Wait for the shell prompt to appear, indicating readiness for next command."""
|
|
613
|
-
start_time = time.time()
|
|
614
|
-
while time.time() - start_time < timeout:
|
|
615
|
-
with self.stdout_lock:
|
|
616
|
-
if self.stdout_lines and self.stdout_lines[-1].strip().endswith('$'):
|
|
617
|
-
return True
|
|
618
|
-
time.sleep(0.1)
|
|
619
|
-
return False
|
|
620
|
-
|
|
621
|
-
def cleanup(self):
|
|
622
|
-
"""Clean up the shell process."""
|
|
623
|
-
print("๐งน Cleaning up persistent shell...")
|
|
624
|
-
self.is_running = False
|
|
625
|
-
|
|
626
|
-
if self.process:
|
|
627
|
-
try:
|
|
628
|
-
# Send exit command
|
|
629
|
-
self._send_command_raw("exit")
|
|
630
|
-
|
|
631
|
-
# Wait for process to terminate
|
|
632
|
-
try:
|
|
633
|
-
self.process.wait(timeout=5)
|
|
634
|
-
except subprocess.TimeoutExpired:
|
|
635
|
-
# Force kill if it doesn't exit gracefully
|
|
636
|
-
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
|
637
|
-
try:
|
|
638
|
-
self.process.wait(timeout=2)
|
|
639
|
-
except subprocess.TimeoutExpired:
|
|
640
|
-
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
|
641
|
-
|
|
642
|
-
except Exception as e:
|
|
643
|
-
print(f"Error during cleanup: {e}")
|
|
644
|
-
finally:
|
|
645
|
-
self.process = None
|
|
646
|
-
|
|
647
|
-
print("โ
Shell cleanup completed")
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
class CommandListManager:
|
|
651
|
-
"""Manages a dynamic list of setup commands with status tracking and LLM-suggested fixes."""
|
|
652
|
-
|
|
653
|
-
def __init__(self, initial_commands=None):
|
|
654
|
-
self.commands = []
|
|
655
|
-
self.executed_commands = []
|
|
656
|
-
self.failed_commands = []
|
|
657
|
-
self.suggested_fixes = []
|
|
658
|
-
self.current_index = 0
|
|
659
|
-
self.total_commands = 0
|
|
660
|
-
|
|
661
|
-
if initial_commands:
|
|
662
|
-
self.add_commands(initial_commands)
|
|
663
|
-
|
|
664
|
-
def add_commands(self, commands):
|
|
665
|
-
"""Add new commands to the list."""
|
|
666
|
-
if isinstance(commands, str):
|
|
667
|
-
commands = [commands]
|
|
668
|
-
|
|
669
|
-
added_count = 0
|
|
670
|
-
for cmd in commands:
|
|
671
|
-
if cmd and cmd.strip():
|
|
672
|
-
self.commands.append({
|
|
673
|
-
'command': cmd.strip(),
|
|
674
|
-
'status': 'pending',
|
|
675
|
-
'index': len(self.commands),
|
|
676
|
-
'stdout': '',
|
|
677
|
-
'stderr': '',
|
|
678
|
-
'execution_time': None,
|
|
679
|
-
'fix_attempts': 0,
|
|
680
|
-
'max_fix_attempts': 3
|
|
681
|
-
})
|
|
682
|
-
added_count += 1
|
|
683
|
-
|
|
684
|
-
self.total_commands = len(self.commands)
|
|
685
|
-
if added_count > 0:
|
|
686
|
-
print(f"๐ Added {added_count} commands to list. Total: {self.total_commands}")
|
|
687
|
-
|
|
688
|
-
def add_command_dynamically(self, command, priority='normal'):
|
|
689
|
-
"""Add a single command dynamically during execution."""
|
|
690
|
-
if not command or not command.strip():
|
|
691
|
-
return False
|
|
692
|
-
|
|
693
|
-
new_command = {
|
|
694
|
-
'command': command.strip(),
|
|
695
|
-
'status': 'pending',
|
|
696
|
-
'index': len(self.commands),
|
|
697
|
-
'stdout': '',
|
|
698
|
-
'stderr': '',
|
|
699
|
-
'execution_time': None,
|
|
700
|
-
'fix_attempts': 0,
|
|
701
|
-
'max_fix_attempts': 3,
|
|
702
|
-
'priority': priority
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if priority == 'high':
|
|
706
|
-
# Insert at the beginning of pending commands
|
|
707
|
-
self.commands.insert(self.current_index, new_command)
|
|
708
|
-
# Update indices for all commands after insertion
|
|
709
|
-
for i in range(self.current_index + 1, len(self.commands)):
|
|
710
|
-
self.commands[i]['index'] = i
|
|
711
|
-
else:
|
|
712
|
-
# Add to the end
|
|
713
|
-
self.commands.append(new_command)
|
|
714
|
-
|
|
715
|
-
self.total_commands = len(self.commands)
|
|
716
|
-
print(f"๐ Added dynamic command: {command.strip()}")
|
|
717
|
-
return True
|
|
718
|
-
|
|
719
|
-
def add_suggested_fix(self, original_command, fix_command, reason=""):
|
|
720
|
-
"""Add a LLM-suggested fix for a failed command."""
|
|
721
|
-
fix_entry = {
|
|
722
|
-
'original_command': original_command,
|
|
723
|
-
'fix_command': fix_command,
|
|
724
|
-
'reason': reason,
|
|
725
|
-
'status': 'pending',
|
|
726
|
-
'index': len(self.suggested_fixes),
|
|
727
|
-
'stdout': '',
|
|
728
|
-
'stderr': '',
|
|
729
|
-
'execution_time': None
|
|
730
|
-
}
|
|
731
|
-
self.suggested_fixes.append(fix_entry)
|
|
732
|
-
print(f"๐ง Added suggested fix: {fix_command}")
|
|
733
|
-
return len(self.suggested_fixes) - 1
|
|
734
|
-
|
|
735
|
-
def get_next_command(self):
|
|
736
|
-
"""Get the next pending command to execute."""
|
|
737
|
-
# First, try to get a pending command from the main list
|
|
738
|
-
for i in range(self.current_index, len(self.commands)):
|
|
739
|
-
if self.commands[i]['status'] == 'pending':
|
|
740
|
-
return self.commands[i], 'main'
|
|
741
|
-
|
|
742
|
-
# If no pending commands in main list, check suggested fixes
|
|
743
|
-
for fix in self.suggested_fixes:
|
|
744
|
-
if fix['status'] == 'pending':
|
|
745
|
-
return fix, 'fix'
|
|
746
|
-
|
|
747
|
-
return None, None
|
|
748
|
-
|
|
749
|
-
def mark_command_executed(self, command_index, command_type='main', success=True, stdout='', stderr='', execution_time=None):
|
|
750
|
-
"""Mark a command as executed with results."""
|
|
751
|
-
if command_type == 'main':
|
|
752
|
-
if 0 <= command_index < len(self.commands):
|
|
753
|
-
self.commands[command_index].update({
|
|
754
|
-
'status': 'success' if success else 'failed',
|
|
755
|
-
'stdout': stdout,
|
|
756
|
-
'stderr': stderr,
|
|
757
|
-
'execution_time': execution_time
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
if success:
|
|
761
|
-
self.executed_commands.append(self.commands[command_index])
|
|
762
|
-
print(f"โ
Command {command_index + 1}/{self.total_commands} completed successfully")
|
|
763
|
-
else:
|
|
764
|
-
self.failed_commands.append(self.commands[command_index])
|
|
765
|
-
print(f"โ Command {command_index + 1}/{self.total_commands} failed")
|
|
766
|
-
|
|
767
|
-
self.current_index = max(self.current_index, command_index + 1)
|
|
768
|
-
|
|
769
|
-
elif command_type == 'fix':
|
|
770
|
-
if 0 <= command_index < len(self.suggested_fixes):
|
|
771
|
-
self.suggested_fixes[command_index].update({
|
|
772
|
-
'status': 'success' if success else 'failed',
|
|
773
|
-
'stdout': stdout,
|
|
774
|
-
'stderr': stderr,
|
|
775
|
-
'execution_time': execution_time
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
if success:
|
|
779
|
-
print(f"โ
Fix command {command_index + 1} completed successfully")
|
|
780
|
-
else:
|
|
781
|
-
print(f"โ Fix command {command_index + 1} failed")
|
|
782
|
-
|
|
783
|
-
def get_status_summary(self):
|
|
784
|
-
"""Get a summary of command execution status."""
|
|
785
|
-
total_main = len(self.commands)
|
|
786
|
-
total_fixes = len(self.suggested_fixes)
|
|
787
|
-
executed_main = len([c for c in self.commands if c['status'] == 'success'])
|
|
788
|
-
failed_main = len([c for c in self.commands if c['status'] == 'failed'])
|
|
789
|
-
pending_main = len([c for c in self.commands if c['status'] == 'pending'])
|
|
790
|
-
executed_fixes = len([f for f in self.suggested_fixes if f['status'] == 'success'])
|
|
791
|
-
failed_fixes = len([f for f in self.suggested_fixes if f['status'] == 'failed'])
|
|
792
|
-
|
|
793
|
-
return {
|
|
794
|
-
'total_main_commands': total_main,
|
|
795
|
-
'executed_main_commands': executed_main,
|
|
796
|
-
'failed_main_commands': failed_main,
|
|
797
|
-
'pending_main_commands': pending_main,
|
|
798
|
-
'total_fix_commands': total_fixes,
|
|
799
|
-
'executed_fix_commands': executed_fixes,
|
|
800
|
-
'failed_fix_commands': failed_fixes,
|
|
801
|
-
'progress_percentage': (executed_main / total_main * 100) if total_main > 0 else 0
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
def print_status(self):
|
|
805
|
-
"""Print current status of all commands."""
|
|
806
|
-
summary = self.get_status_summary()
|
|
807
|
-
|
|
808
|
-
print("\n" + "="*60)
|
|
809
|
-
print("๐ COMMAND EXECUTION STATUS")
|
|
810
|
-
print("="*60)
|
|
811
|
-
|
|
812
|
-
# Main commands status
|
|
813
|
-
print(f"๐ Main Commands: {summary['executed_main_commands']}/{summary['total_main_commands']} completed")
|
|
814
|
-
print(f" โ
Successful: {summary['executed_main_commands']}")
|
|
815
|
-
print(f" โ Failed: {summary['failed_main_commands']}")
|
|
816
|
-
print(f" โณ Pending: {summary['pending_main_commands']}")
|
|
817
|
-
|
|
818
|
-
# Fix commands status
|
|
819
|
-
if summary['total_fix_commands'] > 0:
|
|
820
|
-
print(f"๐ง Fix Commands: {summary['executed_fix_commands']}/{summary['total_fix_commands']} completed")
|
|
821
|
-
print(f" โ
Successful: {summary['executed_fix_commands']}")
|
|
822
|
-
print(f" โ Failed: {summary['failed_fix_commands']}")
|
|
823
|
-
|
|
824
|
-
# Progress bar
|
|
825
|
-
progress = summary['progress_percentage']
|
|
826
|
-
bar_length = 30
|
|
827
|
-
filled_length = int(bar_length * progress / 100)
|
|
828
|
-
bar = 'โ' * filled_length + 'โ' * (bar_length - filled_length)
|
|
829
|
-
print(f"๐ Progress: [{bar}] {progress:.1f}%")
|
|
830
|
-
|
|
831
|
-
# Show current command if any
|
|
832
|
-
next_cmd, cmd_type = self.get_next_command()
|
|
833
|
-
if next_cmd:
|
|
834
|
-
cmd_type_str = "main" if cmd_type == 'main' else "fix"
|
|
835
|
-
cmd_text = next_cmd.get('command', next_cmd.get('fix_command', 'Unknown command'))
|
|
836
|
-
print(f"๐ Current: {cmd_type_str} command - {cmd_text[:50]}...")
|
|
837
|
-
|
|
838
|
-
print("="*60)
|
|
839
|
-
|
|
840
|
-
def get_failed_commands_for_llm(self):
|
|
841
|
-
"""Get failed commands for LLM analysis."""
|
|
842
|
-
failed_commands = []
|
|
843
|
-
|
|
844
|
-
# Get failed main commands
|
|
845
|
-
for cmd in self.commands:
|
|
846
|
-
if cmd['status'] == 'failed':
|
|
847
|
-
failed_commands.append({
|
|
848
|
-
'command': cmd['command'],
|
|
849
|
-
'stderr': cmd['stderr'],
|
|
850
|
-
'stdout': cmd['stdout'],
|
|
851
|
-
'type': 'main'
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
# Get failed fix commands
|
|
855
|
-
for fix in self.suggested_fixes:
|
|
856
|
-
if fix['status'] == 'failed':
|
|
857
|
-
failed_commands.append({
|
|
858
|
-
'command': fix['fix_command'],
|
|
859
|
-
'stderr': fix['stderr'],
|
|
860
|
-
'stdout': fix['stdout'],
|
|
861
|
-
'type': 'fix',
|
|
862
|
-
'original_command': fix['original_command']
|
|
863
|
-
})
|
|
864
|
-
|
|
865
|
-
return failed_commands
|
|
866
|
-
|
|
867
|
-
def has_pending_commands(self):
|
|
868
|
-
"""Check if there are any pending commands."""
|
|
869
|
-
return any(cmd['status'] == 'pending' for cmd in self.commands) or \
|
|
870
|
-
any(fix['status'] == 'pending' for fix in self.suggested_fixes)
|
|
871
|
-
|
|
872
|
-
def get_all_commands(self):
|
|
873
|
-
"""Get all commands (main + fixes) in execution order."""
|
|
874
|
-
all_commands = []
|
|
875
|
-
|
|
876
|
-
# Add main commands
|
|
877
|
-
for cmd in self.commands:
|
|
878
|
-
all_commands.append({
|
|
879
|
-
**cmd,
|
|
880
|
-
'type': 'main'
|
|
881
|
-
})
|
|
882
|
-
|
|
883
|
-
# Add fix commands
|
|
884
|
-
for fix in self.suggested_fixes:
|
|
885
|
-
all_commands.append({
|
|
886
|
-
**fix,
|
|
887
|
-
'type': 'fix'
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
return all_commands
|
|
891
|
-
|
|
892
|
-
def analyze_failed_commands_with_llm(self, api_key=None, current_dir=None, sandbox=None):
|
|
893
|
-
"""Analyze all failed commands using LLM and add suggested fixes."""
|
|
894
|
-
failed_commands = self.get_failed_commands_for_llm()
|
|
895
|
-
|
|
896
|
-
if not failed_commands:
|
|
897
|
-
print("โ
No failed commands to analyze")
|
|
898
|
-
return []
|
|
899
|
-
|
|
900
|
-
print(f"๐ Analyzing {len(failed_commands)} failed commands with LLM...")
|
|
901
|
-
|
|
902
|
-
# Use unified batch debugging for efficiency
|
|
903
|
-
fixes = call_llm_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
|
|
904
|
-
|
|
905
|
-
# Add the fixes to the command list
|
|
906
|
-
added_fixes = []
|
|
907
|
-
for fix in fixes:
|
|
908
|
-
fix_index = self.add_suggested_fix(
|
|
909
|
-
fix['original_command'],
|
|
910
|
-
fix['fix_command'],
|
|
911
|
-
fix['reason']
|
|
912
|
-
)
|
|
913
|
-
added_fixes.append(fix_index)
|
|
914
|
-
|
|
915
|
-
print(f"๐ง Added {len(added_fixes)} LLM-suggested fixes to command list")
|
|
916
|
-
return added_fixes
|
|
917
|
-
|
|
918
|
-
def should_skip_original_command(self, original_command, fix_command, fix_stdout, fix_stderr, api_key=None):
|
|
919
|
-
"""
|
|
920
|
-
Use LLM to determine if the original command should be skipped after a successful fix.
|
|
921
|
-
|
|
922
|
-
Args:
|
|
923
|
-
original_command: The original command that failed
|
|
924
|
-
fix_command: The fix command that succeeded
|
|
925
|
-
fix_stdout: The stdout from the fix command
|
|
926
|
-
fix_stderr: The stderr from the fix command
|
|
927
|
-
api_key: OpenAI API key
|
|
928
|
-
|
|
929
|
-
Returns:
|
|
930
|
-
tuple: (should_skip, reason)
|
|
931
|
-
"""
|
|
932
|
-
try:
|
|
933
|
-
# Get API key if not provided
|
|
934
|
-
if not api_key:
|
|
935
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
936
|
-
if not api_key:
|
|
937
|
-
# Try to load from saved file
|
|
938
|
-
key_file = os.path.expanduser("~/.gitarsenal/openai_key")
|
|
939
|
-
if os.path.exists(key_file):
|
|
940
|
-
with open(key_file, "r") as f:
|
|
941
|
-
api_key = f.read().strip()
|
|
942
|
-
|
|
943
|
-
if not api_key:
|
|
944
|
-
print("โ ๏ธ No OpenAI API key available for command list analysis")
|
|
945
|
-
return False, "No API key available"
|
|
946
|
-
|
|
947
|
-
# Get all commands for context
|
|
948
|
-
all_commands = self.get_all_commands()
|
|
949
|
-
commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}" for i, cmd in enumerate(all_commands)])
|
|
950
|
-
|
|
951
|
-
# Prepare the prompt
|
|
952
|
-
prompt = f"""
|
|
953
|
-
I need to determine if an original command should be skipped after a successful fix command.
|
|
954
|
-
|
|
955
|
-
Original command (failed): {original_command}
|
|
956
|
-
Fix command (succeeded): {fix_command}
|
|
957
|
-
|
|
958
|
-
Fix command stdout:
|
|
959
|
-
{fix_stdout}
|
|
960
|
-
|
|
961
|
-
Fix command stderr:
|
|
962
|
-
{fix_stderr}
|
|
963
|
-
|
|
964
|
-
Current command list:
|
|
965
|
-
{commands_context}
|
|
966
|
-
|
|
967
|
-
Based on this information, should I skip running the original command again?
|
|
968
|
-
Consider:
|
|
969
|
-
1. If the fix command already accomplished what the original command was trying to do
|
|
970
|
-
2. If running the original command again would be redundant or cause errors
|
|
971
|
-
3. If the original command is still necessary after the fix
|
|
972
|
-
|
|
973
|
-
Respond with ONLY:
|
|
974
|
-
SKIP: <reason>
|
|
975
|
-
or
|
|
976
|
-
RUN: <reason>
|
|
977
|
-
"""
|
|
978
|
-
|
|
979
|
-
# Call OpenAI API
|
|
980
|
-
import openai
|
|
981
|
-
client = openai.OpenAI(api_key=api_key)
|
|
982
|
-
|
|
983
|
-
print("๐ Analyzing if original command should be skipped...")
|
|
984
|
-
|
|
985
|
-
response = client.chat.completions.create(
|
|
986
|
-
model="gpt-3.5-turbo",
|
|
987
|
-
messages=[
|
|
988
|
-
{"role": "system", "content": "You are a helpful assistant that analyzes command execution."},
|
|
989
|
-
{"role": "user", "content": prompt}
|
|
990
|
-
],
|
|
991
|
-
max_tokens=100,
|
|
992
|
-
temperature=0.3
|
|
993
|
-
)
|
|
994
|
-
|
|
995
|
-
response_text = response.choices[0].message.content.strip()
|
|
996
|
-
|
|
997
|
-
# Parse the response
|
|
998
|
-
if response_text.startswith("SKIP:"):
|
|
999
|
-
reason = response_text.replace("SKIP:", "").strip()
|
|
1000
|
-
print(f"๐ LLM suggests skipping original command: {reason}")
|
|
1001
|
-
return True, reason
|
|
1002
|
-
elif response_text.startswith("RUN:"):
|
|
1003
|
-
reason = response_text.replace("RUN:", "").strip()
|
|
1004
|
-
print(f"๐ LLM suggests running original command: {reason}")
|
|
1005
|
-
return False, reason
|
|
1006
|
-
else:
|
|
1007
|
-
# Try to interpret a free-form response
|
|
1008
|
-
if "skip" in response_text.lower() and "should" in response_text.lower():
|
|
1009
|
-
print(f"๐ Interpreting response as SKIP: {response_text}")
|
|
1010
|
-
return True, response_text
|
|
1011
|
-
else:
|
|
1012
|
-
print(f"๐ Interpreting response as RUN: {response_text}")
|
|
1013
|
-
return False, response_text
|
|
1014
|
-
|
|
1015
|
-
except Exception as e:
|
|
1016
|
-
print(f"โ ๏ธ Error analyzing command skip decision: {e}")
|
|
1017
|
-
return False, f"Error: {e}"
|
|
1018
|
-
|
|
1019
|
-
def replace_command(self, command_index, new_command, reason=""):
|
|
1020
|
-
"""
|
|
1021
|
-
Replace a command in the list with a new command.
|
|
1022
|
-
|
|
1023
|
-
Args:
|
|
1024
|
-
command_index: The index of the command to replace
|
|
1025
|
-
new_command: The new command to use
|
|
1026
|
-
reason: The reason for the replacement
|
|
1027
|
-
|
|
1028
|
-
Returns:
|
|
1029
|
-
bool: True if the command was replaced, False otherwise
|
|
1030
|
-
"""
|
|
1031
|
-
if 0 <= command_index < len(self.commands):
|
|
1032
|
-
old_command = self.commands[command_index]['command']
|
|
1033
|
-
self.commands[command_index]['command'] = new_command
|
|
1034
|
-
self.commands[command_index]['status'] = 'pending' # Reset status
|
|
1035
|
-
self.commands[command_index]['stdout'] = ''
|
|
1036
|
-
self.commands[command_index]['stderr'] = ''
|
|
1037
|
-
self.commands[command_index]['execution_time'] = None
|
|
1038
|
-
self.commands[command_index]['replacement_reason'] = reason
|
|
1039
|
-
|
|
1040
|
-
print(f"๐ Replaced command {command_index + 1}: '{old_command}' with '{new_command}'")
|
|
1041
|
-
print(f"๐ Reason: {reason}")
|
|
1042
|
-
return True
|
|
1043
|
-
else:
|
|
1044
|
-
print(f"โ Invalid command index for replacement: {command_index}")
|
|
1045
|
-
return False
|
|
1046
|
-
|
|
1047
|
-
def update_command_list_with_llm(self, api_key=None):
|
|
1048
|
-
"""
|
|
1049
|
-
Use LLM to analyze and update the entire command list.
|
|
1050
|
-
|
|
1051
|
-
Args:
|
|
1052
|
-
api_key: OpenAI API key
|
|
1053
|
-
|
|
1054
|
-
Returns:
|
|
1055
|
-
bool: True if the list was updated, False otherwise
|
|
1056
|
-
"""
|
|
1057
|
-
try:
|
|
1058
|
-
# Get API key if not provided
|
|
1059
|
-
if not api_key:
|
|
1060
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
1061
|
-
if not api_key:
|
|
1062
|
-
# Try to load from saved file
|
|
1063
|
-
key_file = os.path.expanduser("~/.gitarsenal/openai_key")
|
|
1064
|
-
if os.path.exists(key_file):
|
|
1065
|
-
with open(key_file, "r") as f:
|
|
1066
|
-
api_key = f.read().strip()
|
|
1067
|
-
|
|
1068
|
-
if not api_key:
|
|
1069
|
-
print("โ ๏ธ No OpenAI API key available for command list analysis")
|
|
1070
|
-
return False
|
|
1071
|
-
|
|
1072
|
-
# Get all commands for context
|
|
1073
|
-
all_commands = self.get_all_commands()
|
|
1074
|
-
commands_context = "\n".join([f"{i+1}. {cmd['command']} - {cmd['status']}"
|
|
1075
|
-
for i, cmd in enumerate(all_commands)])
|
|
1076
|
-
|
|
1077
|
-
# Get executed commands with their outputs for context
|
|
1078
|
-
executed_context = ""
|
|
1079
|
-
for cmd in self.executed_commands:
|
|
1080
|
-
executed_context += f"Command: {cmd['command']}\n"
|
|
1081
|
-
executed_context += f"Status: {cmd['status']}\n"
|
|
1082
|
-
if cmd['stdout']:
|
|
1083
|
-
executed_context += f"Stdout: {cmd['stdout'][:500]}...\n" if len(cmd['stdout']) > 500 else f"Stdout: {cmd['stdout']}\n"
|
|
1084
|
-
if cmd['stderr']:
|
|
1085
|
-
executed_context += f"Stderr: {cmd['stderr'][:500]}...\n" if len(cmd['stderr']) > 500 else f"Stderr: {cmd['stderr']}\n"
|
|
1086
|
-
executed_context += "\n"
|
|
1087
|
-
|
|
1088
|
-
# Prepare the prompt
|
|
1089
|
-
prompt = f"""
|
|
1090
|
-
I need you to analyze and optimize this command list. Some commands have been executed,
|
|
1091
|
-
and some are still pending. Based on what has already been executed, I need you to:
|
|
1092
|
-
|
|
1093
|
-
1. Identify any pending commands that are now redundant or unnecessary
|
|
1094
|
-
2. Identify any pending commands that should be modified based on previous command results
|
|
1095
|
-
3. Suggest any new commands that should be added
|
|
1096
|
-
|
|
1097
|
-
Current command list:
|
|
1098
|
-
{commands_context}
|
|
1099
|
-
|
|
1100
|
-
Details of executed commands:
|
|
1101
|
-
{executed_context}
|
|
1102
|
-
|
|
1103
|
-
For each pending command (starting from the next command to be executed), tell me if it should be:
|
|
1104
|
-
1. KEEP: Keep the command as is
|
|
1105
|
-
2. SKIP: Skip the command (mark as completed without running)
|
|
1106
|
-
3. MODIFY: Modify the command (provide the new command)
|
|
1107
|
-
4. ADD_AFTER: Add a new command after this one
|
|
1108
|
-
|
|
1109
|
-
Format your response as a JSON array of actions:
|
|
1110
|
-
[
|
|
1111
|
-
{{
|
|
1112
|
-
"command_index": <index>,
|
|
1113
|
-
"action": "KEEP|SKIP|MODIFY|ADD_AFTER",
|
|
1114
|
-
"new_command": "<new command if MODIFY or ADD_AFTER>",
|
|
1115
|
-
"reason": "<reason for this action>"
|
|
1116
|
-
}},
|
|
1117
|
-
...
|
|
1118
|
-
]
|
|
1119
|
-
|
|
1120
|
-
Only include commands that need changes (SKIP, MODIFY, ADD_AFTER), not KEEP actions.
|
|
1121
|
-
"""
|
|
1122
|
-
|
|
1123
|
-
# Call OpenAI API
|
|
1124
|
-
import openai
|
|
1125
|
-
import json
|
|
1126
|
-
client = openai.OpenAI(api_key=api_key)
|
|
1127
|
-
|
|
1128
|
-
print("๐ Analyzing command list for optimizations...")
|
|
1129
|
-
|
|
1130
|
-
response = client.chat.completions.create(
|
|
1131
|
-
model="gpt-4o-mini", # Use a more capable model for this complex task
|
|
1132
|
-
messages=[
|
|
1133
|
-
{"role": "system", "content": "You are a helpful assistant that analyzes and optimizes command lists."},
|
|
1134
|
-
{"role": "user", "content": prompt}
|
|
1135
|
-
],
|
|
1136
|
-
max_tokens=1000,
|
|
1137
|
-
temperature=0.2
|
|
1138
|
-
)
|
|
1139
|
-
|
|
1140
|
-
response_text = response.choices[0].message.content.strip()
|
|
1141
|
-
|
|
1142
|
-
# Extract JSON from the response
|
|
1143
|
-
try:
|
|
1144
|
-
# Find JSON array in the response
|
|
1145
|
-
json_match = re.search(r'\[\s*\{.*\}\s*\]', response_text, re.DOTALL)
|
|
1146
|
-
if json_match:
|
|
1147
|
-
json_str = json_match.group(0)
|
|
1148
|
-
actions = json.loads(json_str)
|
|
1149
|
-
else:
|
|
1150
|
-
# Try to parse the entire response as JSON
|
|
1151
|
-
actions = json.loads(response_text)
|
|
1152
|
-
|
|
1153
|
-
if not isinstance(actions, list):
|
|
1154
|
-
print("โ Invalid response format from LLM - not a list")
|
|
1155
|
-
return False
|
|
1156
|
-
|
|
1157
|
-
# Apply the suggested changes
|
|
1158
|
-
changes_made = 0
|
|
1159
|
-
commands_added = 0
|
|
1160
|
-
|
|
1161
|
-
# Process in reverse order to avoid index shifting issues
|
|
1162
|
-
for action in sorted(actions, key=lambda x: x.get('command_index', 0), reverse=True):
|
|
1163
|
-
cmd_idx = action.get('command_index')
|
|
1164
|
-
action_type = action.get('action')
|
|
1165
|
-
new_cmd = action.get('new_command', '')
|
|
1166
|
-
reason = action.get('reason', 'No reason provided')
|
|
1167
|
-
|
|
1168
|
-
if cmd_idx is None or action_type is None:
|
|
1169
|
-
continue
|
|
1170
|
-
|
|
1171
|
-
# Convert to 0-based index if needed
|
|
1172
|
-
if cmd_idx > 0: # Assume 1-based index from LLM
|
|
1173
|
-
cmd_idx -= 1
|
|
1174
|
-
|
|
1175
|
-
# Skip if the command index is invalid
|
|
1176
|
-
if cmd_idx < 0 or cmd_idx >= len(self.commands):
|
|
1177
|
-
print(f"โ Invalid command index: {cmd_idx}")
|
|
1178
|
-
continue
|
|
1179
|
-
|
|
1180
|
-
# Skip if the command has already been executed
|
|
1181
|
-
if self.commands[cmd_idx]['status'] != 'pending':
|
|
1182
|
-
print(f"โ ๏ธ Command {cmd_idx + 1} already executed, skipping action")
|
|
1183
|
-
continue
|
|
1184
|
-
|
|
1185
|
-
if action_type == "SKIP":
|
|
1186
|
-
# Mark the command as successful without running it
|
|
1187
|
-
self.mark_command_executed(
|
|
1188
|
-
cmd_idx, 'main', True,
|
|
1189
|
-
f"Command skipped: {reason}",
|
|
1190
|
-
"", 0
|
|
1191
|
-
)
|
|
1192
|
-
print(f"๐ Skipped command {cmd_idx + 1}: {reason}")
|
|
1193
|
-
changes_made += 1
|
|
1194
|
-
|
|
1195
|
-
elif action_type == "MODIFY":
|
|
1196
|
-
if new_cmd:
|
|
1197
|
-
if self.replace_command(cmd_idx, new_cmd, reason):
|
|
1198
|
-
changes_made += 1
|
|
1199
|
-
else:
|
|
1200
|
-
print(f"โ No new command provided for MODIFY action on command {cmd_idx + 1}")
|
|
1201
|
-
|
|
1202
|
-
elif action_type == "ADD_AFTER":
|
|
1203
|
-
if new_cmd:
|
|
1204
|
-
# Add new command after the current one
|
|
1205
|
-
insert_idx = cmd_idx + 1
|
|
1206
|
-
new_cmd_obj = {
|
|
1207
|
-
'command': new_cmd,
|
|
1208
|
-
'status': 'pending',
|
|
1209
|
-
'index': insert_idx,
|
|
1210
|
-
'stdout': '',
|
|
1211
|
-
'stderr': '',
|
|
1212
|
-
'execution_time': None,
|
|
1213
|
-
'fix_attempts': 0,
|
|
1214
|
-
'max_fix_attempts': 3,
|
|
1215
|
-
'added_reason': reason
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
# Insert the new command
|
|
1219
|
-
self.commands.insert(insert_idx, new_cmd_obj)
|
|
1220
|
-
|
|
1221
|
-
# Update indices for all commands after insertion
|
|
1222
|
-
for i in range(insert_idx + 1, len(self.commands)):
|
|
1223
|
-
self.commands[i]['index'] = i
|
|
1224
|
-
|
|
1225
|
-
print(f"โ Added new command after {cmd_idx + 1}: '{new_cmd}'")
|
|
1226
|
-
print(f"๐ Reason: {reason}")
|
|
1227
|
-
commands_added += 1
|
|
1228
|
-
else:
|
|
1229
|
-
print(f"โ No new command provided for ADD_AFTER action on command {cmd_idx + 1}")
|
|
1230
|
-
|
|
1231
|
-
# Update total commands count
|
|
1232
|
-
self.total_commands = len(self.commands)
|
|
1233
|
-
|
|
1234
|
-
print(f"โ
Command list updated: {changes_made} changes made, {commands_added} commands added")
|
|
1235
|
-
return changes_made > 0 or commands_added > 0
|
|
1236
|
-
|
|
1237
|
-
except json.JSONDecodeError as e:
|
|
1238
|
-
print(f"โ Failed to parse LLM response as JSON: {e}")
|
|
1239
|
-
print(f"Raw response: {response_text}")
|
|
1240
|
-
return False
|
|
1241
|
-
except Exception as e:
|
|
1242
|
-
print(f"โ Error updating command list: {e}")
|
|
1243
|
-
return False
|
|
1244
|
-
|
|
1245
|
-
except Exception as e:
|
|
1246
|
-
print(f"โ ๏ธ Error analyzing command list: {e}")
|
|
1247
|
-
return False
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
# Import the fetch_modal_tokens module
|
|
1251
|
-
# print("๐ Fetching tokens from proxy server...")
|
|
1252
|
-
from fetch_modal_tokens import get_tokens
|
|
1253
|
-
token_id, token_secret, openai_api_key, _ = get_tokens()
|
|
1254
|
-
|
|
1255
|
-
# Check if we got valid tokens
|
|
1256
|
-
if token_id is None or token_secret is None:
|
|
1257
|
-
raise ValueError("Could not get valid tokens")
|
|
1258
|
-
|
|
1259
|
-
print(f"โ
Tokens fetched successfully")
|
|
1260
|
-
|
|
1261
|
-
# Explicitly set the environment variables again to be sure
|
|
1262
|
-
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
1263
|
-
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
1264
|
-
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
1265
|
-
# Also set the old environment variable for backward compatibility
|
|
1266
|
-
os.environ["MODAL_TOKEN"] = token_id
|
|
1267
|
-
|
|
1268
|
-
# Set token variables for later use
|
|
1269
|
-
token = token_id # For backward compatibility
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
def get_stored_credentials():
|
|
1273
|
-
"""Load stored credentials from ~/.gitarsenal/credentials.json"""
|
|
1274
|
-
import json
|
|
1275
|
-
from pathlib import Path
|
|
1276
|
-
|
|
1277
|
-
try:
|
|
1278
|
-
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
|
1279
|
-
if credentials_file.exists():
|
|
1280
|
-
with open(credentials_file, 'r') as f:
|
|
1281
|
-
credentials = json.load(f)
|
|
1282
|
-
return credentials
|
|
1283
|
-
else:
|
|
1284
|
-
return {}
|
|
1285
|
-
except Exception as e:
|
|
1286
|
-
print(f"โ ๏ธ Error loading stored credentials: {e}")
|
|
1287
|
-
return {}
|
|
1288
|
-
|
|
1289
|
-
def generate_auth_context(stored_credentials):
|
|
1290
|
-
"""Generate simple authentication context for the OpenAI prompt"""
|
|
1291
|
-
if not stored_credentials:
|
|
1292
|
-
return "No stored credentials available."
|
|
1293
|
-
|
|
1294
|
-
auth_context = "Available stored credentials (use actual values in commands):\n"
|
|
1295
|
-
|
|
1296
|
-
for key, value in stored_credentials.items():
|
|
1297
|
-
# Mask the actual value for security in logs, but provide the real value
|
|
1298
|
-
masked_value = value[:8] + "..." if len(value) > 8 else "***"
|
|
1299
|
-
auth_context += f"- {key}: {masked_value} (actual value: {value})\n"
|
|
1300
|
-
|
|
1301
|
-
return auth_context
|
|
1302
|
-
|
|
1303
|
-
def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
|
|
1304
|
-
"""Call OpenAI to debug a failed command and suggest a fix"""
|
|
1305
|
-
print("\n๐ DEBUG: Starting LLM debugging...")
|
|
1306
|
-
print(f"๐ DEBUG: Command: {command}")
|
|
1307
|
-
print(f"๐ DEBUG: Error output length: {len(error_output) if error_output else 0}")
|
|
1308
|
-
print(f"๐ DEBUG: Current directory: {current_dir}")
|
|
1309
|
-
print(f"๐ DEBUG: Sandbox available: {sandbox is not None}")
|
|
1310
|
-
|
|
1311
|
-
# Define _to_str function locally to avoid NameError
|
|
1312
|
-
def _to_str(maybe_bytes):
|
|
1313
|
-
try:
|
|
1314
|
-
return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
|
|
1315
|
-
except UnicodeDecodeError:
|
|
1316
|
-
# Handle non-UTF-8 bytes by replacing invalid characters
|
|
1317
|
-
if isinstance(maybe_bytes, (bytes, bytearray)):
|
|
1318
|
-
return maybe_bytes.decode('utf-8', errors='replace')
|
|
1319
|
-
else:
|
|
1320
|
-
return str(maybe_bytes)
|
|
1321
|
-
except Exception:
|
|
1322
|
-
# Last resort fallback
|
|
1323
|
-
return str(maybe_bytes)
|
|
1324
|
-
|
|
1325
|
-
# Skip debugging for certain commands that commonly return non-zero exit codes
|
|
1326
|
-
# but aren't actually errors (like test commands)
|
|
1327
|
-
if command.strip().startswith("test "):
|
|
1328
|
-
print("๐ Skipping debugging for test command - non-zero exit code is expected behavior")
|
|
1329
|
-
return None
|
|
1330
|
-
|
|
1331
|
-
# Validate error_output - if it's empty, we can't debug effectively
|
|
1332
|
-
if not error_output or not error_output.strip():
|
|
1333
|
-
print("โ ๏ธ Error output is empty. Cannot effectively debug the command.")
|
|
1334
|
-
print("โ ๏ธ Skipping OpenAI debugging due to lack of error information.")
|
|
1335
|
-
return None
|
|
1336
|
-
|
|
1337
|
-
# Try to get API key from multiple sources
|
|
1338
|
-
if not api_key:
|
|
1339
|
-
print("๐ DEBUG: No API key provided, searching for one...")
|
|
1340
|
-
|
|
1341
|
-
# First try environment variable
|
|
1342
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
1343
|
-
print(f"๐ DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
|
|
1344
|
-
if api_key:
|
|
1345
|
-
print(f"๐ DEBUG: Environment API key value: {api_key}")
|
|
1346
|
-
|
|
1347
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
1348
|
-
if not api_key:
|
|
1349
|
-
try:
|
|
1350
|
-
print("๐ DEBUG: Trying to fetch API key from server...")
|
|
1351
|
-
from fetch_modal_tokens import get_tokens
|
|
1352
|
-
_, _, api_key, _ = get_tokens()
|
|
1353
|
-
if api_key:
|
|
1354
|
-
# Set in environment for this session
|
|
1355
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
1356
|
-
else:
|
|
1357
|
-
print("โ ๏ธ Could not fetch OpenAI API key from server")
|
|
1358
|
-
except Exception as e:
|
|
1359
|
-
print(f"โ ๏ธ Error fetching API key from server: {e}")
|
|
1360
|
-
|
|
1361
|
-
# Store the API key in a persistent file if found
|
|
1362
|
-
if api_key:
|
|
1363
|
-
try:
|
|
1364
|
-
os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
|
|
1365
|
-
with open(os.path.expanduser("~/.gitarsenal/openai_key"), "w") as f:
|
|
1366
|
-
f.write(api_key)
|
|
1367
|
-
print("โ
Saved OpenAI API key for future use")
|
|
1368
|
-
except Exception as e:
|
|
1369
|
-
print(f"โ ๏ธ Could not save API key: {e}")
|
|
1370
|
-
|
|
1371
|
-
# Try to load from saved file if not in environment
|
|
1372
|
-
if not api_key:
|
|
1373
|
-
try:
|
|
1374
|
-
key_file = os.path.expanduser("~/.gitarsenal/openai_key")
|
|
1375
|
-
print(f"๐ DEBUG: Checking for saved API key at: {key_file}")
|
|
1376
|
-
if os.path.exists(key_file):
|
|
1377
|
-
with open(key_file, "r") as f:
|
|
1378
|
-
api_key = f.read().strip()
|
|
1379
|
-
if api_key:
|
|
1380
|
-
print("โ
Loaded OpenAI API key from saved file")
|
|
1381
|
-
print(f"๐ DEBUG: API key from file: {api_key}")
|
|
1382
|
-
print(f"๐ DEBUG: API key length: {len(api_key)}")
|
|
1383
|
-
# Also set in environment for this session
|
|
1384
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
1385
|
-
else:
|
|
1386
|
-
print("๐ DEBUG: Saved file exists but is empty")
|
|
1387
|
-
else:
|
|
1388
|
-
print("๐ DEBUG: No saved API key file found")
|
|
1389
|
-
except Exception as e:
|
|
1390
|
-
print(f"โ ๏ธ Could not load saved API key: {e}")
|
|
1391
|
-
|
|
1392
|
-
# Then try credentials manager
|
|
1393
|
-
if not api_key:
|
|
1394
|
-
print("๐ DEBUG: Trying credentials manager...")
|
|
1395
|
-
try:
|
|
1396
|
-
from credentials_manager import CredentialsManager
|
|
1397
|
-
credentials_manager = CredentialsManager()
|
|
1398
|
-
api_key = credentials_manager.get_openai_api_key()
|
|
1399
|
-
if api_key:
|
|
1400
|
-
print(f"๐ DEBUG: API key from credentials manager: Found")
|
|
1401
|
-
print(f"๐ DEBUG: Credentials manager API key value: {api_key}")
|
|
1402
|
-
# Set in environment for this session
|
|
1403
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
1404
|
-
else:
|
|
1405
|
-
print(f"๐ DEBUG: API key from credentials manager: Not found")
|
|
1406
|
-
except ImportError as e:
|
|
1407
|
-
print(f"๐ DEBUG: Credentials manager not available: {e}")
|
|
1408
|
-
# Fall back to direct input if credentials_manager is not available
|
|
1409
|
-
pass
|
|
1410
|
-
|
|
1411
|
-
# Finally, prompt the user if still no API key
|
|
1412
|
-
if not api_key:
|
|
1413
|
-
print("๐ DEBUG: No API key found in any source, prompting user...")
|
|
1414
|
-
print("\n" + "="*60)
|
|
1415
|
-
print("๐ OPENAI API KEY REQUIRED FOR DEBUGGING")
|
|
1416
|
-
print("="*60)
|
|
1417
|
-
print("To debug failed commands, an OpenAI API key is needed.")
|
|
1418
|
-
print("๐ Please paste your OpenAI API key below:")
|
|
1419
|
-
print(" (Your input will be hidden for security)")
|
|
1420
|
-
print("-" * 60)
|
|
1421
|
-
|
|
1422
|
-
try:
|
|
1423
|
-
api_key = getpass.getpass("OpenAI API Key: ").strip()
|
|
1424
|
-
if not api_key:
|
|
1425
|
-
print("โ No API key provided. Skipping debugging.")
|
|
1426
|
-
return None
|
|
1427
|
-
print("โ
API key received successfully!")
|
|
1428
|
-
print(f"๐ DEBUG: User-provided API key: {api_key}")
|
|
1429
|
-
# Save the API key to environment for future use in this session
|
|
1430
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
1431
|
-
except KeyboardInterrupt:
|
|
1432
|
-
print("\nโ API key input cancelled by user.")
|
|
1433
|
-
return None
|
|
1434
|
-
except Exception as e:
|
|
1435
|
-
print(f"โ Error getting API key: {e}")
|
|
1436
|
-
return None
|
|
1437
|
-
|
|
1438
|
-
# If we still don't have an API key, we can't proceed
|
|
1439
|
-
if not api_key:
|
|
1440
|
-
print("โ No OpenAI API key available. Cannot perform LLM debugging.")
|
|
1441
|
-
print("๐ก To enable LLM debugging, set the OPENAI_API_KEY environment variable")
|
|
1442
|
-
return None
|
|
1443
|
-
|
|
1444
|
-
# print(f"โ
OpenAI API key available (length: {len(api_key)})")
|
|
1445
|
-
|
|
1446
|
-
# Gather additional context to help with debugging
|
|
1447
|
-
directory_context = ""
|
|
1448
|
-
system_info = ""
|
|
1449
|
-
command_history = ""
|
|
1450
|
-
file_context = ""
|
|
1451
|
-
|
|
1452
|
-
if sandbox:
|
|
1453
|
-
try:
|
|
1454
|
-
print("๐ Getting system information for better debugging...")
|
|
1455
|
-
|
|
1456
|
-
# Get OS information
|
|
1457
|
-
os_info_cmd = """
|
|
1458
|
-
echo "OS Information:"
|
|
1459
|
-
cat /etc/os-release 2>/dev/null || echo "OS release info not available"
|
|
1460
|
-
echo -e "\nKernel Information:"
|
|
1461
|
-
uname -a
|
|
1462
|
-
echo -e "\nPython Information:"
|
|
1463
|
-
python --version
|
|
1464
|
-
pip --version
|
|
1465
|
-
echo -e "\nPackage Manager:"
|
|
1466
|
-
which apt 2>/dev/null && echo "apt available" || echo "apt not available"
|
|
1467
|
-
which yum 2>/dev/null && echo "yum available" || echo "yum not available"
|
|
1468
|
-
which dnf 2>/dev/null && echo "dnf available" || echo "dnf not available"
|
|
1469
|
-
which apk 2>/dev/null && echo "apk available" || echo "apk not available"
|
|
1470
|
-
echo -e "\nEnvironment Variables:"
|
|
1471
|
-
env | grep -E "^(PATH|PYTHON|VIRTUAL_ENV|HOME|USER|SHELL|LANG)" || echo "No relevant env vars found"
|
|
1472
|
-
"""
|
|
1473
|
-
|
|
1474
|
-
os_result = sandbox.exec("bash", "-c", os_info_cmd)
|
|
1475
|
-
os_output = ""
|
|
1476
|
-
for line in os_result.stdout:
|
|
1477
|
-
os_output += _to_str(line)
|
|
1478
|
-
os_result.wait()
|
|
1479
|
-
|
|
1480
|
-
system_info = f"""
|
|
1481
|
-
System Information:
|
|
1482
|
-
{os_output}
|
|
1483
|
-
"""
|
|
1484
|
-
print("โ
System information gathered successfully")
|
|
1485
|
-
except Exception as e:
|
|
1486
|
-
print(f"โ ๏ธ Error getting system information: {e}")
|
|
1487
|
-
system_info = "System information not available\n"
|
|
1488
|
-
|
|
1489
|
-
if current_dir and sandbox:
|
|
1490
|
-
try:
|
|
1491
|
-
# print("๐ Getting directory context for better debugging...")
|
|
1492
|
-
|
|
1493
|
-
# Get current directory contents
|
|
1494
|
-
ls_result = sandbox.exec("bash", "-c", "ls -la")
|
|
1495
|
-
ls_output = ""
|
|
1496
|
-
for line in ls_result.stdout:
|
|
1497
|
-
ls_output += _to_str(line)
|
|
1498
|
-
ls_result.wait()
|
|
1499
|
-
|
|
1500
|
-
# Get parent directory contents
|
|
1501
|
-
parent_result = sandbox.exec("bash", "-c", "ls -la ../")
|
|
1502
|
-
parent_ls = ""
|
|
1503
|
-
for line in parent_result.stdout:
|
|
1504
|
-
parent_ls += _to_str(line)
|
|
1505
|
-
parent_result.wait()
|
|
1506
|
-
|
|
1507
|
-
directory_context = f"""
|
|
1508
|
-
Current directory contents:
|
|
1509
|
-
{ls_output}
|
|
1510
|
-
|
|
1511
|
-
Parent directory contents:
|
|
1512
|
-
{parent_ls}
|
|
1513
|
-
"""
|
|
1514
|
-
print("โ
Directory context gathered successfully")
|
|
1515
|
-
|
|
1516
|
-
# Check for relevant files that might provide additional context
|
|
1517
|
-
# For example, if error mentions a specific file, try to get its content
|
|
1518
|
-
relevant_files = []
|
|
1519
|
-
error_files = re.findall(r'(?:No such file or directory|cannot open|not found): ([^\s:]+)', error_output)
|
|
1520
|
-
if error_files:
|
|
1521
|
-
for file_path in error_files:
|
|
1522
|
-
# Clean up the file path
|
|
1523
|
-
file_path = file_path.strip("'\"")
|
|
1524
|
-
if not os.path.isabs(file_path):
|
|
1525
|
-
file_path = os.path.join(current_dir, file_path)
|
|
1526
|
-
|
|
1527
|
-
# Try to get the parent directory if the file doesn't exist
|
|
1528
|
-
if '/' in file_path:
|
|
1529
|
-
parent_file_dir = os.path.dirname(file_path)
|
|
1530
|
-
relevant_files.append(parent_file_dir)
|
|
1531
|
-
|
|
1532
|
-
# Look for package.json, requirements.txt, etc.
|
|
1533
|
-
common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py",
|
|
1534
|
-
"Pipfile", "Dockerfile", "docker-compose.yml", "Makefile"]
|
|
1535
|
-
|
|
1536
|
-
for config_file in common_config_files:
|
|
1537
|
-
check_cmd = f"test -f {current_dir}/{config_file}"
|
|
1538
|
-
check_result = sandbox.exec("bash", "-c", check_cmd)
|
|
1539
|
-
check_result.wait()
|
|
1540
|
-
if check_result.returncode == 0:
|
|
1541
|
-
relevant_files.append(f"{current_dir}/{config_file}")
|
|
1542
|
-
|
|
1543
|
-
# Get content of relevant files
|
|
1544
|
-
if relevant_files:
|
|
1545
|
-
file_context = "\nRelevant file contents:\n"
|
|
1546
|
-
for file_path in relevant_files[:2]: # Limit to 2 files to avoid too much context
|
|
1547
|
-
try:
|
|
1548
|
-
file_check_cmd = f"test -f {file_path}"
|
|
1549
|
-
file_check = sandbox.exec("bash", "-c", file_check_cmd)
|
|
1550
|
-
file_check.wait()
|
|
1551
|
-
|
|
1552
|
-
if file_check.returncode == 0:
|
|
1553
|
-
# It's a file, get its content
|
|
1554
|
-
cat_cmd = f"cat {file_path}"
|
|
1555
|
-
cat_result = sandbox.exec("bash", "-c", cat_cmd)
|
|
1556
|
-
file_content = ""
|
|
1557
|
-
for line in cat_result.stdout:
|
|
1558
|
-
file_content += _to_str(line)
|
|
1559
|
-
cat_result.wait()
|
|
1560
|
-
|
|
1561
|
-
# Truncate if too long
|
|
1562
|
-
if len(file_content) > 1000:
|
|
1563
|
-
file_content = file_content[:1000] + "\n... (truncated)"
|
|
1564
|
-
|
|
1565
|
-
file_context += f"\n--- {file_path} ---\n{file_content}\n"
|
|
1566
|
-
else:
|
|
1567
|
-
# It's a directory, list its contents
|
|
1568
|
-
ls_cmd = f"ls -la {file_path}"
|
|
1569
|
-
ls_dir_result = sandbox.exec("bash", "-c", ls_cmd)
|
|
1570
|
-
dir_content = ""
|
|
1571
|
-
for line in ls_dir_result.stdout:
|
|
1572
|
-
dir_content += _to_str(line)
|
|
1573
|
-
ls_dir_result.wait()
|
|
1574
|
-
|
|
1575
|
-
file_context += f"\n--- Directory: {file_path} ---\n{dir_content}\n"
|
|
1576
|
-
except Exception as e:
|
|
1577
|
-
print(f"โ ๏ธ Error getting content of {file_path}: {e}")
|
|
1578
|
-
|
|
1579
|
-
# print(f"โ
Additional file context gathered from {len(relevant_files)} relevant files")
|
|
1580
|
-
|
|
1581
|
-
except Exception as e:
|
|
1582
|
-
print(f"โ ๏ธ Error getting directory context: {e}")
|
|
1583
|
-
directory_context = f"\nCurrent directory: {current_dir}\n"
|
|
1584
|
-
|
|
1585
|
-
# Prepare the API request
|
|
1586
|
-
headers = {
|
|
1587
|
-
"Content-Type": "application/json",
|
|
1588
|
-
"Authorization": f"Bearer {api_key}"
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
stored_credentials = get_stored_credentials()
|
|
1592
|
-
auth_context = generate_auth_context(stored_credentials)
|
|
1593
|
-
|
|
1594
|
-
# Create a prompt for the LLM
|
|
1595
|
-
print("\n" + "="*60)
|
|
1596
|
-
print("DEBUG: ERROR_OUTPUT SENT TO LLM:")
|
|
1597
|
-
print("="*60)
|
|
1598
|
-
print(f"{error_output}")
|
|
1599
|
-
print("="*60 + "\n")
|
|
1600
|
-
|
|
1601
|
-
prompt = f"""
|
|
1602
|
-
I'm trying to run the following command in a Linux environment:
|
|
1603
|
-
|
|
1604
|
-
```
|
|
1605
|
-
{command}
|
|
1606
|
-
```
|
|
1607
|
-
|
|
1608
|
-
But it failed with this error:
|
|
1609
|
-
|
|
1610
|
-
```
|
|
1611
|
-
{error_output}
|
|
1612
|
-
```
|
|
1613
|
-
{system_info}
|
|
1614
|
-
{directory_context}
|
|
1615
|
-
{file_context}
|
|
1616
|
-
|
|
1617
|
-
AVAILABLE CREDENTIALS:
|
|
1618
|
-
{auth_context}
|
|
1619
|
-
|
|
1620
|
-
Please analyze the error and provide ONLY a single terminal command that would fix the issue.
|
|
1621
|
-
Consider the current directory, system information, directory contents, and available credentials carefully before suggesting a solution.
|
|
1622
|
-
|
|
1623
|
-
IMPORTANT GUIDELINES:
|
|
1624
|
-
1. For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
|
|
1625
|
-
- For apt/apt-get: use -y or --yes
|
|
1626
|
-
- For rm: use -f or --force
|
|
1627
|
-
|
|
1628
|
-
2. If the error indicates a file is not found:
|
|
1629
|
-
- FIRST try to search for the file using: find . -name "filename" -type f 2>/dev/null
|
|
1630
|
-
- If found, navigate to that directory using: cd /path/to/directory
|
|
1631
|
-
- If not found, then consider creating the file or installing missing packages
|
|
1632
|
-
|
|
1633
|
-
3. For missing packages or dependencies:
|
|
1634
|
-
- Use pip install for Python packages
|
|
1635
|
-
- Use apt-get install -y for system packages
|
|
1636
|
-
- Use npm install for Node.js packages
|
|
1637
|
-
|
|
1638
|
-
4. For authentication issues:
|
|
1639
|
-
- Analyze the error to determine what type of authentication is needed
|
|
1640
|
-
- ALWAYS use the actual credential values from the AVAILABLE CREDENTIALS section above (NOT placeholders)
|
|
1641
|
-
- Look for the specific API key or token needed in the auth_context and use its exact value
|
|
1642
|
-
- Common patterns:
|
|
1643
|
-
* wandb errors: use wandb login with the actual WANDB_API_KEY value from auth_context
|
|
1644
|
-
* huggingface errors: use huggingface-cli login with the actual HF_TOKEN or HUGGINGFACE_TOKEN value from auth_context
|
|
1645
|
-
* github errors: configure git credentials with the actual GITHUB_TOKEN value from auth_context
|
|
1646
|
-
* kaggle errors: create ~/.kaggle/kaggle.json with the actual KAGGLE_USERNAME and KAGGLE_KEY values from auth_context
|
|
1647
|
-
* API errors: export the appropriate API key as environment variable using the actual value from auth_context
|
|
1648
|
-
|
|
1649
|
-
5. Environment variable exports:
|
|
1650
|
-
- Use export commands for API keys that need to be in environment
|
|
1651
|
-
- ALWAYS use the actual credential values from auth_context, never use placeholders like "YOUR_API_KEY"
|
|
1652
|
-
- Example: export OPENAI_API_KEY="sk-..." (using the actual key from auth_context)
|
|
1653
|
-
|
|
1654
|
-
6. CRITICAL: When using any API key, token, or credential:
|
|
1655
|
-
- Find the exact value in the AVAILABLE CREDENTIALS section
|
|
1656
|
-
- Use that exact value in your command
|
|
1657
|
-
- Do not use generic placeholders or dummy values
|
|
1658
|
-
- The auth_context contains real, usable credentials
|
|
1659
|
-
|
|
1660
|
-
7. For Git SSH authentication failures:
|
|
1661
|
-
- If the error contains "Host key verification failed" or "Could not read from remote repository"
|
|
1662
|
-
- ALWAYS convert SSH URLs to HTTPS URLs for public repositories
|
|
1663
|
-
- Replace git@github.com:username/repo.git with https://github.com/username/repo.git
|
|
1664
|
-
- This works for public repositories without authentication
|
|
1665
|
-
- Example: git clone https://github.com/xg-chu/ARTalk.git
|
|
1666
|
-
|
|
1667
|
-
Do not provide any explanations, just the exact command to run.
|
|
1668
|
-
"""
|
|
1669
|
-
|
|
1670
|
-
# Prepare the API request payload
|
|
1671
|
-
# print("๐ DEBUG: Preparing API request...")
|
|
1672
|
-
|
|
1673
|
-
# Try to use GPT-4 first, but fall back to other models if needed
|
|
1674
|
-
models_to_try = [
|
|
1675
|
-
"gpt-4o-mini", # First choice: GPT-4o (most widely available)
|
|
1676
|
-
]
|
|
1677
|
-
|
|
1678
|
-
# Check if we have a preferred model in environment
|
|
1679
|
-
preferred_model = os.environ.get("OPENAI_MODEL")
|
|
1680
|
-
if preferred_model:
|
|
1681
|
-
# Insert the preferred model at the beginning of the list
|
|
1682
|
-
models_to_try.insert(0, preferred_model)
|
|
1683
|
-
# print(f"โ
Using preferred model from environment: {preferred_model}")
|
|
1684
|
-
|
|
1685
|
-
# Remove duplicates while preserving order
|
|
1686
|
-
models_to_try = list(dict.fromkeys(models_to_try))
|
|
1687
|
-
# print(f"๐ DEBUG: Models to try: {models_to_try}")
|
|
1688
|
-
|
|
1689
|
-
# Function to make the API call with a specific model
|
|
1690
|
-
def try_api_call(model_name, retries=2, backoff_factor=1.5):
|
|
1691
|
-
# print(f"๐ DEBUG: Attempting API call with model: {model_name}")
|
|
1692
|
-
# print(f"๐ DEBUG: API key available: {'Yes' if api_key else 'No'}")
|
|
1693
|
-
# if api_key:
|
|
1694
|
-
# print(f"๐ DEBUG: API key length: {len(api_key)}")
|
|
1695
|
-
# print(f"๐ DEBUG: API key starts with: {api_key[:10]}...")
|
|
1696
|
-
|
|
1697
|
-
payload = {
|
|
1698
|
-
"model": model_name,
|
|
1699
|
-
"messages": [
|
|
1700
|
-
{"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue. Analyze the issue first, understand why it's happening, then provide the command to fix it. For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found. For missing packages, use appropriate package managers (pip, apt-get, npm). For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git). For authentication, suggest login commands with placeholders."},
|
|
1701
|
-
{"role": "user", "content": prompt}
|
|
1702
|
-
],
|
|
1703
|
-
"temperature": 0.2,
|
|
1704
|
-
"max_tokens": 300
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
print(f"๐ DEBUG: Payload prepared, prompt length: {len(prompt)}")
|
|
1708
|
-
|
|
1709
|
-
# Add specific handling for common errors
|
|
1710
|
-
last_error = None
|
|
1711
|
-
for attempt in range(retries + 1):
|
|
1712
|
-
try:
|
|
1713
|
-
if attempt > 0:
|
|
1714
|
-
# Exponential backoff
|
|
1715
|
-
wait_time = backoff_factor * (2 ** (attempt - 1))
|
|
1716
|
-
print(f"โฑ๏ธ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
|
|
1717
|
-
time.sleep(wait_time)
|
|
1718
|
-
|
|
1719
|
-
print(f"๐ค Calling OpenAI with {model_name} model to debug the failed command...")
|
|
1720
|
-
print(f"๐ DEBUG: Making POST request to OpenAI API...")
|
|
1721
|
-
response = requests.post(
|
|
1722
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1723
|
-
headers=headers,
|
|
1724
|
-
json=payload,
|
|
1725
|
-
timeout=45 # Increased timeout for reliability
|
|
1726
|
-
)
|
|
1727
|
-
|
|
1728
|
-
print(f"๐ DEBUG: Response received, status code: {response.status_code}")
|
|
1729
|
-
|
|
1730
|
-
# Handle specific status codes
|
|
1731
|
-
if response.status_code == 200:
|
|
1732
|
-
print(f"๐ DEBUG: Success! Response length: {len(response.text)}")
|
|
1733
|
-
return response.json(), None
|
|
1734
|
-
elif response.status_code == 401:
|
|
1735
|
-
error_msg = "Authentication error: Invalid API key"
|
|
1736
|
-
print(f"โ {error_msg}")
|
|
1737
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
1738
|
-
# Don't retry auth errors
|
|
1739
|
-
return None, error_msg
|
|
1740
|
-
elif response.status_code == 429:
|
|
1741
|
-
error_msg = "Rate limit exceeded or quota reached"
|
|
1742
|
-
print(f"โ ๏ธ {error_msg}")
|
|
1743
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
1744
|
-
# Always retry rate limit errors with increasing backoff
|
|
1745
|
-
last_error = error_msg
|
|
1746
|
-
continue
|
|
1747
|
-
elif response.status_code == 500:
|
|
1748
|
-
error_msg = "OpenAI server error"
|
|
1749
|
-
print(f"โ ๏ธ {error_msg}")
|
|
1750
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
1751
|
-
# Retry server errors
|
|
1752
|
-
last_error = error_msg
|
|
1753
|
-
continue
|
|
1754
|
-
else:
|
|
1755
|
-
error_msg = f"Status code: {response.status_code}, Response: {response.text}"
|
|
1756
|
-
print(f"โ ๏ธ OpenAI API error: {error_msg}")
|
|
1757
|
-
print(f"๐ DEBUG: Full response text: {response.text}")
|
|
1758
|
-
last_error = error_msg
|
|
1759
|
-
# Only retry if we have attempts left
|
|
1760
|
-
if attempt < retries:
|
|
1761
|
-
continue
|
|
1762
|
-
return None, error_msg
|
|
1763
|
-
except requests.exceptions.Timeout:
|
|
1764
|
-
error_msg = "Request timed out"
|
|
1765
|
-
# print(f"โ ๏ธ {error_msg}")
|
|
1766
|
-
# print(f"๐ DEBUG: Timeout after 45 seconds")
|
|
1767
|
-
last_error = error_msg
|
|
1768
|
-
# Always retry timeouts
|
|
1769
|
-
continue
|
|
1770
|
-
except requests.exceptions.ConnectionError:
|
|
1771
|
-
error_msg = "Connection error"
|
|
1772
|
-
print(f"โ ๏ธ {error_msg}")
|
|
1773
|
-
print(f"๐ DEBUG: Connection failed to api.openai.com")
|
|
1774
|
-
last_error = error_msg
|
|
1775
|
-
# Always retry connection errors
|
|
1776
|
-
continue
|
|
1777
|
-
except Exception as e:
|
|
1778
|
-
error_msg = str(e)
|
|
1779
|
-
print(f"โ ๏ธ Unexpected error: {error_msg}")
|
|
1780
|
-
print(f"๐ DEBUG: Exception type: {type(e).__name__}")
|
|
1781
|
-
print(f"๐ DEBUG: Exception details: {str(e)}")
|
|
1782
|
-
last_error = error_msg
|
|
1783
|
-
# Only retry if we have attempts left
|
|
1784
|
-
if attempt < retries:
|
|
1785
|
-
continue
|
|
1786
|
-
return None, error_msg
|
|
1787
|
-
|
|
1788
|
-
# If we get here, all retries failed
|
|
1789
|
-
return None, last_error
|
|
1790
|
-
|
|
1791
|
-
# Try each model in sequence until one works
|
|
1792
|
-
result = None
|
|
1793
|
-
last_error = None
|
|
1794
|
-
|
|
1795
|
-
for model in models_to_try:
|
|
1796
|
-
result, error = try_api_call(model)
|
|
1797
|
-
if result:
|
|
1798
|
-
# print(f"โ
Successfully got response from {model}")
|
|
1799
|
-
break
|
|
1800
|
-
else:
|
|
1801
|
-
print(f"โ ๏ธ Failed to get response from {model}: {error}")
|
|
1802
|
-
last_error = error
|
|
1803
|
-
|
|
1804
|
-
if not result:
|
|
1805
|
-
print(f"โ All model attempts failed. Last error: {last_error}")
|
|
1806
|
-
return None
|
|
1807
|
-
|
|
1808
|
-
# Process the response
|
|
1809
|
-
try:
|
|
1810
|
-
print(f"๐ DEBUG: Processing OpenAI response...")
|
|
1811
|
-
# print(f"๐ DEBUG: Response structure: {list(result.keys())}")
|
|
1812
|
-
print(f"๐ DEBUG: Choices count: {len(result.get('choices', []))}")
|
|
1813
|
-
|
|
1814
|
-
fix_command = result["choices"][0]["message"]["content"].strip()
|
|
1815
|
-
print(f"๐ DEBUG: Raw response content: {fix_command}")
|
|
1816
|
-
|
|
1817
|
-
# Save the original response for debugging
|
|
1818
|
-
original_response = fix_command
|
|
1819
|
-
|
|
1820
|
-
# Extract just the command if it's wrapped in backticks or explanation
|
|
1821
|
-
if "```" in fix_command:
|
|
1822
|
-
# Extract content between backticks
|
|
1823
|
-
import re
|
|
1824
|
-
code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
|
|
1825
|
-
if code_blocks:
|
|
1826
|
-
fix_command = code_blocks[0].strip()
|
|
1827
|
-
print(f"โ
Extracted command from code block: {fix_command}")
|
|
1828
|
-
|
|
1829
|
-
# If the response still has explanatory text, try to extract just the command
|
|
1830
|
-
if len(fix_command.split('\n')) > 1:
|
|
1831
|
-
# First try to find lines that look like commands (start with common command prefixes)
|
|
1832
|
-
command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
|
|
1833
|
-
'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
|
|
1834
|
-
'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
|
|
1835
|
-
'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
|
|
1836
|
-
|
|
1837
|
-
# Check for lines that start with common command prefixes
|
|
1838
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
|
1839
|
-
if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
|
|
1840
|
-
|
|
1841
|
-
if command_lines:
|
|
1842
|
-
# Use the first command line found
|
|
1843
|
-
fix_command = command_lines[0]
|
|
1844
|
-
print(f"โ
Identified command by prefix: {fix_command}")
|
|
1845
|
-
else:
|
|
1846
|
-
# Try to find lines that look like commands (contain common shell patterns)
|
|
1847
|
-
shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
|
|
1848
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
|
1849
|
-
if any(pattern in line for pattern in shell_patterns)]
|
|
1850
|
-
|
|
1851
|
-
if command_lines:
|
|
1852
|
-
# Use the first command line found
|
|
1853
|
-
fix_command = command_lines[0]
|
|
1854
|
-
print(f"โ
Identified command by shell pattern: {fix_command}")
|
|
1855
|
-
else:
|
|
1856
|
-
# Fall back to the shortest non-empty line as it's likely the command
|
|
1857
|
-
lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
|
|
1858
|
-
if lines:
|
|
1859
|
-
# Exclude very short lines that are likely not commands
|
|
1860
|
-
valid_lines = [line for line in lines if len(line) > 5]
|
|
1861
|
-
if valid_lines:
|
|
1862
|
-
fix_command = min(valid_lines, key=len)
|
|
1863
|
-
else:
|
|
1864
|
-
fix_command = min(lines, key=len)
|
|
1865
|
-
print(f"โ
Selected shortest line as command: {fix_command}")
|
|
1866
|
-
|
|
1867
|
-
# Clean up the command - remove any trailing periods or quotes
|
|
1868
|
-
fix_command = fix_command.rstrip('.;"\'')
|
|
1869
|
-
|
|
1870
|
-
# Remove common prefixes that LLMs sometimes add
|
|
1871
|
-
prefixes_to_remove = [
|
|
1872
|
-
"Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
|
|
1873
|
-
"You should run: ", "You can run: ", "You need to run: "
|
|
1874
|
-
]
|
|
1875
|
-
for prefix in prefixes_to_remove:
|
|
1876
|
-
if fix_command.startswith(prefix):
|
|
1877
|
-
fix_command = fix_command[len(prefix):].strip()
|
|
1878
|
-
print(f"โ
Removed prefix: {prefix}")
|
|
1879
|
-
break
|
|
1880
|
-
|
|
1881
|
-
# If the command is still multi-line or very long, it might not be a valid command
|
|
1882
|
-
if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
|
|
1883
|
-
print("โ ๏ธ Extracted command appears invalid (multi-line or too long)")
|
|
1884
|
-
print("๐ Original response from LLM:")
|
|
1885
|
-
print("-" * 60)
|
|
1886
|
-
print(original_response)
|
|
1887
|
-
print("-" * 60)
|
|
1888
|
-
print("โ ๏ธ Using best guess for command")
|
|
1889
|
-
|
|
1890
|
-
print(f"๐ง Suggested fix: {fix_command}")
|
|
1891
|
-
print(f"๐ DEBUG: Returning fix command: {fix_command}")
|
|
1892
|
-
return fix_command
|
|
1893
|
-
except Exception as e:
|
|
1894
|
-
print(f"โ Error processing OpenAI response: {e}")
|
|
1895
|
-
print(f"๐ DEBUG: Exception type: {type(e).__name__}")
|
|
1896
|
-
print(f"๐ DEBUG: Exception details: {str(e)}")
|
|
1897
|
-
return None
|
|
1898
|
-
|
|
1899
|
-
def call_openai_for_batch_debug(failed_commands, api_key=None, current_dir=None, sandbox=None):
|
|
1900
|
-
"""Call OpenAI to debug multiple failed commands and suggest fixes for all of them at once"""
|
|
1901
|
-
print("\n๐ DEBUG: Starting batch LLM debugging...")
|
|
1902
|
-
print(f"๐ DEBUG: Analyzing {len(failed_commands)} failed commands")
|
|
1903
|
-
|
|
1904
|
-
if not failed_commands:
|
|
1905
|
-
print("โ ๏ธ No failed commands to analyze")
|
|
1906
|
-
return []
|
|
1907
|
-
|
|
1908
|
-
if not api_key:
|
|
1909
|
-
print("โ No OpenAI API key provided for batch debugging")
|
|
1910
|
-
return []
|
|
1911
|
-
|
|
1912
|
-
# Prepare context for batch analysis
|
|
1913
|
-
context_parts = []
|
|
1914
|
-
context_parts.append(f"Current directory: {current_dir}")
|
|
1915
|
-
context_parts.append(f"Sandbox available: {sandbox is not None}")
|
|
1916
|
-
|
|
1917
|
-
# Add failed commands with their errors
|
|
1918
|
-
for i, failed_cmd in enumerate(failed_commands, 1):
|
|
1919
|
-
cmd_type = failed_cmd.get('type', 'main')
|
|
1920
|
-
original_cmd = failed_cmd.get('original_command', '')
|
|
1921
|
-
cmd_text = failed_cmd['command']
|
|
1922
|
-
stderr = failed_cmd.get('stderr', '')
|
|
1923
|
-
stdout = failed_cmd.get('stdout', '')
|
|
1924
|
-
|
|
1925
|
-
context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
|
|
1926
|
-
context_parts.append(f"Command: {cmd_text}")
|
|
1927
|
-
if original_cmd and original_cmd != cmd_text:
|
|
1928
|
-
context_parts.append(f"Original Command: {original_cmd}")
|
|
1929
|
-
if stderr:
|
|
1930
|
-
context_parts.append(f"Error Output: {stderr}")
|
|
1931
|
-
if stdout:
|
|
1932
|
-
context_parts.append(f"Standard Output: {stdout}")
|
|
1933
|
-
|
|
1934
|
-
# Create the prompt for batch analysis
|
|
1935
|
-
prompt = f"""You are a debugging assistant analyzing multiple failed commands.
|
|
1936
|
-
|
|
1937
|
-
Context:
|
|
1938
|
-
{chr(10).join(context_parts)}
|
|
1939
|
-
|
|
1940
|
-
Please analyze each failed command and provide a fix command for each one. For each failed command, respond with:
|
|
1941
|
-
|
|
1942
|
-
FIX_COMMAND_{i}: <the fix command>
|
|
1943
|
-
REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
|
|
1944
|
-
|
|
1945
|
-
Guidelines:
|
|
1946
|
-
- For file not found errors, first search for the file using 'find . -name filename -type f'
|
|
1947
|
-
- For missing packages, use appropriate package managers (pip, apt-get, npm)
|
|
1948
|
-
- For Git SSH authentication failures, convert SSH URLs to HTTPS URLs
|
|
1949
|
-
- For permission errors, suggest commands with sudo if appropriate
|
|
1950
|
-
- For network issues, suggest retry commands or alternative URLs
|
|
1951
|
-
- Keep each fix command simple and focused on the specific error
|
|
1952
|
-
|
|
1953
|
-
Provide fixes for all {len(failed_commands)} failed commands:"""
|
|
1954
|
-
|
|
1955
|
-
# Make the API call
|
|
1956
|
-
headers = {
|
|
1957
|
-
"Authorization": f"Bearer {api_key}",
|
|
1958
|
-
"Content-Type": "application/json"
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
payload = {
|
|
1962
|
-
"model": "gpt-4o-mini", # Use a more capable model for batch analysis
|
|
1963
|
-
"messages": [
|
|
1964
|
-
{"role": "system", "content": "You are a debugging assistant. Analyze failed commands and provide specific fix commands. Return only the fix commands and reasons in the specified format."},
|
|
1965
|
-
{"role": "user", "content": prompt}
|
|
1966
|
-
],
|
|
1967
|
-
"temperature": 0.1,
|
|
1968
|
-
"max_tokens": 1000
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
try:
|
|
1972
|
-
print(f"๐ค Calling OpenAI for batch debugging of {len(failed_commands)} commands...")
|
|
1973
|
-
response = requests.post(
|
|
1974
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1975
|
-
headers=headers,
|
|
1976
|
-
json=payload,
|
|
1977
|
-
timeout=60
|
|
1978
|
-
)
|
|
1979
|
-
|
|
1980
|
-
if response.status_code == 200:
|
|
1981
|
-
result = response.json()
|
|
1982
|
-
content = result['choices'][0]['message']['content']
|
|
1983
|
-
print(f"โ
Batch analysis completed")
|
|
1984
|
-
|
|
1985
|
-
# Parse the response to extract fix commands
|
|
1986
|
-
fixes = []
|
|
1987
|
-
for i in range(1, len(failed_commands) + 1):
|
|
1988
|
-
fix_pattern = f"FIX_COMMAND_{i}: (.+)"
|
|
1989
|
-
reason_pattern = f"REASON_{i}: (.+)"
|
|
1990
|
-
|
|
1991
|
-
fix_match = re.search(fix_pattern, content, re.MULTILINE)
|
|
1992
|
-
reason_match = re.search(reason_pattern, content, re.MULTILINE)
|
|
1993
|
-
|
|
1994
|
-
if fix_match:
|
|
1995
|
-
fix_command = fix_match.group(1).strip()
|
|
1996
|
-
reason = reason_match.group(1).strip() if reason_match else "LLM suggested fix"
|
|
1997
|
-
|
|
1998
|
-
# Clean up the fix command
|
|
1999
|
-
if fix_command.startswith('`') and fix_command.endswith('`'):
|
|
2000
|
-
fix_command = fix_command[1:-1]
|
|
2001
|
-
|
|
2002
|
-
fixes.append({
|
|
2003
|
-
'original_command': failed_commands[i-1]['command'],
|
|
2004
|
-
'fix_command': fix_command,
|
|
2005
|
-
'reason': reason,
|
|
2006
|
-
'command_index': i-1
|
|
2007
|
-
})
|
|
2008
|
-
|
|
2009
|
-
print(f"๐ง Generated {len(fixes)} fix commands from batch analysis")
|
|
2010
|
-
return fixes
|
|
2011
|
-
else:
|
|
2012
|
-
print(f"โ OpenAI API error: {response.status_code} - {response.text}")
|
|
2013
|
-
return []
|
|
2014
|
-
|
|
2015
|
-
except Exception as e:
|
|
2016
|
-
print(f"โ Error during batch debugging: {e}")
|
|
2017
|
-
return []
|
|
2018
|
-
|
|
2019
|
-
def call_anthropic_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
|
|
2020
|
-
"""Call Anthropic Claude to debug a failed command and suggest a fix"""
|
|
2021
|
-
print("\n๐ DEBUG: Starting Anthropic Claude debugging...")
|
|
2022
|
-
print(f"๐ DEBUG: Command: {command}")
|
|
2023
|
-
print(f"๐ DEBUG: Error output length: {len(error_output) if error_output else 0}")
|
|
2024
|
-
print(f"๐ DEBUG: Current directory: {current_dir}")
|
|
2025
|
-
print(f"๐ DEBUG: Sandbox available: {sandbox is not None}")
|
|
2026
|
-
|
|
2027
|
-
# Define _to_str function locally to avoid NameError
|
|
2028
|
-
def _to_str(maybe_bytes):
|
|
2029
|
-
try:
|
|
2030
|
-
return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
|
|
2031
|
-
except UnicodeDecodeError:
|
|
2032
|
-
# Handle non-UTF-8 bytes by replacing invalid characters
|
|
2033
|
-
if isinstance(maybe_bytes, (bytes, bytearray)):
|
|
2034
|
-
return maybe_bytes.decode('utf-8', errors='replace')
|
|
2035
|
-
else:
|
|
2036
|
-
return str(maybe_bytes)
|
|
2037
|
-
except Exception:
|
|
2038
|
-
# Last resort fallback
|
|
2039
|
-
return str(maybe_bytes)
|
|
2040
|
-
|
|
2041
|
-
# Skip debugging for certain commands that commonly return non-zero exit codes
|
|
2042
|
-
# but aren't actually errors (like test commands)
|
|
2043
|
-
if command.strip().startswith("test "):
|
|
2044
|
-
print("๐ Skipping debugging for test command - non-zero exit code is expected behavior")
|
|
2045
|
-
return None
|
|
2046
|
-
|
|
2047
|
-
# Validate error_output - if it's empty, we can't debug effectively
|
|
2048
|
-
if not error_output or not error_output.strip():
|
|
2049
|
-
print("โ ๏ธ Error output is empty. Cannot effectively debug the command.")
|
|
2050
|
-
print("โ ๏ธ Skipping Anthropic debugging due to lack of error information.")
|
|
2051
|
-
return None
|
|
2052
|
-
|
|
2053
|
-
# Try to get API key from multiple sources
|
|
2054
|
-
if not api_key:
|
|
2055
|
-
print("๐ DEBUG: No Anthropic API key provided, searching for one...")
|
|
2056
|
-
|
|
2057
|
-
# First try environment variable
|
|
2058
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
2059
|
-
print(f"๐ DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
|
|
2060
|
-
if api_key:
|
|
2061
|
-
print(f"๐ DEBUG: Environment API key value: {api_key}")
|
|
2062
|
-
|
|
2063
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2064
|
-
if not api_key:
|
|
2065
|
-
try:
|
|
2066
|
-
print("๐ DEBUG: Trying to fetch API key from server...")
|
|
2067
|
-
from fetch_modal_tokens import get_tokens
|
|
2068
|
-
_, _, _, api_key = get_tokens()
|
|
2069
|
-
if api_key:
|
|
2070
|
-
# Set in environment for this session
|
|
2071
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2072
|
-
else:
|
|
2073
|
-
print("โ ๏ธ Could not fetch Anthropic API key from server")
|
|
2074
|
-
except Exception as e:
|
|
2075
|
-
print(f"โ ๏ธ Error fetching API key from server: {e}")
|
|
2076
|
-
|
|
2077
|
-
# Then try credentials manager
|
|
2078
|
-
if not api_key:
|
|
2079
|
-
print("๐ DEBUG: Trying credentials manager...")
|
|
2080
|
-
try:
|
|
2081
|
-
from credentials_manager import CredentialsManager
|
|
2082
|
-
credentials_manager = CredentialsManager()
|
|
2083
|
-
api_key = credentials_manager.get_anthropic_api_key()
|
|
2084
|
-
if api_key:
|
|
2085
|
-
print(f"๐ DEBUG: API key from credentials manager: Found")
|
|
2086
|
-
print(f"๐ DEBUG: Credentials manager API key value: {api_key}")
|
|
2087
|
-
# Set in environment for this session
|
|
2088
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2089
|
-
else:
|
|
2090
|
-
print("โ ๏ธ Could not fetch Anthropic API key from credentials manager")
|
|
2091
|
-
except Exception as e:
|
|
2092
|
-
print(f"โ ๏ธ Error fetching API key from credentials manager: {e}")
|
|
2093
|
-
|
|
2094
|
-
# Store the API key in a persistent file if found
|
|
2095
|
-
if api_key:
|
|
2096
|
-
try:
|
|
2097
|
-
os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
|
|
2098
|
-
with open(os.path.expanduser("~/.gitarsenal/anthropic_key"), "w") as f:
|
|
2099
|
-
f.write(api_key)
|
|
2100
|
-
print("โ
Saved Anthropic API key for future use")
|
|
2101
|
-
except Exception as e:
|
|
2102
|
-
print(f"โ ๏ธ Could not save API key: {e}")
|
|
2103
|
-
|
|
2104
|
-
# Try to load from saved file if not in environment
|
|
2105
|
-
if not api_key:
|
|
2106
|
-
try:
|
|
2107
|
-
key_file = os.path.expanduser("~/.gitarsenal/anthropic_key")
|
|
2108
|
-
print(f"๐ DEBUG: Checking for saved API key at: {key_file}")
|
|
2109
|
-
if os.path.exists(key_file):
|
|
2110
|
-
with open(key_file, "r") as f:
|
|
2111
|
-
api_key = f.read().strip()
|
|
2112
|
-
if api_key:
|
|
2113
|
-
print("โ
Loaded Anthropic API key from saved file")
|
|
2114
|
-
print(f"๐ DEBUG: API key from file: {api_key}")
|
|
2115
|
-
print(f"๐ DEBUG: API key length: {len(api_key)}")
|
|
2116
|
-
# Also set in environment for this session
|
|
2117
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2118
|
-
else:
|
|
2119
|
-
print("๐ DEBUG: Saved file exists but is empty")
|
|
2120
|
-
else:
|
|
2121
|
-
print("๐ DEBUG: No saved API key file found")
|
|
2122
|
-
except Exception as e:
|
|
2123
|
-
print(f"โ ๏ธ Could not load saved API key: {e}")
|
|
2124
|
-
|
|
2125
|
-
if not api_key:
|
|
2126
|
-
print("โ No Anthropic API key available for debugging")
|
|
2127
|
-
return None
|
|
2128
|
-
|
|
2129
|
-
# Prepare the prompt for debugging
|
|
2130
|
-
error_str = _to_str(error_output)
|
|
2131
|
-
prompt = f"""You are a debugging assistant. Provide only the terminal command to fix the issue.
|
|
2132
|
-
|
|
2133
|
-
Context:
|
|
2134
|
-
- Current directory: {current_dir}
|
|
2135
|
-
- Sandbox available: {sandbox is not None}
|
|
2136
|
-
- Failed command: {command}
|
|
2137
|
-
- Error output: {error_str}
|
|
2138
|
-
|
|
2139
|
-
Analyze the issue first, understand why it's happening, then provide the command to fix it.
|
|
2140
|
-
|
|
2141
|
-
Guidelines:
|
|
2142
|
-
- For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found
|
|
2143
|
-
- For missing packages, use appropriate package managers (pip, apt-get, npm)
|
|
2144
|
-
- For Git SSH authentication failures, always convert SSH URLs to HTTPS URLs (git@github.com:user/repo.git -> https://github.com/user/repo.git)
|
|
2145
|
-
- For authentication, suggest login commands with placeholders
|
|
2146
|
-
- For permission errors, suggest commands with sudo if appropriate
|
|
2147
|
-
- For network issues, suggest retry commands or alternative URLs
|
|
2148
|
-
|
|
2149
|
-
Return only the command to fix the issue, nothing else."""
|
|
2150
|
-
|
|
2151
|
-
# Set up headers for Anthropic API
|
|
2152
|
-
headers = {
|
|
2153
|
-
"x-api-key": api_key,
|
|
2154
|
-
"anthropic-version": "2023-06-01",
|
|
2155
|
-
"content-type": "application/json"
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
# Models to try in order of preference
|
|
2159
|
-
models_to_try = ["claude-4-sonnet"]
|
|
2160
|
-
|
|
2161
|
-
def try_api_call(model_name, retries=2, backoff_factor=1.5):
|
|
2162
|
-
payload = {
|
|
2163
|
-
"model": model_name,
|
|
2164
|
-
"max_tokens": 300,
|
|
2165
|
-
"messages": [
|
|
2166
|
-
{"role": "user", "content": prompt}
|
|
2167
|
-
]
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
print(f"๐ DEBUG: Payload prepared, prompt length: {len(prompt)}")
|
|
2171
|
-
|
|
2172
|
-
# Add specific handling for common errors
|
|
2173
|
-
last_error = None
|
|
2174
|
-
for attempt in range(retries + 1):
|
|
2175
|
-
try:
|
|
2176
|
-
if attempt > 0:
|
|
2177
|
-
# Exponential backoff
|
|
2178
|
-
wait_time = backoff_factor * (2 ** (attempt - 1))
|
|
2179
|
-
print(f"โฑ๏ธ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
|
|
2180
|
-
time.sleep(wait_time)
|
|
2181
|
-
|
|
2182
|
-
print(f"๐ค Calling Anthropic Claude with {model_name} model to debug the failed command...")
|
|
2183
|
-
print(f"๐ DEBUG: Making POST request to Anthropic API...")
|
|
2184
|
-
response = requests.post(
|
|
2185
|
-
"https://api.anthropic.com/v1/messages",
|
|
2186
|
-
headers=headers,
|
|
2187
|
-
json=payload,
|
|
2188
|
-
timeout=45 # Increased timeout for reliability
|
|
2189
|
-
)
|
|
2190
|
-
|
|
2191
|
-
print(f"๐ DEBUG: Response received, status code: {response.status_code}")
|
|
2192
|
-
|
|
2193
|
-
# Handle specific status codes
|
|
2194
|
-
if response.status_code == 200:
|
|
2195
|
-
print(f"๐ DEBUG: Success! Response length: {len(response.text)}")
|
|
2196
|
-
return response.json(), None
|
|
2197
|
-
elif response.status_code == 401:
|
|
2198
|
-
error_msg = "Authentication error: Invalid API key"
|
|
2199
|
-
print(f"โ {error_msg}")
|
|
2200
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
2201
|
-
# Don't retry auth errors
|
|
2202
|
-
return None, error_msg
|
|
2203
|
-
elif response.status_code == 429:
|
|
2204
|
-
error_msg = "Rate limit exceeded or quota reached"
|
|
2205
|
-
print(f"โ ๏ธ {error_msg}")
|
|
2206
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
2207
|
-
# Always retry rate limit errors with increasing backoff
|
|
2208
|
-
last_error = error_msg
|
|
2209
|
-
continue
|
|
2210
|
-
elif response.status_code == 500:
|
|
2211
|
-
error_msg = "Anthropic server error"
|
|
2212
|
-
print(f"โ ๏ธ {error_msg}")
|
|
2213
|
-
print(f"๐ DEBUG: Response text: {response.text}")
|
|
2214
|
-
# Retry server errors
|
|
2215
|
-
last_error = error_msg
|
|
2216
|
-
continue
|
|
2217
|
-
else:
|
|
2218
|
-
error_msg = f"Status code: {response.status_code}, Response: {response.text}"
|
|
2219
|
-
print(f"โ ๏ธ Anthropic API error: {error_msg}")
|
|
2220
|
-
print(f"๐ DEBUG: Full response text: {response.text}")
|
|
2221
|
-
last_error = error_msg
|
|
2222
|
-
# Only retry if we have attempts left
|
|
2223
|
-
if attempt < retries:
|
|
2224
|
-
continue
|
|
2225
|
-
return None, error_msg
|
|
2226
|
-
except requests.exceptions.Timeout:
|
|
2227
|
-
error_msg = "Request timed out"
|
|
2228
|
-
last_error = error_msg
|
|
2229
|
-
# Always retry timeouts
|
|
2230
|
-
continue
|
|
2231
|
-
except requests.exceptions.ConnectionError:
|
|
2232
|
-
error_msg = "Connection error"
|
|
2233
|
-
print(f"โ ๏ธ {error_msg}")
|
|
2234
|
-
print(f"๐ DEBUG: Connection failed to api.anthropic.com")
|
|
2235
|
-
last_error = error_msg
|
|
2236
|
-
# Always retry connection errors
|
|
2237
|
-
continue
|
|
2238
|
-
except Exception as e:
|
|
2239
|
-
error_msg = str(e)
|
|
2240
|
-
print(f"โ ๏ธ Unexpected error: {error_msg}")
|
|
2241
|
-
print(f"๐ DEBUG: Exception type: {type(e).__name__}")
|
|
2242
|
-
print(f"๐ DEBUG: Exception details: {str(e)}")
|
|
2243
|
-
last_error = error_msg
|
|
2244
|
-
# Only retry if we have attempts left
|
|
2245
|
-
if attempt < retries:
|
|
2246
|
-
continue
|
|
2247
|
-
return None, error_msg
|
|
2248
|
-
|
|
2249
|
-
# If we get here, all retries failed
|
|
2250
|
-
return None, last_error
|
|
2251
|
-
|
|
2252
|
-
# Try each model in sequence until one works
|
|
2253
|
-
result = None
|
|
2254
|
-
last_error = None
|
|
2255
|
-
|
|
2256
|
-
for model in models_to_try:
|
|
2257
|
-
result, error = try_api_call(model)
|
|
2258
|
-
if result:
|
|
2259
|
-
break
|
|
2260
|
-
else:
|
|
2261
|
-
print(f"โ ๏ธ Failed to get response from {model}: {error}")
|
|
2262
|
-
last_error = error
|
|
2263
|
-
|
|
2264
|
-
if not result:
|
|
2265
|
-
print(f"โ All model attempts failed. Last error: {last_error}")
|
|
2266
|
-
return None
|
|
2267
|
-
|
|
2268
|
-
# Process the response
|
|
2269
|
-
try:
|
|
2270
|
-
print(f"๐ DEBUG: Processing Anthropic response...")
|
|
2271
|
-
print(f"๐ DEBUG: Choices count: {len(result.get('content', []))}")
|
|
2272
|
-
|
|
2273
|
-
fix_command = result["content"][0]["text"].strip()
|
|
2274
|
-
print(f"๐ DEBUG: Raw response content: {fix_command}")
|
|
2275
|
-
|
|
2276
|
-
# Save the original response for debugging
|
|
2277
|
-
original_response = fix_command
|
|
2278
|
-
|
|
2279
|
-
# Extract just the command if it's wrapped in backticks or explanation
|
|
2280
|
-
if "```" in fix_command:
|
|
2281
|
-
# Extract content between backticks
|
|
2282
|
-
import re
|
|
2283
|
-
code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
|
|
2284
|
-
if code_blocks:
|
|
2285
|
-
fix_command = code_blocks[0].strip()
|
|
2286
|
-
print(f"โ
Extracted command from code block: {fix_command}")
|
|
2287
|
-
|
|
2288
|
-
# If the response still has explanatory text, try to extract just the command
|
|
2289
|
-
if len(fix_command.split('\n')) > 1:
|
|
2290
|
-
# First try to find lines that look like commands (start with common command prefixes)
|
|
2291
|
-
command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
|
|
2292
|
-
'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
|
|
2293
|
-
'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
|
|
2294
|
-
'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
|
|
2295
|
-
|
|
2296
|
-
# Check for lines that start with common command prefixes
|
|
2297
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
|
2298
|
-
if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
|
|
2299
|
-
|
|
2300
|
-
if command_lines:
|
|
2301
|
-
# Use the first command line found
|
|
2302
|
-
fix_command = command_lines[0]
|
|
2303
|
-
print(f"โ
Identified command by prefix: {fix_command}")
|
|
2304
|
-
else:
|
|
2305
|
-
# Try to find lines that look like commands (contain common shell patterns)
|
|
2306
|
-
shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
|
|
2307
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
|
2308
|
-
if any(pattern in line for pattern in shell_patterns)]
|
|
2309
|
-
|
|
2310
|
-
if command_lines:
|
|
2311
|
-
# Use the first command line found
|
|
2312
|
-
fix_command = command_lines[0]
|
|
2313
|
-
print(f"โ
Identified command by shell pattern: {fix_command}")
|
|
2314
|
-
else:
|
|
2315
|
-
# Fall back to the shortest non-empty line as it's likely the command
|
|
2316
|
-
lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
|
|
2317
|
-
if lines:
|
|
2318
|
-
# Exclude very short lines that are likely not commands
|
|
2319
|
-
valid_lines = [line for line in lines if len(line) > 5]
|
|
2320
|
-
if valid_lines:
|
|
2321
|
-
fix_command = min(valid_lines, key=len)
|
|
2322
|
-
else:
|
|
2323
|
-
fix_command = min(lines, key=len)
|
|
2324
|
-
print(f"โ
Selected shortest line as command: {fix_command}")
|
|
2325
|
-
|
|
2326
|
-
# Clean up the command - remove any trailing periods or quotes
|
|
2327
|
-
fix_command = fix_command.rstrip('.;"\'')
|
|
2328
|
-
|
|
2329
|
-
# Remove common prefixes that LLMs sometimes add
|
|
2330
|
-
prefixes_to_remove = [
|
|
2331
|
-
"Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
|
|
2332
|
-
"You should run: ", "You can run: ", "You need to run: "
|
|
2333
|
-
]
|
|
2334
|
-
for prefix in prefixes_to_remove:
|
|
2335
|
-
if fix_command.startswith(prefix):
|
|
2336
|
-
fix_command = fix_command[len(prefix):].strip()
|
|
2337
|
-
print(f"โ
Removed prefix: {prefix}")
|
|
2338
|
-
break
|
|
2339
|
-
|
|
2340
|
-
# If the command is still multi-line or very long, it might not be a valid command
|
|
2341
|
-
if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
|
|
2342
|
-
print("โ ๏ธ Extracted command appears invalid (multi-line or too long)")
|
|
2343
|
-
print("๐ Original response from LLM:")
|
|
2344
|
-
print("-" * 60)
|
|
2345
|
-
print(original_response)
|
|
2346
|
-
print("-" * 60)
|
|
2347
|
-
print("โ ๏ธ Using best guess for command")
|
|
2348
|
-
|
|
2349
|
-
print(f"๐ง Suggested fix: {fix_command}")
|
|
2350
|
-
print(f"๐ DEBUG: Returning fix command: {fix_command}")
|
|
2351
|
-
return fix_command
|
|
2352
|
-
except Exception as e:
|
|
2353
|
-
print(f"โ Error processing Anthropic response: {e}")
|
|
2354
|
-
print(f"๐ DEBUG: Exception type: {type(e).__name__}")
|
|
2355
|
-
print(f"๐ DEBUG: Exception details: {str(e)}")
|
|
2356
|
-
return None
|
|
2357
|
-
|
|
2358
|
-
def switch_to_anthropic_models():
|
|
2359
|
-
"""Switch the debugging system to use Anthropic Claude models instead of OpenAI"""
|
|
2360
|
-
print("\n๐ Switching to Anthropic Claude models for debugging...")
|
|
2361
|
-
|
|
2362
|
-
# Set environment variable to indicate Anthropic preference
|
|
2363
|
-
os.environ["GITARSENAL_DEBUG_MODEL"] = "anthropic"
|
|
2364
|
-
|
|
2365
|
-
# Try to get Anthropic API key
|
|
2366
|
-
try:
|
|
2367
|
-
from credentials_manager import CredentialsManager
|
|
2368
|
-
credentials_manager = CredentialsManager()
|
|
2369
|
-
api_key = credentials_manager.get_anthropic_api_key()
|
|
2370
|
-
if api_key:
|
|
2371
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2372
|
-
print("โ
Anthropic API key configured")
|
|
2373
|
-
print("โ
Debugging will now use Anthropic Claude models")
|
|
2374
|
-
return True
|
|
2375
|
-
else:
|
|
2376
|
-
print("โ ๏ธ No Anthropic API key found")
|
|
2377
|
-
print("๐ก You can set your Anthropic API key using:")
|
|
2378
|
-
print(" export ANTHROPIC_API_KEY='your-key'")
|
|
2379
|
-
print(" Or run the credentials manager to set it up")
|
|
2380
|
-
return False
|
|
2381
|
-
except Exception as e:
|
|
2382
|
-
print(f"โ Error configuring Anthropic: {e}")
|
|
2383
|
-
return False
|
|
2384
|
-
|
|
2385
|
-
def switch_to_openai_models():
|
|
2386
|
-
"""Switch the debugging system to use OpenAI models (default)"""
|
|
2387
|
-
print("\n๐ Switching to OpenAI models for debugging...")
|
|
2388
|
-
|
|
2389
|
-
# Set environment variable to indicate OpenAI preference
|
|
2390
|
-
os.environ["GITARSENAL_DEBUG_MODEL"] = "openai"
|
|
2391
|
-
|
|
2392
|
-
# Try to get OpenAI API key
|
|
2393
|
-
try:
|
|
2394
|
-
from credentials_manager import CredentialsManager
|
|
2395
|
-
credentials_manager = CredentialsManager()
|
|
2396
|
-
api_key = credentials_manager.get_openai_api_key()
|
|
2397
|
-
if api_key:
|
|
2398
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
|
2399
|
-
print("โ
OpenAI API key configured")
|
|
2400
|
-
print("โ
Debugging will now use OpenAI models")
|
|
2401
|
-
return True
|
|
2402
|
-
else:
|
|
2403
|
-
print("โ ๏ธ No OpenAI API key found")
|
|
2404
|
-
print("๐ก You can set your OpenAI API key using:")
|
|
2405
|
-
print(" export OPENAI_API_KEY='your-key'")
|
|
2406
|
-
print(" Or run the credentials manager to set it up")
|
|
2407
|
-
return False
|
|
2408
|
-
except Exception as e:
|
|
2409
|
-
print(f"โ Error configuring OpenAI: {e}")
|
|
2410
|
-
return False
|
|
2411
|
-
|
|
2412
|
-
def get_current_debug_model():
|
|
2413
|
-
"""Get the currently configured debugging model preference"""
|
|
2414
|
-
return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
|
|
2415
|
-
|
|
2416
|
-
def call_llm_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
|
|
2417
|
-
"""Unified function to call LLM for debugging - routes to OpenAI or Anthropic based on configuration"""
|
|
2418
|
-
current_model = get_current_debug_model()
|
|
2419
|
-
|
|
2420
|
-
print(f"๐ DEBUG: Using {current_model.upper()} for debugging...")
|
|
2421
|
-
|
|
2422
|
-
if current_model == "anthropic":
|
|
2423
|
-
# Try to get Anthropic API key if not provided
|
|
2424
|
-
if not api_key:
|
|
2425
|
-
# First try environment variable
|
|
2426
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
2427
|
-
|
|
2428
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2429
|
-
if not api_key:
|
|
2430
|
-
try:
|
|
2431
|
-
from fetch_modal_tokens import get_tokens
|
|
2432
|
-
_, _, _, api_key = get_tokens()
|
|
2433
|
-
except Exception as e:
|
|
2434
|
-
print(f"โ ๏ธ Error fetching Anthropic API key from server: {e}")
|
|
2435
|
-
|
|
2436
|
-
# Then try credentials manager
|
|
2437
|
-
if not api_key:
|
|
2438
|
-
try:
|
|
2439
|
-
from credentials_manager import CredentialsManager
|
|
2440
|
-
credentials_manager = CredentialsManager()
|
|
2441
|
-
api_key = credentials_manager.get_anthropic_api_key()
|
|
2442
|
-
except Exception as e:
|
|
2443
|
-
print(f"โ ๏ธ Error getting Anthropic API key from credentials manager: {e}")
|
|
2444
|
-
|
|
2445
|
-
return call_anthropic_for_debug(command, error_output, api_key, current_dir, sandbox)
|
|
2446
|
-
else:
|
|
2447
|
-
# Default to OpenAI
|
|
2448
|
-
# Try to get OpenAI API key if not provided
|
|
2449
|
-
if not api_key:
|
|
2450
|
-
# First try environment variable
|
|
2451
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
2452
|
-
|
|
2453
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2454
|
-
if not api_key:
|
|
2455
|
-
try:
|
|
2456
|
-
from fetch_modal_tokens import get_tokens
|
|
2457
|
-
_, _, api_key, _ = get_tokens()
|
|
2458
|
-
except Exception as e:
|
|
2459
|
-
print(f"โ ๏ธ Error fetching OpenAI API key from server: {e}")
|
|
2460
|
-
|
|
2461
|
-
# Then try credentials manager
|
|
2462
|
-
if not api_key:
|
|
2463
|
-
try:
|
|
2464
|
-
from credentials_manager import CredentialsManager
|
|
2465
|
-
credentials_manager = CredentialsManager()
|
|
2466
|
-
api_key = credentials_manager.get_openai_api_key()
|
|
2467
|
-
except Exception as e:
|
|
2468
|
-
print(f"โ ๏ธ Error getting OpenAI API key from credentials manager: {e}")
|
|
2469
|
-
|
|
2470
|
-
return call_openai_for_debug(command, error_output, api_key, current_dir, sandbox)
|
|
45
|
+
# Import the fetch_modal_tokens module
|
|
46
|
+
# print("๐ Fetching tokens from proxy server...")
|
|
47
|
+
from fetch_modal_tokens import get_tokens
|
|
48
|
+
token_id, token_secret, openai_api_key, _ = get_tokens()
|
|
2471
49
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
print(f"๐ DEBUG: Using {current_model.upper()} for batch debugging...")
|
|
2477
|
-
|
|
2478
|
-
if current_model == "anthropic":
|
|
2479
|
-
# Try to get Anthropic API key if not provided
|
|
2480
|
-
if not api_key:
|
|
2481
|
-
# First try environment variable
|
|
2482
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
2483
|
-
|
|
2484
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2485
|
-
if not api_key:
|
|
2486
|
-
try:
|
|
2487
|
-
from fetch_modal_tokens import get_tokens
|
|
2488
|
-
_, _, _, api_key = get_tokens()
|
|
2489
|
-
except Exception as e:
|
|
2490
|
-
print(f"โ ๏ธ Error fetching Anthropic API key from server: {e}")
|
|
2491
|
-
|
|
2492
|
-
# Then try credentials manager
|
|
2493
|
-
if not api_key:
|
|
2494
|
-
try:
|
|
2495
|
-
from credentials_manager import CredentialsManager
|
|
2496
|
-
credentials_manager = CredentialsManager()
|
|
2497
|
-
api_key = credentials_manager.get_anthropic_api_key()
|
|
2498
|
-
except Exception as e:
|
|
2499
|
-
print(f"โ ๏ธ Error getting Anthropic API key from credentials manager: {e}")
|
|
2500
|
-
|
|
2501
|
-
return call_anthropic_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
|
|
2502
|
-
else:
|
|
2503
|
-
# Default to OpenAI
|
|
2504
|
-
# Try to get OpenAI API key if not provided
|
|
2505
|
-
if not api_key:
|
|
2506
|
-
# First try environment variable
|
|
2507
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
|
2508
|
-
|
|
2509
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2510
|
-
if not api_key:
|
|
2511
|
-
try:
|
|
2512
|
-
from fetch_modal_tokens import get_tokens
|
|
2513
|
-
_, _, api_key, _ = get_tokens()
|
|
2514
|
-
except Exception as e:
|
|
2515
|
-
print(f"โ ๏ธ Error fetching OpenAI API key from server: {e}")
|
|
2516
|
-
|
|
2517
|
-
# Then try credentials manager
|
|
2518
|
-
if not api_key:
|
|
2519
|
-
try:
|
|
2520
|
-
from credentials_manager import CredentialsManager
|
|
2521
|
-
credentials_manager = CredentialsManager()
|
|
2522
|
-
api_key = credentials_manager.get_openai_api_key()
|
|
2523
|
-
except Exception as e:
|
|
2524
|
-
print(f"โ ๏ธ Error getting OpenAI API key from credentials manager: {e}")
|
|
2525
|
-
|
|
2526
|
-
return call_openai_for_batch_debug(failed_commands, api_key, current_dir, sandbox)
|
|
50
|
+
# Check if we got valid tokens
|
|
51
|
+
if token_id is None or token_secret is None:
|
|
52
|
+
raise ValueError("Could not get valid tokens")
|
|
2527
53
|
|
|
2528
|
-
|
|
2529
|
-
"""Call Anthropic Claude to debug multiple failed commands and suggest fixes for all of them at once"""
|
|
2530
|
-
print("\n๐ DEBUG: Starting batch Anthropic Claude debugging...")
|
|
2531
|
-
print(f"๐ DEBUG: Analyzing {len(failed_commands)} failed commands")
|
|
2532
|
-
|
|
2533
|
-
if not failed_commands:
|
|
2534
|
-
print("โ ๏ธ No failed commands to analyze")
|
|
2535
|
-
return []
|
|
2536
|
-
|
|
2537
|
-
if not api_key:
|
|
2538
|
-
print("๐ DEBUG: No Anthropic API key provided, searching for one...")
|
|
2539
|
-
|
|
2540
|
-
# First try environment variable
|
|
2541
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
2542
|
-
print(f"๐ DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
|
|
2543
|
-
if api_key:
|
|
2544
|
-
print(f"๐ DEBUG: Environment API key value: {api_key}")
|
|
2545
|
-
|
|
2546
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
|
2547
|
-
if not api_key:
|
|
2548
|
-
try:
|
|
2549
|
-
print("๐ DEBUG: Trying to fetch API key from server...")
|
|
2550
|
-
from fetch_modal_tokens import get_tokens
|
|
2551
|
-
_, _, _, api_key = get_tokens()
|
|
2552
|
-
if api_key:
|
|
2553
|
-
# Set in environment for this session
|
|
2554
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2555
|
-
else:
|
|
2556
|
-
print("โ ๏ธ Could not fetch Anthropic API key from server")
|
|
2557
|
-
except Exception as e:
|
|
2558
|
-
print(f"โ ๏ธ Error fetching API key from server: {e}")
|
|
2559
|
-
|
|
2560
|
-
# Then try credentials manager
|
|
2561
|
-
if not api_key:
|
|
2562
|
-
print("๐ DEBUG: Trying credentials manager...")
|
|
2563
|
-
try:
|
|
2564
|
-
from credentials_manager import CredentialsManager
|
|
2565
|
-
credentials_manager = CredentialsManager()
|
|
2566
|
-
api_key = credentials_manager.get_anthropic_api_key()
|
|
2567
|
-
if api_key:
|
|
2568
|
-
print(f"๐ DEBUG: API key from credentials manager: Found")
|
|
2569
|
-
print(f"๐ DEBUG: Credentials manager API key value: {api_key}")
|
|
2570
|
-
# Set in environment for this session
|
|
2571
|
-
os.environ["ANTHROPIC_API_KEY"] = api_key
|
|
2572
|
-
else:
|
|
2573
|
-
print("โ ๏ธ Could not fetch Anthropic API key from credentials manager")
|
|
2574
|
-
except Exception as e:
|
|
2575
|
-
print(f"โ ๏ธ Error fetching API key from credentials manager: {e}")
|
|
2576
|
-
|
|
2577
|
-
if not api_key:
|
|
2578
|
-
print("โ No Anthropic API key available for batch debugging")
|
|
2579
|
-
return []
|
|
2580
|
-
|
|
2581
|
-
# Prepare context for batch analysis
|
|
2582
|
-
context_parts = []
|
|
2583
|
-
context_parts.append(f"Current directory: {current_dir}")
|
|
2584
|
-
context_parts.append(f"Sandbox available: {sandbox is not None}")
|
|
2585
|
-
|
|
2586
|
-
# Add failed commands with their errors
|
|
2587
|
-
for i, failed_cmd in enumerate(failed_commands, 1):
|
|
2588
|
-
cmd_type = failed_cmd.get('type', 'main')
|
|
2589
|
-
original_cmd = failed_cmd.get('original_command', '')
|
|
2590
|
-
cmd_text = failed_cmd['command']
|
|
2591
|
-
stderr = failed_cmd.get('stderr', '')
|
|
2592
|
-
stdout = failed_cmd.get('stdout', '')
|
|
2593
|
-
|
|
2594
|
-
context_parts.append(f"\n--- Failed Command {i} ({cmd_type}) ---")
|
|
2595
|
-
context_parts.append(f"Command: {cmd_text}")
|
|
2596
|
-
if original_cmd and original_cmd != cmd_text:
|
|
2597
|
-
context_parts.append(f"Original Command: {original_cmd}")
|
|
2598
|
-
if stderr:
|
|
2599
|
-
context_parts.append(f"Error Output: {stderr}")
|
|
2600
|
-
if stdout:
|
|
2601
|
-
context_parts.append(f"Standard Output: {stdout}")
|
|
2602
|
-
|
|
2603
|
-
# Create the prompt for batch analysis
|
|
2604
|
-
prompt = f"""You are a debugging assistant analyzing multiple failed commands.
|
|
54
|
+
print(f"โ
Tokens fetched successfully")
|
|
2605
55
|
|
|
2606
|
-
|
|
2607
|
-
|
|
56
|
+
# Explicitly set the environment variables again to be sure
|
|
57
|
+
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
58
|
+
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
59
|
+
os.environ["OPENAI_API_KEY"] = openai_api_key
|
|
60
|
+
# Also set the old environment variable for backward compatibility
|
|
61
|
+
os.environ["MODAL_TOKEN"] = token_id
|
|
2608
62
|
|
|
2609
|
-
|
|
63
|
+
# Set token variables for later use
|
|
64
|
+
token = token_id # For backward compatibility
|
|
2610
65
|
|
|
2611
|
-
FIX_COMMAND_{i}: <the fix command>
|
|
2612
|
-
REASON_{i}: <brief explanation of why the original command failed and how the fix addresses it>
|
|
2613
66
|
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
- For network issues, suggest retry commands or alternative URLs
|
|
2620
|
-
- Keep each fix command simple and focused on the specific error
|
|
67
|
+
def generate_random_password(length=16):
|
|
68
|
+
"""Generate a random password for SSH access"""
|
|
69
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
70
|
+
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
|
71
|
+
return password
|
|
2621
72
|
|
|
2622
|
-
Provide fixes for all {len(failed_commands)} failed commands:"""
|
|
2623
73
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
"content-type": "application/json"
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
payload = {
|
|
2632
|
-
"model": "claude-3-5-sonnet-20241022", # Use a more capable model for batch analysis
|
|
2633
|
-
"max_tokens": 1000,
|
|
2634
|
-
"messages": [
|
|
2635
|
-
{"role": "user", "content": prompt}
|
|
2636
|
-
]
|
|
2637
|
-
}
|
|
74
|
+
def get_stored_credentials():
|
|
75
|
+
"""Load stored credentials from ~/.gitarsenal/credentials.json"""
|
|
76
|
+
import json
|
|
77
|
+
from pathlib import Path
|
|
2638
78
|
|
|
2639
79
|
try:
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
timeout=60
|
|
2646
|
-
)
|
|
2647
|
-
|
|
2648
|
-
if response.status_code == 200:
|
|
2649
|
-
result = response.json()
|
|
2650
|
-
content = result['content'][0]['text']
|
|
2651
|
-
print(f"โ
Batch analysis completed")
|
|
2652
|
-
|
|
2653
|
-
# Parse the response to extract fix commands
|
|
2654
|
-
fixes = []
|
|
2655
|
-
for i in range(1, len(failed_commands) + 1):
|
|
2656
|
-
fix_pattern = f"FIX_COMMAND_{i}: (.+)"
|
|
2657
|
-
reason_pattern = f"REASON_{i}: (.+)"
|
|
2658
|
-
|
|
2659
|
-
fix_match = re.search(fix_pattern, content, re.MULTILINE)
|
|
2660
|
-
reason_match = re.search(reason_pattern, content, re.MULTILINE)
|
|
2661
|
-
|
|
2662
|
-
if fix_match:
|
|
2663
|
-
fix_command = fix_match.group(1).strip()
|
|
2664
|
-
reason = reason_match.group(1).strip() if reason_match else "Anthropic Claude suggested fix"
|
|
2665
|
-
|
|
2666
|
-
# Clean up the fix command
|
|
2667
|
-
if fix_command.startswith('`') and fix_command.endswith('`'):
|
|
2668
|
-
fix_command = fix_command[1:-1]
|
|
2669
|
-
|
|
2670
|
-
fixes.append({
|
|
2671
|
-
'original_command': failed_commands[i-1]['command'],
|
|
2672
|
-
'fix_command': fix_command,
|
|
2673
|
-
'reason': reason,
|
|
2674
|
-
'command_index': i-1
|
|
2675
|
-
})
|
|
2676
|
-
|
|
2677
|
-
print(f"๐ง Generated {len(fixes)} fix commands from batch analysis")
|
|
2678
|
-
return fixes
|
|
80
|
+
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
|
81
|
+
if credentials_file.exists():
|
|
82
|
+
with open(credentials_file, 'r') as f:
|
|
83
|
+
credentials = json.load(f)
|
|
84
|
+
return credentials
|
|
2679
85
|
else:
|
|
2680
|
-
|
|
2681
|
-
return []
|
|
2682
|
-
|
|
86
|
+
return {}
|
|
2683
87
|
except Exception as e:
|
|
2684
|
-
print(f"
|
|
2685
|
-
return
|
|
2686
|
-
|
|
2687
|
-
def generate_random_password(length=16):
|
|
2688
|
-
"""Generate a random password for SSH access"""
|
|
2689
|
-
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
2690
|
-
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
|
2691
|
-
return password
|
|
88
|
+
print(f"โ ๏ธ Error loading stored credentials: {e}")
|
|
89
|
+
return {}
|
|
2692
90
|
|
|
2693
91
|
|
|
2694
92
|
# Now modify the create_modal_ssh_container function to use the PersistentShell
|
|
@@ -2900,6 +298,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
2900
298
|
try:
|
|
2901
299
|
print("๐ฆ Building SSH-enabled image...")
|
|
2902
300
|
|
|
301
|
+
# Get the current directory path for mounting local Python sources
|
|
302
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
303
|
+
print(f"๐ Current directory for mounting: {current_dir}")
|
|
304
|
+
|
|
2903
305
|
# Use a more stable CUDA base image and avoid problematic packages
|
|
2904
306
|
ssh_image = (
|
|
2905
307
|
# modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
|
|
@@ -2909,7 +311,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
2909
311
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
|
2910
312
|
"gpg", "ca-certificates", "software-properties-common"
|
|
2911
313
|
)
|
|
2912
|
-
.uv_pip_install("uv", "modal", "requests", "openai") # Remove problematic CUDA packages
|
|
314
|
+
.uv_pip_install("uv", "modal", "requests", "openai", "anthropic") # Remove problematic CUDA packages
|
|
2913
315
|
.run_commands(
|
|
2914
316
|
# Create SSH directory
|
|
2915
317
|
"mkdir -p /var/run/sshd",
|
|
@@ -2931,6 +333,12 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
2931
333
|
# Set up a nice bash prompt
|
|
2932
334
|
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
2933
335
|
)
|
|
336
|
+
.add_local_file(os.path.join(current_dir, "shell.py"), "/python/shell.py") # Mount shell.py
|
|
337
|
+
.add_local_file(os.path.join(current_dir, "command_manager.py"), "/python/command_manager.py") # Mount command_manager.py
|
|
338
|
+
.add_local_file(os.path.join(current_dir, "fetch_modal_tokens.py"), "/python/fetch_modal_tokens.py") # Mount fetch_modal_token.py
|
|
339
|
+
.add_local_file(os.path.join(current_dir, "llm_debugging.py"), "/python/llm_debugging.py") # Mount llm_debugging.py
|
|
340
|
+
.add_local_file(os.path.join(current_dir, "credentials_manager.py"), "/python/credentials_manager.py") # Mount credentials_manager.py
|
|
341
|
+
|
|
2934
342
|
)
|
|
2935
343
|
print("โ
SSH image built successfully")
|
|
2936
344
|
except Exception as e:
|
|
@@ -2964,6 +372,31 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
2964
372
|
import time
|
|
2965
373
|
import os
|
|
2966
374
|
import json
|
|
375
|
+
import sys
|
|
376
|
+
|
|
377
|
+
# Add the mounted python directory to the Python path
|
|
378
|
+
sys.path.insert(0, "/python")
|
|
379
|
+
|
|
380
|
+
# Import the required classes from the mounted modules
|
|
381
|
+
try:
|
|
382
|
+
from command_manager import CommandListManager
|
|
383
|
+
from shell import PersistentShell
|
|
384
|
+
from llm_debugging import get_stored_credentials, generate_auth_context, call_llm_for_debug, call_llm_for_batch_debug, call_anthropic_for_debug, call_openai_for_debug, call_openai_for_batch_debug, call_anthropic_for_batch_debug, get_current_debug_model
|
|
385
|
+
|
|
386
|
+
print("โ
Successfully imported CommandListManager, PersistentShell, and all llm_debugging functions from mounted modules")
|
|
387
|
+
except ImportError as e:
|
|
388
|
+
print(f"โ Failed to import modules from mounted directory: {e}")
|
|
389
|
+
print("๐ Available files in /python:")
|
|
390
|
+
try:
|
|
391
|
+
import os
|
|
392
|
+
if os.path.exists("/python"):
|
|
393
|
+
for file in os.listdir("/python"):
|
|
394
|
+
print(f" - {file}")
|
|
395
|
+
else:
|
|
396
|
+
print(" /python directory does not exist")
|
|
397
|
+
except Exception as list_error:
|
|
398
|
+
print(f" Error listing files: {list_error}")
|
|
399
|
+
raise
|
|
2967
400
|
|
|
2968
401
|
# Set root password
|
|
2969
402
|
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
@@ -4684,8 +2117,6 @@ if __name__ == "__main__":
|
|
|
4684
2117
|
import sys
|
|
4685
2118
|
|
|
4686
2119
|
parser = argparse.ArgumentParser()
|
|
4687
|
-
parser.add_argument('--gpu', type=str, help='GPU type (e.g., A10G, T4, A100-80GB). If not provided, will prompt for GPU selection.')
|
|
4688
|
-
parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
|
|
4689
2120
|
parser.add_argument('--repo-name', type=str, help='Repository name override')
|
|
4690
2121
|
parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
|
|
4691
2122
|
parser.add_argument('--setup-commands-json', type=str, help='Setup commands as JSON array')
|
|
@@ -4701,6 +2132,11 @@ if __name__ == "__main__":
|
|
|
4701
2132
|
parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
|
|
4702
2133
|
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
|
4703
2134
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
|
2135
|
+
|
|
2136
|
+
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
2137
|
+
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
2138
|
+
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
|
2139
|
+
parser.add_argument('--repo-url', help='Repository URL')
|
|
4704
2140
|
|
|
4705
2141
|
# Authentication-related arguments
|
|
4706
2142
|
parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
|
|
@@ -5001,4 +2437,4 @@ if __name__ == "__main__":
|
|
|
5001
2437
|
# print(f"\nโ Error: {e}")
|
|
5002
2438
|
# print("๐งน Cleaning up resources...")
|
|
5003
2439
|
cleanup_modal_token()
|
|
5004
|
-
|
|
2440
|
+
|