Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions pdd/code_generator_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,13 +733,13 @@ def _match_one(patterns_list: List[str]) -> List[str]:
git_add_files(files_to_stage_for_rollback, verbose=verbose)

# Preprocess both prompts: expand includes, substitute vars, then double
orig_proc = pdd_preprocess(original_prompt_content_for_incremental, recursive=True, double_curly_brackets=False)
orig_proc = pdd_preprocess(original_prompt_content_for_incremental, recursive=True, double_curly_brackets=False, quiet=quiet)
orig_proc = _expand_vars(orig_proc, env_vars)
orig_proc = pdd_preprocess(orig_proc, recursive=False, double_curly_brackets=True)
orig_proc = pdd_preprocess(orig_proc, recursive=False, double_curly_brackets=True, quiet=quiet)

new_proc = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False)
new_proc = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, quiet=quiet)
new_proc = _expand_vars(new_proc, env_vars)
new_proc = pdd_preprocess(new_proc, recursive=False, double_curly_brackets=True)
new_proc = pdd_preprocess(new_proc, recursive=False, double_curly_brackets=True, quiet=quiet)

generated_code_content, was_incremental_operation, total_cost, model_name = incremental_code_generator_func(
original_prompt=orig_proc,
Expand Down Expand Up @@ -770,9 +770,9 @@ def _match_one(patterns_list: List[str]) -> List[str]:
if not current_execution_is_local:
if verbose: console.print("Attempting cloud code generation...")
# Expand includes, substitute vars, then double
processed_prompt_for_cloud = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, exclude_keys=[])
processed_prompt_for_cloud = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, exclude_keys=[], quiet=quiet)
processed_prompt_for_cloud = _expand_vars(processed_prompt_for_cloud, env_vars)
processed_prompt_for_cloud = pdd_preprocess(processed_prompt_for_cloud, recursive=False, double_curly_brackets=True, exclude_keys=[])
processed_prompt_for_cloud = pdd_preprocess(processed_prompt_for_cloud, recursive=False, double_curly_brackets=True, exclude_keys=[], quiet=quiet)
if verbose: console.print(Panel(Text(processed_prompt_for_cloud, overflow="fold"), title="[cyan]Preprocessed Prompt for Cloud[/cyan]", expand=False))

# Extract and display pinned example ID if present in prompt
Expand All @@ -788,7 +788,8 @@ def _match_one(patterns_list: List[str]) -> List[str]:
if cloud_only:
console.print("[red]Cloud authentication failed.[/red]")
raise click.UsageError("Cloud authentication failed")
console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
if not quiet:
console.print("[yellow]Cloud authentication failed. Falling back to local execution.[/yellow]")
current_execution_is_local = True

if jwt_token and not current_execution_is_local:
Expand Down Expand Up @@ -828,7 +829,8 @@ def _match_one(patterns_list: List[str]) -> List[str]:
if cloud_only:
console.print("[red]Cloud execution returned no code.[/red]")
raise click.UsageError("Cloud execution returned no code")
console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
if not quiet:
console.print("[yellow]Cloud execution returned no code. Falling back to local.[/yellow]")
current_execution_is_local = True
elif verbose:
# Display example info if available
Expand All @@ -843,7 +845,8 @@ def _match_one(patterns_list: List[str]) -> List[str]:
if cloud_only:
console.print(f"[red]Cloud execution timed out ({get_cloud_timeout()}s).[/red]")
raise click.UsageError("Cloud execution timed out")
console.print(f"[yellow]Cloud execution timed out ({get_cloud_timeout()}s). Falling back to local.[/yellow]")
if not quiet:
console.print(f"[yellow]Cloud execution timed out ({get_cloud_timeout()}s). Falling back to local.[/yellow]")
current_execution_is_local = True
except requests.exceptions.HTTPError as e:
status_code = e.response.status_code if e.response else 0
Expand Down Expand Up @@ -873,27 +876,30 @@ def _match_one(patterns_list: List[str]) -> List[str]:
if cloud_only:
console.print(f"[red]Cloud HTTP error ({status_code}): {err_content}[/red]")
raise click.UsageError(f"Cloud HTTP error ({status_code}): {err_content}")
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
if not quiet:
console.print(f"[yellow]Cloud HTTP error ({status_code}): {err_content}. Falling back to local.[/yellow]")
current_execution_is_local = True
except requests.exceptions.RequestException as e:
if cloud_only:
console.print(f"[red]Cloud network error: {e}[/red]")
raise click.UsageError(f"Cloud network error: {e}")
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
if not quiet:
console.print(f"[yellow]Cloud network error: {e}. Falling back to local.[/yellow]")
current_execution_is_local = True
except json.JSONDecodeError:
if cloud_only:
console.print("[red]Cloud returned invalid JSON.[/red]")
raise click.UsageError("Cloud returned invalid JSON")
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
if not quiet:
console.print("[yellow]Cloud returned invalid JSON. Falling back to local.[/yellow]")
current_execution_is_local = True

if current_execution_is_local:
if verbose: console.print("Executing code generator locally...")
# Expand includes, substitute vars, then double; pass to local generator with preprocess_prompt=False
local_prompt = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, exclude_keys=[])
local_prompt = pdd_preprocess(prompt_content, recursive=True, double_curly_brackets=False, exclude_keys=[], quiet=quiet)
local_prompt = _expand_vars(local_prompt, env_vars)
local_prompt = pdd_preprocess(local_prompt, recursive=False, double_curly_brackets=True, exclude_keys=[])
local_prompt = pdd_preprocess(local_prompt, recursive=False, double_curly_brackets=True, exclude_keys=[], quiet=quiet)
# Language already resolved (front matter overrides detection if present)
gen_language = language

Expand Down Expand Up @@ -1131,7 +1137,7 @@ def _subst_arg(arg: str) -> str:

p_output.write_text(final_content, encoding="utf-8")
if verbose or not quiet:
console.print(f"Generated code saved to: [green]{p_output.resolve()}[/green]")
click.echo(f"Generated code saved to: {p_output.resolve()}")
# Safety net: ensure architecture HTML is generated post-write if applicable
try:
# Prefer resolved script if available; else default for architecture outputs
Expand Down
6 changes: 6 additions & 0 deletions pdd/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ def cli(
# Suppress verbose if quiet is enabled
if quiet:
ctx.obj["verbose"] = False
os.environ["PDD_QUIET"] = "1"
from ..llm_invoke import set_quiet_mode
set_quiet_mode()
# Monkey-patch Rich/Click output to suppress non-error output globally
from ..quiet import enable_quiet_mode
enable_quiet_mode()

# Warn users who have not completed interactive setup unless they are running it now
if _should_show_onboarding_reminder(ctx):
Expand Down
30 changes: 14 additions & 16 deletions pdd/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,20 @@ def handle_error(exception: Exception, command_name: str, quiet: bool):
}
)

if not quiet:
console.print(f"[error]Error during '{command_name}' command:[/error]", style="error")
if isinstance(exception, FileNotFoundError):
console.print(f" [error]File not found:[/error] {exception}", style="error")
elif isinstance(exception, (ValueError, IOError)):
console.print(f" [error]Input/Output Error:[/error] {exception}", style="error")
elif isinstance(exception, click.UsageError): # Handle Click usage errors explicitly if needed
console.print(f" [error]Usage Error:[/error] {exception}", style="error")
# click.UsageError should typically exit with 2, so we re-raise it
raise exception
elif isinstance(exception, MarkupError):
console.print(" [error]Markup Error:[/error] Invalid Rich markup encountered.", style="error")
# Print the error message safely escaped
console.print(escape(str(exception)))
else:
console.print(f" [error]An unexpected error occurred:[/error] {exception}", style="error")
# Errors are always shown, even in quiet mode
console.print(f"[error]Error during '{command_name}' command:[/error]", style="error")
if isinstance(exception, FileNotFoundError):
console.print(f" [error]File not found:[/error] {exception}", style="error")
elif isinstance(exception, (ValueError, IOError)):
console.print(f" [error]Input/Output Error:[/error] {exception}", style="error")
elif isinstance(exception, click.UsageError):
console.print(f" [error]Usage Error:[/error] {exception}", style="error")
raise exception
elif isinstance(exception, MarkupError):
console.print(" [error]Markup Error:[/error] Invalid Rich markup encountered.", style="error")
console.print(escape(str(exception)))
else:
console.print(f" [error]An unexpected error occurred:[/error] {exception}", style="error")
strict_exit = os.environ.get("PDD_STRICT_EXIT", "").strip().lower() in {"1", "true", "yes", "on"}
if strict_exit:
raise SystemExit(1)
Expand Down
51 changes: 39 additions & 12 deletions pdd/llm_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@
if not litellm_logger.handlers:
litellm_logger.addHandler(console_handler)

def set_quiet_mode():
"""Suppress all log output, LiteLLM verbose output, and warnings in quiet mode."""
# Set all loggers to CRITICAL to suppress everything below
logger.setLevel(logging.CRITICAL)
litellm_logger.setLevel(logging.CRITICAL)
# Also raise handler levels so propagation doesn't leak through
for handler in logger.handlers:
handler.setLevel(logging.CRITICAL)
for handler in litellm_logger.handlers:
handler.setLevel(logging.CRITICAL)
# Suppress LiteLLM's own print-based verbose output
litellm.set_verbose = False
# Suppress the root logger and all its handlers
root = logging.getLogger()
root.setLevel(logging.CRITICAL)
for handler in root.handlers:
handler.setLevel(logging.CRITICAL)
# Disable the lastResort handler that prints WARNING: lines
logging.lastResort = None
# Suppress Python warnings (Pydantic serialization warnings, etc.)
import warnings
warnings.filterwarnings("ignore")
Comment on lines +59 to +80
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_quiet_mode() makes several process-global changes (root logger level/handlers, logging.lastResort, and warnings.filterwarnings('ignore')). This is hard to safely undo in test runs and can cause unrelated tests to behave differently. Consider implementing a reversible quiet-mode (store/restore previous logger levels, lastResort, and warnings filters) rather than permanently mutating global state.

Copilot uses AI. Check for mistakes.

# Function to set up file logging if needed
def setup_file_logging(log_file_path=None):
"""Configure rotating file handler for logging"""
Expand Down Expand Up @@ -398,7 +421,7 @@ def _llm_invoke_cloud(
try:
result = _validate_with_pydantic(result, output_pydantic)
except (ValidationError, ValueError) as e:
logger.warning(f"Cloud response validation failed: {e}")
logger.debug(f"Cloud response validation failed: {e}")
# Return raw result if validation fails
pass

Expand Down Expand Up @@ -606,19 +629,19 @@ def _is_env_path_package_dir(env_path: Path) -> bool:
# Selection order
if user_model_csv_path.is_file():
LLM_MODEL_CSV_PATH = user_model_csv_path
logger.info(f"Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
logger.debug(f"Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
elif PROJECT_ROOT_FROM_ENV and project_csv_from_env.is_file():
# Honor an explicitly-set PDD_PATH pointing to a real project directory
LLM_MODEL_CSV_PATH = project_csv_from_env
logger.info(f"Using project-specific LLM model CSV (from PDD_PATH): {LLM_MODEL_CSV_PATH}")
logger.debug(f"Using project-specific LLM model CSV (from PDD_PATH): {LLM_MODEL_CSV_PATH}")
elif project_csv_from_cwd.is_file():
# Otherwise, prefer the project relative to the current working directory
LLM_MODEL_CSV_PATH = project_csv_from_cwd
logger.info(f"Using project-specific LLM model CSV (from CWD): {LLM_MODEL_CSV_PATH}")
logger.debug(f"Using project-specific LLM model CSV (from CWD): {LLM_MODEL_CSV_PATH}")
else:
# Neither exists, we'll use a marker path that _load_model_data will handle
LLM_MODEL_CSV_PATH = None
logger.info("No local LLM model CSV found, will use package default")
logger.debug("No local LLM model CSV found, will use package default")
# ---------------------------------

# Load environment variables from .env file
Expand Down Expand Up @@ -669,7 +692,7 @@ def _is_env_path_package_dir(env_path: Path) -> bool:
s3_endpoint_url=GCS_ENDPOINT_URL,
)
litellm.cache = configured_cache
logger.info(f"LiteLLM cache configured for GCS bucket (S3 compatible): {GCS_BUCKET_NAME}")
logger.debug(f"LiteLLM cache configured for GCS bucket (S3 compatible): {GCS_BUCKET_NAME}")
cache_configured = True

except Exception as e:
Expand All @@ -695,7 +718,7 @@ def _is_env_path_package_dir(env_path: Path) -> bool:

# Check if caching is disabled via environment variable
if os.getenv("LITELLM_CACHE_DISABLE") == "1":
logger.info("LiteLLM caching disabled via LITELLM_CACHE_DISABLE=1")
logger.debug("LiteLLM caching disabled via LITELLM_CACHE_DISABLE=1")
litellm.cache = None
cache_configured = True

Expand All @@ -705,7 +728,7 @@ def _is_env_path_package_dir(env_path: Path) -> bool:
sqlite_cache_path = PROJECT_ROOT / "litellm_cache.sqlite"
configured_cache = Cache(type="disk", disk_cache_dir=str(sqlite_cache_path))
litellm.cache = configured_cache
logger.info(f"LiteLLM disk cache configured at {sqlite_cache_path}")
logger.debug(f"LiteLLM disk cache configured at {sqlite_cache_path}")
cache_configured = True
except Exception as e2:
warnings.warn(f"Failed to configure LiteLLM disk cache: {e2}. Caching is disabled.")
Expand Down Expand Up @@ -1746,16 +1769,20 @@ def llm_invoke(
)
except CloudFallbackError as e:
# Notify user and fall back to local execution
console.print(f"[yellow]Cloud execution failed ({e}), falling back to local execution...[/yellow]")
logger.warning(f"Cloud fallback: {e}")
_quiet = os.environ.get("PDD_QUIET", "") == "1"
if not _quiet:
console.print(f"[yellow]Cloud execution failed ({e}), falling back to local execution...[/yellow]")
logger.debug(f"Cloud fallback: {e}")
# Continue to local execution below
except InsufficientCreditsError:
# Re-raise credit errors - user needs to know
raise
except CloudInvocationError as e:
# Non-recoverable cloud error - notify and fall back
console.print(f"[yellow]Cloud error ({e}), falling back to local execution...[/yellow]")
logger.warning(f"Cloud invocation error: {e}")
_quiet = os.environ.get("PDD_QUIET", "") == "1"
if not _quiet:
console.print(f"[yellow]Cloud error ({e}), falling back to local execution...[/yellow]")
logger.debug(f"Cloud invocation error: {e}")
# Continue to local execution below

# --- 1. Load Environment & Validate Inputs ---
Expand Down
17 changes: 12 additions & 5 deletions pdd/load_prompt_template.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path
from typing import Optional
from rich import print
Expand All @@ -7,7 +8,11 @@ def print_formatted(message: str) -> None:
"""Print message with raw formatting tags for testing compatibility."""
print(message)

def load_prompt_template(prompt_name: str) -> Optional[str]:
def _is_quiet(quiet: bool) -> bool:
"""Check if quiet mode is active via parameter or environment."""
return quiet or os.environ.get("PDD_QUIET", "") == "1"

def load_prompt_template(prompt_name: str, quiet: bool = False) -> Optional[str]:
"""
Load a prompt template from a file.

Expand Down Expand Up @@ -39,15 +44,17 @@ def load_prompt_template(prompt_name: str) -> Optional[str]:
prompt_candidates.append(root / 'pdd' / 'prompts' / f"{prompt_name}.prompt")

tried = "\n".join(str(c) for c in prompt_candidates)
print_formatted(
f"[red]Prompt file not found in any candidate locations for '{prompt_name}'. Tried:\n{tried}[/red]"
)
if not _is_quiet(quiet):
print_formatted(
f"[red]Prompt file not found in any candidate locations for '{prompt_name}'. Tried:\n{tried}[/red]"
)
return None

try:
with open(prompt_path, 'r', encoding='utf-8') as file:
prompt_template = file.read()
print_formatted(f"[green]Successfully loaded prompt: {prompt_name}[/green]")
if not _is_quiet(quiet):
print_formatted(f"[green]Successfully loaded prompt: {prompt_name}[/green]")
return prompt_template

except IOError as e:
Expand Down
Loading