pdd-cli 0.0.45__py3-none-any.whl → 0.0.90__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 +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +73 -21
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +258 -82
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +330 -76
- pdd/fix_error_loop.py +207 -61
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +306 -272
- pdd/fix_verification_main.py +28 -9
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +9 -2
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1269 -103
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +100 -905
- pdd/prompts/detect_change_LLM.prompt +122 -20
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +228 -108
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +49 -6
- pdd/sync_determine_operation.py +543 -98
- pdd/sync_main.py +81 -31
- pdd/sync_orchestration.py +1334 -751
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/update_main.py
CHANGED
|
@@ -1,104 +1,586 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from typing import Tuple, Optional
|
|
2
|
+
from typing import Tuple, Optional, List, Dict, Any
|
|
3
3
|
import click
|
|
4
4
|
from rich import print as rprint
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import git
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import (
|
|
10
|
+
Progress,
|
|
11
|
+
SpinnerColumn,
|
|
12
|
+
TextColumn,
|
|
13
|
+
BarColumn,
|
|
14
|
+
TimeRemainingColumn,
|
|
15
|
+
)
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.theme import Theme
|
|
5
18
|
|
|
6
19
|
from .construct_paths import construct_paths
|
|
20
|
+
from .get_language import get_language
|
|
7
21
|
from .update_prompt import update_prompt
|
|
8
22
|
from .git_update import git_update
|
|
23
|
+
from .agentic_common import get_available_agents
|
|
24
|
+
from .agentic_update import run_agentic_update
|
|
9
25
|
from . import DEFAULT_TIME
|
|
26
|
+
|
|
27
|
+
custom_theme = Theme({
|
|
28
|
+
"info": "cyan",
|
|
29
|
+
"warning": "yellow",
|
|
30
|
+
"error": "bold red",
|
|
31
|
+
"success": "green",
|
|
32
|
+
"path": "dim blue",
|
|
33
|
+
})
|
|
34
|
+
console = Console(theme=custom_theme)
|
|
35
|
+
|
|
36
|
+
def resolve_prompt_code_pair(code_file_path: str, quiet: bool = False, output_dir: Optional[str] = None) -> Tuple[str, str]:
|
|
37
|
+
"""
|
|
38
|
+
Derives the corresponding prompt file path from a code file path.
|
|
39
|
+
Searches for and creates prompts only in the specified output directory or 'prompts' directory.
|
|
40
|
+
If the prompt file does not exist, it creates an empty one in the target directory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
code_file_path: Path to the code file
|
|
44
|
+
quiet: Whether to suppress output messages
|
|
45
|
+
output_dir: Custom output directory (overrides default 'prompts' directory)
|
|
46
|
+
"""
|
|
47
|
+
language = get_language(os.path.splitext(code_file_path)[1])
|
|
48
|
+
language = language.lower() if language else "unknown"
|
|
49
|
+
|
|
50
|
+
# Extract the filename without extension and directory
|
|
51
|
+
code_filename = os.path.basename(code_file_path)
|
|
52
|
+
base_name, _ = os.path.splitext(code_filename)
|
|
53
|
+
|
|
54
|
+
# Determine the output directory
|
|
55
|
+
if output_dir:
|
|
56
|
+
# Use the custom output directory (absolute path)
|
|
57
|
+
prompts_dir = os.path.abspath(output_dir)
|
|
58
|
+
else:
|
|
59
|
+
# Find the repository root (where the code file is located)
|
|
60
|
+
code_file_abs_path = os.path.abspath(code_file_path)
|
|
61
|
+
code_dir = os.path.dirname(code_file_abs_path)
|
|
62
|
+
|
|
63
|
+
# For repository mode, find the actual repo root
|
|
64
|
+
repo_root = code_dir
|
|
65
|
+
try:
|
|
66
|
+
import git
|
|
67
|
+
repo = git.Repo(code_dir, search_parent_directories=True)
|
|
68
|
+
repo_root = repo.working_tree_dir
|
|
69
|
+
except:
|
|
70
|
+
# If not a git repo, use the directory containing the code file
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# Use the default prompts directory at repo root
|
|
74
|
+
prompts_dir = os.path.join(repo_root, "prompts")
|
|
75
|
+
|
|
76
|
+
# Construct the prompt filename in the determined directory
|
|
77
|
+
prompt_filename = f"{base_name}_{language}.prompt"
|
|
78
|
+
prompt_path_str = os.path.join(prompts_dir, prompt_filename)
|
|
79
|
+
prompt_path = Path(prompt_path_str)
|
|
80
|
+
|
|
81
|
+
# Ensure prompts directory exists
|
|
82
|
+
prompts_path = Path(prompts_dir)
|
|
83
|
+
if not prompts_path.exists():
|
|
84
|
+
try:
|
|
85
|
+
prompts_path.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
if not quiet:
|
|
87
|
+
console.print(f"[success]Created prompts directory:[/success] [path]{prompts_dir}[/path]")
|
|
88
|
+
except OSError as e:
|
|
89
|
+
console.print(f"[error]Failed to create prompts directory {prompts_dir}: {e}[/error]")
|
|
90
|
+
|
|
91
|
+
if not prompt_path.exists():
|
|
92
|
+
try:
|
|
93
|
+
prompt_path.touch()
|
|
94
|
+
if not quiet:
|
|
95
|
+
console.print(f"[success]Created missing prompt file:[/success] [path]{prompt_path_str}[/path]")
|
|
96
|
+
except OSError as e:
|
|
97
|
+
console.print(f"[error]Failed to create file {prompt_path_str}: {e}[/error]")
|
|
98
|
+
# Even if creation fails, return the intended path
|
|
99
|
+
|
|
100
|
+
return prompt_path_str, code_file_path
|
|
101
|
+
|
|
102
|
+
def find_and_resolve_all_pairs(repo_root: str, quiet: bool = False, extensions: Optional[str] = None, output_dir: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
103
|
+
"""
|
|
104
|
+
Scans the repo for code files, resolves their prompt pairs, and returns all pairs.
|
|
105
|
+
"""
|
|
106
|
+
pairs = []
|
|
107
|
+
ignored_dirs = {'.git', '.idea', '.vscode', '__pycache__', 'node_modules', '.venv', 'venv', 'dist', 'build'}
|
|
108
|
+
|
|
109
|
+
if not quiet:
|
|
110
|
+
console.print(f"[info]Scanning repository and resolving prompt/code pairs...[/info]")
|
|
111
|
+
|
|
112
|
+
allowed_extensions: Optional[set] = None
|
|
113
|
+
if extensions:
|
|
114
|
+
ext_list = [e.strip().lower() for e in extensions.split(',')]
|
|
115
|
+
allowed_extensions = {f'.{e}' if not e.startswith('.') else e for e in ext_list}
|
|
116
|
+
if not quiet:
|
|
117
|
+
console.print(f"[info]Filtering for extensions: {', '.join(allowed_extensions)}[/info]")
|
|
118
|
+
|
|
119
|
+
all_files = []
|
|
120
|
+
for root, dirs, files in os.walk(repo_root, topdown=True):
|
|
121
|
+
dirs[:] = [d for d in dirs if d not in ignored_dirs]
|
|
122
|
+
for file in files:
|
|
123
|
+
all_files.append(os.path.join(root, file))
|
|
124
|
+
|
|
125
|
+
code_files = [
|
|
126
|
+
f for f in all_files
|
|
127
|
+
if (
|
|
128
|
+
get_language(f) is not None and
|
|
129
|
+
not f.endswith('.prompt') and
|
|
130
|
+
not os.path.splitext(os.path.basename(f))[0].startswith('test_') and
|
|
131
|
+
not os.path.splitext(os.path.basename(f))[0].endswith('_example')
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if allowed_extensions:
|
|
136
|
+
code_files = [
|
|
137
|
+
f for f in code_files
|
|
138
|
+
if os.path.splitext(f)[1].lower() in allowed_extensions
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
for file_path in code_files:
|
|
142
|
+
prompt_path, code_path = resolve_prompt_code_pair(file_path, quiet, output_dir)
|
|
143
|
+
pairs.append((prompt_path, code_path))
|
|
144
|
+
|
|
145
|
+
return pairs
|
|
146
|
+
|
|
147
|
+
def update_file_pair(prompt_file: str, code_file: str, ctx: click.Context, repo: git.Repo, simple: bool = False) -> Dict[str, Any]:
|
|
148
|
+
"""
|
|
149
|
+
Wrapper to update a single file pair, choosing the correct method based on Git status and prompt content.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
verbose = ctx.obj.get("verbose", False)
|
|
153
|
+
quiet = ctx.obj.get("quiet", False)
|
|
154
|
+
|
|
155
|
+
# Agentic routing - try first before legacy paths
|
|
156
|
+
use_agentic = not simple and get_available_agents()
|
|
157
|
+
|
|
158
|
+
if use_agentic:
|
|
159
|
+
success, message, agentic_cost, provider, changed_files = run_agentic_update(
|
|
160
|
+
prompt_file=prompt_file,
|
|
161
|
+
code_file=code_file,
|
|
162
|
+
test_files=None,
|
|
163
|
+
verbose=verbose,
|
|
164
|
+
quiet=quiet,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if success:
|
|
168
|
+
with open(prompt_file, 'r') as f:
|
|
169
|
+
modified_prompt = f.read()
|
|
170
|
+
return {
|
|
171
|
+
"prompt_file": prompt_file,
|
|
172
|
+
"status": "✅ Success (agentic)",
|
|
173
|
+
"cost": agentic_cost,
|
|
174
|
+
"model": provider,
|
|
175
|
+
"error": "",
|
|
176
|
+
}
|
|
177
|
+
# Agentic failed - fall through to legacy
|
|
178
|
+
|
|
179
|
+
# Legacy path: Read the prompt first to decide the strategy.
|
|
180
|
+
try:
|
|
181
|
+
with open(prompt_file, 'r') as f:
|
|
182
|
+
input_prompt = f.read()
|
|
183
|
+
except FileNotFoundError:
|
|
184
|
+
input_prompt = ""
|
|
185
|
+
|
|
186
|
+
relative_code_path = os.path.relpath(code_file, repo.working_tree_dir)
|
|
187
|
+
is_untracked = relative_code_path in repo.untracked_files
|
|
188
|
+
|
|
189
|
+
# GENERATION MODE: Trigger if the file is new OR if the prompt is empty.
|
|
190
|
+
if is_untracked or not input_prompt.strip():
|
|
191
|
+
if not quiet:
|
|
192
|
+
if is_untracked:
|
|
193
|
+
console.print(f"[info]New untracked file detected, generating new prompt for:[/info] [path]{relative_code_path}[/path]")
|
|
194
|
+
else:
|
|
195
|
+
console.print(f"[info]Empty prompt detected, generating new prompt for:[/info] [path]{relative_code_path}[/path]")
|
|
196
|
+
|
|
197
|
+
with open(code_file, 'r') as f:
|
|
198
|
+
modified_code = f.read()
|
|
199
|
+
|
|
200
|
+
modified_prompt, total_cost, model_name = update_prompt(
|
|
201
|
+
input_prompt="no prompt exists yet, create a new one",
|
|
202
|
+
input_code="", # No previous version for generation
|
|
203
|
+
modified_code=modified_code,
|
|
204
|
+
strength=ctx.obj.get("strength", 0.5),
|
|
205
|
+
temperature=ctx.obj.get("temperature", 0),
|
|
206
|
+
verbose=verbose,
|
|
207
|
+
time=ctx.obj.get('time', DEFAULT_TIME),
|
|
208
|
+
)
|
|
209
|
+
# UPDATE MODE: Only trigger if the file is tracked AND the prompt has content.
|
|
210
|
+
else:
|
|
211
|
+
modified_prompt, total_cost, model_name = git_update(
|
|
212
|
+
input_prompt=input_prompt,
|
|
213
|
+
modified_code_file=code_file,
|
|
214
|
+
strength=ctx.obj.get("strength", 0.5),
|
|
215
|
+
temperature=ctx.obj.get("temperature", 0),
|
|
216
|
+
verbose=verbose,
|
|
217
|
+
time=ctx.obj.get('time', DEFAULT_TIME),
|
|
218
|
+
simple=True, # Force legacy since we already tried agentic
|
|
219
|
+
quiet=quiet,
|
|
220
|
+
prompt_file=prompt_file,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if modified_prompt is not None:
|
|
224
|
+
# Overwrite the original prompt file
|
|
225
|
+
with open(prompt_file, "w") as f:
|
|
226
|
+
f.write(modified_prompt)
|
|
227
|
+
return {
|
|
228
|
+
"prompt_file": prompt_file,
|
|
229
|
+
"status": "✅ Success",
|
|
230
|
+
"cost": total_cost,
|
|
231
|
+
"model": model_name,
|
|
232
|
+
"error": "",
|
|
233
|
+
}
|
|
234
|
+
else:
|
|
235
|
+
return {
|
|
236
|
+
"prompt_file": prompt_file,
|
|
237
|
+
"status": "❌ Failed",
|
|
238
|
+
"cost": 0.0,
|
|
239
|
+
"model": "",
|
|
240
|
+
"error": "Update process returned no result.",
|
|
241
|
+
}
|
|
242
|
+
except click.Abort:
|
|
243
|
+
# User cancelled - re-raise to stop the sync loop
|
|
244
|
+
raise
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return {
|
|
247
|
+
"prompt_file": prompt_file,
|
|
248
|
+
"status": "❌ Failed",
|
|
249
|
+
"cost": 0.0,
|
|
250
|
+
"model": "",
|
|
251
|
+
"error": str(e),
|
|
252
|
+
}
|
|
253
|
+
|
|
10
254
|
def update_main(
|
|
11
255
|
ctx: click.Context,
|
|
12
|
-
input_prompt_file: str,
|
|
13
|
-
modified_code_file: str,
|
|
256
|
+
input_prompt_file: Optional[str],
|
|
257
|
+
modified_code_file: Optional[str],
|
|
14
258
|
input_code_file: Optional[str],
|
|
15
259
|
output: Optional[str],
|
|
16
|
-
|
|
17
|
-
|
|
260
|
+
use_git: bool = False,
|
|
261
|
+
repo: bool = False,
|
|
262
|
+
extensions: Optional[str] = None,
|
|
263
|
+
strength: Optional[float] = None,
|
|
264
|
+
temperature: Optional[float] = None,
|
|
265
|
+
simple: bool = False,
|
|
266
|
+
) -> Optional[Tuple[str, float, str]]:
|
|
18
267
|
"""
|
|
19
268
|
CLI wrapper for updating prompts based on modified code.
|
|
269
|
+
Can operate on a single file or an entire repository.
|
|
20
270
|
|
|
21
271
|
:param ctx: Click context object containing CLI options and parameters.
|
|
22
272
|
:param input_prompt_file: Path to the original prompt file.
|
|
23
273
|
:param modified_code_file: Path to the modified code file.
|
|
24
274
|
:param input_code_file: Optional path to the original code file. If None, Git history is used if --git is True.
|
|
25
275
|
:param output: Optional path to save the updated prompt.
|
|
26
|
-
:param
|
|
276
|
+
:param use_git: Use Git history to retrieve the original code if True.
|
|
277
|
+
:param repo: If True, run in repository-wide mode.
|
|
278
|
+
:param extensions: Comma-separated string of file extensions to filter by in repo mode.
|
|
279
|
+
:param strength: Optional strength parameter (overrides ctx.obj if provided).
|
|
280
|
+
:param temperature: Optional temperature parameter (overrides ctx.obj if provided).
|
|
27
281
|
:return: Tuple containing the updated prompt, total cost, and model name.
|
|
28
282
|
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
283
|
+
quiet = ctx.obj.get("quiet", False)
|
|
284
|
+
# Resolve strength/temperature (prefer passed parameters over ctx.obj)
|
|
285
|
+
resolved_strength = strength if strength is not None else ctx.obj.get("strength", 0.5)
|
|
286
|
+
resolved_temperature = temperature if temperature is not None else ctx.obj.get("temperature", 0)
|
|
287
|
+
# Update ctx.obj so internal calls use the resolved values
|
|
288
|
+
ctx.obj["strength"] = resolved_strength
|
|
289
|
+
ctx.obj["temperature"] = resolved_temperature
|
|
290
|
+
if repo:
|
|
291
|
+
try:
|
|
292
|
+
# Find the repo root by searching up from the current directory
|
|
293
|
+
repo_obj = git.Repo(os.getcwd(), search_parent_directories=True)
|
|
294
|
+
repo_root = repo_obj.working_tree_dir
|
|
295
|
+
except git.InvalidGitRepositoryError:
|
|
296
|
+
rprint("[bold red]Error:[/bold red] Repository-wide mode requires the current directory to be within a Git repository.")
|
|
297
|
+
# Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
pairs = find_and_resolve_all_pairs(repo_root, quiet, extensions, output)
|
|
301
|
+
|
|
302
|
+
if not pairs:
|
|
303
|
+
rprint("[info]No scannable code files found in the repository.[/info]")
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
rprint(f"[info]Found {len(pairs)} total prompt/code pairs to process.[/info]")
|
|
307
|
+
|
|
308
|
+
results = []
|
|
309
|
+
total_repo_cost = 0.0
|
|
310
|
+
|
|
311
|
+
progress = Progress(
|
|
312
|
+
SpinnerColumn(),
|
|
313
|
+
TextColumn("[progress.description]{task.description}", justify="right"),
|
|
314
|
+
BarColumn(bar_width=None),
|
|
315
|
+
TextColumn("[progress.percentage]{task.percentage:>3.1f}%"),
|
|
316
|
+
TextColumn("•"),
|
|
317
|
+
TimeRemainingColumn(),
|
|
318
|
+
TextColumn("•"),
|
|
319
|
+
TextColumn("Total Cost: $[bold green]{task.fields[total_cost]:.6f}[/bold green]"),
|
|
320
|
+
console=console,
|
|
321
|
+
transient=True,
|
|
51
322
|
)
|
|
52
323
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
324
|
+
with progress:
|
|
325
|
+
task = progress.add_task(
|
|
326
|
+
"Updating prompts...",
|
|
327
|
+
total=len(pairs),
|
|
328
|
+
total_cost=0.0
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
for prompt_path, code_path in pairs:
|
|
332
|
+
relative_path = os.path.relpath(code_path, repo_root)
|
|
333
|
+
progress.update(task, description=f"Processing [path]{relative_path}[/path]")
|
|
334
|
+
|
|
335
|
+
result = update_file_pair(prompt_path, code_path, ctx, repo_obj, simple=simple)
|
|
336
|
+
results.append(result)
|
|
337
|
+
|
|
338
|
+
total_repo_cost += result.get("cost", 0.0)
|
|
339
|
+
|
|
340
|
+
progress.update(task, advance=1, total_cost=total_repo_cost)
|
|
58
341
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
342
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
343
|
+
table.add_column("Prompt File", style="dim", width=50)
|
|
344
|
+
table.add_column("Status")
|
|
345
|
+
table.add_column("Cost", justify="right")
|
|
346
|
+
table.add_column("Model")
|
|
347
|
+
table.add_column("Error", style="error")
|
|
348
|
+
|
|
349
|
+
models_used = set()
|
|
350
|
+
for res in sorted(results, key=lambda x: x["prompt_file"]):
|
|
351
|
+
table.add_row(
|
|
352
|
+
os.path.relpath(res["prompt_file"], repo_root),
|
|
353
|
+
res["status"],
|
|
354
|
+
f"${res['cost']:.6f}",
|
|
355
|
+
res["model"],
|
|
356
|
+
res["error"],
|
|
70
357
|
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
358
|
+
if res["model"]:
|
|
359
|
+
models_used.add(res["model"])
|
|
360
|
+
|
|
361
|
+
console.print("\n[bold]Repository Update Summary[/bold]")
|
|
362
|
+
console.print(table)
|
|
363
|
+
console.print(f"\n[bold]Total Estimated Cost: ${total_repo_cost:.6f}[/bold]")
|
|
364
|
+
|
|
365
|
+
final_model_str = ", ".join(sorted(models_used)) if models_used else "N/A"
|
|
366
|
+
return "Repository update complete.", total_repo_cost, final_model_str
|
|
367
|
+
|
|
368
|
+
# --- Single file logic ---
|
|
369
|
+
try:
|
|
370
|
+
# Case 1: Regeneration Mode.
|
|
371
|
+
# Triggered when ONLY the modified_code_file is provided.
|
|
372
|
+
# This creates a new prompt or overwrites an existing one from scratch.
|
|
373
|
+
is_regeneration_mode = (input_prompt_file is None and input_code_file is None)
|
|
374
|
+
|
|
375
|
+
if is_regeneration_mode:
|
|
376
|
+
if not quiet:
|
|
377
|
+
rprint("[bold yellow]Regeneration mode: Creating or overwriting prompt from code file.[/bold yellow]")
|
|
378
|
+
|
|
379
|
+
# Determine output path based on --output flag
|
|
380
|
+
if output:
|
|
381
|
+
# Check if output is a directory or file path
|
|
382
|
+
if os.path.isdir(output) or output.endswith('/'):
|
|
383
|
+
# Output is a directory, pass as output_dir to resolve_prompt_code_pair
|
|
384
|
+
prompt_path, _ = resolve_prompt_code_pair(modified_code_file, quiet, output)
|
|
385
|
+
else:
|
|
386
|
+
# Output is a specific file path, use it directly
|
|
387
|
+
prompt_path = os.path.abspath(output)
|
|
388
|
+
# Ensure the directory exists
|
|
389
|
+
os.makedirs(os.path.dirname(prompt_path), exist_ok=True)
|
|
390
|
+
else:
|
|
391
|
+
# No output specified, use default behavior
|
|
392
|
+
prompt_path, _ = resolve_prompt_code_pair(modified_code_file, quiet)
|
|
393
|
+
|
|
394
|
+
# Agentic routing for regeneration mode
|
|
395
|
+
use_agentic = not simple and get_available_agents()
|
|
396
|
+
verbose = ctx.obj.get("verbose", False)
|
|
397
|
+
|
|
398
|
+
if use_agentic:
|
|
399
|
+
# Ensure prompt file exists for agentic
|
|
400
|
+
Path(prompt_path).touch(exist_ok=True)
|
|
401
|
+
|
|
402
|
+
success, message, agentic_cost, provider, changed_files = run_agentic_update(
|
|
403
|
+
prompt_file=prompt_path,
|
|
404
|
+
code_file=modified_code_file,
|
|
405
|
+
test_files=None,
|
|
406
|
+
verbose=verbose,
|
|
407
|
+
quiet=quiet,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if success:
|
|
411
|
+
with open(prompt_path, 'r') as f:
|
|
412
|
+
generated_prompt = f.read()
|
|
413
|
+
|
|
414
|
+
if not quiet:
|
|
415
|
+
rprint("[bold green]Prompt generated successfully (agentic).[/bold green]")
|
|
416
|
+
rprint(f"[bold]Provider:[/bold] {provider}")
|
|
417
|
+
rprint(f"[bold]Total cost:[/bold] ${agentic_cost:.6f}")
|
|
418
|
+
rprint(f"[bold]Prompt saved to:[/bold] {prompt_path}")
|
|
419
|
+
|
|
420
|
+
return generated_prompt, agentic_cost, provider
|
|
421
|
+
|
|
422
|
+
# Agentic failed - fall through to legacy
|
|
423
|
+
if not quiet:
|
|
424
|
+
rprint(f"[warning]Agentic failed: {message}. Falling back to legacy.[/warning]")
|
|
425
|
+
|
|
426
|
+
# Legacy path
|
|
427
|
+
with open(modified_code_file, 'r') as f:
|
|
428
|
+
modified_code_content = f.read()
|
|
429
|
+
|
|
74
430
|
modified_prompt, total_cost, model_name = update_prompt(
|
|
75
|
-
input_prompt=
|
|
76
|
-
input_code=
|
|
77
|
-
modified_code=
|
|
431
|
+
input_prompt="no prompt exists yet, create a new one",
|
|
432
|
+
input_code="",
|
|
433
|
+
modified_code=modified_code_content,
|
|
78
434
|
strength=ctx.obj.get("strength", 0.5),
|
|
79
435
|
temperature=ctx.obj.get("temperature", 0),
|
|
80
|
-
verbose=
|
|
81
|
-
time=time
|
|
436
|
+
verbose=verbose,
|
|
437
|
+
time=ctx.obj.get('time', DEFAULT_TIME)
|
|
82
438
|
)
|
|
83
439
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
440
|
+
# Write the result to the derived/correct prompt path.
|
|
441
|
+
with open(prompt_path, "w") as f:
|
|
442
|
+
f.write(modified_prompt)
|
|
443
|
+
|
|
444
|
+
if not quiet:
|
|
445
|
+
rprint("[bold green]Prompt generated successfully.[/bold green]")
|
|
446
|
+
rprint(f"[bold]Model used:[/bold] {model_name}")
|
|
447
|
+
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
448
|
+
rprint(f"[bold]Prompt saved to:[/bold] {prompt_path}")
|
|
449
|
+
|
|
450
|
+
return modified_prompt, total_cost, model_name
|
|
451
|
+
|
|
452
|
+
# Case 2: True Update Mode.
|
|
453
|
+
# Triggered when the user provides the prompt file, indicating a desire to update it.
|
|
454
|
+
else:
|
|
455
|
+
actual_input_prompt_file = input_prompt_file
|
|
456
|
+
final_output_path = output or actual_input_prompt_file
|
|
457
|
+
verbose = ctx.obj.get("verbose", False)
|
|
458
|
+
|
|
459
|
+
# Agentic routing for true update mode (try before construct_paths)
|
|
460
|
+
use_agentic = not simple and get_available_agents()
|
|
461
|
+
|
|
462
|
+
if use_agentic:
|
|
463
|
+
success, message, agentic_cost, provider, changed_files = run_agentic_update(
|
|
464
|
+
prompt_file=actual_input_prompt_file,
|
|
465
|
+
code_file=modified_code_file,
|
|
466
|
+
test_files=None,
|
|
467
|
+
verbose=verbose,
|
|
468
|
+
quiet=quiet,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if success:
|
|
472
|
+
with open(actual_input_prompt_file, 'r') as f:
|
|
473
|
+
updated_prompt = f.read()
|
|
474
|
+
|
|
475
|
+
# Handle output path if different from input
|
|
476
|
+
if final_output_path != actual_input_prompt_file:
|
|
477
|
+
with open(final_output_path, 'w') as f:
|
|
478
|
+
f.write(updated_prompt)
|
|
479
|
+
|
|
480
|
+
if not quiet:
|
|
481
|
+
rprint("[bold green]Prompt updated successfully (agentic).[/bold green]")
|
|
482
|
+
rprint(f"[bold]Provider:[/bold] {provider}")
|
|
483
|
+
rprint(f"[bold]Total cost:[/bold] ${agentic_cost:.6f}")
|
|
484
|
+
rprint(f"[bold]Updated prompt saved to:[/bold] {final_output_path}")
|
|
485
|
+
|
|
486
|
+
return updated_prompt, agentic_cost, provider
|
|
487
|
+
|
|
488
|
+
# Agentic failed - fall through to legacy
|
|
489
|
+
if not quiet:
|
|
490
|
+
rprint(f"[warning]Agentic failed: {message}. Falling back to legacy.[/warning]")
|
|
491
|
+
|
|
492
|
+
# Legacy path: Prepare input_file_paths for construct_paths
|
|
493
|
+
input_file_paths = {
|
|
494
|
+
"input_prompt_file": actual_input_prompt_file,
|
|
495
|
+
"modified_code_file": modified_code_file
|
|
496
|
+
}
|
|
497
|
+
if input_code_file:
|
|
498
|
+
input_file_paths["input_code_file"] = input_code_file
|
|
499
|
+
|
|
500
|
+
command_options = {"output": final_output_path}
|
|
501
|
+
|
|
502
|
+
_, input_strings, output_file_paths, _ = construct_paths(
|
|
503
|
+
input_file_paths=input_file_paths,
|
|
504
|
+
force=ctx.obj.get("force", False),
|
|
505
|
+
quiet=quiet,
|
|
506
|
+
command="update",
|
|
507
|
+
command_options=command_options,
|
|
508
|
+
context_override=ctx.obj.get('context'),
|
|
509
|
+
confirm_callback=ctx.obj.get('confirm_callback')
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
input_prompt = input_strings["input_prompt_file"]
|
|
513
|
+
modified_code = input_strings["modified_code_file"]
|
|
514
|
+
input_code = input_strings.get("input_code_file")
|
|
515
|
+
time = ctx.obj.get('time', DEFAULT_TIME)
|
|
516
|
+
|
|
517
|
+
if not modified_code.strip():
|
|
518
|
+
raise ValueError("Modified code file cannot be empty when updating or generating a prompt.")
|
|
519
|
+
|
|
520
|
+
if not input_prompt.strip():
|
|
521
|
+
input_prompt = "no prompt exists yet, create a new one"
|
|
522
|
+
if not use_git and input_code is None:
|
|
523
|
+
input_code = ""
|
|
524
|
+
if not quiet:
|
|
525
|
+
rprint("[bold yellow]Empty prompt file detected. Generating a new prompt from the modified code.[/bold yellow]")
|
|
526
|
+
|
|
527
|
+
if use_git:
|
|
528
|
+
if input_code_file:
|
|
529
|
+
raise ValueError("Cannot use both --git and provide an input code file.")
|
|
530
|
+
modified_prompt, total_cost, model_name = git_update(
|
|
531
|
+
input_prompt=input_prompt,
|
|
532
|
+
modified_code_file=modified_code_file,
|
|
533
|
+
strength=ctx.obj.get("strength", 0.5),
|
|
534
|
+
temperature=ctx.obj.get("temperature", 0),
|
|
535
|
+
verbose=verbose,
|
|
536
|
+
time=time,
|
|
537
|
+
simple=True if use_agentic else simple, # Force legacy if agentic was tried
|
|
538
|
+
quiet=quiet,
|
|
539
|
+
prompt_file=actual_input_prompt_file,
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
if input_code is None:
|
|
543
|
+
# This will now only be triggered if --git is not used and no input_code_file is provided,
|
|
544
|
+
# which is an error state for a true update.
|
|
545
|
+
raise ValueError("For a true update, you must either provide an original code file or use the --git flag.")
|
|
546
|
+
|
|
547
|
+
modified_prompt, total_cost, model_name = update_prompt(
|
|
548
|
+
input_prompt=input_prompt,
|
|
549
|
+
input_code=input_code,
|
|
550
|
+
modified_code=modified_code,
|
|
551
|
+
strength=ctx.obj.get("strength", 0.5),
|
|
552
|
+
temperature=ctx.obj.get("temperature", 0),
|
|
553
|
+
verbose=verbose,
|
|
554
|
+
time=time
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Defense-in-depth: validate prompt is not empty before writing
|
|
558
|
+
if not modified_prompt or not modified_prompt.strip():
|
|
559
|
+
raise ValueError(
|
|
560
|
+
"Update produced an empty prompt. The LLM may have failed to generate a valid response."
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
with open(output_file_paths["output"], "w") as f:
|
|
564
|
+
f.write(modified_prompt)
|
|
87
565
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
rprint(f"[bold]Updated prompt saved to:[/bold] {output_file_paths['output']}")
|
|
566
|
+
if not quiet:
|
|
567
|
+
rprint("[bold green]Prompt updated successfully.[/bold green]")
|
|
568
|
+
rprint(f"[bold]Model used:[/bold] {model_name}")
|
|
569
|
+
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
570
|
+
rprint(f"[bold]Updated prompt saved to:[/bold] {output_file_paths['output']}")
|
|
94
571
|
|
|
95
|
-
|
|
572
|
+
return modified_prompt, total_cost, model_name
|
|
96
573
|
|
|
97
|
-
except ValueError as e:
|
|
98
|
-
if not
|
|
574
|
+
except (ValueError, git.InvalidGitRepositoryError) as e:
|
|
575
|
+
if not quiet:
|
|
99
576
|
rprint(f"[bold red]Input error:[/bold red] {str(e)}")
|
|
100
|
-
sys.exit(1)
|
|
577
|
+
# Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
|
|
578
|
+
return None
|
|
579
|
+
except click.Abort:
|
|
580
|
+
# User cancelled - re-raise to stop the sync loop
|
|
581
|
+
raise
|
|
101
582
|
except Exception as e:
|
|
102
|
-
if not
|
|
583
|
+
if not quiet:
|
|
103
584
|
rprint(f"[bold red]Error:[/bold red] {str(e)}")
|
|
104
|
-
sys.exit(1)
|
|
585
|
+
# Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
|
|
586
|
+
return None
|
pdd/update_model_costs.py
CHANGED
|
@@ -404,8 +404,8 @@ def main():
|
|
|
404
404
|
parser.add_argument(
|
|
405
405
|
"--csv-path",
|
|
406
406
|
type=str,
|
|
407
|
-
default="
|
|
408
|
-
help="Path to the llm_model.csv file (default:
|
|
407
|
+
default=".pdd/llm_model.csv",
|
|
408
|
+
help="Path to the llm_model.csv file (default: .pdd/llm_model.csv)"
|
|
409
409
|
)
|
|
410
410
|
args = parser.parse_args()
|
|
411
411
|
|