pdd-cli 0.0.49__py3-none-any.whl → 0.0.51__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.

@@ -23,18 +23,39 @@ def load_prompt_template(prompt_name: str) -> Optional[str]:
23
23
  print_formatted("[red]Unexpected error loading prompt template[/red]")
24
24
  return None
25
25
 
26
- # Step 1: Get project path from environment variable
27
- project_path = os.getenv('PDD_PATH')
28
- if not project_path:
29
- print_formatted("[red]PDD_PATH environment variable is not set[/red]")
30
- return None
26
+ # Step 1: Get project path from environment variable (preferred),
27
+ # else fall back to auto-detect based on this module's location or CWD.
28
+ project_path_env = os.getenv('PDD_PATH')
29
+ candidate_paths = []
30
+ if project_path_env:
31
+ candidate_paths.append(Path(project_path_env))
32
+
33
+ # Fallback 1: repository root inferred from this module (pdd/ => repo root)
34
+ try:
35
+ module_root = Path(__file__).resolve().parent # pdd/
36
+ repo_root = module_root.parent # repo root
37
+ candidate_paths.append(repo_root)
38
+ except Exception:
39
+ pass
31
40
 
32
- # Construct the full path to the prompt file
33
- prompt_path = Path(project_path) / 'prompts' / f"{prompt_name}.prompt"
41
+ # Fallback 2: current working directory
42
+ candidate_paths.append(Path.cwd())
43
+
44
+ # Build candidate prompt paths to try in order
45
+ prompt_candidates = [cp / 'prompts' / f"{prompt_name}.prompt" for cp in candidate_paths]
34
46
 
35
47
  # Step 2: Load and return the prompt template
36
- if not prompt_path.exists():
37
- print_formatted(f"[red]Prompt file not found: {prompt_path}[/red]")
48
+ prompt_path: Optional[Path] = None
49
+ for candidate in prompt_candidates:
50
+ if candidate.exists():
51
+ prompt_path = candidate
52
+ break
53
+
54
+ if prompt_path is None:
55
+ tried = "\n".join(str(c) for c in prompt_candidates)
56
+ print_formatted(
57
+ f"[red]Prompt file not found in any candidate locations for '{prompt_name}'. Tried:\n{tried}[/red]"
58
+ )
38
59
  return None
39
60
 
40
61
  try:
pdd/pdd_completion.fish CHANGED
@@ -27,7 +27,7 @@ complete -c pdd -n "__fish_use_subcommand" -a conflicts -d "Analyze conflicts be
27
27
  complete -c pdd -n "__fish_use_subcommand" -a crash -d "Fix code causing program crash"
28
28
  complete -c pdd -n "__fish_use_subcommand" -a trace -d "Trace code line to prompt"
29
29
  complete -c pdd -n "__fish_use_subcommand" -a bug -d "Generate unit test from bug report"
30
- complete -c pdd -n "__fish_use_subcommand" -a auto-deps -d "Analyze and insert dependencies"
30
+ complete -c pdd -n "__fish_use_subcommand" -a auto-deps -d "Analyze and insert dependencies from directory or glob"
31
31
  complete -c pdd -n "__fish_use_subcommand" -a verify -d "Verify functional correctness using LLM judgment"
32
32
 
33
33
  # Command-specific completions
@@ -131,4 +131,4 @@ complete -c pdd -n "__fish_seen_subcommand_from generate example test preprocess
131
131
  complete -c pdd -n "__fish_seen_subcommand_from generate example test preprocess fix split change update detect conflicts crash trace bug auto-deps verify" -a "(__fish_complete_suffix .log .txt .csv)"
132
132
 
133
133
  # Help completion
134
- complete -c pdd -n "__fish_seen_subcommand_from help" -a "generate example test preprocess fix split change update detect conflicts crash trace bug auto-deps verify" -d "Show help for specific command"
134
+ complete -c pdd -n "__fish_seen_subcommand_from help" -a "generate example test preprocess fix split change update detect conflicts crash trace bug auto-deps verify" -d "Show help for specific command"
pdd/pdd_completion.zsh CHANGED
@@ -349,7 +349,7 @@ _pdd_bug() {
349
349
  # --force-scan
350
350
  # Args:
351
351
  # 1: PROMPT_FILE
352
- # 2: DIRECTORY_PATH
352
+ # 2: DIRECTORY_PATH (directory or glob pattern)
353
353
  _pdd_auto_deps() {
354
354
  _arguments -s \
355
355
  $_pdd_global_opts \
@@ -357,7 +357,7 @@ _pdd_auto_deps() {
357
357
  '--csv=[CSV file for dependency info (default: project_dependencies.csv).]:filename:_files' \
358
358
  '--force-scan[Force rescanning of all potential dependency files.]' \
359
359
  '1:prompt-file:_files' \
360
- '2:directory:_files -/' \
360
+ '2:directory-or-glob:_files -/' \
361
361
  '*:filename:_files'
362
362
  }
363
363
 
@@ -410,7 +410,7 @@ _pdd() {
410
410
  'crash:Fix errors in a code module and its calling program'
411
411
  'trace:Find the prompt file line number associated with a code line'
412
412
  'bug:Generate a unit test based on incorrect vs desired outputs'
413
- 'auto-deps:Analyze a prompt file and directory for needed dependencies'
413
+ 'auto-deps:Analyze a prompt and include deps from a directory or glob'
414
414
  'verify:Verify functional correctness using LLM judgment and iteratively fix'
415
415
  )
416
416
 
@@ -487,4 +487,4 @@ else
487
487
  echo >&2 "Warning: Could not register pdd completion. Make sure ZSH completion system is working."
488
488
  fi
489
489
 
490
- # End of pdd_completion.zsh
490
+ # End of pdd_completion.zsh
pdd/postprocess.py CHANGED
@@ -3,7 +3,7 @@ from rich import print
3
3
  from pydantic import BaseModel, Field
4
4
  from .load_prompt_template import load_prompt_template
5
5
  from .llm_invoke import llm_invoke
6
- from . import DEFAULT_TIME
6
+ from . import DEFAULT_TIME, DEFAULT_STRENGTH
7
7
 
8
8
  class ExtractedCode(BaseModel):
9
9
  """Pydantic model for the extracted code."""
@@ -36,7 +36,7 @@ def postprocess_0(text: str) -> str:
36
36
  def postprocess(
37
37
  llm_output: str,
38
38
  language: str,
39
- strength: float = 0.9,
39
+ strength: float = DEFAULT_STRENGTH,
40
40
  temperature: float = 0,
41
41
  time: float = DEFAULT_TIME,
42
42
  verbose: bool = False
@@ -1,14 +1,13 @@
1
- % You are an expert Software Engineer. Your goal is to extract the updated prompt from the LLM output.
1
+ % You are an expert Software Engineer. Your goal is to extract the updated prompt from the LLM output in JSON format.
2
2
 
3
3
  % Here is the generated llm_output: <llm_output>{llm_output}</llm_output>
4
4
 
5
- % The LLM output contains the modified prompt that will generate the modified code, possibly with some additional commentary or explanation.
6
- % Your task is to identify and extract ONLY the modified prompt itself, without adding any JSON structure or additional formatting.
5
+ % The LLM output contains the modified prompt that will generate the modified code, possibly with some additional commentary or explanation. Your task is to identify and extract ONLY the modified prompt itself, without adding any additional formatting.
7
6
 
8
7
  % Ensure you:
9
- % 1. Remove any "# Modified Prompt" headers or similar text that isn't part of the actual prompt
10
- % 2. Preserve all markdown, code blocks, and formatting within the actual prompt
11
- % 3. Don't add any explanatory text, JSON wrappers, or your own commentary
12
- % 4. Return only the text that constitutes the actual prompt
8
+ 1. Remove any "# Modified Prompt" headers or similar text that isn't part of the actual prompt
9
+ 2. Preserve all markdown, code blocks, and formatting within the actual prompt
10
+ 3. Don't add any explanatory text, JSON wrappers, or your own commentary
11
+ 4. Return only the text that constitutes the actual prompt
13
12
 
14
- % The "modified_prompt" should be the complete, standalone prompt that could be used directly to generate the modified code.
13
+ % The "modified_prompt" JSON key should be the complete, standalone prompt that could be used directly to generate the modified code.
@@ -75,8 +75,8 @@ def main():
75
75
  temperature = 1
76
76
  verbose = False
77
77
 
78
- strength = 0.5
79
- while strength <= 0.5:
78
+ strength = 0.0
79
+ while strength <= 1:
80
80
  print(f"\nStrength: {strength}")
81
81
 
82
82
  # Example 1: Unstructured Output
@@ -215,8 +215,8 @@ def main():
215
215
  temperature = 1
216
216
  verbose = False
217
217
 
218
- strength = 0.5
219
- while strength <= 0.5:
218
+ strength = 0.0
219
+ while strength <= 1:
220
220
  print(f"\nStrength: {strength}")
221
221
 
222
222
  # Example 1: Unstructured Output
@@ -1,18 +1,102 @@
1
1
  % You are tasked with determining whether a given prompt has finished outputting everything or if it still needs to continue. This is crucial for ensuring that all necessary information has been provided before proceeding with further actions. You will often be provided the last few hundred characters of the prompt_text to analyze and determine if it appears to be complete or if it seems to be cut off or unfinished. You are just looking at the prompt_text and not the entire prompt file. The beginning part of the prompt_text is not always provided, so you will need to make a judgment based on the text you are given.
2
2
 
3
+ % IMPORTANT:
4
+ % - The prompt_text may contain code in various languages without Markdown fences.
5
+ % - Do NOT require triple backticks for completeness; judge the code/text itself.
6
+ % - Prefer concrete syntactic signals of completeness over stylistic ones.
7
+
3
8
  % Here is the prompt text to analyze:
4
9
  <prompt_text>
5
10
  {PROMPT_TEXT}
6
11
  </prompt_text>
7
12
 
13
+ % Optional language hint (may be empty or missing). If not provided, infer the language from the text:
14
+ <language>
15
+ {LANGUAGE}
16
+ </language>
17
+
8
18
  % Carefully examine the provided prompt text and determine if it appears to be complete or if it seems to be cut off or unfinished. Consider the following factors:
9
19
  1. Sentence structure: Are all sentences grammatically complete?
10
20
  2. Content flow: Does the text end abruptly or does it have a natural conclusion?
11
21
  3. Context: Based on the content, does it seem like all necessary information has been provided?
12
22
  4. Formatting: Are there any unclosed parentheses, quotation marks, or other formatting issues that suggest incompleteness?
13
23
 
24
+ % Multi-language code completeness heuristics (apply when text looks like code):
25
+ - If the text forms a syntactically complete module/snippet for the language, treat it as finished (even without Markdown fences).
26
+ - Generic signals across languages:
27
+ * Balanced delimiters: (), [], {{}}, quotes, and block comments are closed.
28
+ * No mid-token/mid-statement tail: it does not end on `return a +`, `a =`, `def foo(`, `function f(`, trailing `.`, `->`, `::`, trailing `,`, or a line-continuation like `\\`.
29
+ * Block closure: constructs that open a block are closed (e.g., Python indentation after `:`, or matching `{{}}` in C/Java/JS/TS/Go).
30
+ - Language specifics (use LANGUAGE if given; otherwise infer from the text):
31
+ * Python: colon-introduced blocks closed; indentation consistent; triple-quoted strings balanced.
32
+ * JS/TS: braces and parentheses balanced; no dangling `export`/`import` without a following specifier; `/* ... */` comments closed.
33
+ * Java/C/C++/C#: braces and parentheses balanced; string/char literals closed; block comments closed.
34
+ * Go: braces balanced; no dangling keyword indicating an unfinished clause.
35
+ * HTML/XML: tags properly nested/closed; attributes properly quoted; no unfinished `<tag` or dangling `</`.
36
+ - If this is only the tail of a longer file, mark finished when the tail itself is syntactically complete and does not indicate a dangling continuation.
37
+
14
38
  % Provide your reasoning for why you believe the prompt is complete or incomplete.
15
39
 
16
40
  % Output a JSON object with two keys:
17
41
  1. "reasoning": A string containing your structured reasoning
18
- 2. "is_finished": A boolean value (true if the prompt is complete, false if it's incomplete)
42
+ 2. "is_finished": A boolean value (true if the prompt is complete, false if it's incomplete)
43
+
44
+ % Examples (concise):
45
+ <examples>
46
+ <example1>
47
+ <input>
48
+ <prompt_text>
49
+ def add(a, b):\n return a + b\n
50
+ </prompt_text>
51
+ </input>
52
+ <output>
53
+ {{"reasoning": "Python code parses; blocks and quotes are closed; ends on a complete return statement.", "is_finished": true}}
54
+ </output>
55
+ </example1>
56
+ <example2>
57
+ <input>
58
+ <prompt_text>
59
+ def add(a, b):\n return a +
60
+ </prompt_text>
61
+ </input>
62
+ <output>
63
+ {{"reasoning": "Ends mid-expression (`return a +`), indicates unfinished statement.", "is_finished": false}}
64
+ </output>
65
+ </example2>
66
+ <example3>
67
+ <input>
68
+ <prompt_text>
69
+ function add(a, b) {{\n return a + b;\n}}\n
70
+ </prompt_text>
71
+ <language>
72
+ JavaScript
73
+ </language>
74
+ </input>
75
+ <output>
76
+ {{"reasoning": "JS braces and parentheses balanced; ends at a statement boundary; no dangling tokens.", "is_finished": true}}
77
+ </output>
78
+ </example3>
79
+ <example4>
80
+ <input>
81
+ <prompt_text>
82
+ <div class=\"box\">Hello
83
+ </prompt_text>
84
+ <language>
85
+ HTML
86
+ </language>
87
+ </input>
88
+ <output>
89
+ {{"reasoning": "HTML tag not closed (missing </div>); attribute quotes OK but element is unclosed.", "is_finished": false}}
90
+ </output>
91
+ </example4>
92
+ <example5>
93
+ <input>
94
+ <prompt_text>
95
+ class C:\n def f(self):\n x = 1\n
96
+ </prompt_text>
97
+ </input>
98
+ <output>
99
+ {{"reasoning": "All blocks properly indented and closed in the visible tail; no dangling colon blocks or open delimiters; tail is syntactically complete.", "is_finished": true}}
100
+ </output>
101
+ </example5>
102
+ </examples>
@@ -131,8 +131,21 @@ def summarize_directory(
131
131
  raise ValueError("Invalid CSV file format.")
132
132
  existing_data = parse_existing_csv(csv_file, verbose)
133
133
 
134
+ # Expand directory_path: support plain directories or glob patterns
135
+ try:
136
+ normalized_input = normalize_path(directory_path)
137
+ except Exception:
138
+ normalized_input = directory_path
139
+
140
+ if os.path.isdir(normalized_input):
141
+ # Recursively include all files under the directory
142
+ search_pattern = os.path.join(normalized_input, "**", "*")
143
+ else:
144
+ # Treat as a glob pattern (may be a single file path too)
145
+ search_pattern = directory_path
146
+
134
147
  # Get list of files first to ensure consistent order
135
- files = sorted(glob.glob(directory_path, recursive=True))
148
+ files = sorted(glob.glob(search_pattern, recursive=True))
136
149
  if not files:
137
150
  if verbose:
138
151
  print("[yellow]No files found.[/yellow]")
@@ -224,4 +237,4 @@ def summarize_directory(
224
237
 
225
238
  except Exception as e:
226
239
  print(f"[red]An error occurred: {str(e)}[/red]")
227
- raise
240
+ raise
pdd/trace.py CHANGED
@@ -3,6 +3,7 @@ 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
@@ -102,29 +103,148 @@ def trace(
102
103
  # Step 7: Find matching line in prompt file using fuzzy matching
103
104
  prompt_lines = prompt_file.splitlines()
104
105
  best_match = None
105
- highest_ratio = 0
106
+ highest_ratio = 0.0
106
107
 
107
108
  if verbose:
108
109
  console.print(f"Searching for line: {prompt_line_str}")
109
110
 
110
- normalized_search = prompt_line_str.strip()
111
+ # Robust normalization for comparison
112
+ def normalize_text(s: str) -> str:
113
+ if s is None:
114
+ return ""
115
+ s = s.replace("\u201c", '"').replace("\u201d", '"') # smart double quotes → straight
116
+ s = s.replace("\u2018", "'").replace("\u2019", "'") # smart single quotes → straight
117
+ s = s.replace("\u00A0", " ") # non-breaking space → space
118
+ s = re.sub(r"\s+", " ", s.strip()) # collapse whitespace
119
+ return s
120
+
121
+ # If the model echoed wrapper tags like <llm_output>...</llm_output>, extract inner text
122
+ raw_search = prompt_line_str
123
+ try:
124
+ m = re.search(r"<\s*llm_output\s*>(.*?)<\s*/\s*llm_output\s*>", raw_search, flags=re.IGNORECASE | re.DOTALL)
125
+ if m:
126
+ raw_search = m.group(1)
127
+ except Exception:
128
+ pass
129
+
130
+ normalized_search = normalize_text(raw_search).casefold()
131
+ best_candidate_idx = None
132
+ best_candidate_len = 0
111
133
 
112
134
  for i, line in enumerate(prompt_lines, 1):
113
- normalized_line = line.strip()
135
+ normalized_line = normalize_text(line).casefold()
136
+ line_len = len(normalized_line)
137
+
138
+ # Base similarity
114
139
  ratio = difflib.SequenceMatcher(None, normalized_search, normalized_line).ratio()
115
140
 
141
+ # Boost if one contains the other, but avoid trivial/short lines
142
+ if normalized_search and line_len >= 8:
143
+ shorter = min(len(normalized_search), line_len)
144
+ longer = max(len(normalized_search), line_len)
145
+ length_ratio = shorter / longer if longer else 0.0
146
+ if length_ratio >= 0.4 and (
147
+ normalized_search in normalized_line or normalized_line in normalized_search
148
+ ):
149
+ ratio = max(ratio, 0.999)
150
+
116
151
  if verbose:
117
152
  console.print(f"Line {i}: '{line}' - Match ratio: {ratio}")
118
153
 
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:
154
+ # Track best candidate overall, skipping empty lines
155
+ if line_len > 0:
156
+ if ratio > highest_ratio:
123
157
  highest_ratio = ratio
124
- best_match = i
125
- break # Exit on exact match
126
- highest_ratio = ratio
158
+ best_candidate_idx = i
159
+ best_candidate_len = line_len
160
+ elif abs(ratio - highest_ratio) < 1e-6 and best_candidate_idx is not None:
161
+ # Tie-breaker: prefer longer normalized line
162
+ if line_len > best_candidate_len:
163
+ best_candidate_idx = i
164
+ best_candidate_len = line_len
165
+
166
+ # Early exit on exact normalized equality
167
+ if normalized_search == normalized_line:
127
168
  best_match = i
169
+ highest_ratio = 1.0
170
+ break
171
+
172
+ # Decide on acceptance thresholds
173
+ primary_threshold = 0.8 # lowered threshold for normal acceptance
174
+ fallback_threshold = 0.6 # low-confidence fallback threshold
175
+
176
+ if best_match is None and best_candidate_idx is not None:
177
+ if highest_ratio >= primary_threshold:
178
+ best_match = best_candidate_idx
179
+ elif highest_ratio >= fallback_threshold:
180
+ best_match = best_candidate_idx
181
+ if verbose:
182
+ console.print(
183
+ f"[yellow]Low-confidence match selected (ratio={highest_ratio:.3f}).[/yellow]"
184
+ )
185
+
186
+ # Step 7b: Multi-line window matching (sizes 2 and 3) if no strong single-line match
187
+ if (best_match is None) or (highest_ratio < primary_threshold):
188
+ if verbose:
189
+ console.print("[blue]No strong single-line match; trying multi-line windows...[/blue]")
190
+
191
+ win_best_ratio = 0.0
192
+ win_best_idx: Optional[int] = None
193
+ win_best_size = 0
194
+
195
+ for window_size in (2, 3):
196
+ if len(prompt_lines) < window_size:
197
+ continue
198
+ for start_idx in range(1, len(prompt_lines) - window_size + 2):
199
+ window_lines = prompt_lines[start_idx - 1 : start_idx - 1 + window_size]
200
+ window_text = " ".join(window_lines)
201
+ normalized_window = normalize_text(window_text).casefold()
202
+ seg_len = len(normalized_window)
203
+ if seg_len == 0:
204
+ continue
205
+
206
+ ratio = difflib.SequenceMatcher(None, normalized_search, normalized_window).ratio()
207
+
208
+ # Containment boost under similar length condition
209
+ shorter = min(len(normalized_search), seg_len)
210
+ longer = max(len(normalized_search), seg_len)
211
+ length_ratio = (shorter / longer) if longer else 0.0
212
+ if (
213
+ normalized_search
214
+ and seg_len >= 8
215
+ and length_ratio >= 0.4
216
+ and (
217
+ normalized_search in normalized_window
218
+ or normalized_window in normalized_search
219
+ )
220
+ ):
221
+ ratio = max(ratio, 0.999)
222
+
223
+ if verbose:
224
+ console.print(
225
+ f"Window {start_idx}-{start_idx+window_size-1}: ratio={ratio}"
226
+ )
227
+
228
+ # Track best window, prefer higher ratio; tie-breaker: larger window, then longer segment
229
+ if ratio > win_best_ratio + 1e-6 or (
230
+ abs(ratio - win_best_ratio) < 1e-6
231
+ and (window_size > win_best_size or (window_size == win_best_size and seg_len > 0))
232
+ ):
233
+ win_best_ratio = ratio
234
+ win_best_idx = start_idx
235
+ win_best_size = window_size
236
+
237
+ if win_best_idx is not None and win_best_ratio > highest_ratio:
238
+ if win_best_ratio >= primary_threshold:
239
+ best_match = win_best_idx
240
+ highest_ratio = win_best_ratio
241
+ elif win_best_ratio >= fallback_threshold and best_match is None:
242
+ best_match = win_best_idx
243
+ highest_ratio = win_best_ratio
244
+ if verbose:
245
+ console.print(
246
+ f"[yellow]Low-confidence multi-line match selected (ratio={win_best_ratio:.3f}).[/yellow]"
247
+ )
128
248
 
129
249
  # Step 8: Return results
130
250
  if verbose:
@@ -136,4 +256,4 @@ def trace(
136
256
 
137
257
  except Exception as e:
138
258
  console.print(f"[bold red]Error in trace function: {str(e)}[/bold red]")
139
- return None, 0.0, ""
259
+ return None, 0.0, ""
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
 
@@ -50,7 +50,7 @@ def trace_main(ctx: click.Context, prompt_file: str, code_file: str, code_line:
50
50
 
51
51
  # Perform trace analysis
52
52
  strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
53
- temperature = ctx.obj.get('temperature', 0.0)
53
+ temperature = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
54
54
  time = ctx.obj.get('time', DEFAULT_TIME)
55
55
  try:
56
56
  prompt_line, total_cost, model_name = trace(
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]")
@@ -116,4 +155,4 @@ if __name__ == "__main__":
116
155
  rprint(f"Cost: ${cost:.6f}")
117
156
  rprint(f"Model: {model}")
118
157
  except Exception as e:
119
- rprint("[red]Error in example:[/red]", str(e))
158
+ rprint("[red]Error in example:[/red]", str(e))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdd-cli
3
- Version: 0.0.49
3
+ Version: 0.0.51
4
4
  Summary: PDD (Prompt-Driven Development) Command Line Interface
5
5
  Author: Greg Tanaka
6
6
  Author-email: glt@alumni.caltech.edu
@@ -38,6 +38,7 @@ Requires-Dist: setuptools
38
38
  Requires-Dist: pytest
39
39
  Requires-Dist: boto3==1.35.99
40
40
  Requires-Dist: python-Levenshtein
41
+ Requires-Dist: google-cloud-aiplatform>=1.3
41
42
  Requires-Dist: openai>=1.99.5
42
43
  Provides-Extra: dev
43
44
  Requires-Dist: commitizen; extra == "dev"
@@ -45,9 +46,11 @@ Requires-Dist: pytest-cov; extra == "dev"
45
46
  Requires-Dist: pytest-mock; extra == "dev"
46
47
  Requires-Dist: pytest-asyncio; extra == "dev"
47
48
  Requires-Dist: z3-solver; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: twine; extra == "dev"
48
51
  Dynamic: license-file
49
52
 
50
- .. image:: https://img.shields.io/badge/pdd--cli-v0.0.49-blue
53
+ .. image:: https://img.shields.io/badge/pdd--cli-v0.0.51-blue
51
54
  :alt: PDD-CLI Version
52
55
 
53
56
  .. image:: https://img.shields.io/badge/Discord-join%20chat-7289DA.svg?logo=discord&logoColor=white&link=https://discord.gg/Yp4RTh8bG7
@@ -124,7 +127,7 @@ After installation, verify:
124
127
 
125
128
  pdd --version
126
129
 
127
- You'll see the current PDD version (e.g., 0.0.49).
130
+ You'll see the current PDD version (e.g., 0.0.51).
128
131
 
129
132
  Getting Started with Examples
130
133
  -----------------------------