pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +506 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +537 -0
- pdd/agentic_common.py +533 -770
- pdd/agentic_crash.py +2 -1
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +582 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +27 -9
- pdd/agentic_verify.py +3 -2
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +236 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +113 -48
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +358 -0
- pdd/commands/fix.py +155 -114
- pdd/commands/generate.py +5 -0
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +225 -163
- pdd/commands/sessions.py +284 -0
- pdd/commands/utility.py +12 -7
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +44 -7
- pdd/core/cloud.py +237 -0
- pdd/core/dump.py +68 -20
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +208 -6
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +531 -97
- pdd/load_prompt_template.py +15 -34
- pdd/operation_log.py +342 -0
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +122 -97
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +19 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1347 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +217 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +289 -211
- pdd/sync_order.py +304 -0
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +68 -26
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
- pdd_cli-0.0.121.dist-info/RECORD +229 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
pdd/fix_main.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import Tuple, Optional
|
|
3
|
+
import json
|
|
3
4
|
import click
|
|
4
5
|
from rich import print as rprint
|
|
5
6
|
from rich.markup import MarkupError, escape
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
6
9
|
|
|
7
10
|
import requests
|
|
8
11
|
import asyncio
|
|
@@ -16,10 +19,24 @@ from .fix_errors_from_unit_tests import fix_errors_from_unit_tests
|
|
|
16
19
|
from .fix_error_loop import fix_error_loop, run_pytest_on_file
|
|
17
20
|
from .get_jwt_token import get_jwt_token
|
|
18
21
|
from .get_language import get_language
|
|
22
|
+
from .core.cloud import CloudConfig
|
|
19
23
|
|
|
20
24
|
# Import DEFAULT_STRENGTH from the package
|
|
21
25
|
from . import DEFAULT_STRENGTH
|
|
22
26
|
|
|
27
|
+
# Cloud request timeout
|
|
28
|
+
CLOUD_REQUEST_TIMEOUT = 400 # seconds
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env_flag_enabled(name: str) -> bool:
|
|
34
|
+
"""Return True when an env var is set to a truthy value."""
|
|
35
|
+
value = os.environ.get(name)
|
|
36
|
+
if value is None:
|
|
37
|
+
return False
|
|
38
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
39
|
+
|
|
23
40
|
def fix_main(
|
|
24
41
|
ctx: click.Context,
|
|
25
42
|
prompt_file: str,
|
|
@@ -111,8 +128,185 @@ def fix_main(
|
|
|
111
128
|
verbose = ctx.obj.get('verbose', False)
|
|
112
129
|
time = ctx.obj.get('time') # Get time from context
|
|
113
130
|
|
|
131
|
+
# Determine cloud vs local execution preference
|
|
132
|
+
is_local_execution_preferred = ctx.obj.get('local', False)
|
|
133
|
+
cloud_only = _env_flag_enabled("PDD_CLOUD_ONLY") or _env_flag_enabled("PDD_NO_LOCAL_FALLBACK")
|
|
134
|
+
current_execution_is_local = is_local_execution_preferred and not cloud_only
|
|
135
|
+
|
|
136
|
+
# Cloud execution is only supported for single-pass mode (not loop mode)
|
|
137
|
+
# because loop mode requires running tests and verification programs locally
|
|
138
|
+
cloud_execution_attempted = False
|
|
139
|
+
cloud_execution_succeeded = False
|
|
140
|
+
|
|
141
|
+
if not loop and not current_execution_is_local:
|
|
142
|
+
if verbose:
|
|
143
|
+
console.print(Panel("Attempting cloud fix execution...", title="[blue]Mode[/blue]", expand=False))
|
|
144
|
+
|
|
145
|
+
jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
|
|
146
|
+
|
|
147
|
+
if not jwt_token:
|
|
148
|
+
if cloud_only:
|
|
149
|
+
console.print("[red]Cloud authentication failed.[/red]")
|
|
150
|
+
raise click.UsageError("Cloud authentication failed")
|
|
151
|
+
console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
|
|
152
|
+
current_execution_is_local = True
|
|
153
|
+
|
|
154
|
+
if jwt_token and not current_execution_is_local:
|
|
155
|
+
cloud_execution_attempted = True
|
|
156
|
+
# Build cloud payload
|
|
157
|
+
payload = {
|
|
158
|
+
"unitTest": input_strings["unit_test_file"],
|
|
159
|
+
"code": input_strings["code_file"],
|
|
160
|
+
"prompt": input_strings["prompt_file"],
|
|
161
|
+
"errors": input_strings.get("error_file", ""),
|
|
162
|
+
"language": get_language(os.path.splitext(code_file)[1]),
|
|
163
|
+
"strength": strength,
|
|
164
|
+
"temperature": temperature,
|
|
165
|
+
"time": time if time is not None else 0.25,
|
|
166
|
+
"verbose": verbose,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
headers = {
|
|
170
|
+
"Authorization": f"Bearer {jwt_token}",
|
|
171
|
+
"Content-Type": "application/json"
|
|
172
|
+
}
|
|
173
|
+
cloud_url = CloudConfig.get_endpoint_url("fixCode")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
response = requests.post(
|
|
177
|
+
cloud_url,
|
|
178
|
+
json=payload,
|
|
179
|
+
headers=headers,
|
|
180
|
+
timeout=CLOUD_REQUEST_TIMEOUT
|
|
181
|
+
)
|
|
182
|
+
response.raise_for_status()
|
|
183
|
+
|
|
184
|
+
response_data = response.json()
|
|
185
|
+
fixed_unit_test = response_data.get("fixedUnitTest", "")
|
|
186
|
+
fixed_code = response_data.get("fixedCode", "")
|
|
187
|
+
analysis_results = response_data.get("analysis", "")
|
|
188
|
+
total_cost = float(response_data.get("totalCost", 0.0))
|
|
189
|
+
model_name = response_data.get("modelName", "cloud_model")
|
|
190
|
+
success = response_data.get("success", False)
|
|
191
|
+
update_unit_test = response_data.get("updateUnitTest", False)
|
|
192
|
+
update_code = response_data.get("updateCode", False)
|
|
193
|
+
|
|
194
|
+
if not (fixed_unit_test or fixed_code):
|
|
195
|
+
if cloud_only:
|
|
196
|
+
console.print("[red]Cloud execution returned no fixed code.[/red]")
|
|
197
|
+
raise click.UsageError("Cloud execution returned no fixed code")
|
|
198
|
+
console.print("[yellow]Cloud execution returned no fixed code. Falling back to local.[/yellow]")
|
|
199
|
+
current_execution_is_local = True
|
|
200
|
+
else:
|
|
201
|
+
cloud_execution_succeeded = True
|
|
202
|
+
attempts = 1
|
|
203
|
+
|
|
204
|
+
# Validate the fix by running tests (same as local)
|
|
205
|
+
if update_unit_test or update_code:
|
|
206
|
+
import tempfile
|
|
207
|
+
import shutil as shutil_module
|
|
208
|
+
|
|
209
|
+
test_dir = tempfile.mkdtemp(prefix="pdd_fix_validate_")
|
|
210
|
+
temp_test_file = os.path.join(test_dir, "test_temp.py")
|
|
211
|
+
temp_code_file = os.path.join(test_dir, "code_temp.py")
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
test_content = fixed_unit_test if fixed_unit_test else input_strings["unit_test_file"]
|
|
215
|
+
code_content = fixed_code if fixed_code else input_strings["code_file"]
|
|
216
|
+
|
|
217
|
+
with open(temp_test_file, 'w') as f:
|
|
218
|
+
f.write(test_content)
|
|
219
|
+
with open(temp_code_file, 'w') as f:
|
|
220
|
+
f.write(code_content)
|
|
221
|
+
|
|
222
|
+
fails, errors_count, warnings, test_output = run_pytest_on_file(temp_test_file)
|
|
223
|
+
success = (fails == 0 and errors_count == 0)
|
|
224
|
+
|
|
225
|
+
if verbose:
|
|
226
|
+
rprint(f"[cyan]Fix validation: {fails} failures, {errors_count} errors, {warnings} warnings[/cyan]")
|
|
227
|
+
if not success:
|
|
228
|
+
rprint("[yellow]Fix suggested by cloud did not pass tests[/yellow]")
|
|
229
|
+
finally:
|
|
230
|
+
try:
|
|
231
|
+
shutil_module.rmtree(test_dir)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
else:
|
|
235
|
+
success = False
|
|
236
|
+
|
|
237
|
+
if verbose:
|
|
238
|
+
console.print(Panel(
|
|
239
|
+
f"Cloud fix completed. Model: {model_name}, Cost: ${total_cost:.6f}",
|
|
240
|
+
title="[green]Cloud Success[/green]",
|
|
241
|
+
expand=False
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
except requests.exceptions.Timeout:
|
|
245
|
+
if cloud_only:
|
|
246
|
+
console.print(f"[red]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s).[/red]")
|
|
247
|
+
raise click.UsageError("Cloud execution timed out")
|
|
248
|
+
console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
|
|
249
|
+
current_execution_is_local = True
|
|
250
|
+
|
|
251
|
+
except requests.exceptions.HTTPError as e:
|
|
252
|
+
status_code = e.response.status_code if e.response else 0
|
|
253
|
+
err_content = e.response.text[:200] if e.response else "No response content"
|
|
254
|
+
|
|
255
|
+
# Non-recoverable errors: do NOT fall back to local
|
|
256
|
+
if status_code == 402: # Insufficient credits
|
|
257
|
+
try:
|
|
258
|
+
error_data = e.response.json()
|
|
259
|
+
current_balance = error_data.get("currentBalance", "unknown")
|
|
260
|
+
estimated_cost = error_data.get("estimatedCost", "unknown")
|
|
261
|
+
console.print(f"[red]Insufficient credits. Current balance: {current_balance}, estimated cost: {estimated_cost}[/red]")
|
|
262
|
+
except Exception:
|
|
263
|
+
console.print(f"[red]Insufficient credits: {err_content}[/red]")
|
|
264
|
+
raise click.UsageError("Insufficient credits for cloud fix")
|
|
265
|
+
elif status_code == 401: # Authentication error
|
|
266
|
+
console.print(f"[red]Authentication failed: {err_content}[/red]")
|
|
267
|
+
raise click.UsageError("Cloud authentication failed")
|
|
268
|
+
elif status_code == 403: # Authorization error (not approved)
|
|
269
|
+
console.print(f"[red]Access denied: {err_content}[/red]")
|
|
270
|
+
raise click.UsageError("Access denied - user not approved")
|
|
271
|
+
elif status_code == 400: # Validation error
|
|
272
|
+
console.print(f"[red]Invalid request: {err_content}[/red]")
|
|
273
|
+
raise click.UsageError(f"Invalid request: {err_content}")
|
|
274
|
+
else:
|
|
275
|
+
# Recoverable errors (5xx, unexpected errors): fall back to local
|
|
276
|
+
if cloud_only:
|
|
277
|
+
console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
|
|
278
|
+
raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
|
|
279
|
+
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
|
|
280
|
+
current_execution_is_local = True
|
|
281
|
+
|
|
282
|
+
except requests.exceptions.RequestException as e:
|
|
283
|
+
if cloud_only:
|
|
284
|
+
console.print(f"[red]Cloud network error: {e}[/red]")
|
|
285
|
+
raise click.UsageError(f"Cloud network error: {e}")
|
|
286
|
+
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
|
|
287
|
+
current_execution_is_local = True
|
|
288
|
+
|
|
289
|
+
except json.JSONDecodeError:
|
|
290
|
+
if cloud_only:
|
|
291
|
+
console.print("[red]Cloud returned invalid JSON.[/red]")
|
|
292
|
+
raise click.UsageError("Cloud returned invalid JSON")
|
|
293
|
+
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
|
|
294
|
+
current_execution_is_local = True
|
|
295
|
+
|
|
296
|
+
# Local execution path (for loop mode or when cloud failed/skipped)
|
|
114
297
|
if loop:
|
|
115
|
-
#
|
|
298
|
+
# Determine if loop should use cloud for LLM calls (hybrid mode)
|
|
299
|
+
# Local test execution stays local, but LLM fix calls can go to cloud
|
|
300
|
+
use_cloud_for_loop = not is_local_execution_preferred and not cloud_only
|
|
301
|
+
|
|
302
|
+
# If cloud_only is set but we're in loop mode, we still use hybrid approach
|
|
303
|
+
if cloud_only and not is_local_execution_preferred:
|
|
304
|
+
use_cloud_for_loop = True
|
|
305
|
+
|
|
306
|
+
if verbose:
|
|
307
|
+
mode_desc = "hybrid (local tests + cloud LLM)" if use_cloud_for_loop else "local"
|
|
308
|
+
console.print(Panel(f"Performing {mode_desc} fix loop...", title="[blue]Mode[/blue]", expand=False))
|
|
309
|
+
|
|
116
310
|
success, fixed_unit_test, fixed_code, attempts, total_cost, model_name = fix_error_loop(
|
|
117
311
|
unit_test_file=unit_test_file,
|
|
118
312
|
code_file=code_file,
|
|
@@ -126,10 +320,13 @@ def fix_main(
|
|
|
126
320
|
budget=budget,
|
|
127
321
|
error_log_file=output_file_paths.get("output_results"),
|
|
128
322
|
verbose=verbose,
|
|
129
|
-
agentic_fallback=agentic_fallback
|
|
323
|
+
agentic_fallback=agentic_fallback,
|
|
324
|
+
use_cloud=use_cloud_for_loop
|
|
130
325
|
)
|
|
131
|
-
|
|
132
|
-
# Use fix_errors_from_unit_tests for single-pass fixing
|
|
326
|
+
elif not cloud_execution_succeeded:
|
|
327
|
+
# Use fix_errors_from_unit_tests for single-pass fixing (local fallback)
|
|
328
|
+
if verbose:
|
|
329
|
+
console.print(Panel("Performing local fix...", title="[blue]Mode[/blue]", expand=False))
|
|
133
330
|
update_unit_test, update_code, fixed_unit_test, fixed_code, analysis_results, total_cost, model_name = fix_errors_from_unit_tests(
|
|
134
331
|
unit_test=input_strings["unit_test_file"],
|
|
135
332
|
code=input_strings["code_file"],
|
|
@@ -220,8 +417,10 @@ def fix_main(
|
|
|
220
417
|
rprint(f"[bold red]Error printing analysis preview: {e}[/bold red]")
|
|
221
418
|
if success:
|
|
222
419
|
rprint("[bold green]Fixed files saved:[/bold green]")
|
|
223
|
-
|
|
224
|
-
|
|
420
|
+
if fixed_unit_test:
|
|
421
|
+
rprint(f" Test file: {output_file_paths['output_test']}")
|
|
422
|
+
if fixed_code:
|
|
423
|
+
rprint(f" Code file: {output_file_paths['output_code']}")
|
|
225
424
|
if output_file_paths.get("output_results"):
|
|
226
425
|
rprint(f" Results file: {output_file_paths['output_results']}")
|
|
227
426
|
|
|
@@ -342,6 +541,9 @@ def fix_main(
|
|
|
342
541
|
except click.Abort:
|
|
343
542
|
# User cancelled - re-raise to stop the sync loop
|
|
344
543
|
raise
|
|
544
|
+
except click.UsageError:
|
|
545
|
+
# Re-raise UsageError for proper CLI handling (e.g., cloud auth failures, insufficient credits)
|
|
546
|
+
raise
|
|
345
547
|
except Exception as e:
|
|
346
548
|
if not ctx.obj.get('quiet', False):
|
|
347
549
|
# Safely handle and print MarkupError
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
@@ -8,6 +9,8 @@ from typing import Dict, Tuple, Any, Optional
|
|
|
8
9
|
from xml.sax.saxutils import escape
|
|
9
10
|
import time
|
|
10
11
|
|
|
12
|
+
import requests
|
|
13
|
+
|
|
11
14
|
from rich.console import Console
|
|
12
15
|
|
|
13
16
|
# Use relative import assuming fix_verification_errors is in the same package
|
|
@@ -31,6 +34,78 @@ from .get_language import get_language
|
|
|
31
34
|
from .agentic_langtest import default_verify_cmd_for
|
|
32
35
|
from .agentic_verify import run_agentic_verify
|
|
33
36
|
|
|
37
|
+
# Cloud configuration
|
|
38
|
+
try:
|
|
39
|
+
from .core.cloud import CloudConfig
|
|
40
|
+
CLOUD_AVAILABLE = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
CLOUD_AVAILABLE = False
|
|
43
|
+
CloudConfig = None
|
|
44
|
+
|
|
45
|
+
# Cloud request timeout for verify fix
|
|
46
|
+
CLOUD_REQUEST_TIMEOUT = 400 # seconds
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cloud_verify_fix(
|
|
50
|
+
program: str,
|
|
51
|
+
prompt: str,
|
|
52
|
+
code: str,
|
|
53
|
+
output: str,
|
|
54
|
+
strength: float,
|
|
55
|
+
temperature: float,
|
|
56
|
+
time_param: float,
|
|
57
|
+
verbose: bool,
|
|
58
|
+
language: str = "python",
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Call cloud verifyCode endpoint for LLM verification fix.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict with keys: fixed_code, fixed_program, explanation, verification_issues_count, total_cost, model_name
|
|
65
|
+
"""
|
|
66
|
+
if not CLOUD_AVAILABLE or CloudConfig is None:
|
|
67
|
+
raise RuntimeError("Cloud configuration not available")
|
|
68
|
+
|
|
69
|
+
jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
|
|
70
|
+
if not jwt_token:
|
|
71
|
+
raise RuntimeError("Cloud authentication failed - no JWT token")
|
|
72
|
+
|
|
73
|
+
payload = {
|
|
74
|
+
"programContent": program,
|
|
75
|
+
"promptContent": prompt,
|
|
76
|
+
"codeContent": code,
|
|
77
|
+
"outputContent": output,
|
|
78
|
+
"language": language,
|
|
79
|
+
"strength": strength,
|
|
80
|
+
"temperature": temperature,
|
|
81
|
+
"time": time_param if time_param is not None else 0.25,
|
|
82
|
+
"verbose": verbose,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
headers = {
|
|
86
|
+
"Authorization": f"Bearer {jwt_token}",
|
|
87
|
+
"Content-Type": "application/json"
|
|
88
|
+
}
|
|
89
|
+
cloud_url = CloudConfig.get_endpoint_url("verifyCode")
|
|
90
|
+
|
|
91
|
+
response = requests.post(
|
|
92
|
+
cloud_url,
|
|
93
|
+
json=payload,
|
|
94
|
+
headers=headers,
|
|
95
|
+
timeout=CLOUD_REQUEST_TIMEOUT
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
|
|
99
|
+
response_data = response.json()
|
|
100
|
+
return {
|
|
101
|
+
"fixed_code": response_data.get("fixedCode", code),
|
|
102
|
+
"fixed_program": response_data.get("fixedProgram", program),
|
|
103
|
+
"explanation": response_data.get("explanation", ""),
|
|
104
|
+
"verification_issues_count": response_data.get("issuesCount", 0),
|
|
105
|
+
"total_cost": float(response_data.get("totalCost", 0.0)),
|
|
106
|
+
"model_name": response_data.get("modelName", "cloud_model"),
|
|
107
|
+
}
|
|
108
|
+
|
|
34
109
|
def _normalize_agentic_result(result):
|
|
35
110
|
"""
|
|
36
111
|
Normalize run_agentic_verify result into: (success: bool, msg: str, cost: float, model: str, changed_files: List[str])
|
|
@@ -156,11 +231,17 @@ def fix_verification_errors_loop(
|
|
|
156
231
|
program_args: Optional[list[str]] = None,
|
|
157
232
|
llm_time: float = DEFAULT_TIME, # Add time parameter
|
|
158
233
|
agentic_fallback: bool = True,
|
|
234
|
+
use_cloud: bool = False,
|
|
159
235
|
) -> Dict[str, Any]:
|
|
160
236
|
"""
|
|
161
237
|
Attempts to fix errors in a code file based on program execution output
|
|
162
238
|
against the prompt's intent, iterating multiple times with secondary verification.
|
|
163
239
|
|
|
240
|
+
Hybrid Cloud Support:
|
|
241
|
+
When use_cloud=True, the LLM fix calls are routed to the cloud verifyCode endpoint
|
|
242
|
+
while local program execution stays local. This allows the loop to pass local
|
|
243
|
+
verification results to the cloud for analysis and fixes.
|
|
244
|
+
|
|
164
245
|
Args:
|
|
165
246
|
program_file: Path to the Python program exercising the code.
|
|
166
247
|
code_file: Path to the code file being tested/verified.
|
|
@@ -178,6 +259,7 @@ def fix_verification_errors_loop(
|
|
|
178
259
|
program_args: Optional list of command-line arguments for the program_file.
|
|
179
260
|
llm_time: Time parameter for fix_verification_errors calls (default: DEFAULT_TIME).
|
|
180
261
|
agentic_fallback: Enable agentic fallback if the primary fix mechanism fails.
|
|
262
|
+
use_cloud: If True, use cloud LLM for fix calls while keeping verification execution local.
|
|
181
263
|
|
|
182
264
|
Returns:
|
|
183
265
|
A dictionary containing:
|
|
@@ -196,8 +278,55 @@ def fix_verification_errors_loop(
|
|
|
196
278
|
lang = get_language(os.path.splitext(code_file)[1])
|
|
197
279
|
verify_cmd = default_verify_cmd_for(lang, verification_program)
|
|
198
280
|
if not verify_cmd:
|
|
199
|
-
|
|
200
|
-
|
|
281
|
+
# No verify command available (e.g., Java without maven/gradle).
|
|
282
|
+
# Trigger agentic fallback directly.
|
|
283
|
+
console.print(f"[cyan]No verification command for {lang}. Triggering agentic fallback directly...[/cyan]")
|
|
284
|
+
verification_log_path = Path(verification_log_file)
|
|
285
|
+
verification_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
286
|
+
# Create minimal error log if it doesn't exist
|
|
287
|
+
if not verification_log_path.exists() or verification_log_path.stat().st_size == 0:
|
|
288
|
+
with open(verification_log_path, "w") as f:
|
|
289
|
+
f.write(f"No verification command available for language: {lang}\n")
|
|
290
|
+
f.write("Agentic fix will attempt to resolve the issue.\n")
|
|
291
|
+
|
|
292
|
+
agent_cwd = Path(prompt_file).parent if prompt_file else None
|
|
293
|
+
console.print(f"[cyan]Attempting agentic verify fallback (prompt_file={prompt_file!r})...[/cyan]")
|
|
294
|
+
success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_verify(
|
|
295
|
+
prompt_file=prompt_file,
|
|
296
|
+
code_file=code_file,
|
|
297
|
+
program_file=verification_program,
|
|
298
|
+
verification_log_file=verification_log_file,
|
|
299
|
+
verbose=verbose,
|
|
300
|
+
cwd=agent_cwd,
|
|
301
|
+
)
|
|
302
|
+
if not success:
|
|
303
|
+
console.print(f"[bold red]Agentic verify fallback failed: {agent_msg}[/bold red]")
|
|
304
|
+
if agent_changed_files:
|
|
305
|
+
console.print(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
|
|
306
|
+
for f in agent_changed_files:
|
|
307
|
+
console.print(f" • {f}")
|
|
308
|
+
final_program = ""
|
|
309
|
+
final_code = ""
|
|
310
|
+
try:
|
|
311
|
+
with open(verification_program, "r") as f:
|
|
312
|
+
final_program = f.read()
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
try:
|
|
316
|
+
with open(code_file, "r") as f:
|
|
317
|
+
final_code = f.read()
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
return {
|
|
321
|
+
"success": success,
|
|
322
|
+
"final_program": final_program,
|
|
323
|
+
"final_code": final_code,
|
|
324
|
+
"total_attempts": 1,
|
|
325
|
+
"total_cost": agent_cost,
|
|
326
|
+
"model_name": agent_model,
|
|
327
|
+
"statistics": {},
|
|
328
|
+
}
|
|
329
|
+
|
|
201
330
|
verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, shell=True)
|
|
202
331
|
pytest_output = (verify_result.stdout or "") + "\n" + (verify_result.stderr or "")
|
|
203
332
|
console.print("[cyan]Non-Python target detected. Triggering agentic fallback...[/cyan]")
|
|
@@ -310,6 +439,11 @@ def fix_verification_errors_loop(
|
|
|
310
439
|
program_contents = "" # Keep track of current contents
|
|
311
440
|
code_contents = "" # Keep track of current contents
|
|
312
441
|
|
|
442
|
+
# Create backup directory in .pdd/backups/ to avoid polluting code/test directories
|
|
443
|
+
backup_timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
444
|
+
backup_dir = Path.cwd() / '.pdd' / 'backups' / code_path.stem / backup_timestamp
|
|
445
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
446
|
+
|
|
313
447
|
# --- Step 3: Determine Initial State ---
|
|
314
448
|
if verbose:
|
|
315
449
|
console.print("[bold cyan]Step 3: Determining Initial State...[/bold cyan]")
|
|
@@ -324,6 +458,21 @@ def fix_verification_errors_loop(
|
|
|
324
458
|
stats['status_message'] = f'Error reading initial files: {e}' # Add status message
|
|
325
459
|
return {"success": False, "final_program": "", "final_code": "", "total_attempts": 0, "total_cost": 0.0, "model_name": None, "statistics": stats}
|
|
326
460
|
|
|
461
|
+
# 3a-pre: Validate code file is not empty (prevents infinite loops with empty content)
|
|
462
|
+
if not initial_code_content or len(initial_code_content.strip()) == 0:
|
|
463
|
+
error_msg = f"Code file is empty or contains only whitespace: {code_path}"
|
|
464
|
+
console.print(f"[bold red]Error: {error_msg}[/bold red]")
|
|
465
|
+
stats['status_message'] = f'Error: Code file is empty - cannot verify'
|
|
466
|
+
return {
|
|
467
|
+
"success": False,
|
|
468
|
+
"final_program": initial_program_content,
|
|
469
|
+
"final_code": "",
|
|
470
|
+
"total_attempts": 0,
|
|
471
|
+
"total_cost": 0.0,
|
|
472
|
+
"model_name": None,
|
|
473
|
+
"statistics": stats
|
|
474
|
+
}
|
|
475
|
+
|
|
327
476
|
# 3a: Run initial program with args
|
|
328
477
|
initial_return_code, initial_output = _run_program(program_path, args=program_args)
|
|
329
478
|
if verbose:
|
|
@@ -370,16 +519,46 @@ def fix_verification_errors_loop(
|
|
|
370
519
|
if verbose:
|
|
371
520
|
console.print("Running initial assessment with fix_verification_errors...")
|
|
372
521
|
# Use actual strength/temp for realistic initial assessment
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
522
|
+
# Use cloud or local based on use_cloud parameter
|
|
523
|
+
if use_cloud:
|
|
524
|
+
try:
|
|
525
|
+
initial_fix_result = cloud_verify_fix(
|
|
526
|
+
program=initial_program_content,
|
|
527
|
+
prompt=prompt,
|
|
528
|
+
code=initial_code_content,
|
|
529
|
+
output=initial_output,
|
|
530
|
+
strength=strength,
|
|
531
|
+
temperature=temperature,
|
|
532
|
+
time_param=llm_time,
|
|
533
|
+
verbose=verbose,
|
|
534
|
+
language="python" if is_python else get_language(os.path.splitext(code_file)[1]),
|
|
535
|
+
)
|
|
536
|
+
if verbose:
|
|
537
|
+
console.print(f"[cyan]Cloud verify fix completed.[/cyan]")
|
|
538
|
+
except (requests.exceptions.RequestException, RuntimeError) as cloud_err:
|
|
539
|
+
# Cloud failed - fall back to local
|
|
540
|
+
console.print(f"[yellow]Cloud verify fix failed: {cloud_err}. Falling back to local.[/yellow]")
|
|
541
|
+
initial_fix_result = fix_verification_errors(
|
|
542
|
+
program=initial_program_content,
|
|
543
|
+
prompt=prompt,
|
|
544
|
+
code=initial_code_content,
|
|
545
|
+
output=initial_output,
|
|
546
|
+
strength=strength,
|
|
547
|
+
temperature=temperature,
|
|
548
|
+
verbose=verbose,
|
|
549
|
+
time=llm_time
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
initial_fix_result = fix_verification_errors(
|
|
553
|
+
program=initial_program_content,
|
|
554
|
+
prompt=prompt,
|
|
555
|
+
code=initial_code_content,
|
|
556
|
+
output=initial_output,
|
|
557
|
+
strength=strength,
|
|
558
|
+
temperature=temperature,
|
|
559
|
+
verbose=verbose,
|
|
560
|
+
time=llm_time # Pass time
|
|
561
|
+
)
|
|
383
562
|
# 3e: Add cost
|
|
384
563
|
initial_cost = initial_fix_result.get('total_cost', 0.0)
|
|
385
564
|
total_cost += initial_cost
|
|
@@ -539,9 +718,9 @@ def fix_verification_errors_loop(
|
|
|
539
718
|
# code_contents = code_path.read_text(encoding="utf-8")
|
|
540
719
|
# except IOError as e: ...
|
|
541
720
|
|
|
542
|
-
# 4d: Create backups
|
|
543
|
-
program_backup_path =
|
|
544
|
-
code_backup_path =
|
|
721
|
+
# 4d: Create backups in .pdd/backups/ (backup_dir already created above)
|
|
722
|
+
program_backup_path = backup_dir / f"program_{current_attempt}{program_path.suffix}"
|
|
723
|
+
code_backup_path = backup_dir / f"code_{current_attempt}{code_path.suffix}"
|
|
545
724
|
try:
|
|
546
725
|
# Copy from the *current* state before this iteration's fix
|
|
547
726
|
program_path.write_text(program_contents, encoding="utf-8") # Ensure file matches memory state
|
|
@@ -561,7 +740,7 @@ def fix_verification_errors_loop(
|
|
|
561
740
|
stats['status_message'] = f'Error creating backups on attempt {current_attempt}'
|
|
562
741
|
break # Don't proceed without backups
|
|
563
742
|
|
|
564
|
-
# 4e: Call fix_verification_errors
|
|
743
|
+
# 4e: Call fix_verification_errors (cloud or local based on use_cloud parameter)
|
|
565
744
|
iteration_log_xml += f' <InputsToFixer>\n'
|
|
566
745
|
iteration_log_xml += f' <Program>{escape(program_contents)}</Program>\n'
|
|
567
746
|
iteration_log_xml += f' <Code>{escape(code_contents)}</Code>\n'
|
|
@@ -573,16 +752,46 @@ def fix_verification_errors_loop(
|
|
|
573
752
|
try:
|
|
574
753
|
if verbose:
|
|
575
754
|
console.print("Calling fix_verification_errors...")
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
755
|
+
# Use cloud or local based on use_cloud parameter
|
|
756
|
+
if use_cloud:
|
|
757
|
+
try:
|
|
758
|
+
fix_result = cloud_verify_fix(
|
|
759
|
+
program=program_contents,
|
|
760
|
+
prompt=prompt,
|
|
761
|
+
code=code_contents,
|
|
762
|
+
output=program_output,
|
|
763
|
+
strength=strength,
|
|
764
|
+
temperature=temperature,
|
|
765
|
+
time_param=llm_time,
|
|
766
|
+
verbose=verbose,
|
|
767
|
+
language="python" if is_python else get_language(os.path.splitext(code_file)[1]),
|
|
768
|
+
)
|
|
769
|
+
if verbose:
|
|
770
|
+
console.print(f"[cyan]Cloud verify fix completed.[/cyan]")
|
|
771
|
+
except (requests.exceptions.RequestException, RuntimeError) as cloud_err:
|
|
772
|
+
# Cloud failed - fall back to local
|
|
773
|
+
console.print(f"[yellow]Cloud verify fix failed: {cloud_err}. Falling back to local.[/yellow]")
|
|
774
|
+
fix_result = fix_verification_errors(
|
|
775
|
+
program=program_contents,
|
|
776
|
+
prompt=prompt,
|
|
777
|
+
code=code_contents,
|
|
778
|
+
output=program_output,
|
|
779
|
+
strength=strength,
|
|
780
|
+
temperature=temperature,
|
|
781
|
+
verbose=verbose,
|
|
782
|
+
time=llm_time
|
|
783
|
+
)
|
|
784
|
+
else:
|
|
785
|
+
fix_result = fix_verification_errors(
|
|
786
|
+
program=program_contents,
|
|
787
|
+
prompt=prompt,
|
|
788
|
+
code=code_contents,
|
|
789
|
+
output=program_output,
|
|
790
|
+
strength=strength,
|
|
791
|
+
temperature=temperature,
|
|
792
|
+
verbose=verbose,
|
|
793
|
+
time=llm_time # Pass time
|
|
794
|
+
)
|
|
586
795
|
|
|
587
796
|
# 4f: Add cost
|
|
588
797
|
attempt_cost = fix_result.get('total_cost', 0.0)
|