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/context_generator.py CHANGED
@@ -16,6 +16,9 @@ def context_generator(
16
16
  temperature: float = 0,
17
17
  time: Optional[float] = DEFAULT_TIME,
18
18
  verbose: bool = False,
19
+ source_file_path: str = None,
20
+ example_file_path: str = None,
21
+ module_name: str = None,
19
22
  ) -> tuple:
20
23
  """
21
24
  Generates a concise example on how to use a given code module properly.
@@ -79,7 +82,10 @@ def context_generator(
79
82
  input_json={
80
83
  "code_module": code_module,
81
84
  "processed_prompt": processed_prompt,
82
- "language": language
85
+ "language": language,
86
+ "source_file_path": source_file_path or "",
87
+ "example_file_path": example_file_path or "",
88
+ "module_name": module_name or ""
83
89
  },
84
90
  strength=strength,
85
91
  temperature=temperature,
@@ -95,6 +101,7 @@ def context_generator(
95
101
  strength=0.5,
96
102
  temperature=temperature,
97
103
  time=time,
104
+ language=language,
98
105
  verbose=verbose
99
106
  )
100
107
  except Exception as e:
@@ -112,6 +119,7 @@ def context_generator(
112
119
  strength=strength,
113
120
  temperature=temperature,
114
121
  time=time,
122
+ language=language,
115
123
  verbose=verbose
116
124
  )
117
125
  total_cost = llm_response['cost'] + unfinished_cost + continue_cost
@@ -149,4 +157,4 @@ if __name__ == "__main__":
149
157
  print("[bold green]Generated Example Code:[/bold green]")
150
158
  print(example_code)
151
159
  print(f"[bold blue]Total Cost: ${total_cost:.6f}[/bold blue]")
152
- print(f"[bold blue]Model Name: {model_name}[/bold blue]")
160
+ print(f"[bold blue]Model Name: {model_name}[/bold blue]")
@@ -1,91 +1,190 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import asyncio
4
+ import json
5
+ import os
1
6
  import sys
2
- from typing import Tuple, Optional
7
+ from pathlib import Path
8
+ from typing import Optional, Tuple, Dict, Any
3
9
  import click
4
- from rich import print as rprint
5
-
10
+ import httpx
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.text import Text
6
14
  from .construct_paths import construct_paths
7
15
  from .context_generator import context_generator
16
+ from .core.cloud import CloudConfig
17
+ # get_jwt_token imports removed - using CloudConfig.get_jwt_token() instead
18
+ from .preprocess import preprocess
19
+ from . import DEFAULT_STRENGTH, DEFAULT_TEMPERATURE
8
20
 
9
- def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
10
- """
11
- Main function to generate example code from a prompt file and an existing code file.
21
+ console = Console()
22
+ CLOUD_TIMEOUT_SECONDS = 400.0
12
23
 
13
- :param ctx: Click context containing command-line parameters.
14
- :param prompt_file: Path to the prompt file that generated the code.
15
- :param code_file: Path to the existing code file.
16
- :param output: Optional path to save the generated example code.
17
- :return: A tuple containing the generated example code, total cost, and model name used.
18
- """
24
+ def _validate_and_fix_python_syntax(code: str, quiet: bool) -> str:
19
25
  try:
20
- # Construct file paths
21
- input_file_paths = {
22
- "prompt_file": prompt_file,
23
- "code_file": code_file
24
- }
25
- command_options = {
26
- "output": output
27
- }
28
- resolved_config, input_strings, output_file_paths, language = construct_paths(
29
- input_file_paths=input_file_paths,
30
- force=ctx.obj.get('force', False),
31
- quiet=ctx.obj.get('quiet', False),
32
- command="example",
33
- command_options=command_options
34
- )
26
+ ast.parse(code)
27
+ return code
28
+ except SyntaxError:
29
+ if not quiet:
30
+ console.print("[yellow]Warning: Generated code has syntax errors. Attempting to fix...[/yellow]")
31
+ lines = code.splitlines()
32
+ json_markers = ['"explanation":', '"focus":', '"description":', '"code":', '"filename":']
33
+ cut_index = -1
34
+ for i in range(len(lines) - 1, -1, -1):
35
+ line = lines[i].strip()
36
+ if any(marker in line for marker in json_markers) or line == "}" or line == "},":
37
+ cut_index = i
38
+ if cut_index != -1:
39
+ candidate = "\n".join(lines[:cut_index])
40
+ try:
41
+ ast.parse(candidate)
42
+ if not quiet:
43
+ console.print("[green]Fix successful: Removed trailing metadata.[/green]")
44
+ return candidate
45
+ except SyntaxError:
46
+ pass
47
+ low = 0
48
+ high = cut_index if cut_index != -1 else len(lines)
49
+ valid_len = 0
50
+ while low < high:
51
+ mid = (low + high + 1) // 2
52
+ candidate = "\n".join(lines[:mid])
53
+ try:
54
+ ast.parse(candidate)
55
+ valid_len = mid
56
+ low = mid
57
+ except SyntaxError:
58
+ high = mid - 1
59
+ for i in range(len(lines), max(0, len(lines) - 50), -1):
60
+ candidate = "\n".join(lines[:i])
61
+ try:
62
+ ast.parse(candidate)
63
+ if not quiet:
64
+ console.print("[green]Fix successful: Truncated invalid tail content.[/green]")
65
+ return candidate
66
+ except SyntaxError:
67
+ continue
68
+ if not quiet:
69
+ console.print("[red]Fix failed: Could not automatically repair syntax.[/red]")
70
+ return code
35
71
 
36
- # Load input files
37
- prompt_content = input_strings["prompt_file"]
38
- code_content = input_strings["code_file"]
72
+ async def _run_cloud_generation(prompt_content: str, code_content: str, language: str, strength: float, temperature: float, verbose: bool, pdd_env: str, token: str) -> Tuple[Optional[str], float, str]:
73
+ """Run cloud generation with the provided JWT token.
39
74
 
40
- # Generate example code
41
- strength = ctx.obj.get('strength', 0.5)
42
- temperature = ctx.obj.get('temperature', 0)
43
- time = ctx.obj.get('time')
44
- example_code, total_cost, model_name = context_generator(
45
- language=language,
46
- code_module=code_content,
47
- prompt=prompt_content,
48
- strength=strength,
49
- temperature=temperature,
50
- time=time,
51
- verbose=ctx.obj.get('verbose', False)
52
- )
53
-
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}")
59
- if final_output_path and example_code is not None:
60
- with open(final_output_path, 'w') as f:
61
- f.write(example_code)
62
- 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")
66
-
67
- # Provide user feedback
68
- if not ctx.obj.get('quiet', False):
69
- if example_code is not None:
70
- rprint("[bold green]Example code generated successfully.[/bold green]")
71
- rprint(f"[bold]Model used:[/bold] {model_name}")
72
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
73
- if final_output_path and example_code is not None:
74
- rprint(f"[bold]Example code saved to:[/bold] {final_output_path}")
75
- else:
76
- rprint("[bold red]Example code generation failed.[/bold red]")
77
- rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
75
+ Note: JWT token must be obtained BEFORE calling this async function to avoid
76
+ nested asyncio.run() calls (CloudConfig.get_jwt_token() uses asyncio.run internally).
77
+ """
78
+ try:
79
+ processed_prompt = preprocess(prompt_content, recursive=True, double_curly_brackets=False)
80
+ except Exception as e:
81
+ return None, 0.0, f"Preprocessing failed: {e}"
82
+ if verbose:
83
+ console.print(Panel(Text(processed_prompt[:500] + "..." if len(processed_prompt) > 500 else processed_prompt, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))
84
+ headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
85
+ payload = {"promptContent": processed_prompt, "codeContent": code_content, "language": language, "strength": strength, "temperature": temperature, "verbose": verbose}
86
+ async with httpx.AsyncClient(timeout=CLOUD_TIMEOUT_SECONDS) as client:
87
+ try:
88
+ cloud_url = CloudConfig.get_endpoint_url("generateExample")
89
+ response = await client.post(cloud_url, json=payload, headers=headers)
90
+ response.raise_for_status()
91
+ data = response.json()
92
+ generated_code = data.get("generatedExample", "")
93
+ total_cost = float(data.get("totalCost", 0.0))
94
+ model_name = data.get("modelName", "cloud-model")
95
+ if not generated_code:
96
+ return None, 0.0, "Cloud function returned empty code."
97
+ return generated_code, total_cost, model_name
98
+ except Exception as e:
99
+ return None, 0.0, f"Cloud error: {e}"
78
100
 
79
- # Always print example code, even in quiet mode (if it exists)
80
- if example_code is not None:
81
- rprint("[bold]Generated Example Code:[/bold]")
82
- rprint(example_code)
101
+ def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
102
+ try:
103
+ input_file_paths = {"prompt_file": prompt_file, "code_file": code_file}
104
+ command_options = {"output": output}
105
+ resolved_config, input_strings, output_file_paths, language = construct_paths(input_file_paths=input_file_paths, force=ctx.obj.get('force', False), quiet=ctx.obj.get('quiet', False), command="example", command_options=command_options, context_override=ctx.obj.get('context'), confirm_callback=ctx.obj.get('confirm_callback'))
106
+ prompt_content = input_strings.get("prompt_file", "")
107
+ code_content = input_strings.get("code_file", "")
108
+ if output and not output.endswith("/") and not Path(output).is_dir():
109
+ resolved_output = output
83
110
  else:
84
- rprint("[bold red]No example code generated due to errors.[/bold red]")
111
+ resolved_output = output_file_paths.get("output")
112
+ is_local = ctx.obj.get("local", False)
113
+ strength = ctx.obj.get('strength', DEFAULT_STRENGTH)
114
+ temperature = ctx.obj.get('temperature', DEFAULT_TEMPERATURE)
115
+ verbose = ctx.obj.get('verbose', False)
116
+ quiet = ctx.obj.get('quiet', False)
117
+ pdd_env = os.environ.get("PDD_ENV", "local")
118
+ generated_code = None
119
+ total_cost = 0.0
120
+ model_name = ""
121
+ if not is_local:
122
+ if verbose:
123
+ console.print("Attempting cloud example generation...")
85
124
 
86
- return example_code, total_cost, model_name
125
+ # Get JWT token BEFORE entering async context to avoid nested asyncio.run() calls
126
+ # (CloudConfig.get_jwt_token() uses asyncio.run internally for device flow auth)
127
+ jwt_token = CloudConfig.get_jwt_token(verbose=verbose)
128
+ if not jwt_token:
129
+ if not quiet:
130
+ console.print("[yellow]Cloud authentication failed. Falling back to local.[/yellow]")
131
+ is_local = True
132
+ else:
133
+ try:
134
+ generated_code, total_cost, model_name = asyncio.run(_run_cloud_generation(prompt_content, code_content, language, strength, temperature, verbose, pdd_env, jwt_token))
135
+ if generated_code:
136
+ if verbose:
137
+ console.print(Panel(f"Cloud generation successful. Model: {model_name}, Cost: ${total_cost:.6f}", title="[green]Cloud Success[/green]", expand=False))
138
+ except httpx.TimeoutException:
139
+ if not quiet:
140
+ console.print(f"[yellow]Cloud execution timed out ({CLOUD_TIMEOUT_SECONDS}s). Falling back to local.[/yellow]")
141
+ generated_code = None
142
+ except httpx.HTTPStatusError as e:
143
+ status_code = e.response.status_code
144
+ response_text = e.response.text or ""
145
+ err_content = response_text[:200] if response_text else "No response content"
146
+ if status_code == 402:
147
+ console.print(f"[red]Insufficient credits: {err_content}[/red]")
148
+ raise click.UsageError("Insufficient credits for cloud example generation")
149
+ elif status_code == 401:
150
+ console.print(f"[red]Authentication failed: {err_content}[/red]")
151
+ raise click.UsageError("Cloud authentication failed")
152
+ elif status_code == 403:
153
+ console.print(f"[red]Access denied: {err_content}[/red]")
154
+ raise click.UsageError("Access denied - user not approved")
155
+ else:
156
+ if not quiet:
157
+ console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
158
+ generated_code = None
159
+ except Exception as e:
160
+ if verbose:
161
+ console.print(f"[yellow]Cloud error: {e}. Falling back to local.[/yellow]")
162
+ generated_code = None
87
163
 
164
+ if generated_code is None:
165
+ if not quiet:
166
+ console.print("[yellow]Cloud execution failed. Falling back to local.[/yellow]")
167
+ is_local = True
168
+ if is_local:
169
+ # Compute file path info if not already computed (when --local flag is used from start)
170
+ source_file_path = str(Path(code_file).resolve())
171
+ example_file_path = str(Path(resolved_output).resolve()) if resolved_output else ""
172
+ module_name = Path(code_file).stem
173
+ generated_code, total_cost, model_name = context_generator(code_module=code_content, prompt=prompt_content, language=language, strength=strength, temperature=temperature, verbose=not quiet, source_file_path=source_file_path, example_file_path=example_file_path, module_name=module_name, time=ctx.obj.get('time'))
174
+ if not generated_code:
175
+ raise click.UsageError("Example generation failed, no code produced.")
176
+ if language and language.lower() == "python":
177
+ generated_code = _validate_and_fix_python_syntax(generated_code, quiet)
178
+ if resolved_output:
179
+ out_path = Path(resolved_output)
180
+ out_path.parent.mkdir(parents=True, exist_ok=True)
181
+ out_path.write_text(generated_code, encoding="utf-8")
182
+ if not quiet:
183
+ console.print("[bold green]Example generation completed successfully.[/bold green]")
184
+ console.print(f"[bold]Model used:[/bold] {model_name}")
185
+ console.print(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
186
+ return generated_code, total_cost, model_name
88
187
  except Exception as e:
89
188
  if not ctx.obj.get('quiet', False):
90
- rprint(f"[bold red]Error:[/bold red] {str(e)}")
91
- sys.exit(1)
189
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
190
+ raise 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
  """
@@ -78,16 +84,23 @@ def continue_generation(
78
84
  temperature=0,
79
85
  time=time,
80
86
  output_pydantic=TrimResultsStartOutput,
81
- verbose=verbose
87
+ verbose=verbose,
88
+ language=language,
82
89
  )
83
90
  total_cost += trim_start_response['cost']
84
91
  code_block = trim_start_response['result'].code_block
85
92
 
86
93
  # Step 4: Continue generation loop
87
- while True:
94
+ while loop_count < MAX_GENERATION_LOOPS:
88
95
  loop_count += 1
89
96
  if verbose:
90
97
  console.print(f"[cyan]Generation loop {loop_count}[/cyan]")
98
+
99
+ # Check for maximum loops reached
100
+ if loop_count >= MAX_GENERATION_LOOPS:
101
+ logger.warning(f"Reached maximum generation loops ({MAX_GENERATION_LOOPS}), terminating")
102
+ console.print(f"[yellow]Warning: Reached maximum generation loops ({MAX_GENERATION_LOOPS}), terminating[/yellow]")
103
+ break
91
104
 
92
105
  # Generate continuation
93
106
  continue_response = llm_invoke(
@@ -99,26 +112,55 @@ def continue_generation(
99
112
  strength=strength,
100
113
  temperature=temperature,
101
114
  time=time,
102
- verbose=verbose
115
+ verbose=verbose,
116
+ language=language,
103
117
  )
104
118
 
105
119
  total_cost += continue_response['cost']
106
120
  model_name = continue_response['model_name']
107
121
  continue_result = continue_response['result']
108
122
 
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(
123
+ if verbose:
124
+ try:
125
+ preview = (continue_result[:160] + '...') if isinstance(continue_result, str) and len(continue_result) > 160 else continue_result
126
+ except Exception:
127
+ preview = "<non-str>"
128
+ console.print(f"[blue]Continue model:[/blue] {model_name}")
129
+ console.print(f"[blue]Continue preview:[/blue] {preview!r}")
130
+
131
+ # If the model produced no continuation, avoid an endless loop
132
+ if not isinstance(continue_result, str) or not continue_result.strip():
133
+ logger.warning("Empty continuation received; stopping to avoid loop.")
134
+ break
135
+
136
+ # Build prospective new block and check completeness on the updated tail
137
+ new_code_block = code_block + continue_result
138
+ last_chunk = new_code_block[-600:] if len(new_code_block) > 600 else new_code_block
139
+ reasoning, is_finished, check_cost, check_model = unfinished_prompt(
112
140
  prompt_text=last_chunk,
113
141
  strength=0.5,
114
142
  temperature=0,
115
143
  time=time,
144
+ language=language,
116
145
  verbose=verbose
117
146
  )
118
147
  total_cost += check_cost
119
148
 
149
+ if verbose:
150
+ console.print(f"[magenta]Tail length:[/magenta] {len(last_chunk)}")
151
+ # Show a safe, shortened representation of the tail
152
+ try:
153
+ tail_preview = (last_chunk[-200:] if len(last_chunk) > 200 else last_chunk)
154
+ except Exception:
155
+ tail_preview = "<unprintable tail>"
156
+ console.print(f"[magenta]Tail preview (last 200 chars):[/magenta]\n{tail_preview}")
157
+ console.print(f"[magenta]Unfinished check model:[/magenta] {check_model}")
158
+ console.print(f"[magenta]is_finished:[/magenta] {is_finished}")
159
+ console.print(f"[magenta]Reasoning:[/magenta] {reasoning}")
160
+
120
161
  if not is_finished:
121
- code_block += continue_result
162
+ code_block = new_code_block
163
+ # Continue to next iteration
122
164
  else:
123
165
  # Trim and append final continuation
124
166
  trim_response = llm_invoke(
@@ -131,7 +173,8 @@ def continue_generation(
131
173
  temperature=0,
132
174
  time=time,
133
175
  output_pydantic=TrimResultsOutput,
134
- verbose=verbose
176
+ verbose=verbose,
177
+ language=language,
135
178
  )
136
179
  total_cost += trim_response['cost']
137
180
  code_block += trim_response['result'].trimmed_continued_generation
@@ -146,4 +189,4 @@ def continue_generation(
146
189
 
147
190
  except Exception as e:
148
191
  console.print(f"[bold red]Error in continue_generation: {str(e)}[/bold red]")
149
- raise
192
+ raise
pdd/core/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Core utilities and configuration for PDD CLI.
3
+ """
4
+
5
+ from .cloud import (
6
+ CloudConfig,
7
+ AuthError,
8
+ NetworkError,
9
+ TokenError,
10
+ UserCancelledError,
11
+ RateLimitError,
12
+ FIREBASE_API_KEY_ENV,
13
+ GITHUB_CLIENT_ID_ENV,
14
+ PDD_CLOUD_URL_ENV,
15
+ PDD_JWT_TOKEN_ENV,
16
+ DEFAULT_BASE_URL,
17
+ CLOUD_ENDPOINTS,
18
+ )
19
+
20
+ __all__ = [
21
+ 'CloudConfig',
22
+ 'AuthError',
23
+ 'NetworkError',
24
+ 'TokenError',
25
+ 'UserCancelledError',
26
+ 'RateLimitError',
27
+ 'FIREBASE_API_KEY_ENV',
28
+ 'GITHUB_CLIENT_ID_ENV',
29
+ 'PDD_CLOUD_URL_ENV',
30
+ 'PDD_JWT_TOKEN_ENV',
31
+ 'DEFAULT_BASE_URL',
32
+ 'CLOUD_ENDPOINTS',
33
+ ]