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.
Files changed (114) hide show
  1. pdd/__init__.py +4 -4
  2. pdd/agentic_common.py +863 -0
  3. pdd/agentic_crash.py +534 -0
  4. pdd/agentic_fix.py +1179 -0
  5. pdd/agentic_langtest.py +162 -0
  6. pdd/agentic_update.py +370 -0
  7. pdd/agentic_verify.py +183 -0
  8. pdd/auto_deps_main.py +15 -5
  9. pdd/auto_include.py +63 -5
  10. pdd/bug_main.py +3 -2
  11. pdd/bug_to_unit_test.py +2 -0
  12. pdd/change_main.py +11 -4
  13. pdd/cli.py +22 -1181
  14. pdd/cmd_test_main.py +73 -21
  15. pdd/code_generator.py +58 -18
  16. pdd/code_generator_main.py +672 -25
  17. pdd/commands/__init__.py +42 -0
  18. pdd/commands/analysis.py +248 -0
  19. pdd/commands/fix.py +140 -0
  20. pdd/commands/generate.py +257 -0
  21. pdd/commands/maintenance.py +174 -0
  22. pdd/commands/misc.py +79 -0
  23. pdd/commands/modify.py +230 -0
  24. pdd/commands/report.py +144 -0
  25. pdd/commands/templates.py +215 -0
  26. pdd/commands/utility.py +110 -0
  27. pdd/config_resolution.py +58 -0
  28. pdd/conflicts_main.py +8 -3
  29. pdd/construct_paths.py +258 -82
  30. pdd/context_generator.py +10 -2
  31. pdd/context_generator_main.py +113 -11
  32. pdd/continue_generation.py +47 -7
  33. pdd/core/__init__.py +0 -0
  34. pdd/core/cli.py +503 -0
  35. pdd/core/dump.py +554 -0
  36. pdd/core/errors.py +63 -0
  37. pdd/core/utils.py +90 -0
  38. pdd/crash_main.py +44 -11
  39. pdd/data/language_format.csv +71 -63
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +330 -76
  43. pdd/fix_error_loop.py +207 -61
  44. pdd/fix_errors_from_unit_tests.py +4 -3
  45. pdd/fix_main.py +75 -18
  46. pdd/fix_verification_errors.py +12 -100
  47. pdd/fix_verification_errors_loop.py +306 -272
  48. pdd/fix_verification_main.py +28 -9
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +9 -2
  52. pdd/get_run_command.py +73 -0
  53. pdd/get_test_command.py +68 -0
  54. pdd/git_update.py +70 -19
  55. pdd/incremental_code_generator.py +2 -2
  56. pdd/insert_includes.py +11 -3
  57. pdd/llm_invoke.py +1269 -103
  58. pdd/load_prompt_template.py +36 -10
  59. pdd/pdd_completion.fish +25 -2
  60. pdd/pdd_completion.sh +30 -4
  61. pdd/pdd_completion.zsh +79 -4
  62. pdd/postprocess.py +10 -3
  63. pdd/preprocess.py +228 -15
  64. pdd/preprocess_main.py +8 -5
  65. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  66. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  67. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  68. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  69. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  70. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  71. pdd/prompts/auto_include_LLM.prompt +100 -905
  72. pdd/prompts/detect_change_LLM.prompt +122 -20
  73. pdd/prompts/example_generator_LLM.prompt +22 -1
  74. pdd/prompts/extract_code_LLM.prompt +5 -1
  75. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  76. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  77. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  78. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  79. pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
  80. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
  81. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  82. pdd/prompts/generate_test_LLM.prompt +21 -6
  83. pdd/prompts/increase_tests_LLM.prompt +1 -5
  84. pdd/prompts/insert_includes_LLM.prompt +228 -108
  85. pdd/prompts/trace_LLM.prompt +25 -22
  86. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  87. pdd/prompts/update_prompt_LLM.prompt +22 -1
  88. pdd/pytest_output.py +127 -12
  89. pdd/render_mermaid.py +236 -0
  90. pdd/setup_tool.py +648 -0
  91. pdd/simple_math.py +2 -0
  92. pdd/split_main.py +3 -2
  93. pdd/summarize_directory.py +49 -6
  94. pdd/sync_determine_operation.py +543 -98
  95. pdd/sync_main.py +81 -31
  96. pdd/sync_orchestration.py +1334 -751
  97. pdd/sync_tui.py +848 -0
  98. pdd/template_registry.py +264 -0
  99. pdd/templates/architecture/architecture_json.prompt +242 -0
  100. pdd/templates/generic/generate_prompt.prompt +174 -0
  101. pdd/trace.py +168 -12
  102. pdd/trace_main.py +4 -3
  103. pdd/track_cost.py +151 -61
  104. pdd/unfinished_prompt.py +49 -3
  105. pdd/update_main.py +549 -67
  106. pdd/update_model_costs.py +2 -2
  107. pdd/update_prompt.py +19 -4
  108. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
  109. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  110. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  111. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  112. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  113. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  114. {pdd_cli-0.0.45.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
- normalized_search = prompt_line_str.strip()
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.strip()
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
- # Increase threshold to 0.9 for more precise matching
120
- if ratio > highest_ratio and ratio > 0.9:
121
- # Additional check for exact content match after normalization
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
- best_match = i
125
- break # Exit on exact match
126
- highest_ratio = 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
- return None, 0.0, ""
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', 0.0)
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
- try:
18
- result = func(*args, **kwargs)
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
- if ctx.obj and hasattr(ctx.obj, 'get'):
25
- output_cost_path = ctx.obj.get('output_cost') or os.getenv('PDD_OUTPUT_COST_PATH')
26
- else:
27
- output_cost_path = os.getenv('PDD_OUTPUT_COST_PATH')
28
-
29
- if not output_cost_path:
30
- return result
31
-
32
- command_name = ctx.command.name
33
-
34
- cost, model_name = extract_cost_and_model(result)
35
-
36
- input_files, output_files = collect_files(args, kwargs)
37
-
38
- timestamp = start_time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
39
-
40
- row = {
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
- rprint(f"[red]Error tracking cost: {e}[/red]")
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
- # Collect from args
79
- for arg in args:
80
- if isinstance(arg, str):
81
- input_files.append(arg)
82
- elif isinstance(arg, list):
83
- input_files.extend([f for f in arg if isinstance(f, str)])
84
-
85
- # Collect from kwargs
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 == 'output_cost':
147
+ if k in ('ctx', 'context', 'output_cost'):
88
148
  continue
89
- if isinstance(v, str):
90
- if k.startswith('output'):
91
- output_files.append(v)
92
- else:
93
- input_files.append(v)
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
- if k.startswith('output'):
96
- output_files.extend([f for f in v if isinstance(f, str)])
97
- else:
98
- input_files.extend([f for f in v if isinstance(f, str)])
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: PromptAnalysis = response['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))