pdd-cli 0.0.90__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
pdd/context_generator_main.py
CHANGED
|
@@ -1,193 +1,190 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
-
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
4
7
|
from pathlib import Path
|
|
8
|
+
from typing import Optional, Tuple, Dict, Any
|
|
5
9
|
import click
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
import httpx
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.text import Text
|
|
8
14
|
from .construct_paths import construct_paths
|
|
9
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
|
|
10
20
|
|
|
21
|
+
console = Console()
|
|
22
|
+
CLOUD_TIMEOUT_SECONDS = 400.0
|
|
11
23
|
|
|
12
|
-
def
|
|
13
|
-
"""
|
|
14
|
-
Validate that the code is valid Python syntax.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Tuple of (is_valid, error_message)
|
|
18
|
-
"""
|
|
24
|
+
def _validate_and_fix_python_syntax(code: str, quiet: bool) -> str:
|
|
19
25
|
try:
|
|
20
26
|
ast.parse(code)
|
|
21
|
-
return
|
|
22
|
-
except SyntaxError
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
39
34
|
for i in range(len(lines) - 1, -1, -1):
|
|
40
35
|
line = lines[i].strip()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
54
67
|
continue
|
|
68
|
+
if not quiet:
|
|
69
|
+
console.print("[red]Fix failed: Could not automatically repair syntax.[/red]")
|
|
70
|
+
return code
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
def context_generator_main(ctx: click.Context, prompt_file: str, code_file: str, output: Optional[str]) -> Tuple[str, float, str]:
|
|
65
|
-
"""
|
|
66
|
-
Main function to generate example code from a prompt file and an existing 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.
|
|
67
74
|
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
:param code_file: Path to the existing code file.
|
|
71
|
-
:param output: Optional path to save the generated example code.
|
|
72
|
-
:return: A tuple containing the generated example code, total cost, and model name used.
|
|
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).
|
|
73
77
|
"""
|
|
74
78
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
# Generate example code
|
|
109
|
-
strength = ctx.obj.get('strength', 0.5)
|
|
110
|
-
temperature = ctx.obj.get('temperature', 0)
|
|
111
|
-
time = ctx.obj.get('time')
|
|
112
|
-
example_code, total_cost, model_name = context_generator(
|
|
113
|
-
language=language,
|
|
114
|
-
code_module=code_content,
|
|
115
|
-
prompt=prompt_content,
|
|
116
|
-
strength=strength,
|
|
117
|
-
temperature=temperature,
|
|
118
|
-
time=time,
|
|
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
|
|
123
|
-
)
|
|
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}"
|
|
124
100
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
128
110
|
else:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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...")
|
|
124
|
+
|
|
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
|
|
135
132
|
else:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if not
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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")
|
|
156
155
|
else:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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:
|
|
163
175
|
raise click.UsageError("Example generation failed, no code produced.")
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
rprint(f"[bold]Total cost:[/bold] ${total_cost:.6f}")
|
|
176
|
-
|
|
177
|
-
# Always print example code, even in quiet mode (if it exists)
|
|
178
|
-
if example_code is not None:
|
|
179
|
-
rprint("[bold]Generated Example Code:[/bold]")
|
|
180
|
-
rprint(example_code)
|
|
181
|
-
else:
|
|
182
|
-
rprint("[bold red]No example code generated due to errors.[/bold red]")
|
|
183
|
-
|
|
184
|
-
return example_code, total_cost, model_name
|
|
185
|
-
|
|
186
|
-
except click.Abort:
|
|
187
|
-
# User cancelled - re-raise to stop the sync loop
|
|
188
|
-
raise
|
|
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
|
|
189
187
|
except Exception as e:
|
|
190
188
|
if not ctx.obj.get('quiet', False):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return "", 0.0, f"Error: {e}"
|
|
189
|
+
console.print(f"[bold red]Error:[/bold red] {str(e)}")
|
|
190
|
+
raise e
|
pdd/continue_generation.py
CHANGED
|
@@ -84,7 +84,8 @@ def continue_generation(
|
|
|
84
84
|
temperature=0,
|
|
85
85
|
time=time,
|
|
86
86
|
output_pydantic=TrimResultsStartOutput,
|
|
87
|
-
verbose=verbose
|
|
87
|
+
verbose=verbose,
|
|
88
|
+
language=language,
|
|
88
89
|
)
|
|
89
90
|
total_cost += trim_start_response['cost']
|
|
90
91
|
code_block = trim_start_response['result'].code_block
|
|
@@ -111,7 +112,8 @@ def continue_generation(
|
|
|
111
112
|
strength=strength,
|
|
112
113
|
temperature=temperature,
|
|
113
114
|
time=time,
|
|
114
|
-
verbose=verbose
|
|
115
|
+
verbose=verbose,
|
|
116
|
+
language=language,
|
|
115
117
|
)
|
|
116
118
|
|
|
117
119
|
total_cost += continue_response['cost']
|
|
@@ -171,7 +173,8 @@ def continue_generation(
|
|
|
171
173
|
temperature=0,
|
|
172
174
|
time=time,
|
|
173
175
|
output_pydantic=TrimResultsOutput,
|
|
174
|
-
verbose=verbose
|
|
176
|
+
verbose=verbose,
|
|
177
|
+
language=language,
|
|
175
178
|
)
|
|
176
179
|
total_cost += trim_response['cost']
|
|
177
180
|
code_block += trim_response['result'].trimmed_continued_generation
|
pdd/core/__init__.py
CHANGED
|
@@ -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
|
+
]
|
pdd/core/cli.py
CHANGED
|
@@ -194,7 +194,7 @@ class PDDCLI(click.Group):
|
|
|
194
194
|
"--force",
|
|
195
195
|
is_flag=True,
|
|
196
196
|
default=False,
|
|
197
|
-
help="
|
|
197
|
+
help="Skip all interactive prompts (file overwrites, API key requests). Useful for CI/automation.",
|
|
198
198
|
)
|
|
199
199
|
@click.option(
|
|
200
200
|
"--strength",
|
|
@@ -296,6 +296,8 @@ def cli(
|
|
|
296
296
|
|
|
297
297
|
ctx.ensure_object(dict)
|
|
298
298
|
ctx.obj["force"] = force
|
|
299
|
+
if force:
|
|
300
|
+
os.environ['PDD_FORCE'] = '1'
|
|
299
301
|
# Only set strength/temperature if explicitly provided (not None)
|
|
300
302
|
# This allows .get("key", default) to return the default when CLI didn't pass a value
|
|
301
303
|
if strength is not None:
|
|
@@ -307,6 +309,9 @@ def cli(
|
|
|
307
309
|
ctx.obj["output_cost"] = output_cost
|
|
308
310
|
ctx.obj["review_examples"] = review_examples
|
|
309
311
|
ctx.obj["local"] = local
|
|
312
|
+
# Propagate --local flag to environment for llm_invoke cloud detection
|
|
313
|
+
if local:
|
|
314
|
+
os.environ['PDD_FORCE_LOCAL'] = '1'
|
|
310
315
|
# Use DEFAULT_TIME if time is not provided
|
|
311
316
|
ctx.obj["time"] = time if time is not None else DEFAULT_TIME
|
|
312
317
|
# Persist context override for downstream calls
|
|
@@ -441,7 +446,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
441
446
|
console.print(f" [error]Step {i+1} ({command_name}):[/error] Command failed.")
|
|
442
447
|
# Check if the result is the expected tuple structure from @track_cost or preprocess success
|
|
443
448
|
elif isinstance(result_tuple, tuple) and len(result_tuple) == 3:
|
|
444
|
-
|
|
449
|
+
result_data, cost, model_name = result_tuple
|
|
445
450
|
total_cost += cost
|
|
446
451
|
if not ctx.obj.get("quiet"):
|
|
447
452
|
# Special handling for preprocess success message (check actual command name)
|
|
@@ -451,6 +456,25 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
451
456
|
else:
|
|
452
457
|
# Generic output using potentially "Unknown Command" name
|
|
453
458
|
console.print(f" [info]Step {i+1} ({command_name}):[/info] Cost: ${cost:.6f}, Model: {model_name}")
|
|
459
|
+
|
|
460
|
+
# Display examples used for grounding
|
|
461
|
+
if isinstance(result_data, dict) and result_data.get("examplesUsed"):
|
|
462
|
+
console.print(" Examples used:")
|
|
463
|
+
for ex in result_data["examplesUsed"]:
|
|
464
|
+
slug = ex.get("slug", "unknown")
|
|
465
|
+
title = ex.get("title", "Untitled")
|
|
466
|
+
console.print(f" - {slug} (\"{title}\")")
|
|
467
|
+
|
|
468
|
+
# Handle dicts with examplesUsed (e.g. from commands not using track_cost but returning metadata)
|
|
469
|
+
elif isinstance(result_tuple, dict) and result_tuple.get("examplesUsed"):
|
|
470
|
+
if not ctx.obj.get("quiet"):
|
|
471
|
+
console.print(f" [info]Step {i+1} ({command_name}):[/info] Command completed.")
|
|
472
|
+
console.print(" Examples used:")
|
|
473
|
+
for ex in result_tuple["examplesUsed"]:
|
|
474
|
+
slug = ex.get("slug", "unknown")
|
|
475
|
+
title = ex.get("title", "Untitled")
|
|
476
|
+
console.print(f" - {slug} (\"{title}\")")
|
|
477
|
+
|
|
454
478
|
else:
|
|
455
479
|
# Handle unexpected return types if necessary
|
|
456
480
|
if not ctx.obj.get("quiet"):
|
|
@@ -463,7 +487,7 @@ def process_commands(ctx: click.Context, results: List[Optional[Tuple[Any, float
|
|
|
463
487
|
if any(res is not None and isinstance(res, tuple) and len(res) == 3 for res in normalized_results):
|
|
464
488
|
console.print(f"[info]Total Estimated Cost:[/info] ${total_cost:.6f}")
|
|
465
489
|
# Indicate if the chain might have been incomplete due to errors
|
|
466
|
-
if num_results < num_commands and not all(res is None for res in results): # Avoid printing if all failed
|
|
490
|
+
if num_results < num_commands and results is not None and not all(res is None for res in results): # Avoid printing if all failed
|
|
467
491
|
console.print("[warning]Note: Chain may have terminated early due to errors.[/warning]")
|
|
468
492
|
console.print("[info]-------------------------------------[/info]")
|
|
469
493
|
|