pdd-cli 0.0.45__py3-none-any.whl → 0.0.118__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 (195) hide show
  1. pdd/__init__.py +40 -8
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +497 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +526 -0
  6. pdd/agentic_common.py +598 -0
  7. pdd/agentic_crash.py +534 -0
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +426 -0
  10. pdd/agentic_fix.py +1294 -0
  11. pdd/agentic_langtest.py +162 -0
  12. pdd/agentic_update.py +387 -0
  13. pdd/agentic_verify.py +183 -0
  14. pdd/architecture_sync.py +565 -0
  15. pdd/auth_service.py +210 -0
  16. pdd/auto_deps_main.py +71 -51
  17. pdd/auto_include.py +245 -5
  18. pdd/auto_update.py +125 -47
  19. pdd/bug_main.py +196 -23
  20. pdd/bug_to_unit_test.py +2 -0
  21. pdd/change_main.py +11 -4
  22. pdd/cli.py +22 -1181
  23. pdd/cmd_test_main.py +350 -150
  24. pdd/code_generator.py +60 -18
  25. pdd/code_generator_main.py +790 -57
  26. pdd/commands/__init__.py +48 -0
  27. pdd/commands/analysis.py +306 -0
  28. pdd/commands/auth.py +309 -0
  29. pdd/commands/connect.py +290 -0
  30. pdd/commands/fix.py +163 -0
  31. pdd/commands/generate.py +257 -0
  32. pdd/commands/maintenance.py +175 -0
  33. pdd/commands/misc.py +87 -0
  34. pdd/commands/modify.py +256 -0
  35. pdd/commands/report.py +144 -0
  36. pdd/commands/sessions.py +284 -0
  37. pdd/commands/templates.py +215 -0
  38. pdd/commands/utility.py +110 -0
  39. pdd/config_resolution.py +58 -0
  40. pdd/conflicts_main.py +8 -3
  41. pdd/construct_paths.py +589 -111
  42. pdd/context_generator.py +10 -2
  43. pdd/context_generator_main.py +175 -76
  44. pdd/continue_generation.py +53 -10
  45. pdd/core/__init__.py +33 -0
  46. pdd/core/cli.py +527 -0
  47. pdd/core/cloud.py +237 -0
  48. pdd/core/dump.py +554 -0
  49. pdd/core/errors.py +67 -0
  50. pdd/core/remote_session.py +61 -0
  51. pdd/core/utils.py +90 -0
  52. pdd/crash_main.py +262 -33
  53. pdd/data/language_format.csv +71 -63
  54. pdd/data/llm_model.csv +20 -18
  55. pdd/detect_change_main.py +5 -4
  56. pdd/docs/prompting_guide.md +864 -0
  57. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  58. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  59. pdd/fix_code_loop.py +523 -95
  60. pdd/fix_code_module_errors.py +6 -2
  61. pdd/fix_error_loop.py +491 -92
  62. pdd/fix_errors_from_unit_tests.py +4 -3
  63. pdd/fix_main.py +278 -21
  64. pdd/fix_verification_errors.py +12 -100
  65. pdd/fix_verification_errors_loop.py +529 -286
  66. pdd/fix_verification_main.py +294 -89
  67. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  68. pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
  69. pdd/frontend/dist/index.html +376 -0
  70. pdd/frontend/dist/logo.svg +33 -0
  71. pdd/generate_output_paths.py +139 -15
  72. pdd/generate_test.py +218 -146
  73. pdd/get_comment.py +19 -44
  74. pdd/get_extension.py +8 -9
  75. pdd/get_jwt_token.py +318 -22
  76. pdd/get_language.py +8 -7
  77. pdd/get_run_command.py +75 -0
  78. pdd/get_test_command.py +68 -0
  79. pdd/git_update.py +70 -19
  80. pdd/incremental_code_generator.py +2 -2
  81. pdd/insert_includes.py +13 -4
  82. pdd/llm_invoke.py +1711 -181
  83. pdd/load_prompt_template.py +19 -12
  84. pdd/path_resolution.py +140 -0
  85. pdd/pdd_completion.fish +25 -2
  86. pdd/pdd_completion.sh +30 -4
  87. pdd/pdd_completion.zsh +79 -4
  88. pdd/postprocess.py +14 -4
  89. pdd/preprocess.py +293 -24
  90. pdd/preprocess_main.py +41 -6
  91. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  92. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  93. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  94. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  95. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  96. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  97. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  98. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  99. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  100. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  101. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  102. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  103. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
  104. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  105. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  106. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  107. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  108. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  109. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  110. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  111. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  112. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  113. pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
  114. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  115. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  116. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  117. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  118. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  119. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  120. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  121. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  122. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  123. pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
  124. pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
  125. pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
  126. pdd/prompts/agentic_update_LLM.prompt +925 -0
  127. pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
  128. pdd/prompts/auto_include_LLM.prompt +122 -905
  129. pdd/prompts/change_LLM.prompt +3093 -1
  130. pdd/prompts/detect_change_LLM.prompt +686 -27
  131. pdd/prompts/example_generator_LLM.prompt +22 -1
  132. pdd/prompts/extract_code_LLM.prompt +5 -1
  133. pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
  134. pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
  135. pdd/prompts/extract_promptline_LLM.prompt +17 -11
  136. pdd/prompts/find_verification_errors_LLM.prompt +6 -0
  137. pdd/prompts/fix_code_module_errors_LLM.prompt +12 -2
  138. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +9 -0
  139. pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
  140. pdd/prompts/generate_test_LLM.prompt +41 -7
  141. pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
  142. pdd/prompts/increase_tests_LLM.prompt +1 -5
  143. pdd/prompts/insert_includes_LLM.prompt +316 -186
  144. pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
  145. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  146. pdd/prompts/trace_LLM.prompt +25 -22
  147. pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
  148. pdd/prompts/update_prompt_LLM.prompt +22 -1
  149. pdd/pytest_output.py +127 -12
  150. pdd/remote_session.py +876 -0
  151. pdd/render_mermaid.py +236 -0
  152. pdd/server/__init__.py +52 -0
  153. pdd/server/app.py +335 -0
  154. pdd/server/click_executor.py +587 -0
  155. pdd/server/executor.py +338 -0
  156. pdd/server/jobs.py +661 -0
  157. pdd/server/models.py +241 -0
  158. pdd/server/routes/__init__.py +31 -0
  159. pdd/server/routes/architecture.py +451 -0
  160. pdd/server/routes/auth.py +364 -0
  161. pdd/server/routes/commands.py +929 -0
  162. pdd/server/routes/config.py +42 -0
  163. pdd/server/routes/files.py +603 -0
  164. pdd/server/routes/prompts.py +1322 -0
  165. pdd/server/routes/websocket.py +473 -0
  166. pdd/server/security.py +243 -0
  167. pdd/server/terminal_spawner.py +209 -0
  168. pdd/server/token_counter.py +222 -0
  169. pdd/setup_tool.py +648 -0
  170. pdd/simple_math.py +2 -0
  171. pdd/split_main.py +3 -2
  172. pdd/summarize_directory.py +237 -195
  173. pdd/sync_animation.py +8 -4
  174. pdd/sync_determine_operation.py +839 -112
  175. pdd/sync_main.py +351 -57
  176. pdd/sync_orchestration.py +1400 -756
  177. pdd/sync_tui.py +848 -0
  178. pdd/template_expander.py +161 -0
  179. pdd/template_registry.py +264 -0
  180. pdd/templates/architecture/architecture_json.prompt +237 -0
  181. pdd/templates/generic/generate_prompt.prompt +174 -0
  182. pdd/trace.py +168 -12
  183. pdd/trace_main.py +4 -3
  184. pdd/track_cost.py +140 -63
  185. pdd/unfinished_prompt.py +51 -4
  186. pdd/update_main.py +567 -67
  187. pdd/update_model_costs.py +2 -2
  188. pdd/update_prompt.py +19 -4
  189. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +29 -11
  190. pdd_cli-0.0.118.dist-info/RECORD +227 -0
  191. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +1 -1
  192. pdd_cli-0.0.45.dist-info/RECORD +0 -116
  193. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
  194. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
  195. {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/cmd_test_main.py CHANGED
@@ -2,182 +2,382 @@
2
2
  Main entry point for the 'test' command.
3
3
  """
4
4
  from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+
5
10
  import click
6
- # pylint: disable=redefined-builtin
7
- from rich import print
11
+ import requests
12
+ from rich.console import Console
13
+ from rich.panel import Panel
8
14
 
15
+ from .config_resolution import resolve_effective_config
9
16
  from .construct_paths import construct_paths
17
+ from .core.cloud import CloudConfig
10
18
  from .generate_test import generate_test
11
19
  from .increase_tests import increase_tests
12
20
 
21
+ # Cloud request timeout
22
+ CLOUD_REQUEST_TIMEOUT = 400 # seconds
23
+
24
+ console = Console()
25
+
13
26
 
14
- # pylint: disable=too-many-arguments, too-many-locals, too-many-return-statements, too-many-branches, too-many-statements, broad-except
15
27
  def cmd_test_main(
16
28
  ctx: click.Context,
17
29
  prompt_file: str,
18
30
  code_file: str,
19
- output: str | None,
20
- language: str | None,
21
- coverage_report: str | None,
22
- existing_tests: str | None,
23
- target_coverage: float | None,
24
- merge: bool | None,
31
+ output: str | None = None,
32
+ language: str | None = None,
33
+ coverage_report: str | None = None,
34
+ existing_tests: list[str] | None = None,
35
+ target_coverage: float | None = None,
36
+ merge: bool = False,
37
+ strength: float | None = None,
38
+ temperature: float | None = None,
25
39
  ) -> tuple[str, float, str]:
26
40
  """
27
41
  CLI wrapper for generating or enhancing unit tests.
28
42
 
29
- Reads a prompt file and a code file, generates unit tests using the `generate_test` function,
30
- and handles the output location.
31
-
32
43
  Args:
33
- ctx (click.Context): The Click context object.
34
- prompt_file (str): Path to the prompt file.
35
- code_file (str): Path to the code file.
36
- output (str | None): Path to save the generated test file.
37
- language (str | None): Programming language.
38
- coverage_report (str | None): Path to the coverage report file.
39
- existing_tests (str | None): Path to the existing unit test file.
40
- target_coverage (float | None): Desired code coverage percentage.
41
- merge (bool | None): Whether to merge new tests with existing tests.
44
+ ctx: Click context object containing global options.
45
+ prompt_file: Path to the prompt file.
46
+ code_file: Path to the code file.
47
+ output: Optional path for the output test file.
48
+ language: Optional programming language.
49
+ coverage_report: Optional path to a coverage report (triggers enhancement mode).
50
+ existing_tests: List of paths to existing test files (required if coverage_report is used).
51
+ target_coverage: Desired coverage percentage (not currently used by logic but accepted).
52
+ merge: If True, merge output into the first existing test file.
53
+ strength: Optional override for LLM strength.
54
+ temperature: Optional override for LLM temperature.
42
55
 
43
56
  Returns:
44
- tuple[str, float, str]: Generated unit test code, total cost, and model name.
57
+ tuple: (generated_test_code, total_cost, model_name)
45
58
  """
46
- # Initialize variables
47
- unit_test = ""
48
- total_cost = 0.0
49
- model_name = ""
50
- output_file_paths = {"output": output}
51
- input_strings = {}
52
-
53
- verbose = ctx.obj["verbose"]
54
- strength = ctx.obj["strength"]
55
- temperature = ctx.obj["temperature"]
56
- time = ctx.obj.get("time")
57
-
58
- if verbose:
59
- print(f"[bold blue]Prompt file:[/bold blue] {prompt_file}")
60
- print(f"[bold blue]Code file:[/bold blue] {code_file}")
61
- if output:
62
- print(f"[bold blue]Output:[/bold blue] {output}")
63
- if language:
64
- print(f"[bold blue]Language:[/bold blue] {language}")
65
-
66
- # Construct input strings, output file paths, and determine language
67
- try:
68
- input_file_paths = {
69
- "prompt_file": prompt_file,
70
- "code_file": code_file,
71
- }
72
- if coverage_report:
73
- input_file_paths["coverage_report"] = coverage_report
59
+ # 1. Prepare inputs for path construction
60
+ input_file_paths = {
61
+ "prompt_file": prompt_file,
62
+ "code_file": code_file,
63
+ }
64
+
65
+ # If coverage report is provided, we need existing tests
66
+ if coverage_report:
67
+ if not existing_tests:
68
+ console.print(
69
+ "[bold red]Error: 'existing_tests' is required when "
70
+ "'coverage_report' is provided.[/bold red]"
71
+ )
72
+ return "", 0.0, "Error: Missing existing_tests"
73
+
74
+ input_file_paths["coverage_report"] = coverage_report
75
+ # We pass the first existing test to help construct_paths resolve context if needed,
76
+ # though construct_paths primarily uses prompt/code files for language detection.
74
77
  if existing_tests:
75
- input_file_paths["existing_tests"] = existing_tests
78
+ input_file_paths["existing_test_0"] = existing_tests[0]
76
79
 
77
- command_options = {
78
- "output": output,
79
- "language": language,
80
- "merge": merge,
81
- "target_coverage": target_coverage,
82
- }
80
+ command_options = {
81
+ "output": output,
82
+ "language": language,
83
+ "merge": merge,
84
+ "target_coverage": target_coverage,
85
+ }
83
86
 
84
- resolved_config, input_strings, output_file_paths, language = construct_paths(
87
+ # 2. Construct paths and read inputs
88
+ try:
89
+ resolved_config, input_strings, output_file_paths, detected_language = construct_paths(
85
90
  input_file_paths=input_file_paths,
86
- force=ctx.obj["force"],
87
- quiet=ctx.obj["quiet"],
88
- command="test",
89
91
  command_options=command_options,
92
+ force=ctx.obj.get("force", False),
93
+ quiet=ctx.obj.get("quiet", False),
94
+ command="test",
95
+ context_override=ctx.obj.get("context"),
96
+ confirm_callback=ctx.obj.get("confirm_callback"),
90
97
  )
91
- except Exception as exception:
92
- # Catching a general exception is necessary here to handle a wide range of
93
- # potential errors during file I/O and path construction, ensuring the
94
- # CLI remains robust.
95
- print(f"[bold red]Error constructing paths: {exception}[/bold red]")
96
- ctx.exit(1)
97
- return "", 0.0, ""
98
-
99
- if verbose:
100
- print(f"[bold blue]Language detected:[/bold blue] {language}")
101
-
102
- # Generate or enhance unit tests
103
- if not coverage_report:
98
+ except Exception as e:
99
+ console.print(f"[bold red]Error constructing paths: {e}[/bold red]")
100
+ return "", 0.0, f"Error: {e}"
101
+
102
+ # 3. Resolve effective configuration (strength, temperature, time)
103
+ # Priority: Function Arg > CLI Context > Config File > Default
104
+ eff_config = resolve_effective_config(
105
+ ctx,
106
+ resolved_config,
107
+ param_overrides={"strength": strength, "temperature": temperature}
108
+ )
109
+
110
+ eff_strength = eff_config["strength"]
111
+ eff_temperature = eff_config["temperature"]
112
+ eff_time = eff_config["time"]
113
+ verbose = ctx.obj.get("verbose", False)
114
+ is_local = ctx.obj.get("local", False)
115
+
116
+ # 4. Prepare content variables
117
+ prompt_content = input_strings.get("prompt_file", "")
118
+ code_content = input_strings.get("code_file", "")
119
+
120
+ # Handle existing tests concatenation
121
+ concatenated_tests = None
122
+ if existing_tests:
123
+ test_contents = []
124
+ for et_path in existing_tests:
125
+ try:
126
+ # We read these manually because construct_paths only reads
127
+ # what's in input_file_paths keys. While we added
128
+ # existing_test_0, we might have multiple. To be safe and
129
+ # consistent with the requirement "read all files", we read
130
+ # them here. Note: construct_paths might have read
131
+ # 'existing_test_0', but we need all of them.
132
+ p = Path(et_path).expanduser().resolve()
133
+ if p.exists():
134
+ test_contents.append(p.read_text(encoding="utf-8"))
135
+ except Exception as e:
136
+ console.print(
137
+ f"[yellow]Warning: Could not read existing test file {et_path}: {e}[/yellow]"
138
+ )
139
+
140
+ if test_contents:
141
+ concatenated_tests = "\n".join(test_contents)
142
+ # Update input_strings for consistency if needed downstream
143
+ input_strings["existing_tests"] = concatenated_tests
144
+
145
+ # 5. Determine Execution Mode (Generate vs Increase)
146
+ mode = "increase" if coverage_report else "generate"
147
+
148
+ # Prepare metadata for generation
149
+ source_file_path = str(Path(code_file).expanduser().resolve())
150
+ # output_file_paths['output_file'] is set by construct_paths based on
151
+ # --output or defaults
152
+ test_file_path = str(
153
+ Path(output_file_paths.get("output_file", "test_output.py"))
154
+ .expanduser().resolve()
155
+ )
156
+ module_name = Path(source_file_path).stem
157
+
158
+ # Determine if code file is an example (for TDD style generation)
159
+ is_example = Path(code_file).stem.endswith("_example")
160
+
161
+ # Check for cloud-only mode
162
+ cloud_only = os.environ.get("PDD_CLOUD_ONLY", "").lower() in ("1", "true", "yes")
163
+
164
+ # 6. Execution Logic (Cloud vs Local)
165
+ generated_content = ""
166
+ total_cost = 0.0
167
+ model_name = "unknown"
168
+
169
+ # --- Cloud Execution ---
170
+ if not is_local:
104
171
  try:
105
- unit_test, total_cost, model_name = generate_test(
106
- input_strings["prompt_file"],
107
- input_strings["code_file"],
108
- strength=strength,
109
- temperature=temperature,
110
- time=time,
111
- language=language,
112
- verbose=verbose,
113
- )
114
- except Exception as exception:
115
- # A general exception is caught to handle various errors that can occur
116
- # during the test generation process, which involves external model
117
- # interactions and complex logic.
118
- print(f"[bold red]Error generating tests: {exception}[/bold red]")
119
- ctx.exit(1)
120
- return "", 0.0, ""
121
- else:
122
- if not existing_tests:
123
- print(
124
- "[bold red]Error: --existing-tests is required "
125
- "when using --coverage-report[/bold red]"
172
+ if verbose:
173
+ console.print(
174
+ Panel(
175
+ f"Attempting Cloud Execution (Mode: {mode})",
176
+ title="Cloud Status",
177
+ style="blue"
178
+ )
179
+ )
180
+
181
+ # Get JWT Token using CloudConfig
182
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
183
+
184
+ if not jwt_token:
185
+ raise Exception("Failed to obtain JWT token.")
186
+
187
+ # Prepare Payload
188
+ payload = {
189
+ "promptContent": prompt_content,
190
+ "language": detected_language,
191
+ "strength": eff_strength,
192
+ "temperature": eff_temperature,
193
+ "time": eff_time,
194
+ "verbose": verbose,
195
+ "sourceFilePath": source_file_path,
196
+ "testFilePath": test_file_path,
197
+ "moduleName": module_name,
198
+ "mode": mode,
199
+ }
200
+
201
+ if mode == "generate":
202
+ if is_example:
203
+ payload["example"] = code_content
204
+ payload["codeContent"] = None
205
+ else:
206
+ payload["codeContent"] = code_content
207
+ payload["example"] = None
208
+
209
+ if concatenated_tests:
210
+ payload["existingTests"] = concatenated_tests
211
+
212
+ elif mode == "increase":
213
+ payload["codeContent"] = code_content
214
+ payload["existingTests"] = concatenated_tests
215
+ payload["coverageReport"] = input_strings.get("coverage_report", "")
216
+
217
+ # Make Request
218
+ cloud_url = CloudConfig.get_endpoint_url("generateTest")
219
+ headers = {"Authorization": f"Bearer {jwt_token}"}
220
+ response = requests.post(
221
+ cloud_url,
222
+ json=payload,
223
+ headers=headers,
224
+ timeout=CLOUD_REQUEST_TIMEOUT
126
225
  )
127
- ctx.exit(1)
128
- return "", 0.0, ""
226
+
227
+ # Check for HTTP errors explicitly
228
+ response.raise_for_status()
229
+
230
+ # Parse response
231
+ try:
232
+ data = response.json()
233
+ except json.JSONDecodeError as json_err:
234
+ if cloud_only:
235
+ raise click.UsageError(f"Cloud returned invalid JSON: {json_err}")
236
+ console.print("[yellow]Cloud returned invalid JSON, falling back to local.[/yellow]")
237
+ is_local = True
238
+ raise # Re-raise to exit try block
239
+
240
+ generated_content = data.get("generatedTest", "")
241
+ total_cost = float(data.get("totalCost", 0.0))
242
+ model_name = data.get("modelName", "cloud-model")
243
+
244
+ # Check for empty response
245
+ if not generated_content or not generated_content.strip():
246
+ if cloud_only:
247
+ raise click.UsageError("Cloud returned empty test content")
248
+ console.print("[yellow]Cloud returned empty test content, falling back to local.[/yellow]")
249
+ is_local = True
250
+ else:
251
+ # Success!
252
+ console.print("[green]Cloud Success[/green]")
253
+
254
+ except click.UsageError:
255
+ # Re-raise UsageError without wrapping
256
+ raise
257
+ except requests.exceptions.Timeout as timeout_err:
258
+ if cloud_only:
259
+ raise click.UsageError(f"Cloud request timed out: {timeout_err}")
260
+ console.print("[yellow]Cloud request timed out, falling back to local.[/yellow]")
261
+ is_local = True
262
+ except requests.exceptions.HTTPError as http_err:
263
+ # Handle HTTP errors from raise_for_status()
264
+ # HTTPError from requests always has a response attribute
265
+ response_obj = getattr(http_err, 'response', None)
266
+ status_code = response_obj.status_code if response_obj is not None else None
267
+ error_text = response_obj.text if response_obj is not None else str(http_err)
268
+
269
+ # Non-recoverable errors - raise UsageError
270
+ if status_code == 402:
271
+ # Display balance info
272
+ try:
273
+ error_data = http_err.response.json()
274
+ balance = error_data.get("currentBalance", "unknown")
275
+ cost = error_data.get("estimatedCost", "unknown")
276
+ console.print(f"[red]Current balance: {balance}, Estimated cost: {cost}[/red]")
277
+ except Exception:
278
+ pass
279
+ raise click.UsageError(f"Insufficient credits: {error_text}")
280
+ elif status_code == 401:
281
+ raise click.UsageError(f"Cloud authentication failed: {error_text}")
282
+ elif status_code == 403:
283
+ raise click.UsageError(f"Access denied: {error_text}")
284
+ elif status_code == 400:
285
+ raise click.UsageError(f"Invalid request: {error_text}")
286
+
287
+ # 5xx and other errors - fall back if allowed
288
+ if cloud_only:
289
+ raise click.UsageError(f"Cloud execution failed (HTTP {status_code}): {error_text}")
290
+ console.print(f"[yellow]Cloud execution failed (HTTP {status_code}), falling back to local.[/yellow]")
291
+ is_local = True
292
+ except json.JSONDecodeError:
293
+ # Already handled above, just ensure we fall through to local
294
+ pass
295
+ except Exception as e:
296
+ if cloud_only:
297
+ raise click.UsageError(f"Cloud execution failed: {e}")
298
+ console.print(f"[yellow]Cloud execution failed: {e}. Falling back to local.[/yellow]")
299
+ is_local = True
300
+
301
+ # --- Local Execution ---
302
+ if is_local:
129
303
  try:
130
- unit_test, total_cost, model_name = increase_tests(
131
- existing_unit_tests=input_strings["existing_tests"],
132
- coverage_report=input_strings["coverage_report"],
133
- code=input_strings["code_file"],
134
- prompt_that_generated_code=input_strings["prompt_file"],
135
- language=language,
136
- strength=strength,
137
- temperature=temperature,
138
- time=time,
139
- verbose=verbose,
140
- )
141
- except Exception as exception:
142
- # This broad exception is used to catch any issue that might arise
143
- # while increasing test coverage, including problems with parsing
144
- # reports or interacting with the language model.
145
- print(f"[bold red]Error increasing test coverage: {exception}[/bold red]")
146
- ctx.exit(1)
147
- return "", 0.0, ""
148
-
149
- # Handle output - prioritize orchestration output path over construct_paths result
150
- output_file = output or output_file_paths["output"]
151
- if merge and existing_tests:
152
- output_file = existing_tests
153
-
154
- if not output_file:
155
- print("[bold red]Error: Output file path could not be determined.[/bold red]")
156
- ctx.exit(1)
157
- return "", 0.0, ""
158
-
159
- # Check if unit_test content is empty
160
- if not unit_test or not unit_test.strip():
161
- print(f"[bold red]Error: Generated unit test content is empty or whitespace-only.[/bold red]")
162
- print(f"[bold yellow]Debug: unit_test length: {len(unit_test) if unit_test else 0}[/bold yellow]")
163
- print(f"[bold yellow]Debug: unit_test content preview: {repr(unit_test[:100]) if unit_test else 'None'}[/bold yellow]")
164
- ctx.exit(1)
165
- return "", 0.0, ""
166
-
304
+ if verbose:
305
+ console.print(
306
+ Panel(
307
+ f"Running Local Execution (Mode: {mode})",
308
+ title="Local Status",
309
+ style="green"
310
+ )
311
+ )
312
+
313
+ if mode == "generate":
314
+ # Determine args based on is_example
315
+ code_arg = None if is_example else code_content
316
+ example_arg = code_content if is_example else None
317
+
318
+ generated_content, total_cost, model_name = generate_test(
319
+ prompt=prompt_content,
320
+ code=code_arg,
321
+ example=example_arg,
322
+ strength=eff_strength,
323
+ temperature=eff_temperature,
324
+ time=eff_time,
325
+ language=detected_language,
326
+ verbose=verbose,
327
+ source_file_path=source_file_path,
328
+ test_file_path=test_file_path,
329
+ module_name=module_name,
330
+ existing_tests=concatenated_tests
331
+ )
332
+ else: # mode == "increase"
333
+ generated_content, total_cost, model_name = increase_tests(
334
+ existing_unit_tests=concatenated_tests if concatenated_tests else "",
335
+ coverage_report=input_strings.get("coverage_report", ""),
336
+ code=code_content,
337
+ prompt_that_generated_code=prompt_content,
338
+ language=detected_language,
339
+ strength=eff_strength,
340
+ temperature=eff_temperature,
341
+ time=eff_time,
342
+ verbose=verbose
343
+ )
344
+
345
+ except Exception as e:
346
+ console.print(f"[bold red]Error during local execution: {e}[/bold red]")
347
+ return "", 0.0, f"Error: {e}"
348
+
349
+ # 7. Validate Output
350
+ if not generated_content or not generated_content.strip():
351
+ console.print("[bold red]Error: Generated test content is empty.[/bold red]")
352
+ return "", 0.0, "Error: Empty output"
353
+
354
+ # 8. Write Output
167
355
  try:
168
- with open(output_file, "w", encoding="utf-8") as file_handle:
169
- file_handle.write(unit_test)
170
- print(f"[bold green]Unit tests saved to:[/bold green] {output_file}")
171
- except Exception as exception:
172
- # A broad exception is caught here to handle potential file system errors
173
- # (e.g., permissions, disk space) that can occur when writing the
174
- # output file, preventing the program from crashing unexpectedly.
175
- print(f"[bold red]Error saving tests to file: {exception}[/bold red]")
176
- ctx.exit(1)
177
- return "", 0.0, ""
178
-
179
- if verbose:
180
- print(f"[bold blue]Total cost:[/bold blue] ${total_cost:.6f}")
181
- print(f"[bold blue]Model used:[/bold blue] {model_name}")
182
-
183
- return unit_test, total_cost, model_name
356
+ final_output_path = Path(output_file_paths["output"])
357
+
358
+ # Ensure parent directory exists
359
+ final_output_path.parent.mkdir(parents=True, exist_ok=True)
360
+
361
+ write_mode = "w"
362
+ content_to_write = generated_content
363
+
364
+ # Handle merge logic
365
+ if merge and existing_tests:
366
+ # If merging, we write to the first existing test file
367
+ final_output_path = Path(existing_tests[0])
368
+ write_mode = "a"
369
+ content_to_write = "\n\n" + generated_content
370
+ if verbose:
371
+ console.print(f"Merging new tests into existing file: {final_output_path}")
372
+
373
+ with open(str(final_output_path), write_mode, encoding="utf-8") as f:
374
+ f.write(content_to_write)
375
+
376
+ if not ctx.obj.get("quiet", False):
377
+ console.print(f"[green]Successfully wrote tests to {final_output_path}[/green]")
378
+
379
+ except Exception as e:
380
+ console.print(f"[bold red]Error writing output file: {e}[/bold red]")
381
+ return "", 0.0, f"Error: {e}"
382
+
383
+ return generated_content, total_cost, model_name
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,39 @@ 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
+ language=language,
91
+ )
92
+ else:
93
+ response = llm_invoke(
94
+ prompt=processed_prompt,
95
+ input_json={},
96
+ strength=strength,
97
+ temperature=temperature,
98
+ time=time,
99
+ verbose=verbose,
100
+ output_schema=output_schema,
101
+ language=language,
102
+ )
73
103
  initial_output = response['result']
74
104
  total_cost += response['cost']
75
105
  model_name = response['model_name']
@@ -83,6 +113,7 @@ def code_generator(
83
113
  strength=0.5,
84
114
  temperature=0.0,
85
115
  time=time,
116
+ language=language,
86
117
  verbose=verbose
87
118
  )
88
119
  total_cost += check_cost
@@ -97,6 +128,7 @@ def code_generator(
97
128
  strength=strength,
98
129
  temperature=temperature,
99
130
  time=time,
131
+ language=language,
100
132
  verbose=verbose
101
133
  )
102
134
  total_cost += continue_cost
@@ -107,15 +139,25 @@ def code_generator(
107
139
  # Step 4: Postprocess the output
108
140
  if verbose:
109
141
  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
142
+
143
+ # For structured JSON targets, skip extract_code to avoid losing or altering schema-constrained payloads.
144
+ if (isinstance(language, str) and language.strip().lower() == "json") or output_schema:
145
+ if isinstance(final_output, str):
146
+ runnable_code = final_output
147
+ else:
148
+ runnable_code = json.dumps(final_output)
149
+ postprocess_cost = 0.0
150
+ model_name_post = model_name
151
+ else:
152
+ runnable_code, postprocess_cost, model_name_post = postprocess(
153
+ llm_output=final_output,
154
+ language=language,
155
+ strength=EXTRACTION_STRENGTH,
156
+ temperature=0.0,
157
+ time=time,
158
+ verbose=verbose
159
+ )
160
+ total_cost += postprocess_cost
119
161
 
120
162
  return runnable_code, total_cost, model_name
121
163
 
@@ -126,4 +168,4 @@ def code_generator(
126
168
  except Exception as e:
127
169
  if verbose:
128
170
  console.print(f"[bold red]Unexpected Error: {str(e)}[/bold red]")
129
- raise
171
+ raise