pdd-cli 0.0.30__py3-none-any.whl → 0.0.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pdd-cli might be problematic. Click here for more details.
- pdd/__init__.py +10 -2
- pdd/cli.py +26 -4
- pdd/code_generator_main.py +335 -105
- pdd/fix_verification_errors_loop.py +57 -31
- pdd/fix_verification_main.py +76 -48
- pdd/incremental_code_generator.py +198 -0
- pdd/prompts/code_patcher_LLM.prompt +63 -0
- pdd/prompts/diff_analyzer_LLM.prompt +69 -0
- pdd/prompts/find_verification_errors_LLM.prompt +13 -13
- pdd/prompts/fix_verification_errors_LLM.prompt +17 -16
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/METADATA +3 -3
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/RECORD +16 -13
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/WHEEL +1 -1
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.31.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.31"
|
|
2
2
|
|
|
3
3
|
# Strength parameter used for LLM extraction across the codebase
|
|
4
4
|
# Used in postprocessing, XML tagging, code generation, and other extraction operations. The module should have a large context window and be affordable.
|
|
@@ -6,9 +6,17 @@ EXTRACTION_STRENGTH = 0.9
|
|
|
6
6
|
|
|
7
7
|
DEFAULT_STRENGTH = 0.9
|
|
8
8
|
|
|
9
|
+
DEFAULT_TEMPERATURE = 0.0
|
|
10
|
+
|
|
11
|
+
DEFAULT_TIME = 0.25
|
|
12
|
+
|
|
9
13
|
"""PDD - Prompt Driven Development"""
|
|
10
14
|
|
|
11
15
|
# Define constants used across the package
|
|
12
16
|
DEFAULT_LLM_MODEL = "gpt-4.1-nano"
|
|
17
|
+
# When going to production, set the following constants:
|
|
18
|
+
# REACT_APP_FIREBASE_API_KEY
|
|
19
|
+
# GITHUB_CLIENT_ID
|
|
20
|
+
|
|
21
|
+
# You can add other package-level initializations or imports here
|
|
13
22
|
|
|
14
|
-
# You can add other package-level initializations or imports here
|
pdd/cli.py
CHANGED
|
@@ -239,17 +239,39 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
239
239
|
default=None,
|
|
240
240
|
help="Specify where to save the generated code (file or directory).",
|
|
241
241
|
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--original-prompt",
|
|
244
|
+
"original_prompt_file_path",
|
|
245
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
246
|
+
default=None,
|
|
247
|
+
help="Path to the original prompt file for incremental generation.",
|
|
248
|
+
)
|
|
249
|
+
@click.option(
|
|
250
|
+
"--force-incremental",
|
|
251
|
+
"force_incremental_flag",
|
|
252
|
+
is_flag=True,
|
|
253
|
+
default=False,
|
|
254
|
+
help="Force incremental generation even if full regeneration is suggested.",
|
|
255
|
+
)
|
|
242
256
|
@click.pass_context
|
|
243
257
|
@track_cost
|
|
244
|
-
def generate(
|
|
258
|
+
def generate(
|
|
259
|
+
ctx: click.Context,
|
|
260
|
+
prompt_file: str,
|
|
261
|
+
output: Optional[str],
|
|
262
|
+
original_prompt_file_path: Optional[str],
|
|
263
|
+
force_incremental_flag: bool,
|
|
264
|
+
) -> Optional[Tuple[str, float, str]]: # Modified return type
|
|
245
265
|
"""Create runnable code from a prompt file."""
|
|
246
266
|
quiet = ctx.obj.get("quiet", False)
|
|
247
267
|
command_name = "generate"
|
|
248
268
|
try:
|
|
249
|
-
generated_code, total_cost, model_name = code_generator_main(
|
|
269
|
+
generated_code, incremental, total_cost, model_name = code_generator_main(
|
|
250
270
|
ctx=ctx,
|
|
251
271
|
prompt_file=prompt_file,
|
|
252
272
|
output=output,
|
|
273
|
+
original_prompt_file_path=original_prompt_file_path,
|
|
274
|
+
force_incremental_flag=force_incremental_flag,
|
|
253
275
|
)
|
|
254
276
|
return generated_code, total_cost, model_name
|
|
255
277
|
except Exception as e:
|
|
@@ -1009,7 +1031,7 @@ def verify(
|
|
|
1009
1031
|
quiet = ctx.obj.get("quiet", False)
|
|
1010
1032
|
command_name = "verify"
|
|
1011
1033
|
try:
|
|
1012
|
-
success, final_program, final_code, attempts,
|
|
1034
|
+
success, final_program, final_code, attempts, total_cost_value, model_name_value = fix_verification_main(
|
|
1013
1035
|
ctx=ctx,
|
|
1014
1036
|
prompt_file=prompt_file,
|
|
1015
1037
|
code_file=code_file,
|
|
@@ -1029,7 +1051,7 @@ def verify(
|
|
|
1029
1051
|
"verified_program_path": output_program,
|
|
1030
1052
|
"results_log_path": output_results,
|
|
1031
1053
|
}
|
|
1032
|
-
return result_data,
|
|
1054
|
+
return result_data, total_cost_value, model_name_value
|
|
1033
1055
|
except Exception as e:
|
|
1034
1056
|
handle_error(e, command_name, quiet)
|
|
1035
1057
|
return None # Return None on failure
|
pdd/code_generator_main.py
CHANGED
|
@@ -1,124 +1,354 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Optional, Tuple, Dict, Any, List
|
|
5
9
|
|
|
6
|
-
import
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
7
14
|
|
|
15
|
+
# Relative imports for PDD package structure
|
|
16
|
+
from . import DEFAULT_STRENGTH, DEFAULT_TIME, EXTRACTION_STRENGTH # Assuming these are in __init__.py
|
|
8
17
|
from .construct_paths import construct_paths
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
18
|
+
from .preprocess import preprocess as pdd_preprocess
|
|
19
|
+
from .code_generator import code_generator as local_code_generator_func
|
|
20
|
+
from .incremental_code_generator import incremental_code_generator as incremental_code_generator_func
|
|
21
|
+
from .get_jwt_token import get_jwt_token, AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError
|
|
22
|
+
|
|
23
|
+
# Environment variable names for Firebase/GitHub auth
|
|
24
|
+
FIREBASE_API_KEY_ENV_VAR = "NEXT_PUBLIC_FIREBASE_API_KEY"
|
|
25
|
+
GITHUB_CLIENT_ID_ENV_VAR = "GITHUB_CLIENT_ID"
|
|
26
|
+
PDD_APP_NAME = "PDD Code Generator"
|
|
27
|
+
|
|
28
|
+
# Cloud function URL
|
|
29
|
+
CLOUD_GENERATE_URL = "https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode"
|
|
30
|
+
CLOUD_REQUEST_TIMEOUT = 200 # seconds
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
# --- Git Helper Functions ---
|
|
35
|
+
def _run_git_command(command: List[str], cwd: Optional[str] = None) -> Tuple[int, str, str]:
|
|
36
|
+
"""Runs a git command and returns (return_code, stdout, stderr)."""
|
|
37
|
+
try:
|
|
38
|
+
process = subprocess.run(command, capture_output=True, text=True, check=False, cwd=cwd, encoding='utf-8')
|
|
39
|
+
return process.returncode, process.stdout.strip(), process.stderr.strip()
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
return -1, "", "Git command not found. Ensure git is installed and in your PATH."
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return -2, "", f"Error running git command {' '.join(command)}: {e}"
|
|
44
|
+
|
|
45
|
+
def is_git_repository(path: Optional[str] = None) -> bool:
|
|
46
|
+
"""Checks if the given path (or current dir) is a git repository."""
|
|
47
|
+
start_path = pathlib.Path(path).resolve() if path else pathlib.Path.cwd()
|
|
48
|
+
# Check for .git in current or any parent directory
|
|
49
|
+
current_path = start_path
|
|
50
|
+
while True:
|
|
51
|
+
if (current_path / ".git").is_dir():
|
|
52
|
+
# Verify it's the root of the work tree or inside it
|
|
53
|
+
returncode, stdout, _ = _run_git_command(["git", "rev-parse", "--is-inside-work-tree"], cwd=str(start_path))
|
|
54
|
+
return returncode == 0 and stdout == "true"
|
|
55
|
+
parent = current_path.parent
|
|
56
|
+
if parent == current_path: # Reached root directory
|
|
57
|
+
break
|
|
58
|
+
current_path = parent
|
|
59
|
+
return False
|
|
12
60
|
|
|
13
|
-
def code_generator_main(ctx: click.Context, prompt_file: str, output: Optional[str]) -> Tuple[str, float, str]:
|
|
14
|
-
"""
|
|
15
|
-
Main function to generate code from a prompt file.
|
|
16
61
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
62
|
+
def get_git_committed_content(file_path: str) -> Optional[str]:
|
|
63
|
+
"""Gets the content of the file as it was in the last commit (HEAD)."""
|
|
64
|
+
abs_file_path = pathlib.Path(file_path).resolve()
|
|
65
|
+
if not is_git_repository(str(abs_file_path.parent)):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
returncode_rev, git_root_str, stderr_rev = _run_git_command(["git", "rev-parse", "--show-toplevel"], cwd=str(abs_file_path.parent))
|
|
69
|
+
if returncode_rev != 0:
|
|
70
|
+
# console.print(f"[yellow]Git (rev-parse) warning for {file_path}: {stderr_rev}[/yellow]")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
git_root = pathlib.Path(git_root_str)
|
|
74
|
+
try:
|
|
75
|
+
relative_path = abs_file_path.relative_to(git_root)
|
|
76
|
+
except ValueError:
|
|
77
|
+
# console.print(f"[yellow]File {file_path} is not under git root {git_root}.[/yellow]")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
returncode, stdout, stderr = _run_git_command(["git", "show", f"HEAD:{relative_path.as_posix()}"], cwd=str(git_root))
|
|
81
|
+
if returncode == 0:
|
|
82
|
+
return stdout
|
|
83
|
+
else:
|
|
84
|
+
# File might be new, or other git error. Not necessarily an error for this function's purpose.
|
|
85
|
+
# if "does not exist" not in stderr and "exists on disk, but not in 'HEAD'" not in stderr and console.is_terminal:
|
|
86
|
+
# console.print(f"[yellow]Git (show) warning for {file_path}: {stderr}[/yellow]")
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def get_file_git_status(file_path: str) -> str:
|
|
90
|
+
"""Gets the git status of a single file (e.g., ' M', '??', 'A '). Empty if clean."""
|
|
91
|
+
abs_file_path = pathlib.Path(file_path).resolve()
|
|
92
|
+
if not is_git_repository(str(abs_file_path.parent)) or not abs_file_path.exists():
|
|
93
|
+
return ""
|
|
94
|
+
returncode, stdout, _ = _run_git_command(["git", "status", "--porcelain", str(abs_file_path)], cwd=str(abs_file_path.parent))
|
|
95
|
+
if returncode == 0:
|
|
96
|
+
# stdout might be " M path/to/file" or "?? path/to/file"
|
|
97
|
+
# We only want the status codes part
|
|
98
|
+
status_part = stdout.split(str(abs_file_path.name))[0].strip() if str(abs_file_path.name) in stdout else stdout.strip()
|
|
99
|
+
return status_part
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
def git_add_files(file_paths: List[str], verbose: bool = False) -> bool:
|
|
103
|
+
"""Stages the given files using 'git add'."""
|
|
104
|
+
if not file_paths:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Resolve paths and ensure they are absolute for git command
|
|
108
|
+
abs_paths = [str(pathlib.Path(fp).resolve()) for fp in file_paths]
|
|
109
|
+
|
|
110
|
+
# Determine common parent directory to run git command from, or git root
|
|
111
|
+
# For simplicity, assume they are in the same repo and run from one of their parents
|
|
112
|
+
if not is_git_repository(str(pathlib.Path(abs_paths[0]).parent)):
|
|
113
|
+
if verbose:
|
|
114
|
+
console.print(f"[yellow]Cannot stage files: {abs_paths[0]} is not in a git repository.[/yellow]")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
returncode, _, stderr = _run_git_command(["git", "add"] + abs_paths, cwd=str(pathlib.Path(abs_paths[0]).parent))
|
|
118
|
+
if returncode == 0:
|
|
119
|
+
if verbose:
|
|
120
|
+
console.print(f"Successfully staged: [cyan]{', '.join(abs_paths)}[/cyan]")
|
|
121
|
+
return True
|
|
122
|
+
else:
|
|
123
|
+
console.print(f"[red]Error staging files with git:[/red] {stderr}")
|
|
124
|
+
return False
|
|
125
|
+
# --- End Git Helper Functions ---
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def code_generator_main(
|
|
129
|
+
ctx: click.Context,
|
|
130
|
+
prompt_file: str,
|
|
131
|
+
output: Optional[str],
|
|
132
|
+
original_prompt_file_path: Optional[str],
|
|
133
|
+
force_incremental_flag: bool,
|
|
134
|
+
) -> Tuple[str, bool, float, str]:
|
|
135
|
+
"""
|
|
136
|
+
CLI wrapper for generating code from prompts. Handles full and incremental generation,
|
|
137
|
+
local vs. cloud execution, and output.
|
|
21
138
|
"""
|
|
139
|
+
cli_params = ctx.obj or {}
|
|
140
|
+
is_local_execution_preferred = cli_params.get('local', False)
|
|
141
|
+
strength = cli_params.get('strength', DEFAULT_STRENGTH)
|
|
142
|
+
temperature = cli_params.get('temperature', 0.0)
|
|
143
|
+
time_budget = cli_params.get('time', DEFAULT_TIME)
|
|
144
|
+
verbose = cli_params.get('verbose', False)
|
|
145
|
+
force_overwrite = cli_params.get('force', False)
|
|
146
|
+
quiet = cli_params.get('quiet', False)
|
|
147
|
+
|
|
148
|
+
generated_code_content: Optional[str] = None
|
|
149
|
+
was_incremental_operation = False
|
|
150
|
+
total_cost = 0.0
|
|
151
|
+
model_name = "unknown"
|
|
152
|
+
|
|
153
|
+
input_file_paths_dict: Dict[str, str] = {"prompt_file": prompt_file}
|
|
154
|
+
if original_prompt_file_path:
|
|
155
|
+
input_file_paths_dict["original_prompt_file"] = original_prompt_file_path
|
|
156
|
+
|
|
157
|
+
command_options: Dict[str, Any] = {"output": output}
|
|
158
|
+
|
|
22
159
|
try:
|
|
23
|
-
# Construct file paths
|
|
24
|
-
input_file_paths = {
|
|
25
|
-
"prompt_file": prompt_file
|
|
26
|
-
}
|
|
27
|
-
command_options = {
|
|
28
|
-
"output": output
|
|
29
|
-
}
|
|
30
160
|
input_strings, output_file_paths, language = construct_paths(
|
|
31
|
-
input_file_paths=
|
|
32
|
-
force=
|
|
33
|
-
quiet=
|
|
161
|
+
input_file_paths=input_file_paths_dict,
|
|
162
|
+
force=force_overwrite,
|
|
163
|
+
quiet=quiet,
|
|
34
164
|
command="generate",
|
|
35
|
-
command_options=command_options
|
|
165
|
+
command_options=command_options,
|
|
36
166
|
)
|
|
37
|
-
|
|
38
|
-
# Load input file
|
|
39
167
|
prompt_content = input_strings["prompt_file"]
|
|
168
|
+
output_path = output_file_paths.get("output")
|
|
169
|
+
|
|
170
|
+
except FileNotFoundError as e:
|
|
171
|
+
console.print(f"[red]Error: Input file not found: {e.filename}[/red]")
|
|
172
|
+
return "", False, 0.0, "error"
|
|
173
|
+
except Exception as e:
|
|
174
|
+
console.print(f"[red]Error during path construction: {e}[/red]")
|
|
175
|
+
return "", False, 0.0, "error"
|
|
176
|
+
|
|
177
|
+
can_attempt_incremental = False
|
|
178
|
+
existing_code_content: Optional[str] = None
|
|
179
|
+
original_prompt_content_for_incremental: Optional[str] = None
|
|
180
|
+
|
|
181
|
+
if output_path and pathlib.Path(output_path).exists():
|
|
182
|
+
try:
|
|
183
|
+
existing_code_content = pathlib.Path(output_path).read_text(encoding="utf-8")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
console.print(f"[yellow]Warning: Could not read existing output file {output_path}: {e}[/yellow]")
|
|
186
|
+
existing_code_content = None
|
|
187
|
+
|
|
188
|
+
if existing_code_content is not None:
|
|
189
|
+
if "original_prompt_file" in input_strings:
|
|
190
|
+
original_prompt_content_for_incremental = input_strings["original_prompt_file"]
|
|
191
|
+
can_attempt_incremental = True
|
|
192
|
+
if verbose:
|
|
193
|
+
console.print(f"Using specified original prompt: [cyan]{original_prompt_file_path}[/cyan]")
|
|
194
|
+
elif is_git_repository(str(pathlib.Path(prompt_file).parent)):
|
|
195
|
+
original_prompt_git_content = get_git_committed_content(prompt_file)
|
|
196
|
+
if original_prompt_git_content is not None:
|
|
197
|
+
original_prompt_content_for_incremental = original_prompt_git_content
|
|
198
|
+
can_attempt_incremental = True
|
|
199
|
+
if verbose:
|
|
200
|
+
console.print(f"Using last committed version of [cyan]{prompt_file}[/cyan] as original prompt.")
|
|
201
|
+
elif verbose:
|
|
202
|
+
console.print(f"[yellow]Warning: Could not find committed version of {prompt_file} in git for incremental generation.[/yellow]")
|
|
203
|
+
|
|
204
|
+
if force_incremental_flag and existing_code_content:
|
|
205
|
+
if not (original_prompt_content_for_incremental or "original_prompt_file" in input_strings): # Check if original prompt is actually available
|
|
206
|
+
console.print(
|
|
207
|
+
"[yellow]Warning: --incremental flag used, but original prompt could not be determined. "
|
|
208
|
+
"Falling back to full generation.[/yellow]"
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
can_attempt_incremental = True
|
|
212
|
+
|
|
213
|
+
if force_incremental_flag and (not output_path or not pathlib.Path(output_path).exists()):
|
|
214
|
+
console.print(
|
|
215
|
+
"[yellow]Warning: --incremental flag used, but output file does not exist or path not specified. "
|
|
216
|
+
"Performing full generation.[/yellow]"
|
|
217
|
+
)
|
|
218
|
+
can_attempt_incremental = False
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
if can_attempt_incremental and existing_code_content is not None and original_prompt_content_for_incremental is not None:
|
|
222
|
+
if verbose:
|
|
223
|
+
console.print(Panel("Attempting incremental code generation...", title="[blue]Mode[/blue]", expand=False))
|
|
40
224
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
225
|
+
if is_git_repository(str(pathlib.Path(prompt_file).parent)):
|
|
226
|
+
files_to_stage_for_rollback: List[str] = []
|
|
227
|
+
paths_to_check = [pathlib.Path(prompt_file).resolve()]
|
|
228
|
+
if output_path and pathlib.Path(output_path).exists():
|
|
229
|
+
paths_to_check.append(pathlib.Path(output_path).resolve())
|
|
230
|
+
|
|
231
|
+
for p_to_check in paths_to_check:
|
|
232
|
+
if not p_to_check.exists(): continue
|
|
233
|
+
|
|
234
|
+
is_untracked = get_file_git_status(str(p_to_check)).startswith("??")
|
|
235
|
+
# Check if different from HEAD or untracked
|
|
236
|
+
is_different_from_head_rc = 1 if is_untracked else _run_git_command(["git", "diff", "--quiet", "HEAD", "--", str(p_to_check)], cwd=str(p_to_check.parent))[0]
|
|
237
|
+
|
|
238
|
+
if is_different_from_head_rc != 0: # Different from HEAD or untracked
|
|
239
|
+
files_to_stage_for_rollback.append(str(p_to_check))
|
|
240
|
+
|
|
241
|
+
if files_to_stage_for_rollback:
|
|
242
|
+
git_add_files(files_to_stage_for_rollback, verbose=verbose)
|
|
243
|
+
|
|
244
|
+
generated_code_content, was_incremental_operation, total_cost, model_name = incremental_code_generator_func(
|
|
245
|
+
original_prompt=original_prompt_content_for_incremental,
|
|
246
|
+
new_prompt=prompt_content,
|
|
247
|
+
existing_code=existing_code_content,
|
|
248
|
+
language=language,
|
|
249
|
+
strength=strength,
|
|
250
|
+
temperature=temperature,
|
|
251
|
+
time=time_budget,
|
|
252
|
+
force_incremental=force_incremental_flag,
|
|
253
|
+
verbose=verbose,
|
|
254
|
+
preprocess_prompt=True
|
|
56
255
|
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
import asyncio
|
|
61
|
-
import os
|
|
62
|
-
# Get JWT token for cloud authentication
|
|
63
|
-
jwt_token = asyncio.run(get_jwt_token(
|
|
64
|
-
firebase_api_key=os.environ.get("REACT_APP_FIREBASE_API_KEY"),
|
|
65
|
-
github_client_id=os.environ.get("GITHUB_CLIENT_ID"),
|
|
66
|
-
app_name="PDD Code Generator"
|
|
67
|
-
))
|
|
68
|
-
# Call cloud code generator
|
|
69
|
-
headers = {
|
|
70
|
-
"Authorization": f"Bearer {jwt_token}",
|
|
71
|
-
"Content-Type": "application/json"
|
|
72
|
-
}
|
|
73
|
-
# Preprocess the prompt
|
|
74
|
-
processed_prompt = preprocess(prompt_content, recursive=False, double_curly_brackets=True)
|
|
256
|
+
|
|
257
|
+
if not was_incremental_operation:
|
|
75
258
|
if verbose:
|
|
76
|
-
print(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"language": language,
|
|
80
|
-
"strength": strength,
|
|
81
|
-
"temperature": temperature,
|
|
82
|
-
"verbose": verbose
|
|
83
|
-
}
|
|
84
|
-
response = requests.post(
|
|
85
|
-
"https://us-central1-prompt-driven-development.cloudfunctions.net/generateCode",
|
|
86
|
-
headers=headers,
|
|
87
|
-
json=data
|
|
88
|
-
)
|
|
89
|
-
response.raise_for_status()
|
|
90
|
-
result = response.json()
|
|
91
|
-
generated_code = result["generatedCode"]
|
|
92
|
-
total_cost = result["totalCost"]
|
|
93
|
-
model_name = result["modelName"]
|
|
94
|
-
|
|
95
|
-
except Exception as e:
|
|
96
|
-
if not ctx.obj.get('quiet', False):
|
|
97
|
-
rprint("[bold red]Cloud execution failed, falling back to local mode[/bold red]")
|
|
98
|
-
generated_code, total_cost, model_name = code_generator(
|
|
99
|
-
prompt_content,
|
|
100
|
-
language,
|
|
101
|
-
strength,
|
|
102
|
-
temperature,
|
|
103
|
-
verbose=verbose
|
|
104
|
-
)
|
|
259
|
+
console.print(Panel("Incremental generator suggested full regeneration. Falling back.", title="[yellow]Fallback[/yellow]", expand=False))
|
|
260
|
+
elif verbose:
|
|
261
|
+
console.print(Panel(f"Incremental update successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Incremental Success[/green]", expand=False))
|
|
105
262
|
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
263
|
+
if not was_incremental_operation: # Full generation path
|
|
264
|
+
if verbose:
|
|
265
|
+
console.print(Panel("Performing full code generation...", title="[blue]Mode[/blue]", expand=False))
|
|
266
|
+
|
|
267
|
+
current_execution_is_local = is_local_execution_preferred
|
|
268
|
+
|
|
269
|
+
if not current_execution_is_local:
|
|
270
|
+
if verbose: console.print("Attempting cloud code generation...")
|
|
271
|
+
|
|
272
|
+
processed_prompt_for_cloud = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=True, exclude_keys=[])
|
|
273
|
+
if verbose: console.print(Panel(Text(processed_prompt_for_cloud, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
|
|
274
|
+
|
|
275
|
+
jwt_token: Optional[str] = None
|
|
276
|
+
try:
|
|
277
|
+
firebase_api_key_val = os.environ.get(FIREBASE_API_KEY_ENV_VAR)
|
|
278
|
+
github_client_id_val = os.environ.get(GITHUB_CLIENT_ID_ENV_VAR)
|
|
110
279
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rprint("[bold green]Code generation completed successfully.[/bold green]")
|
|
114
|
-
rprint(f"[bold]Model used:[/bold] {model_name}")
|
|
115
|
-
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
116
|
-
if output:
|
|
117
|
-
rprint(f"[bold]Code saved to:[/bold] {output_file_paths['output']}")
|
|
280
|
+
if not firebase_api_key_val: raise AuthError(f"{FIREBASE_API_KEY_ENV_VAR} not set.")
|
|
281
|
+
if not github_client_id_val: raise AuthError(f"{GITHUB_CLIENT_ID_ENV_VAR} not set.")
|
|
118
282
|
|
|
119
|
-
|
|
283
|
+
jwt_token = asyncio.run(get_jwt_token(
|
|
284
|
+
firebase_api_key=firebase_api_key_val,
|
|
285
|
+
github_client_id=github_client_id_val,
|
|
286
|
+
app_name=PDD_APP_NAME
|
|
287
|
+
))
|
|
288
|
+
except (AuthError, NetworkError, TokenError, UserCancelledError, RateLimitError) as e:
|
|
289
|
+
console.print(f"[yellow]Cloud authentication/token error: {e}. Falling back to local execution.[/yellow]")
|
|
290
|
+
current_execution_is_local = True
|
|
291
|
+
except Exception as e:
|
|
292
|
+
console.print(f"[yellow]Unexpected error during cloud authentication: {e}. Falling back to local execution.[/yellow]")
|
|
293
|
+
current_execution_is_local = True
|
|
294
|
+
|
|
295
|
+
if jwt_token and not current_execution_is_local:
|
|
296
|
+
payload = {"promptContent": processed_prompt_for_cloud, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
|
|
297
|
+
headers = {"Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json"}
|
|
298
|
+
try:
|
|
299
|
+
response = requests.post(CLOUD_GENERATE_URL, json=payload, headers=headers, timeout=CLOUD_REQUEST_TIMEOUT)
|
|
300
|
+
response.raise_for_status()
|
|
301
|
+
|
|
302
|
+
response_data = response.json()
|
|
303
|
+
generated_code_content = response_data.get("generatedCode")
|
|
304
|
+
total_cost = float(response_data.get("totalCost", 0.0))
|
|
305
|
+
model_name = response_data.get("modelName", "cloud_model")
|
|
306
|
+
|
|
307
|
+
if generated_code_content is None:
|
|
308
|
+
console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
|
|
309
|
+
current_execution_is_local = True
|
|
310
|
+
elif verbose:
|
|
311
|
+
console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
|
|
312
|
+
except requests.exceptions.Timeout:
|
|
313
|
+
console.print(f"[yellow]Cloud execution timed out ({CLOUD_REQUEST_TIMEOUT}s). Falling back to local.[/yellow]")
|
|
314
|
+
current_execution_is_local = True
|
|
315
|
+
except requests.exceptions.HTTPError as e:
|
|
316
|
+
err_content = e.response.text[:200] if e.response else "No response content"
|
|
317
|
+
console.print(f"[yellow]Cloud HTTP error ({e.response.status_code}): {err_content}. Falling back to local.[/yellow]")
|
|
318
|
+
current_execution_is_local = True
|
|
319
|
+
except requests.exceptions.RequestException as e:
|
|
320
|
+
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
|
|
321
|
+
current_execution_is_local = True
|
|
322
|
+
except json.JSONDecodeError:
|
|
323
|
+
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
|
|
324
|
+
current_execution_is_local = True
|
|
325
|
+
|
|
326
|
+
if current_execution_is_local:
|
|
327
|
+
if verbose: console.print("Executing code generator locally...")
|
|
328
|
+
generated_code_content, total_cost, model_name = local_code_generator_func(
|
|
329
|
+
prompt=prompt_content, language=language, strength=strength,
|
|
330
|
+
temperature=temperature, verbose=verbose
|
|
331
|
+
)
|
|
332
|
+
if verbose:
|
|
333
|
+
console.print(Panel(f"Local generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Local Success[/green]", expand=False))
|
|
334
|
+
|
|
335
|
+
if generated_code_content is not None:
|
|
336
|
+
if output_path:
|
|
337
|
+
p_output = pathlib.Path(output_path)
|
|
338
|
+
p_output.parent.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
p_output.write_text(generated_code_content, encoding="utf-8")
|
|
340
|
+
if verbose or not quiet:
|
|
341
|
+
console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
|
|
342
|
+
elif not quiet: # No output path, print to console if not quiet
|
|
343
|
+
console.print(Panel(Text(generated_code_content, overflow="fold"), title="[cyan]Generated Code[/cyan]", expand=True))
|
|
344
|
+
else:
|
|
345
|
+
console.print("[red]Error: Code generation failed. No code was produced.[/red]")
|
|
346
|
+
return "", was_incremental_operation, total_cost, model_name or "error"
|
|
120
347
|
|
|
121
348
|
except Exception as e:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
349
|
+
console.print(f"[red]An unexpected error occurred: {e}[/red]")
|
|
350
|
+
import traceback
|
|
351
|
+
if verbose: console.print(traceback.format_exc())
|
|
352
|
+
return "", was_incremental_operation, total_cost, "error"
|
|
353
|
+
|
|
354
|
+
return generated_code_content or "", was_incremental_operation, total_cost, model_name
|
|
@@ -507,44 +507,70 @@ def fix_verification_errors_loop(
|
|
|
507
507
|
# FIX: Restructured logic for success check and secondary verification
|
|
508
508
|
secondary_verification_passed = True # Assume pass unless changes made and verification fails
|
|
509
509
|
changes_applied_this_iteration = False
|
|
510
|
+
verify_ret_code = 0 # Default for skipped verification
|
|
511
|
+
verify_output = "Secondary verification not run." # Default for skipped
|
|
510
512
|
|
|
511
|
-
# Run secondary verification ONLY if code was updated
|
|
512
513
|
if code_updated:
|
|
513
514
|
if verbose:
|
|
514
|
-
console.print("Code change suggested,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
console.print("Code change suggested, attempting secondary verification...")
|
|
516
|
+
|
|
517
|
+
if verification_program is not None and verification_program_path.is_file():
|
|
518
|
+
try:
|
|
519
|
+
# Temporarily write the proposed code change
|
|
520
|
+
code_path.write_text(fixed_code, encoding="utf-8")
|
|
518
521
|
|
|
519
|
-
|
|
520
|
-
|
|
522
|
+
# Run verification program
|
|
523
|
+
# Consider if verification_program_path needs arguments or specific env vars
|
|
524
|
+
# For now, assuming it can run directly or uses env vars set externally
|
|
525
|
+
current_verify_ret_code, current_verify_output = _run_program(verification_program_path)
|
|
521
526
|
|
|
522
|
-
|
|
523
|
-
|
|
527
|
+
# Determine pass/fail (simple: exit code 0 = pass)
|
|
528
|
+
secondary_verification_passed = (current_verify_ret_code == 0)
|
|
529
|
+
verify_ret_code = current_verify_ret_code
|
|
530
|
+
verify_output = current_verify_output
|
|
524
531
|
|
|
532
|
+
if verbose:
|
|
533
|
+
console.print(f"Secondary verification ran. Exit code: {verify_ret_code}")
|
|
534
|
+
console.print(f"Secondary verification passed: {secondary_verification_passed}")
|
|
535
|
+
# console.print(f"Secondary verification output:\\n{verify_output}")
|
|
536
|
+
|
|
537
|
+
if not secondary_verification_passed:
|
|
538
|
+
console.print("[yellow]Secondary verification failed. Restoring code file from memory.[/yellow]")
|
|
539
|
+
code_path.write_text(code_contents, encoding="utf-8") # Restore from memory state before this attempt
|
|
540
|
+
|
|
541
|
+
except IOError as e:
|
|
542
|
+
console.print(f"[bold red]Error during secondary verification I/O: {e}[/bold red]")
|
|
543
|
+
verify_output = f"Error during secondary verification I/O: {str(e)}"
|
|
544
|
+
secondary_verification_passed = False # Treat I/O error as failure
|
|
545
|
+
verify_ret_code = -1 # Indicate error
|
|
546
|
+
try:
|
|
547
|
+
code_path.write_text(code_contents, encoding="utf-8")
|
|
548
|
+
except IOError:
|
|
549
|
+
console.print(f"[bold red]Failed to restore code file after I/O error.[/bold red]")
|
|
550
|
+
else:
|
|
551
|
+
# No valid verification program provided, or it's not a file
|
|
552
|
+
secondary_verification_passed = True # Effectively skipped, so it doesn't block progress
|
|
553
|
+
verify_ret_code = 0
|
|
554
|
+
if verification_program is None:
|
|
555
|
+
verify_output = "Secondary verification skipped: No verification program provided."
|
|
556
|
+
else:
|
|
557
|
+
verify_output = f"Secondary verification skipped: Verification program '{verification_program}' not found or is not a file at '{verification_program_path}'."
|
|
525
558
|
if verbose:
|
|
526
|
-
console.print(f"
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
console.print(f"[bold red]Error during secondary verification I/O: {e}[/bold red]")
|
|
542
|
-
iteration_log_xml += f' <Status>Error during secondary verification I/O: {escape(str(e))}</Status>\n'
|
|
543
|
-
secondary_verification_passed = False # Treat I/O error as failure
|
|
544
|
-
try:
|
|
545
|
-
code_path.write_text(code_contents, encoding="utf-8")
|
|
546
|
-
except IOError:
|
|
547
|
-
console.print(f"[bold red]Failed to restore code file after I/O error.[/bold red]")
|
|
559
|
+
console.print(f"[dim]{verify_output}[/dim]")
|
|
560
|
+
else:
|
|
561
|
+
# Code was not updated by the fixer, so secondary verification is not strictly needed
|
|
562
|
+
secondary_verification_passed = True # No changes to verify
|
|
563
|
+
verify_ret_code = 0
|
|
564
|
+
verify_output = "Secondary verification not needed: Code was not modified by the fixer."
|
|
565
|
+
if verbose:
|
|
566
|
+
console.print(f"[dim]{verify_output}[/dim]")
|
|
567
|
+
|
|
568
|
+
# Always log the SecondaryVerification block
|
|
569
|
+
passed_str = str(secondary_verification_passed).lower()
|
|
570
|
+
iteration_log_xml += f' <SecondaryVerification passed="{passed_str}">\n'
|
|
571
|
+
iteration_log_xml += f' <ExitCode>{verify_ret_code}</ExitCode>\n'
|
|
572
|
+
iteration_log_xml += f' <Output>{escape(verify_output)}</Output>\n'
|
|
573
|
+
iteration_log_xml += f' </SecondaryVerification>\n'
|
|
548
574
|
|
|
549
575
|
# Now, decide outcome based on issue count and verification status
|
|
550
576
|
if secondary_verification_passed:
|