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.
Files changed (119) 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 +80 -19
  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 +281 -81
  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 -62
  40. pdd/data/llm_model.csv +20 -18
  41. pdd/detect_change_main.py +5 -4
  42. pdd/fix_code_loop.py +331 -77
  43. pdd/fix_error_loop.py +209 -60
  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 +319 -272
  48. pdd/fix_verification_main.py +57 -17
  49. pdd/generate_output_paths.py +93 -10
  50. pdd/generate_test.py +16 -5
  51. pdd/get_jwt_token.py +48 -9
  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/increase_tests.py +7 -0
  56. pdd/incremental_code_generator.py +2 -2
  57. pdd/insert_includes.py +11 -3
  58. pdd/llm_invoke.py +1278 -110
  59. pdd/load_prompt_template.py +36 -10
  60. pdd/pdd_completion.fish +25 -2
  61. pdd/pdd_completion.sh +30 -4
  62. pdd/pdd_completion.zsh +79 -4
  63. pdd/postprocess.py +10 -3
  64. pdd/preprocess.py +228 -15
  65. pdd/preprocess_main.py +8 -5
  66. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  67. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  68. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  69. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  70. pdd/prompts/agentic_update_LLM.prompt +1071 -0
  71. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  72. pdd/prompts/auto_include_LLM.prompt +98 -101
  73. pdd/prompts/change_LLM.prompt +1 -3
  74. pdd/prompts/detect_change_LLM.prompt +562 -3
  75. pdd/prompts/example_generator_LLM.prompt +22 -1
  76. pdd/prompts/extract_code_LLM.prompt +5 -1
  77. pdd/prompts/extract_program_code_fix_LLM.prompt +14 -2
  78. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  79. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  80. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  81. pdd/prompts/fix_code_module_errors_LLM.prompt +16 -4
  82. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +6 -41
  83. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  84. pdd/prompts/generate_test_LLM.prompt +21 -6
  85. pdd/prompts/increase_tests_LLM.prompt +1 -2
  86. pdd/prompts/insert_includes_LLM.prompt +1181 -6
  87. pdd/prompts/split_LLM.prompt +1 -62
  88. pdd/prompts/trace_LLM.prompt +25 -22
  89. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  90. pdd/prompts/update_prompt_LLM.prompt +22 -1
  91. pdd/prompts/xml_convertor_LLM.prompt +3246 -7
  92. pdd/pytest_output.py +188 -21
  93. pdd/python_env_detector.py +151 -0
  94. pdd/render_mermaid.py +236 -0
  95. pdd/setup_tool.py +648 -0
  96. pdd/simple_math.py +2 -0
  97. pdd/split_main.py +3 -2
  98. pdd/summarize_directory.py +56 -7
  99. pdd/sync_determine_operation.py +918 -186
  100. pdd/sync_main.py +82 -32
  101. pdd/sync_orchestration.py +1456 -453
  102. pdd/sync_tui.py +848 -0
  103. pdd/template_registry.py +264 -0
  104. pdd/templates/architecture/architecture_json.prompt +242 -0
  105. pdd/templates/generic/generate_prompt.prompt +174 -0
  106. pdd/trace.py +168 -12
  107. pdd/trace_main.py +4 -3
  108. pdd/track_cost.py +151 -61
  109. pdd/unfinished_prompt.py +49 -3
  110. pdd/update_main.py +549 -67
  111. pdd/update_model_costs.py +2 -2
  112. pdd/update_prompt.py +19 -4
  113. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +20 -7
  114. pdd_cli-0.0.90.dist-info/RECORD +153 -0
  115. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
  116. pdd_cli-0.0.42.dist-info/RECORD +0 -115
  117. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
  118. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
  119. {pdd_cli-0.0.42.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,66 @@
1
1
  import sys
2
+ import ast
2
3
  from typing import Tuple, Optional
4
+ from pathlib import Path
3
5
  import click
4
6
  from rich import print as rprint
5
7
 
6
8
  from .construct_paths import construct_paths
7
9
  from .context_generator import context_generator
8
10
 
11
+
12
+ def _validate_python_syntax(code: str) -> Tuple[bool, Optional[str]]:
13
+ """
14
+ Validate that the code is valid Python syntax.
15
+
16
+ Returns:
17
+ Tuple of (is_valid, error_message)
18
+ """
19
+ try:
20
+ ast.parse(code)
21
+ return True, None
22
+ except SyntaxError as e:
23
+ return False, f"SyntaxError at line {e.lineno}: {e.msg}"
24
+
25
+
26
+ def _try_fix_json_garbage(code: str) -> Optional[str]:
27
+ """
28
+ Attempt to fix code that has JSON metadata garbage appended at the end.
29
+ This is a common LLM extraction failure pattern.
30
+
31
+ Returns:
32
+ Fixed code if successful, None if not fixable.
33
+ """
34
+ lines = code.split('\n')
35
+
36
+ # Look for JSON-like patterns at the end
37
+ json_patterns = ['"explanation":', '"focus":', '"description":', '"code":']
38
+
39
+ for i in range(len(lines) - 1, -1, -1):
40
+ line = lines[i].strip()
41
+
42
+ # If we find a line that's clearly JSON garbage
43
+ if any(pattern in line for pattern in json_patterns):
44
+ # Go back and find where valid Python ends
45
+ for j in range(i - 1, -1, -1):
46
+ candidate = '\n'.join(lines[:j + 1])
47
+ is_valid, _ = _validate_python_syntax(candidate)
48
+ if is_valid:
49
+ return candidate
50
+ break
51
+
52
+ # If line is just "}" or "]", likely JSON ending
53
+ if line in ['}', ']', '},', '],']:
54
+ continue
55
+
56
+ # If line ends with '", (JSON string end)
57
+ if line.endswith('",') or line.endswith('"'):
58
+ # Check if this looks like inside a JSON object
59
+ if any(pattern in lines[i] if i < len(lines) else '' for pattern in json_patterns):
60
+ continue
61
+
62
+ return None
63
+
9
64
  def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
10
65
  """
11
66
  Main function to generate example code from a prompt file and an existing code file.
@@ -30,13 +85,26 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
30
85
  force=ctx.obj.get('force', False),
31
86
  quiet=ctx.obj.get('quiet', False),
32
87
  command="example",
33
- command_options=command_options
88
+ command_options=command_options,
89
+ context_override=ctx.obj.get('context'),
90
+ confirm_callback=ctx.obj.get('confirm_callback')
34
91
  )
35
92
 
36
93
  # Load input files
37
94
  prompt_content = input_strings["prompt_file"]
38
95
  code_content = input_strings["code_file"]
39
96
 
97
+ # Get resolved output path for file path information
98
+ resolved_output = output_file_paths["output"]
99
+
100
+ # Determine file path information for correct imports
101
+ from pathlib import Path
102
+ source_file_path = str(Path(code_file).resolve())
103
+ example_file_path = str(Path(resolved_output).resolve()) if resolved_output else ""
104
+
105
+ # Extract module name from the code file
106
+ module_name = Path(code_file).stem
107
+
40
108
  # Generate example code
41
109
  strength = ctx.obj.get('strength', 0.5)
42
110
  temperature = ctx.obj.get('temperature', 0)
@@ -48,21 +116,51 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
48
116
  strength=strength,
49
117
  temperature=temperature,
50
118
  time=time,
51
- verbose=ctx.obj.get('verbose', False)
119
+ verbose=ctx.obj.get('verbose', False),
120
+ source_file_path=source_file_path,
121
+ example_file_path=example_file_path,
122
+ module_name=module_name
52
123
  )
53
124
 
54
- # Save results - prioritize orchestration output path over construct_paths result
55
- final_output_path = output or output_file_paths["output"]
56
- print(f"DEBUG: output param = {output}")
57
- print(f"DEBUG: output_file_paths['output'] = {output_file_paths['output']}")
58
- print(f"DEBUG: final_output_path = {final_output_path}")
125
+ # Save results - if output is a directory, use resolved file path from construct_paths
126
+ if output is None:
127
+ final_output_path = resolved_output
128
+ else:
129
+ try:
130
+ is_dir_hint = output.endswith('/')
131
+ except Exception:
132
+ is_dir_hint = False
133
+ if is_dir_hint or (Path(output).exists() and Path(output).is_dir()):
134
+ final_output_path = resolved_output
135
+ else:
136
+ final_output_path = output
59
137
  if final_output_path and example_code is not None:
138
+ # Validate Python syntax before saving
139
+ if language == "python":
140
+ is_valid, error_msg = _validate_python_syntax(example_code)
141
+ if not is_valid:
142
+ if not ctx.obj.get('quiet', False):
143
+ rprint(f"[yellow]Warning: Generated code has syntax error: {error_msg}[/yellow]")
144
+ rprint("[yellow]Attempting to fix JSON garbage pattern...[/yellow]")
145
+
146
+ # Try to fix JSON garbage at end of file
147
+ fixed_code = _try_fix_json_garbage(example_code)
148
+ if fixed_code:
149
+ is_valid_fixed, _ = _validate_python_syntax(fixed_code)
150
+ if is_valid_fixed:
151
+ if not ctx.obj.get('quiet', False):
152
+ rprint("[green]Successfully removed garbage and fixed syntax.[/green]")
153
+ example_code = fixed_code
154
+ else:
155
+ rprint("[red]Could not fix syntax error. Saving as-is.[/red]")
156
+ else:
157
+ rprint("[red]Could not detect fixable pattern. Saving as-is.[/red]")
158
+
60
159
  with open(final_output_path, 'w') as f:
61
160
  f.write(example_code)
62
161
  elif final_output_path and example_code is None:
63
- # Log the error but don't crash
64
- if not ctx.obj.get('quiet', False):
65
- rprint("[bold red]Warning:[/bold red] Example generation failed, skipping file write")
162
+ # Raise error instead of just warning
163
+ raise click.UsageError("Example generation failed, no code produced.")
66
164
 
67
165
  # Provide user feedback
68
166
  if not ctx.obj.get('quiet', False):
@@ -85,7 +183,11 @@ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str,
85
183
 
86
184
  return example_code, total_cost, model_name
87
185
 
186
+ except click.Abort:
187
+ # User cancelled - re-raise to stop the sync loop
188
+ raise
88
189
  except Exception as e:
89
190
  if not ctx.obj.get('quiet', False):
90
191
  rprint(f"[bold red]Error:[/bold red] {str(e)}")
91
- sys.exit(1)
192
+ # Return error result instead of sys.exit(1) to allow orchestrator to handle gracefully
193
+ return "", 0.0, f"Error: {e}"
@@ -1,4 +1,5 @@
1
- from typing import Tuple
1
+ from typing import Tuple, Optional
2
+ import logging
2
3
  from rich.console import Console
3
4
  from rich.syntax import Syntax
4
5
  from pydantic import BaseModel, Field
@@ -9,6 +10,10 @@ from .unfinished_prompt import unfinished_prompt
9
10
  from . import EXTRACTION_STRENGTH, DEFAULT_TIME
10
11
 
11
12
  console = Console()
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Maximum number of generation loops to prevent infinite loops
16
+ MAX_GENERATION_LOOPS = 20
12
17
 
13
18
  class TrimResultsStartOutput(BaseModel):
14
19
  explanation: str = Field(description="The explanation of how you determined what to cut out")
@@ -24,6 +29,7 @@ def continue_generation(
24
29
  strength: float,
25
30
  temperature: float,
26
31
  time: float = DEFAULT_TIME,
32
+ language: Optional[str] = None,
27
33
  verbose: bool = False
28
34
  ) -> Tuple[str, float, str]:
29
35
  """
@@ -84,10 +90,16 @@ def continue_generation(
84
90
  code_block = trim_start_response['result'].code_block
85
91
 
86
92
  # Step 4: Continue generation loop
87
- while True:
93
+ while loop_count < MAX_GENERATION_LOOPS:
88
94
  loop_count += 1
89
95
  if verbose:
90
96
  console.print(f"[cyan]Generation loop {loop_count}[/cyan]")
97
+
98
+ # Check for maximum loops reached
99
+ if loop_count >= MAX_GENERATION_LOOPS:
100
+ logger.warning(f"Reached maximum generation loops ({MAX_GENERATION_LOOPS}), terminating")
101
+ console.print(f"[yellow]Warning: Reached maximum generation loops ({MAX_GENERATION_LOOPS}), terminating[/yellow]")
102
+ break
91
103
 
92
104
  # Generate continuation
93
105
  continue_response = llm_invoke(
@@ -106,19 +118,47 @@ def continue_generation(
106
118
  model_name = continue_response['model_name']
107
119
  continue_result = continue_response['result']
108
120
 
109
- # Check if generation is complete
110
- last_chunk = code_block[-600:] if len(code_block) > 600 else code_block
111
- _, is_finished, check_cost, _ = unfinished_prompt(
121
+ if verbose:
122
+ try:
123
+ preview = (continue_result[:160] + '...') if isinstance(continue_result, str) and len(continue_result) > 160 else continue_result
124
+ except Exception:
125
+ preview = "<non-str>"
126
+ console.print(f"[blue]Continue model:[/blue] {model_name}")
127
+ console.print(f"[blue]Continue preview:[/blue] {preview!r}")
128
+
129
+ # If the model produced no continuation, avoid an endless loop
130
+ if not isinstance(continue_result, str) or not continue_result.strip():
131
+ logger.warning("Empty continuation received; stopping to avoid loop.")
132
+ break
133
+
134
+ # Build prospective new block and check completeness on the updated tail
135
+ new_code_block = code_block + continue_result
136
+ last_chunk = new_code_block[-600:] if len(new_code_block) > 600 else new_code_block
137
+ reasoning, is_finished, check_cost, check_model = unfinished_prompt(
112
138
  prompt_text=last_chunk,
113
139
  strength=0.5,
114
140
  temperature=0,
115
141
  time=time,
142
+ language=language,
116
143
  verbose=verbose
117
144
  )
118
145
  total_cost += check_cost
119
146
 
147
+ if verbose:
148
+ console.print(f"[magenta]Tail length:[/magenta] {len(last_chunk)}")
149
+ # Show a safe, shortened representation of the tail
150
+ try:
151
+ tail_preview = (last_chunk[-200:] if len(last_chunk) > 200 else last_chunk)
152
+ except Exception:
153
+ tail_preview = "<unprintable tail>"
154
+ console.print(f"[magenta]Tail preview (last 200 chars):[/magenta]\n{tail_preview}")
155
+ console.print(f"[magenta]Unfinished check model:[/magenta] {check_model}")
156
+ console.print(f"[magenta]is_finished:[/magenta] {is_finished}")
157
+ console.print(f"[magenta]Reasoning:[/magenta] {reasoning}")
158
+
120
159
  if not is_finished:
121
- code_block += continue_result
160
+ code_block = new_code_block
161
+ # Continue to next iteration
122
162
  else:
123
163
  # Trim and append final continuation
124
164
  trim_response = llm_invoke(
@@ -146,4 +186,4 @@ def continue_generation(
146
186
 
147
187
  except Exception as e:
148
188
  console.print(f"[bold red]Error in continue_generation: {str(e)}[/bold red]")
149
- raise
189
+ raise
pdd/core/__init__.py ADDED
File without changes