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
pdd/cmd_test_main.py CHANGED
@@ -3,9 +3,11 @@ Main entry point for the 'test' command.
3
3
  """
4
4
  from __future__ import annotations
5
5
  import click
6
+ from pathlib import Path
6
7
  # pylint: disable=redefined-builtin
7
8
  from rich import print
8
9
 
10
+ from .config_resolution import resolve_effective_config
9
11
  from .construct_paths import construct_paths
10
12
  from .generate_test import generate_test
11
13
  from .increase_tests import increase_tests
@@ -19,9 +21,11 @@ def cmd_test_main(
19
21
  output: str | None,
20
22
  language: str | None,
21
23
  coverage_report: str | None,
22
- existing_tests: str | None,
24
+ existing_tests: list[str] | None,
23
25
  target_coverage: float | None,
24
26
  merge: bool | None,
27
+ strength: float | None = None,
28
+ temperature: float | None = None,
25
29
  ) -> tuple[str, float, str]:
26
30
  """
27
31
  CLI wrapper for generating or enhancing unit tests.
@@ -36,7 +40,7 @@ def cmd_test_main(
36
40
  output (str | None): Path to save the generated test file.
37
41
  language (str | None): Programming language.
38
42
  coverage_report (str | None): Path to the coverage report file.
39
- existing_tests (str | None): Path to the existing unit test file.
43
+ existing_tests (list[str] | None): Paths to the existing unit test files.
40
44
  target_coverage (float | None): Desired code coverage percentage.
41
45
  merge (bool | None): Whether to merge new tests with existing tests.
42
46
 
@@ -51,9 +55,9 @@ def cmd_test_main(
51
55
  input_strings = {}
52
56
 
53
57
  verbose = ctx.obj["verbose"]
54
- strength = ctx.obj["strength"]
55
- temperature = ctx.obj["temperature"]
56
- time = ctx.obj.get("time")
58
+ # Note: strength/temperature will be resolved after construct_paths using resolve_effective_config
59
+ param_strength = strength # Store the parameter value for later resolution
60
+ param_temperature = temperature # Store the parameter value for later resolution
57
61
 
58
62
  if verbose:
59
63
  print(f"[bold blue]Prompt file:[/bold blue] {prompt_file}")
@@ -72,7 +76,7 @@ def cmd_test_main(
72
76
  if coverage_report:
73
77
  input_file_paths["coverage_report"] = coverage_report
74
78
  if existing_tests:
75
- input_file_paths["existing_tests"] = existing_tests
79
+ input_file_paths["existing_tests"] = existing_tests[0]
76
80
 
77
81
  command_options = {
78
82
  "output": output,
@@ -87,18 +91,58 @@ def cmd_test_main(
87
91
  quiet=ctx.obj["quiet"],
88
92
  command="test",
89
93
  command_options=command_options,
94
+ context_override=ctx.obj.get('context'),
95
+ confirm_callback=ctx.obj.get('confirm_callback')
90
96
  )
97
+
98
+ # Read multiple existing test files and concatenate their content
99
+ if existing_tests:
100
+ existing_tests_content = ""
101
+ for test_file in existing_tests:
102
+ with open(test_file, 'r') as f:
103
+ existing_tests_content += f.read() + "\n"
104
+ input_strings["existing_tests"] = existing_tests_content
105
+
106
+ # Use centralized config resolution with proper priority:
107
+ # CLI > pddrc > defaults
108
+ effective_config = resolve_effective_config(
109
+ ctx,
110
+ resolved_config,
111
+ param_overrides={"strength": param_strength, "temperature": param_temperature}
112
+ )
113
+ strength = effective_config["strength"]
114
+ temperature = effective_config["temperature"]
115
+ time = effective_config["time"]
116
+ except click.Abort:
117
+ # User cancelled - re-raise to stop the sync loop
118
+ raise
91
119
  except Exception as exception:
92
120
  # Catching a general exception is necessary here to handle a wide range of
93
121
  # potential errors during file I/O and path construction, ensuring the
94
122
  # CLI remains robust.
95
123
  print(f"[bold red]Error constructing paths: {exception}[/bold red]")
96
- ctx.exit(1)
97
- return "", 0.0, ""
124
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
125
+ return "", 0.0, f"Error: {exception}"
98
126
 
99
127
  if verbose:
100
128
  print(f"[bold blue]Language detected:[/bold blue] {language}")
101
129
 
130
+ # Determine where the generated tests will be written so we can share it with the LLM
131
+ # Always use resolved_output since construct_paths handles numbering for test/bug commands
132
+ resolved_output = output_file_paths["output"]
133
+ output_file = resolved_output
134
+ if merge and existing_tests:
135
+ output_file = existing_tests[0]
136
+
137
+ if not output_file:
138
+ print("[bold red]Error: Output file path could not be determined.[/bold red]")
139
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
140
+ return "", 0.0, "Error: Output file path could not be determined"
141
+
142
+ source_file_path_for_prompt = str(Path(code_file).expanduser().resolve())
143
+ test_file_path_for_prompt = str(Path(output_file).expanduser().resolve())
144
+ module_name_for_prompt = Path(source_file_path_for_prompt).stem if source_file_path_for_prompt else ""
145
+
102
146
  # Generate or enhance unit tests
103
147
  if not coverage_report:
104
148
  try:
@@ -110,22 +154,25 @@ def cmd_test_main(
110
154
  time=time,
111
155
  language=language,
112
156
  verbose=verbose,
157
+ source_file_path=source_file_path_for_prompt,
158
+ test_file_path=test_file_path_for_prompt,
159
+ module_name=module_name_for_prompt,
113
160
  )
114
161
  except Exception as exception:
115
162
  # A general exception is caught to handle various errors that can occur
116
163
  # during the test generation process, which involves external model
117
164
  # interactions and complex logic.
118
165
  print(f"[bold red]Error generating tests: {exception}[/bold red]")
119
- ctx.exit(1)
120
- return "", 0.0, ""
166
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
167
+ return "", 0.0, f"Error: {exception}"
121
168
  else:
122
169
  if not existing_tests:
123
170
  print(
124
171
  "[bold red]Error: --existing-tests is required "
125
172
  "when using --coverage-report[/bold red]"
126
173
  )
127
- ctx.exit(1)
128
- return "", 0.0, ""
174
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
175
+ return "", 0.0, "Error: --existing-tests is required when using --coverage-report"
129
176
  try:
130
177
  unit_test, total_cost, model_name = increase_tests(
131
178
  existing_unit_tests=input_strings["existing_tests"],
@@ -143,19 +190,33 @@ def cmd_test_main(
143
190
  # while increasing test coverage, including problems with parsing
144
191
  # reports or interacting with the language model.
145
192
  print(f"[bold red]Error increasing test coverage: {exception}[/bold red]")
146
- ctx.exit(1)
147
- return "", 0.0, ""
193
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
194
+ return "", 0.0, f"Error: {exception}"
148
195
 
149
- # Handle output - prioritize orchestration output path over construct_paths result
150
- output_file = output or output_file_paths["output"]
196
+ # Handle output - always use resolved file path since construct_paths handles numbering
197
+ resolved_output = output_file_paths["output"]
198
+ output_file = resolved_output
151
199
  if merge and existing_tests:
152
- output_file = existing_tests
200
+ output_file = existing_tests[0] if existing_tests else None
153
201
 
154
202
  if not output_file:
155
203
  print("[bold red]Error: Output file path could not be determined.[/bold red]")
156
204
  ctx.exit(1)
157
205
  return "", 0.0, ""
206
+
207
+ # Check if unit_test content is empty
208
+ if not unit_test or not unit_test.strip():
209
+ print(f"[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
210
+ print(f"[bold yellow]Debug: unit_test length: {len(unit_test) if unit_test else 0}[/bold yellow]")
211
+ print(f"[bold yellow]Debug: unit_test content preview: {repr(unit_test[:100]) if unit_test else 'None'}[/bold yellow]")
212
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
213
+ return "", 0.0, "Error: Generated unit test content is empty"
214
+
158
215
  try:
216
+ # Ensure parent directory exists
217
+ output_path = Path(output_file)
218
+ output_path.parent.mkdir(parents=True, exist_ok=True)
219
+
159
220
  with open(output_file, "w", encoding="utf-8") as file_handle:
160
221
  file_handle.write(unit_test)
161
222
  print(f"[bold green]Unit tests saved to:[/bold green] {output_file}")
@@ -164,8 +225,8 @@ def cmd_test_main(
164
225
  # (e.g., permissions, disk space) that can occur when writing the
165
226
  # output file, preventing the program from crashing unexpectedly.
166
227
  print(f"[bold red]Error saving tests to file: {exception}[/bold red]")
167
- ctx.exit(1)
168
- return "", 0.0, ""
228
+ # Return error result instead of ctx.exit(1) to allow orchestrator to handle gracefully
229
+ return "", 0.0, f"Error: {exception}"
169
230
 
170
231
  if verbose:
171
232
  print(f"[bold blue]Total cost:[/bold blue] ${total_cost:.6f}")
pdd/code_generator.py CHANGED
@@ -1,3 +1,5 @@
1
+ import json
2
+ import re
1
3
  from typing import Tuple, Optional
2
4
  from rich.console import Console
3
5
  from . import EXTRACTION_STRENGTH
@@ -17,6 +19,7 @@ def code_generator(
17
19
  time: Optional[float] = None,
18
20
  verbose: bool = False,
19
21
  preprocess_prompt: bool = True,
22
+ output_schema: Optional[dict] = None,
20
23
  ) -> Tuple[str, float, str]:
21
24
  """
22
25
  Generate code from a prompt using a language model.
@@ -28,6 +31,8 @@ def code_generator(
28
31
  temperature (float, optional): The temperature for the LLM model. Defaults to 0.0
29
32
  time (Optional[float], optional): The time for the LLM model. Defaults to None
30
33
  verbose (bool, optional): Whether to print detailed information. Defaults to False
34
+ preprocess_prompt (bool, optional): Whether to preprocess the prompt. Defaults to True
35
+ output_schema (Optional[dict], optional): JSON schema to enforce structured output. Defaults to None
31
36
 
32
37
  Returns:
33
38
  Tuple[str, float, str]: Tuple containing (runnable_code, total_cost, model_name)
@@ -62,14 +67,37 @@ def code_generator(
62
67
  # Step 2: Generate initial response
63
68
  if verbose:
64
69
  console.print("[bold blue]Step 2: Generating initial response[/bold blue]")
65
- response = llm_invoke(
66
- prompt=processed_prompt,
67
- input_json={},
68
- strength=strength,
69
- temperature=temperature,
70
- time=time,
71
- verbose=verbose
72
- )
70
+
71
+ if 'data:image' in processed_prompt:
72
+ parts = re.split(r'(data:image/[^;]+;base64,[A-Za-z0-9+/=]+)', processed_prompt)
73
+
74
+ content = []
75
+ for part in parts:
76
+ if part.startswith('data:image'):
77
+ content.append({"type": "image_url", "image_url": {"url": part}})
78
+ elif part != "":
79
+ content.append({"type": "text", "text": part})
80
+
81
+ messages = [{"role": "user", "content": content}]
82
+
83
+ response = llm_invoke(
84
+ messages=messages,
85
+ strength=strength,
86
+ temperature=temperature,
87
+ time=time,
88
+ verbose=verbose,
89
+ output_schema=output_schema
90
+ )
91
+ else:
92
+ response = llm_invoke(
93
+ prompt=processed_prompt,
94
+ input_json={},
95
+ strength=strength,
96
+ temperature=temperature,
97
+ time=time,
98
+ verbose=verbose,
99
+ output_schema=output_schema
100
+ )
73
101
  initial_output = response['result']
74
102
  total_cost += response['cost']
75
103
  model_name = response['model_name']
@@ -83,6 +111,7 @@ def code_generator(
83
111
  strength=0.5,
84
112
  temperature=0.0,
85
113
  time=time,
114
+ language=language,
86
115
  verbose=verbose
87
116
  )
88
117
  total_cost += check_cost
@@ -97,6 +126,7 @@ def code_generator(
97
126
  strength=strength,
98
127
  temperature=temperature,
99
128
  time=time,
129
+ language=language,
100
130
  verbose=verbose
101
131
  )
102
132
  total_cost += continue_cost
@@ -107,15 +137,25 @@ def code_generator(
107
137
  # Step 4: Postprocess the output
108
138
  if verbose:
109
139
  console.print("[bold blue]Step 4: Postprocessing output[/bold blue]")
110
- runnable_code, postprocess_cost, model_name_post = postprocess(
111
- llm_output=final_output,
112
- language=language,
113
- strength=EXTRACTION_STRENGTH,
114
- temperature=0.0,
115
- time=time,
116
- verbose=verbose
117
- )
118
- total_cost += postprocess_cost
140
+
141
+ # For structured JSON targets, skip extract_code to avoid losing or altering schema-constrained payloads.
142
+ if (isinstance(language, str) and language.strip().lower() == "json") or output_schema:
143
+ if isinstance(final_output, str):
144
+ runnable_code = final_output
145
+ else:
146
+ runnable_code = json.dumps(final_output)
147
+ postprocess_cost = 0.0
148
+ model_name_post = model_name
149
+ else:
150
+ runnable_code, postprocess_cost, model_name_post = postprocess(
151
+ llm_output=final_output,
152
+ language=language,
153
+ strength=EXTRACTION_STRENGTH,
154
+ temperature=0.0,
155
+ time=time,
156
+ verbose=verbose
157
+ )
158
+ total_cost += postprocess_cost
119
159
 
120
160
  return runnable_code, total_cost, model_name
121
161
 
@@ -126,4 +166,4 @@ def code_generator(
126
166
  except Exception as e:
127
167
  if verbose:
128
168
  console.print(f"[bold red]Unexpected Error: {str(e)}[/bold red]")
129
- raise
169
+ raise