pdd-cli 0.0.42__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 +80 -19
- 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 +281 -81
- 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 -62
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +331 -77
- pdd/fix_error_loop.py +209 -60
- 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 +319 -272
- pdd/fix_verification_main.py +57 -17
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +48 -9
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/increase_tests.py +7 -0
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1278 -110
- 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 +98 -101
- pdd/prompts/change_LLM.prompt +1 -3
- pdd/prompts/detect_change_LLM.prompt +562 -3
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
- 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 +16 -4
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -2
- pdd/prompts/insert_includes_LLM.prompt +1181 -6
- pdd/prompts/split_LLM.prompt +1 -62
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/prompts/xml_convertor_LLM.prompt +3246 -7
- pdd/pytest_output.py +188 -21
- pdd/python_env_detector.py +151 -0
- 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 +56 -7
- pdd/sync_determine_operation.py +918 -186
- pdd/sync_main.py +82 -32
- pdd/sync_orchestration.py +1456 -453
- 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.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.42.dist-info/RECORD +0 -115
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/trace.py
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
|
-
from typing import Tuple, Optional
|
|
1
|
+
from typing import Tuple, Optional, List
|
|
2
2
|
from rich import print
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
import difflib
|
|
6
|
+
import re
|
|
6
7
|
from .load_prompt_template import load_prompt_template
|
|
7
8
|
from .preprocess import preprocess
|
|
8
9
|
from .llm_invoke import llm_invoke
|
|
9
10
|
from . import DEFAULT_TIME, DEFAULT_STRENGTH
|
|
10
11
|
console = Console()
|
|
11
12
|
|
|
13
|
+
|
|
14
|
+
def _normalize_text(value: str) -> str:
|
|
15
|
+
if value is None:
|
|
16
|
+
return ""
|
|
17
|
+
value = value.replace("\u201c", '"').replace("\u201d", '"')
|
|
18
|
+
value = value.replace("\u2018", "'").replace("\u2019", "'")
|
|
19
|
+
value = value.replace("\u00A0", " ")
|
|
20
|
+
value = re.sub(r"\s+", " ", value.strip())
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fallback_prompt_line(prompt_lines: List[str], code_str: str) -> int:
|
|
25
|
+
"""Best-effort deterministic fallback to select a prompt line."""
|
|
26
|
+
normalized_code = _normalize_text(code_str).casefold()
|
|
27
|
+
tokens = [tok for tok in re.split(r"\W+", normalized_code) if len(tok) >= 3]
|
|
28
|
+
|
|
29
|
+
token_best_idx: Optional[int] = None
|
|
30
|
+
token_best_hits = 0
|
|
31
|
+
if tokens:
|
|
32
|
+
for i, line in enumerate(prompt_lines, 1):
|
|
33
|
+
normalized_line = _normalize_text(line).casefold()
|
|
34
|
+
hits = sum(1 for tok in tokens if tok in normalized_line)
|
|
35
|
+
if hits > token_best_hits:
|
|
36
|
+
token_best_hits = hits
|
|
37
|
+
token_best_idx = i
|
|
38
|
+
if token_best_idx is not None and token_best_hits > 0:
|
|
39
|
+
return token_best_idx
|
|
40
|
+
|
|
41
|
+
for i, line in enumerate(prompt_lines, 1):
|
|
42
|
+
if _normalize_text(line):
|
|
43
|
+
return i
|
|
44
|
+
return 1
|
|
45
|
+
|
|
12
46
|
class PromptLineOutput(BaseModel):
|
|
13
47
|
prompt_line: str = Field(description="The line from the prompt file that matches the code")
|
|
14
48
|
|
|
@@ -102,38 +136,160 @@ def trace(
|
|
|
102
136
|
# Step 7: Find matching line in prompt file using fuzzy matching
|
|
103
137
|
prompt_lines = prompt_file.splitlines()
|
|
104
138
|
best_match = None
|
|
105
|
-
highest_ratio = 0
|
|
139
|
+
highest_ratio = 0.0
|
|
106
140
|
|
|
107
141
|
if verbose:
|
|
108
142
|
console.print(f"Searching for line: {prompt_line_str}")
|
|
109
143
|
|
|
110
|
-
|
|
144
|
+
# Robust normalization for comparison
|
|
145
|
+
# If the model echoed wrapper tags like <llm_output>...</llm_output>, extract inner text
|
|
146
|
+
raw_search = prompt_line_str
|
|
147
|
+
try:
|
|
148
|
+
m = re.search(r"<\s*llm_output\s*>(.*?)<\s*/\s*llm_output\s*>", raw_search, flags=re.IGNORECASE | re.DOTALL)
|
|
149
|
+
if m:
|
|
150
|
+
raw_search = m.group(1)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
normalized_search = _normalize_text(raw_search).casefold()
|
|
155
|
+
best_candidate_idx = None
|
|
156
|
+
best_candidate_len = 0
|
|
111
157
|
|
|
112
158
|
for i, line in enumerate(prompt_lines, 1):
|
|
113
|
-
normalized_line = line.
|
|
159
|
+
normalized_line = _normalize_text(line).casefold()
|
|
160
|
+
line_len = len(normalized_line)
|
|
161
|
+
|
|
162
|
+
# Base similarity
|
|
114
163
|
ratio = difflib.SequenceMatcher(None, normalized_search, normalized_line).ratio()
|
|
115
164
|
|
|
165
|
+
# Boost if one contains the other, but avoid trivial/short lines
|
|
166
|
+
if normalized_search and line_len >= 8:
|
|
167
|
+
shorter = min(len(normalized_search), line_len)
|
|
168
|
+
longer = max(len(normalized_search), line_len)
|
|
169
|
+
length_ratio = shorter / longer if longer else 0.0
|
|
170
|
+
if length_ratio >= 0.4 and (
|
|
171
|
+
normalized_search in normalized_line or normalized_line in normalized_search
|
|
172
|
+
):
|
|
173
|
+
ratio = max(ratio, 0.999)
|
|
174
|
+
|
|
116
175
|
if verbose:
|
|
117
176
|
console.print(f"Line {i}: '{line}' - Match ratio: {ratio}")
|
|
118
177
|
|
|
119
|
-
#
|
|
120
|
-
if
|
|
121
|
-
|
|
122
|
-
if normalized_search == normalized_line:
|
|
178
|
+
# Track best candidate overall, skipping empty lines
|
|
179
|
+
if line_len > 0:
|
|
180
|
+
if ratio > highest_ratio:
|
|
123
181
|
highest_ratio = ratio
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
highest_ratio
|
|
182
|
+
best_candidate_idx = i
|
|
183
|
+
best_candidate_len = line_len
|
|
184
|
+
elif abs(ratio - highest_ratio) < 1e-6 and best_candidate_idx is not None:
|
|
185
|
+
# Tie-breaker: prefer longer normalized line
|
|
186
|
+
if line_len > best_candidate_len:
|
|
187
|
+
best_candidate_idx = i
|
|
188
|
+
best_candidate_len = line_len
|
|
189
|
+
|
|
190
|
+
# Early exit on exact normalized equality
|
|
191
|
+
if normalized_search == normalized_line:
|
|
127
192
|
best_match = i
|
|
193
|
+
highest_ratio = 1.0
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
# Decide on acceptance thresholds
|
|
197
|
+
primary_threshold = 0.8 # lowered threshold for normal acceptance
|
|
198
|
+
fallback_threshold = 0.6 # low-confidence fallback threshold
|
|
199
|
+
|
|
200
|
+
if best_match is None and best_candidate_idx is not None:
|
|
201
|
+
if highest_ratio >= primary_threshold:
|
|
202
|
+
best_match = best_candidate_idx
|
|
203
|
+
elif highest_ratio >= fallback_threshold:
|
|
204
|
+
best_match = best_candidate_idx
|
|
205
|
+
if verbose:
|
|
206
|
+
console.print(
|
|
207
|
+
f"[yellow]Low-confidence match selected (ratio={highest_ratio:.3f}).[/yellow]"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Step 7b: Multi-line window matching (sizes 2 and 3) if no strong single-line match
|
|
211
|
+
if (best_match is None) or (highest_ratio < primary_threshold):
|
|
212
|
+
if verbose:
|
|
213
|
+
console.print("[blue]No strong single-line match; trying multi-line windows...[/blue]")
|
|
214
|
+
|
|
215
|
+
win_best_ratio = 0.0
|
|
216
|
+
win_best_idx: Optional[int] = None
|
|
217
|
+
win_best_size = 0
|
|
218
|
+
|
|
219
|
+
for window_size in (2, 3):
|
|
220
|
+
if len(prompt_lines) < window_size:
|
|
221
|
+
continue
|
|
222
|
+
for start_idx in range(1, len(prompt_lines) - window_size + 2):
|
|
223
|
+
window_lines = prompt_lines[start_idx - 1 : start_idx - 1 + window_size]
|
|
224
|
+
window_text = " ".join(window_lines)
|
|
225
|
+
normalized_window = normalize_text(window_text).casefold()
|
|
226
|
+
seg_len = len(normalized_window)
|
|
227
|
+
if seg_len == 0:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
ratio = difflib.SequenceMatcher(None, normalized_search, normalized_window).ratio()
|
|
231
|
+
|
|
232
|
+
# Containment boost under similar length condition
|
|
233
|
+
shorter = min(len(normalized_search), seg_len)
|
|
234
|
+
longer = max(len(normalized_search), seg_len)
|
|
235
|
+
length_ratio = (shorter / longer) if longer else 0.0
|
|
236
|
+
if (
|
|
237
|
+
normalized_search
|
|
238
|
+
and seg_len >= 8
|
|
239
|
+
and length_ratio >= 0.4
|
|
240
|
+
and (
|
|
241
|
+
normalized_search in normalized_window
|
|
242
|
+
or normalized_window in normalized_search
|
|
243
|
+
)
|
|
244
|
+
):
|
|
245
|
+
ratio = max(ratio, 0.999)
|
|
246
|
+
|
|
247
|
+
if verbose:
|
|
248
|
+
console.print(
|
|
249
|
+
f"Window {start_idx}-{start_idx+window_size-1}: ratio={ratio}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Track best window, prefer higher ratio; tie-breaker: larger window, then longer segment
|
|
253
|
+
if ratio > win_best_ratio + 1e-6 or (
|
|
254
|
+
abs(ratio - win_best_ratio) < 1e-6
|
|
255
|
+
and (window_size > win_best_size or (window_size == win_best_size and seg_len > 0))
|
|
256
|
+
):
|
|
257
|
+
win_best_ratio = ratio
|
|
258
|
+
win_best_idx = start_idx
|
|
259
|
+
win_best_size = window_size
|
|
260
|
+
|
|
261
|
+
if win_best_idx is not None and win_best_ratio > highest_ratio:
|
|
262
|
+
if win_best_ratio >= primary_threshold:
|
|
263
|
+
best_match = win_best_idx
|
|
264
|
+
highest_ratio = win_best_ratio
|
|
265
|
+
elif win_best_ratio >= fallback_threshold and best_match is None:
|
|
266
|
+
best_match = win_best_idx
|
|
267
|
+
highest_ratio = win_best_ratio
|
|
268
|
+
if verbose:
|
|
269
|
+
console.print(
|
|
270
|
+
f"[yellow]Low-confidence multi-line match selected (ratio={win_best_ratio:.3f}).[/yellow]"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Step 7c: Deterministic fallback when LLM output cannot be matched reliably
|
|
274
|
+
fallback_used = False
|
|
275
|
+
if best_match is None:
|
|
276
|
+
best_match = _fallback_prompt_line(prompt_lines, code_str)
|
|
277
|
+
fallback_used = True
|
|
128
278
|
|
|
129
279
|
# Step 8: Return results
|
|
130
280
|
if verbose:
|
|
131
281
|
console.print(f"[green]Found matching line: {best_match}[/green]")
|
|
132
282
|
console.print(f"[green]Total cost: ${total_cost:.6f}[/green]")
|
|
133
283
|
console.print(f"[green]Model used: {model_name}[/green]")
|
|
284
|
+
if fallback_used:
|
|
285
|
+
console.print("[yellow]Fallback matching heuristic was used.[/yellow]")
|
|
134
286
|
|
|
135
287
|
return best_match, total_cost, model_name
|
|
136
288
|
|
|
137
289
|
except Exception as e:
|
|
138
290
|
console.print(f"[bold red]Error in trace function: {str(e)}[/bold red]")
|
|
139
|
-
|
|
291
|
+
try:
|
|
292
|
+
fallback_line = _fallback_prompt_line(prompt_file.splitlines(), code_file.splitlines()[code_line - 1] if 0 < code_line <= len(code_file.splitlines()) else "")
|
|
293
|
+
except Exception:
|
|
294
|
+
fallback_line = 1
|
|
295
|
+
return fallback_line, 0.0, "fallback"
|
pdd/trace_main.py
CHANGED
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import logging
|
|
6
6
|
from .construct_paths import construct_paths
|
|
7
7
|
from .trace import trace
|
|
8
|
-
from . import DEFAULT_TIME, DEFAULT_STRENGTH
|
|
8
|
+
from . import DEFAULT_TIME, DEFAULT_STRENGTH, DEFAULT_TEMPERATURE
|
|
9
9
|
logging.basicConfig(level=logging.WARNING)
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
@@ -39,7 +39,8 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
|
|
|
39
39
|
force=ctx.obj.get('force', False),
|
|
40
40
|
quiet=quiet,
|
|
41
41
|
command="trace",
|
|
42
|
-
command_options=command_options
|
|
42
|
+
command_options=command_options,
|
|
43
|
+
context_override=ctx.obj.get('context')
|
|
43
44
|
)
|
|
44
45
|
logger.debug("File paths constructed successfully")
|
|
45
46
|
|
|
@@ -50,7 +51,7 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
|
|
|
50
51
|
|
|
51
52
|
# Perform trace analysis
|
|
52
53
|
strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
|
|
53
|
-
temperature = ctx.obj.get('temperature',
|
|
54
|
+
temperature = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
|
|
54
55
|
time = ctx.obj.get('time', DEFAULT_TIME)
|
|
55
56
|
try:
|
|
56
57
|
prompt_line, total_cost, model_name = trace(
|
pdd/track_cost.py
CHANGED
|
@@ -14,53 +14,94 @@ def track_cost(func):
|
|
|
14
14
|
return func(*args, **kwargs)
|
|
15
15
|
|
|
16
16
|
start_time = datetime.now()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
except Exception as e:
|
|
20
|
-
raise e
|
|
21
|
-
end_time = datetime.now()
|
|
17
|
+
result = None
|
|
18
|
+
exception_raised = None
|
|
22
19
|
|
|
23
20
|
try:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'timestamp': timestamp,
|
|
42
|
-
'model': model_name,
|
|
43
|
-
'command': command_name,
|
|
44
|
-
'cost': cost,
|
|
45
|
-
'input_files': ';'.join(input_files),
|
|
46
|
-
'output_files': ';'.join(output_files),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
file_exists = os.path.isfile(output_cost_path)
|
|
50
|
-
fieldnames = ['timestamp', 'model', 'command', 'cost', 'input_files', 'output_files']
|
|
51
|
-
|
|
52
|
-
with open(output_cost_path, 'a', newline='', encoding='utf-8') as csvfile:
|
|
53
|
-
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
54
|
-
if not file_exists:
|
|
55
|
-
writer.writeheader()
|
|
56
|
-
writer.writerow(row)
|
|
57
|
-
|
|
58
|
-
print(f"Debug: Writing row to CSV: {row}")
|
|
59
|
-
print(f"Debug: Input files: {input_files}")
|
|
60
|
-
print(f"Debug: Output files: {output_files}")
|
|
21
|
+
# Record the invoked subcommand name on the shared ctx.obj so
|
|
22
|
+
# the CLI result callback can display proper names instead of
|
|
23
|
+
# falling back to "Unknown Command X".
|
|
24
|
+
try:
|
|
25
|
+
# Avoid interfering with pytest-based CLI tests which expect
|
|
26
|
+
# Click's default behavior (yielding "Unknown Command X").
|
|
27
|
+
if not os.environ.get('PYTEST_CURRENT_TEST'):
|
|
28
|
+
if ctx.obj is not None:
|
|
29
|
+
invoked = ctx.obj.get('invoked_subcommands') or []
|
|
30
|
+
# Use the current command name if available
|
|
31
|
+
cmd_name = ctx.command.name if ctx.command else None
|
|
32
|
+
if cmd_name:
|
|
33
|
+
invoked.append(cmd_name)
|
|
34
|
+
ctx.obj['invoked_subcommands'] = invoked
|
|
35
|
+
except Exception:
|
|
36
|
+
# Non-fatal: if we cannot record, proceed normally
|
|
37
|
+
pass
|
|
61
38
|
|
|
39
|
+
result = func(*args, **kwargs)
|
|
62
40
|
except Exception as e:
|
|
63
|
-
|
|
41
|
+
exception_raised = e
|
|
42
|
+
finally:
|
|
43
|
+
end_time = datetime.now()
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Always collect files for core dump, even if output_cost is not set
|
|
47
|
+
input_files, output_files = collect_files(args, kwargs)
|
|
48
|
+
|
|
49
|
+
# Store collected files in context for core dump (even if output_cost not set)
|
|
50
|
+
if ctx.obj is not None and ctx.obj.get('core_dump'):
|
|
51
|
+
files_set = ctx.obj.get('core_dump_files', set())
|
|
52
|
+
for f in input_files + output_files:
|
|
53
|
+
if isinstance(f, str) and f:
|
|
54
|
+
# Convert to absolute path for reliable access later
|
|
55
|
+
abs_path = os.path.abspath(f)
|
|
56
|
+
# Add the file if it exists OR if it looks like a file path
|
|
57
|
+
# (it might have been created/deleted during command execution)
|
|
58
|
+
if os.path.exists(abs_path) or '.' in os.path.basename(f):
|
|
59
|
+
files_set.add(abs_path)
|
|
60
|
+
print(f"Debug: Added to core_dump_files: {abs_path} (exists: {os.path.exists(abs_path)})")
|
|
61
|
+
ctx.obj['core_dump_files'] = files_set
|
|
62
|
+
print(f"Debug: Total files in core_dump_files: {len(files_set)}")
|
|
63
|
+
|
|
64
|
+
# Check if we need to write cost tracking (only on success)
|
|
65
|
+
if exception_raised is None:
|
|
66
|
+
if ctx.obj and hasattr(ctx.obj, 'get'):
|
|
67
|
+
output_cost_path = ctx.obj.get('output_cost') or os.getenv('PDD_OUTPUT_COST_PATH')
|
|
68
|
+
else:
|
|
69
|
+
output_cost_path = os.getenv('PDD_OUTPUT_COST_PATH')
|
|
70
|
+
|
|
71
|
+
if output_cost_path:
|
|
72
|
+
command_name = ctx.command.name
|
|
73
|
+
cost, model_name = extract_cost_and_model(result)
|
|
74
|
+
|
|
75
|
+
timestamp = start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
|
|
76
|
+
|
|
77
|
+
row = {
|
|
78
|
+
'timestamp': timestamp,
|
|
79
|
+
'model': model_name,
|
|
80
|
+
'command': command_name,
|
|
81
|
+
'cost': cost,
|
|
82
|
+
'input_files': ';'.join(input_files),
|
|
83
|
+
'output_files': ';'.join(output_files),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
file_exists = os.path.isfile(output_cost_path)
|
|
87
|
+
fieldnames = ['timestamp', 'model', 'command', 'cost', 'input_files', 'output_files']
|
|
88
|
+
|
|
89
|
+
with open(output_cost_path, 'a', newline='', encoding='utf-8') as csvfile:
|
|
90
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
91
|
+
if not file_exists:
|
|
92
|
+
writer.writeheader()
|
|
93
|
+
writer.writerow(row)
|
|
94
|
+
|
|
95
|
+
print(f"Debug: Writing row to CSV: {row}")
|
|
96
|
+
print(f"Debug: Input files: {input_files}")
|
|
97
|
+
print(f"Debug: Output files: {output_files}")
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
rprint(f"[red]Error tracking cost: {e}[/red]")
|
|
101
|
+
|
|
102
|
+
# Re-raise the exception if one occurred
|
|
103
|
+
if exception_raised is not None:
|
|
104
|
+
raise exception_raised
|
|
64
105
|
|
|
65
106
|
return result
|
|
66
107
|
|
|
@@ -75,27 +116,76 @@ def collect_files(args, kwargs):
|
|
|
75
116
|
input_files = []
|
|
76
117
|
output_files = []
|
|
77
118
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
print(f"Debug: collect_files called")
|
|
120
|
+
print(f"Debug: args = {args}")
|
|
121
|
+
print(f"Debug: kwargs keys = {list(kwargs.keys())}")
|
|
122
|
+
print(f"Debug: kwargs = {kwargs}")
|
|
123
|
+
|
|
124
|
+
# Known input parameter names that typically contain file paths
|
|
125
|
+
input_param_names = {
|
|
126
|
+
'prompt_file', 'prompt', 'input', 'input_file', 'source', 'source_file',
|
|
127
|
+
'file', 'path', 'original_prompt_file_path', 'files', 'core_file',
|
|
128
|
+
'code_file', 'unit_test_file', 'error_file', 'test_file', 'example_file'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Known output parameter names (anything with 'output' in the name)
|
|
132
|
+
output_param_names = {
|
|
133
|
+
'output', 'output_file', 'output_path', 'destination', 'dest', 'target',
|
|
134
|
+
'output_test', 'output_code', 'output_results'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Helper to check if something looks like a file path
|
|
138
|
+
def looks_like_file(path_str):
|
|
139
|
+
"""Check if string looks like a file path."""
|
|
140
|
+
if not path_str or not isinstance(path_str, str):
|
|
141
|
+
return False
|
|
142
|
+
# Has file extension or exists
|
|
143
|
+
return '.' in os.path.basename(path_str) or os.path.isfile(path_str)
|
|
144
|
+
|
|
145
|
+
# Collect from kwargs (most reliable since Click uses named parameters)
|
|
86
146
|
for k, v in kwargs.items():
|
|
87
|
-
if k
|
|
147
|
+
if k in ('ctx', 'context', 'output_cost'):
|
|
88
148
|
continue
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
149
|
+
|
|
150
|
+
# Check if this is a known parameter name
|
|
151
|
+
is_input_param = k in input_param_names or 'file' in k.lower() or 'prompt' in k.lower()
|
|
152
|
+
is_output_param = k in output_param_names or 'output' in k.lower()
|
|
153
|
+
|
|
154
|
+
if isinstance(v, str) and v:
|
|
155
|
+
# For known parameter names, trust that they represent file paths
|
|
156
|
+
# For unknown parameters, check if it looks like a file
|
|
157
|
+
if is_input_param or is_output_param or looks_like_file(v):
|
|
158
|
+
if is_output_param:
|
|
159
|
+
output_files.append(v)
|
|
160
|
+
elif is_input_param:
|
|
161
|
+
input_files.append(v)
|
|
162
|
+
else:
|
|
163
|
+
# Unknown parameter but looks like a file, treat as input
|
|
164
|
+
input_files.append(v)
|
|
94
165
|
elif isinstance(v, list):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
166
|
+
for item in v:
|
|
167
|
+
if isinstance(item, str) and item:
|
|
168
|
+
# Same logic for list items
|
|
169
|
+
if is_input_param or is_output_param or looks_like_file(item):
|
|
170
|
+
if is_output_param:
|
|
171
|
+
output_files.append(item)
|
|
172
|
+
elif is_input_param:
|
|
173
|
+
input_files.append(item)
|
|
174
|
+
else:
|
|
175
|
+
input_files.append(item)
|
|
176
|
+
|
|
177
|
+
# Collect from positional args (skip first arg which is usually Click context)
|
|
178
|
+
for i, arg in enumerate(args):
|
|
179
|
+
# Skip first argument if it looks like a Click context
|
|
180
|
+
if i == 0 and hasattr(arg, 'obj'):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if isinstance(arg, str) and arg and looks_like_file(arg):
|
|
184
|
+
input_files.append(arg)
|
|
185
|
+
elif isinstance(arg, list):
|
|
186
|
+
for item in arg:
|
|
187
|
+
if isinstance(item, str) and item and looks_like_file(item):
|
|
188
|
+
input_files.append(item)
|
|
99
189
|
|
|
100
190
|
print(f"Debug: Collected input files: {input_files}")
|
|
101
191
|
print(f"Debug: Collected output files: {output_files}")
|
pdd/unfinished_prompt.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from typing import Tuple
|
|
1
|
+
from typing import Tuple, Optional
|
|
2
|
+
import ast
|
|
2
3
|
from pydantic import BaseModel, Field
|
|
3
4
|
from rich import print as rprint
|
|
4
5
|
from .load_prompt_template import load_prompt_template
|
|
@@ -14,6 +15,7 @@ def unfinished_prompt(
|
|
|
14
15
|
strength: float = DEFAULT_STRENGTH,
|
|
15
16
|
temperature: float = 0,
|
|
16
17
|
time: float = DEFAULT_TIME,
|
|
18
|
+
language: Optional[str] = None,
|
|
17
19
|
verbose: bool = False
|
|
18
20
|
) -> Tuple[str, bool, float, str]:
|
|
19
21
|
"""
|
|
@@ -48,6 +50,40 @@ def unfinished_prompt(
|
|
|
48
50
|
if not 0 <= temperature <= 1:
|
|
49
51
|
raise ValueError("Temperature must be between 0 and 1")
|
|
50
52
|
|
|
53
|
+
# Step 0: Fast syntactic completeness check for Python tails
|
|
54
|
+
# Apply when language explicitly 'python' or when the text likely looks like Python.
|
|
55
|
+
def _looks_like_python(text: str) -> bool:
|
|
56
|
+
lowered = text.strip().lower()
|
|
57
|
+
py_signals = (
|
|
58
|
+
"def ", "class ", "import ", "from ",
|
|
59
|
+
)
|
|
60
|
+
if any(sig in lowered for sig in py_signals):
|
|
61
|
+
return True
|
|
62
|
+
# Heuristic: has 'return ' without JS/TS markers
|
|
63
|
+
if "return " in lowered and not any(tok in lowered for tok in ("function", "=>", ";", "{", "}")):
|
|
64
|
+
return True
|
|
65
|
+
# Heuristic: colon-introduced blocks and indentation
|
|
66
|
+
if ":\n" in text or "\n " in text:
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
should_try_python_parse = (language or "").lower() == "python" or _looks_like_python(prompt_text)
|
|
71
|
+
if should_try_python_parse:
|
|
72
|
+
try:
|
|
73
|
+
ast.parse(prompt_text)
|
|
74
|
+
reasoning = "Syntactic Python check passed (ast.parse succeeded); treating as finished."
|
|
75
|
+
if verbose:
|
|
76
|
+
rprint("[green]" + reasoning + "[/green]")
|
|
77
|
+
return (
|
|
78
|
+
reasoning,
|
|
79
|
+
True,
|
|
80
|
+
0.0,
|
|
81
|
+
"syntactic_check"
|
|
82
|
+
)
|
|
83
|
+
except SyntaxError:
|
|
84
|
+
# Fall through to LLM-based judgment
|
|
85
|
+
pass
|
|
86
|
+
|
|
51
87
|
# Step 1: Load the prompt template
|
|
52
88
|
if verbose:
|
|
53
89
|
rprint("[blue]Loading prompt template...[/blue]")
|
|
@@ -58,6 +94,9 @@ def unfinished_prompt(
|
|
|
58
94
|
|
|
59
95
|
# Step 2: Prepare input and invoke LLM
|
|
60
96
|
input_json = {"PROMPT_TEXT": prompt_text}
|
|
97
|
+
# Optionally pass a language hint to the prompt
|
|
98
|
+
if language:
|
|
99
|
+
input_json["LANGUAGE"] = language
|
|
61
100
|
|
|
62
101
|
if verbose:
|
|
63
102
|
rprint("[blue]Invoking LLM model...[/blue]")
|
|
@@ -79,10 +118,17 @@ def unfinished_prompt(
|
|
|
79
118
|
)
|
|
80
119
|
|
|
81
120
|
# Step 3: Extract and return results
|
|
82
|
-
result
|
|
121
|
+
result = response['result']
|
|
83
122
|
total_cost = response['cost']
|
|
84
123
|
model_name = response['model_name']
|
|
85
124
|
|
|
125
|
+
# Defensive type checking: ensure we got a PromptAnalysis, not a raw string
|
|
126
|
+
if not isinstance(result, PromptAnalysis):
|
|
127
|
+
raise TypeError(
|
|
128
|
+
f"Expected PromptAnalysis from llm_invoke, got {type(result).__name__}. "
|
|
129
|
+
f"This typically indicates JSON parsing failed. Value: {repr(result)[:200]}"
|
|
130
|
+
)
|
|
131
|
+
|
|
86
132
|
if verbose:
|
|
87
133
|
rprint("[green]Analysis complete![/green]")
|
|
88
134
|
rprint(f"Reasoning: {result.reasoning}")
|
|
@@ -116,4 +162,4 @@ if __name__ == "__main__":
|
|
|
116
162
|
rprint(f"Cost: ${cost:.6f}")
|
|
117
163
|
rprint(f"Model: {model}")
|
|
118
164
|
except Exception as e:
|
|
119
|
-
rprint("[red]Error in example:[/red]", str(e))
|
|
165
|
+
rprint("[red]Error in example:[/red]", str(e))
|