pdd-cli 0.0.30__py3-none-any.whl → 0.0.32__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/change_main.py +2 -2
- pdd/cli.py +35 -4
- pdd/code_generator_main.py +335 -105
- pdd/data/llm_model.csv +1 -1
- pdd/fix_verification_errors_loop.py +57 -31
- pdd/fix_verification_main.py +76 -48
- pdd/incremental_code_generator.py +198 -0
- pdd/prompts/change_LLM.prompt +5 -1
- 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.32.dist-info}/METADATA +7 -7
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.32.dist-info}/RECORD +19 -16
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.32.dist-info}/WHEEL +1 -1
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.32.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.32.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.30.dist-info → pdd_cli-0.0.32.dist-info}/top_level.txt +0 -0
pdd/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.32"
|
|
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/change_main.py
CHANGED
|
@@ -31,6 +31,7 @@ def change_main(
|
|
|
31
31
|
input_prompt_file: Optional[str],
|
|
32
32
|
output: Optional[str],
|
|
33
33
|
use_csv: bool,
|
|
34
|
+
budget: float,
|
|
34
35
|
) -> Tuple[str, float, str]:
|
|
35
36
|
"""
|
|
36
37
|
Handles the core logic for the 'change' command.
|
|
@@ -46,6 +47,7 @@ def change_main(
|
|
|
46
47
|
input_prompt_file: Path to the input prompt file (required in non-CSV mode).
|
|
47
48
|
output: Optional output path (file or directory).
|
|
48
49
|
use_csv: Flag indicating whether to use CSV mode.
|
|
50
|
+
budget: Budget for the operation.
|
|
49
51
|
|
|
50
52
|
Returns:
|
|
51
53
|
A tuple containing:
|
|
@@ -64,8 +66,6 @@ def change_main(
|
|
|
64
66
|
quiet: bool = ctx.obj.get("quiet", False)
|
|
65
67
|
strength: float = ctx.obj.get("strength", DEFAULT_STRENGTH)
|
|
66
68
|
temperature: float = ctx.obj.get("temperature", 0.0)
|
|
67
|
-
# Default budget to 5.0 if not specified - needed for process_csv_change
|
|
68
|
-
budget: float = ctx.obj.get("budget", 5.0)
|
|
69
69
|
# --- Get language and extension from context ---
|
|
70
70
|
# These are crucial for knowing the target code file types, especially in CSV mode
|
|
71
71
|
target_language: str = ctx.obj.get("language", "") # Get from context
|
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:
|
|
@@ -567,6 +589,13 @@ def split(
|
|
|
567
589
|
@click.argument("change_prompt_file", type=click.Path(exists=True, dir_okay=False))
|
|
568
590
|
@click.argument("input_code", type=click.Path(exists=True)) # Can be file or dir
|
|
569
591
|
@click.argument("input_prompt_file", type=click.Path(exists=True, dir_okay=False), required=False)
|
|
592
|
+
@click.option(
|
|
593
|
+
"--budget",
|
|
594
|
+
type=float,
|
|
595
|
+
default=5.0,
|
|
596
|
+
show_default=True,
|
|
597
|
+
help="Maximum cost allowed for the change process.",
|
|
598
|
+
)
|
|
570
599
|
@click.option(
|
|
571
600
|
"--output",
|
|
572
601
|
type=click.Path(writable=True),
|
|
@@ -589,6 +618,7 @@ def change(
|
|
|
589
618
|
input_prompt_file: Optional[str],
|
|
590
619
|
output: Optional[str],
|
|
591
620
|
use_csv: bool,
|
|
621
|
+
budget: float,
|
|
592
622
|
) -> Optional[Tuple[str | Dict, float, str]]: # Modified return type
|
|
593
623
|
"""Modify prompt(s) based on change instructions."""
|
|
594
624
|
quiet = ctx.obj.get("quiet", False)
|
|
@@ -617,6 +647,7 @@ def change(
|
|
|
617
647
|
input_prompt_file=input_prompt_file,
|
|
618
648
|
output=output,
|
|
619
649
|
use_csv=use_csv,
|
|
650
|
+
budget=budget,
|
|
620
651
|
)
|
|
621
652
|
return result_data, total_cost, model_name
|
|
622
653
|
except (click.UsageError, Exception) as e: # Catch specific and general exceptions
|
|
@@ -1009,7 +1040,7 @@ def verify(
|
|
|
1009
1040
|
quiet = ctx.obj.get("quiet", False)
|
|
1010
1041
|
command_name = "verify"
|
|
1011
1042
|
try:
|
|
1012
|
-
success, final_program, final_code, attempts,
|
|
1043
|
+
success, final_program, final_code, attempts, total_cost_value, model_name_value = fix_verification_main(
|
|
1013
1044
|
ctx=ctx,
|
|
1014
1045
|
prompt_file=prompt_file,
|
|
1015
1046
|
code_file=code_file,
|
|
@@ -1029,7 +1060,7 @@ def verify(
|
|
|
1029
1060
|
"verified_program_path": output_program,
|
|
1030
1061
|
"results_log_path": output_results,
|
|
1031
1062
|
}
|
|
1032
|
-
return result_data,
|
|
1063
|
+
return result_data, total_cost_value, model_name_value
|
|
1033
1064
|
except Exception as e:
|
|
1034
1065
|
handle_error(e, command_name, quiet)
|
|
1035
1066
|
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
|
pdd/data/llm_model.csv
CHANGED
|
@@ -5,7 +5,7 @@ Anthropic,claude-3-5-haiku-20241022,.8,4,1261,,ANTHROPIC_API_KEY,0,True,none
|
|
|
5
5
|
OpenAI,deepseek/deepseek-chat,.27,1.1,1353,https://api.deepseek.com/beta,DEEPSEEK_API_KEY,0,False,none
|
|
6
6
|
Google,vertex_ai/gemini-2.5-flash-preview-04-17,0.15,0.6,1330,,VERTEX_CREDENTIALS,0,True,effort
|
|
7
7
|
Google,gemini-2.5-pro-exp-03-25,1.25,10.0,1360,,GOOGLE_API_KEY,0,True,none
|
|
8
|
-
Anthropic,claude-
|
|
8
|
+
Anthropic,claude-sonnet-4-20250514,3.0,15.0,1340,,ANTHROPIC_API_KEY,64000,True,budget
|
|
9
9
|
Google,vertex_ai/gemini-2.5-pro-preview-05-06,1.25,10.0,1361,,VERTEX_CREDENTIALS,0,True,none
|
|
10
10
|
OpenAI,o4-mini,1.1,4.4,1333,,OPENAI_API_KEY,0,True,effort
|
|
11
11
|
OpenAI,o3,10.0,40.0,1389,,OPENAI_API_KEY,0,True,effort
|