pdd-cli 0.0.26__py3-none-any.whl → 0.0.27__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/llm_invoke.py CHANGED
@@ -6,36 +6,67 @@ import pandas as pd
6
6
  import litellm
7
7
  import logging # ADDED FOR DETAILED LOGGING
8
8
 
9
- # --- Configure Detailed Logging for LiteLLM --- MODIFIED SECTION
10
- PROJECT_ROOT_FOR_LOG = '/Users/gregtanaka/Documents/pdd_cloud/pdd' # Explicit project root
11
- LOG_FILE_PATH = os.path.join(PROJECT_ROOT_FOR_LOG, 'litellm_debug.log')
9
+ # --- Configure Standard Python Logging ---
10
+ logger = logging.getLogger("pdd.llm_invoke")
12
11
 
13
- # Get the litellm logger specifically
14
- litellm_logger = logging.getLogger("litellm")
15
- litellm_logger.setLevel(logging.DEBUG) # Set its level to DEBUG
16
-
17
- # Create a file handler
18
- file_handler = logging.FileHandler(LOG_FILE_PATH, mode='w')
19
- file_handler.setLevel(logging.DEBUG)
20
-
21
- # Create a formatter and add it to the handler
22
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
23
- file_handler.setFormatter(formatter)
12
+ # Environment variable to control log level
13
+ PDD_LOG_LEVEL = os.getenv("PDD_LOG_LEVEL", "INFO")
14
+ PRODUCTION_MODE = os.getenv("PDD_ENVIRONMENT") == "production"
24
15
 
25
- # Add the handler to the litellm logger
26
- # Check if handlers are already present to avoid duplication if module is reloaded
27
- if not litellm_logger.handlers:
28
- litellm_logger.addHandler(file_handler)
29
-
30
- # Also ensure the root logger has a basic handler if nothing else is configured
31
- # This can help catch messages if litellm logs to root or other unnamed loggers
32
- if not logging.getLogger().handlers: # Check root logger
33
- logging.basicConfig(level=logging.DEBUG) # Default to console for other logs
34
- # --- End Detailed Logging Configuration ---
16
+ # Set default level based on environment
17
+ if PRODUCTION_MODE:
18
+ logger.setLevel(logging.WARNING)
19
+ else:
20
+ logger.setLevel(getattr(logging, PDD_LOG_LEVEL, logging.INFO))
35
21
 
22
+ # Configure LiteLLM logger separately
23
+ litellm_logger = logging.getLogger("litellm")
24
+ litellm_log_level = os.getenv("LITELLM_LOG_LEVEL", "WARNING" if PRODUCTION_MODE else "INFO")
25
+ litellm_logger.setLevel(getattr(logging, litellm_log_level, logging.WARNING))
26
+
27
+ # Add a console handler if none exists
28
+ if not logger.handlers:
29
+ console_handler = logging.StreamHandler()
30
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
31
+ console_handler.setFormatter(formatter)
32
+ logger.addHandler(console_handler)
33
+
34
+ # Only add handler to litellm logger if it doesn't have any
35
+ if not litellm_logger.handlers:
36
+ litellm_logger.addHandler(console_handler)
37
+
38
+ # Function to set up file logging if needed
39
+ def setup_file_logging(log_file_path=None):
40
+ """Configure rotating file handler for logging"""
41
+ if not log_file_path:
42
+ return
43
+
44
+ try:
45
+ from logging.handlers import RotatingFileHandler
46
+ file_handler = RotatingFileHandler(
47
+ log_file_path, maxBytes=10*1024*1024, backupCount=5
48
+ )
49
+ file_handler.setFormatter(logging.Formatter(
50
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
51
+ ))
52
+ logger.addHandler(file_handler)
53
+ litellm_logger.addHandler(file_handler)
54
+ logger.info(f"File logging configured to: {log_file_path}")
55
+ except Exception as e:
56
+ logger.warning(f"Failed to set up file logging: {e}")
57
+
58
+ # Function to set verbose logging
59
+ def set_verbose_logging(verbose=False):
60
+ """Set verbose logging based on flag or environment variable"""
61
+ if verbose or os.getenv("PDD_VERBOSE_LOGGING") == "1":
62
+ logger.setLevel(logging.DEBUG)
63
+ litellm_logger.setLevel(logging.DEBUG)
64
+ logger.debug("Verbose logging enabled")
65
+
66
+ # --- End Logging Configuration ---
36
67
 
37
68
  import json
38
- from rich import print as rprint
69
+ # from rich import print as rprint # Replaced with logger
39
70
  from dotenv import load_dotenv
40
71
  from pathlib import Path
41
72
  from typing import Optional, Dict, List, Any, Type, Union
@@ -63,7 +94,7 @@ if PDD_PATH_ENV:
63
94
  _path_from_env = Path(PDD_PATH_ENV)
64
95
  if _path_from_env.is_dir():
65
96
  PROJECT_ROOT = _path_from_env.resolve()
66
- # print(f"[DEBUG] Using PROJECT_ROOT from PDD_PATH: {PROJECT_ROOT}") # Optional debug
97
+ logger.debug(f"Using PROJECT_ROOT from PDD_PATH: {PROJECT_ROOT}")
67
98
  else:
68
99
  warnings.warn(f"PDD_PATH environment variable ('{PDD_PATH_ENV}') is set but not a valid directory. Attempting auto-detection.")
69
100
 
@@ -81,7 +112,7 @@ if PROJECT_ROOT is None: # If PDD_PATH wasn't set or was invalid
81
112
 
82
113
  if has_git or has_pyproject or has_data or has_dotenv:
83
114
  PROJECT_ROOT = current_dir
84
- # print(f"[DEBUG] Determined PROJECT_ROOT by marker search: {PROJECT_ROOT}") # Optional debug
115
+ logger.debug(f"Determined PROJECT_ROOT by marker search: {PROJECT_ROOT}")
85
116
  break
86
117
 
87
118
  parent_dir = current_dir.parent
@@ -107,21 +138,20 @@ user_model_csv_path = user_pdd_dir / "llm_model.csv"
107
138
 
108
139
  if user_model_csv_path.is_file():
109
140
  LLM_MODEL_CSV_PATH = user_model_csv_path
110
- print(f"[INFO] Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
141
+ logger.info(f"Using user-specific LLM model CSV: {LLM_MODEL_CSV_PATH}")
111
142
  else:
112
143
  LLM_MODEL_CSV_PATH = PROJECT_ROOT / "data" / "llm_model.csv"
113
- print(f"[INFO] Using project LLM model CSV: {LLM_MODEL_CSV_PATH}")
144
+ logger.info(f"Using project LLM model CSV: {LLM_MODEL_CSV_PATH}")
114
145
  # ---------------------------------
115
146
 
116
147
  # Load environment variables from .env file
117
- # print(f"[DEBUG] Attempting to load .env from: {ENV_PATH}") # Optional debug
148
+ logger.debug(f"Attempting to load .env from: {ENV_PATH}")
118
149
  if ENV_PATH.exists():
119
150
  load_dotenv(dotenv_path=ENV_PATH)
120
- # print(f"[DEBUG] Loaded .env file from: {ENV_PATH}") # Optional debug
151
+ logger.debug(f"Loaded .env file from: {ENV_PATH}")
121
152
  else:
122
- # Reduce verbosity if .env is optional or often missing
123
- # warnings.warn(f".env file not found at {ENV_PATH}. API keys might be missing.")
124
- pass # Silently proceed if .env is optional
153
+ # Silently proceed if .env is optional
154
+ logger.debug(f".env file not found at {ENV_PATH}. API keys might need to be provided manually.")
125
155
 
126
156
  # Default model if PDD_MODEL_DEFAULT is not set
127
157
  # Use the imported constant as the default
@@ -154,7 +184,7 @@ if GCS_BUCKET_NAME and GCS_HMAC_ACCESS_KEY_ID and GCS_HMAC_SECRET_ACCESS_KEY:
154
184
  s3_region_name=GCS_REGION_NAME, # Pass region explicitly to cache
155
185
  s3_endpoint_url=GCS_ENDPOINT_URL,
156
186
  )
157
- print(f"[INFO] LiteLLM cache configured for GCS bucket (S3 compatible): {GCS_BUCKET_NAME}")
187
+ logger.info(f"LiteLLM cache configured for GCS bucket (S3 compatible): {GCS_BUCKET_NAME}")
158
188
  cache_configured = True
159
189
 
160
190
  except Exception as e:
@@ -183,7 +213,7 @@ if not cache_configured:
183
213
  # Try SQLite-based cache as a fallback
184
214
  sqlite_cache_path = PROJECT_ROOT / "litellm_cache.sqlite"
185
215
  litellm.cache = litellm.Cache(type="sqlite", cache_path=str(sqlite_cache_path))
186
- print(f"[INFO] LiteLLM SQLite cache configured at {sqlite_cache_path}")
216
+ logger.info(f"LiteLLM SQLite cache configured at {sqlite_cache_path}")
187
217
  cache_configured = True
188
218
  except Exception as e2:
189
219
  warnings.warn(f"Failed to configure LiteLLM SQLite cache: {e2}. Caching is disabled.")
@@ -227,7 +257,7 @@ def _litellm_success_callback(
227
257
  # Attempt 2: If response object failed (e.g., missing provider in model name),
228
258
  # try again using explicit model from kwargs and tokens from usage.
229
259
  # This is often needed for batch completion items.
230
- print(f"[DEBUG] Attempting cost calculation with fallback method: {e1}")
260
+ logger.debug(f"Attempting cost calculation with fallback method: {e1}")
231
261
  try:
232
262
  model_name = kwargs.get("model") # Get original model name from input kwargs
233
263
  if model_name and usage:
@@ -243,12 +273,12 @@ def _litellm_success_callback(
243
273
  # If we can't get model name or usage, fallback to 0
244
274
  calculated_cost = 0.0
245
275
  # Optional: Log the original error e1 if needed
246
- # print(f"[Callback WARN] Failed to calculate cost with response object ({e1}) and fallback failed.")
276
+ # logger.warning(f"[Callback WARN] Failed to calculate cost with response object ({e1}) and fallback failed.")
247
277
  except Exception as e2:
248
278
  # Optional: Log secondary error e2 if needed
249
- # print(f"[Callback WARN] Failed to calculate cost with fallback method: {e2}")
279
+ # logger.warning(f"[Callback WARN] Failed to calculate cost with fallback method: {e2}")
250
280
  calculated_cost = 0.0 # Default to 0 on any error
251
- print(f"[DEBUG] Cost calculation failed with fallback method: {e2}")
281
+ logger.debug(f"Cost calculation failed with fallback method: {e2}")
252
282
 
253
283
  _LAST_CALLBACK_DATA["input_tokens"] = input_tokens
254
284
  _LAST_CALLBACK_DATA["output_tokens"] = output_tokens
@@ -259,7 +289,7 @@ def _litellm_success_callback(
259
289
  # return calculated_cost
260
290
 
261
291
  # Example of logging within the callback (can be expanded)
262
- # print(f"[Callback] Tokens: In={input_tokens}, Out={output_tokens}. Reason: {finish_reason}. Cost: ${calculated_cost:.6f}")
292
+ # logger.info(f"[Callback] Tokens: In={input_tokens}, Out={output_tokens}. Reason: {finish_reason}. Cost: ${calculated_cost:.6f}")
263
293
 
264
294
  # Register the callback with LiteLLM
265
295
  litellm.success_callback = [_litellm_success_callback]
@@ -333,7 +363,7 @@ def _select_model_candidates(
333
363
  # --- Check if filtering resulted in empty (might indicate all models had NaN api_key) ---
334
364
  if available_df.empty:
335
365
  # This case is less likely if notna() is the only filter, but good to check.
336
- rprint("[WARN] No models found after filtering for non-NaN api_key. Check CSV 'api_key' column.")
366
+ logger.warning("No models found after filtering for non-NaN api_key. Check CSV 'api_key' column.")
337
367
  # Decide if this should be a hard error or allow proceeding if logic permits
338
368
  # For now, let's raise an error as it likely indicates a CSV issue.
339
369
  raise ValueError("No models available after initial filtering (all had NaN 'api_key'?).")
@@ -403,16 +433,16 @@ def _select_model_candidates(
403
433
 
404
434
  # --- DEBUGGING PRINT ---
405
435
  if os.getenv("PDD_DEBUG_SELECTOR"): # Add env var check for debug prints
406
- print("\n--- DEBUG: _select_model_candidates ---")
407
- print(f"Strength: {strength}, Base Model: {base_model_name}")
408
- print(f"Metric: {target_metric_value}")
409
- print("Available DF (Sorted by metric):")
436
+ logger.debug("\n--- DEBUG: _select_model_candidates ---")
437
+ logger.debug(f"Strength: {strength}, Base Model: {base_model_name}")
438
+ logger.debug(f"Metric: {target_metric_value}")
439
+ logger.debug("Available DF (Sorted by metric):")
410
440
  # Select columns relevant to the sorting metric
411
441
  sort_cols = ['model', 'avg_cost', 'coding_arena_elo', 'sort_metric']
412
- print(available_df.sort_values(by='sort_metric')[sort_cols])
413
- print("Final Candidates List (Model Names):")
414
- print([c['model'] for c in candidates])
415
- print("---------------------------------------\n")
442
+ logger.debug(available_df.sort_values(by='sort_metric')[sort_cols])
443
+ logger.debug("Final Candidates List (Model Names):")
444
+ logger.debug([c['model'] for c in candidates])
445
+ logger.debug("---------------------------------------\n")
416
446
  # --- END DEBUGGING PRINT ---
417
447
 
418
448
  return candidates
@@ -424,28 +454,28 @@ def _ensure_api_key(model_info: Dict[str, Any], newly_acquired_keys: Dict[str, b
424
454
 
425
455
  if not key_name or key_name == "EXISTING_KEY":
426
456
  if verbose:
427
- rprint(f"[INFO] Skipping API key check for model {model_info.get('model')} (key name: {key_name})")
457
+ logger.info(f"Skipping API key check for model {model_info.get('model')} (key name: {key_name})")
428
458
  return True # Assume key is handled elsewhere or not needed
429
459
 
430
460
  key_value = os.getenv(key_name)
431
461
 
432
462
  if key_value:
433
463
  if verbose:
434
- rprint(f"[INFO] API key '{key_name}' found in environment.")
464
+ logger.info(f"API key '{key_name}' found in environment.")
435
465
  newly_acquired_keys[key_name] = False # Mark as existing
436
466
  return True
437
467
  else:
438
- rprint(f"[WARN] API key environment variable '{key_name}' for model '{model_info.get('model')}' is not set.")
468
+ logger.warning(f"API key environment variable '{key_name}' for model '{model_info.get('model')}' is not set.")
439
469
  try:
440
470
  # Interactive prompt
441
471
  user_provided_key = input(f"Please enter the API key for {key_name}: ").strip()
442
472
  if not user_provided_key:
443
- rprint("[ERROR] No API key provided. Cannot proceed with this model.")
473
+ logger.error("No API key provided. Cannot proceed with this model.")
444
474
  return False
445
475
 
446
476
  # Set environment variable for the current process
447
477
  os.environ[key_name] = user_provided_key
448
- rprint(f"[INFO] API key '{key_name}' set for the current session.")
478
+ logger.info(f"API key '{key_name}' set for the current session.")
449
479
  newly_acquired_keys[key_name] = True # Mark as newly acquired
450
480
 
451
481
  # Update .env file
@@ -483,21 +513,21 @@ def _ensure_api_key(model_info: Dict[str, Any], newly_acquired_keys: Dict[str, b
483
513
  with open(ENV_PATH, 'w') as f:
484
514
  f.writelines(new_lines)
485
515
 
486
- rprint(f"[INFO] API key '{key_name}' saved to {ENV_PATH}.")
487
- rprint("[bold yellow]SECURITY WARNING:[/bold yellow] The API key has been saved to your .env file. "
516
+ logger.info(f"API key '{key_name}' saved to {ENV_PATH}.")
517
+ logger.warning("SECURITY WARNING: The API key has been saved to your .env file. "
488
518
  "Ensure this file is kept secure and is included in your .gitignore.")
489
519
 
490
520
  except IOError as e:
491
- rprint(f"[ERROR] Failed to update .env file at {ENV_PATH}: {e}")
521
+ logger.error(f"Failed to update .env file at {ENV_PATH}: {e}")
492
522
  # Continue since the key is set in the environment for this session
493
523
 
494
524
  return True
495
525
 
496
526
  except EOFError: # Handle non-interactive environments
497
- rprint(f"[ERROR] Cannot prompt for API key '{key_name}' in a non-interactive environment.")
527
+ logger.error(f"Cannot prompt for API key '{key_name}' in a non-interactive environment.")
498
528
  return False
499
529
  except Exception as e:
500
- rprint(f"[ERROR] An unexpected error occurred during API key acquisition: {e}")
530
+ logger.error(f"An unexpected error occurred during API key acquisition: {e}")
501
531
  return False
502
532
 
503
533
 
@@ -563,24 +593,27 @@ def llm_invoke(
563
593
  RuntimeError: If all candidate models fail.
564
594
  openai.*Error: If LiteLLM encounters API errors after retries.
565
595
  """
566
- if verbose: # Print args early if verbose
567
- rprint("[DEBUG llm_invoke start] Arguments received:")
568
- rprint(f" prompt: {'provided' if prompt else 'None'}")
569
- rprint(f" input_json: {'provided' if input_json is not None else 'None'}")
570
- rprint(f" strength: {strength}")
571
- rprint(f" temperature: {temperature}")
572
- rprint(f" verbose: {verbose}")
573
- rprint(f" output_pydantic: {output_pydantic.__name__ if output_pydantic else 'None'}")
574
- rprint(f" time: {time}")
575
- rprint(f" use_batch_mode: {use_batch_mode}")
576
- rprint(f" messages: {'provided' if messages else 'None'}")
596
+ # Set verbose logging if requested
597
+ set_verbose_logging(verbose)
598
+
599
+ if verbose:
600
+ logger.debug("llm_invoke start - Arguments received:")
601
+ logger.debug(f" prompt: {'provided' if prompt else 'None'}")
602
+ logger.debug(f" input_json: {'provided' if input_json is not None else 'None'}")
603
+ logger.debug(f" strength: {strength}")
604
+ logger.debug(f" temperature: {temperature}")
605
+ logger.debug(f" verbose: {verbose}")
606
+ logger.debug(f" output_pydantic: {output_pydantic.__name__ if output_pydantic else 'None'}")
607
+ logger.debug(f" time: {time}")
608
+ logger.debug(f" use_batch_mode: {use_batch_mode}")
609
+ logger.debug(f" messages: {'provided' if messages else 'None'}")
577
610
 
578
611
  # --- 1. Load Environment & Validate Inputs ---
579
612
  # .env loading happens at module level
580
613
 
581
614
  if messages:
582
615
  if verbose:
583
- rprint("[INFO] Using provided 'messages' input.")
616
+ logger.info("Using provided 'messages' input.")
584
617
  # Basic validation of messages format
585
618
  if use_batch_mode:
586
619
  if not isinstance(messages, list) or not all(isinstance(m_list, list) for m_list in messages):
@@ -610,7 +643,7 @@ def llm_invoke(
610
643
  model_df = _load_model_data(LLM_MODEL_CSV_PATH)
611
644
  candidate_models = _select_model_candidates(strength, DEFAULT_BASE_MODEL, model_df)
612
645
  except (FileNotFoundError, ValueError, RuntimeError) as e:
613
- rprint(f"[ERROR] Failed during model loading or selection: {e}")
646
+ logger.error(f"Failed during model loading or selection: {e}")
614
647
  raise
615
648
 
616
649
  if verbose:
@@ -643,22 +676,22 @@ def llm_invoke(
643
676
  return 0.5
644
677
 
645
678
  model_strengths_formatted = [(c['model'], f"{float(calc_strength(c)):.3f}") for c in candidate_models]
646
- rprint("[INFO] Candidate models selected and ordered (with strength):", model_strengths_formatted)
647
- rprint(f"[INFO] Strength: {strength}, Temperature: {temperature}, Time: {time}")
679
+ logger.info("Candidate models selected and ordered (with strength): %s", model_strengths_formatted) # CORRECTED
680
+ logger.info(f"Strength: {strength}, Temperature: {temperature}, Time: {time}")
648
681
  if use_batch_mode:
649
- rprint("[INFO] Batch mode enabled.")
682
+ logger.info("Batch mode enabled.")
650
683
  if output_pydantic:
651
- rprint(f"[INFO] Pydantic output requested: {output_pydantic.__name__}")
684
+ logger.info(f"Pydantic output requested: {output_pydantic.__name__}")
652
685
  try:
653
686
  # Only print input_json if it was actually provided (not when messages were used)
654
687
  if input_json is not None:
655
- rprint("[INFO] Input JSON:")
656
- rprint(input_json)
688
+ logger.info("Input JSON:")
689
+ logger.info(input_json)
657
690
  else:
658
- rprint("[INFO] Input: Using pre-formatted 'messages'.")
691
+ logger.info("Input: Using pre-formatted 'messages'.")
659
692
  except Exception:
660
- print("[INFO] Input JSON/Messages (fallback print):") # Fallback for complex objects rich might fail on
661
- print(input_json if input_json is not None else "[Messages provided directly]")
693
+ logger.info("Input JSON/Messages (fallback print):")
694
+ logger.info(input_json if input_json is not None else "[Messages provided directly]")
662
695
 
663
696
 
664
697
  # --- 3. Iterate Through Candidates and Invoke LLM ---
@@ -671,7 +704,7 @@ def llm_invoke(
671
704
  provider = model_info.get('provider', '').lower()
672
705
 
673
706
  if verbose:
674
- rprint(f"\n[ATTEMPT] Trying model: {model_name_litellm} (Provider: {provider})")
707
+ logger.info(f"\n[ATTEMPT] Trying model: {model_name_litellm} (Provider: {provider})")
675
708
 
676
709
  retry_with_same_model = True
677
710
  while retry_with_same_model:
@@ -681,7 +714,7 @@ def llm_invoke(
681
714
  if not _ensure_api_key(model_info, newly_acquired_keys, verbose):
682
715
  # Problem getting key, break inner loop, try next model candidate
683
716
  if verbose:
684
- rprint(f"[SKIP] Skipping {model_name_litellm} due to API key/credentials issue after prompt.")
717
+ logger.info(f"[SKIP] Skipping {model_name_litellm} due to API key/credentials issue after prompt.")
685
718
  break # Breaks the 'while retry_with_same_model' loop
686
719
 
687
720
  # --- 5. Prepare LiteLLM Arguments ---
@@ -713,30 +746,30 @@ def llm_invoke(
713
746
  litellm_kwargs["vertex_project"] = vertex_project_env
714
747
  litellm_kwargs["vertex_location"] = vertex_location_env
715
748
  if verbose:
716
- rprint(f"[INFO] For Vertex AI: using vertex_credentials from '{credentials_file_path}', project '{vertex_project_env}', location '{vertex_location_env}'.")
749
+ logger.info(f"[INFO] For Vertex AI: using vertex_credentials from '{credentials_file_path}', project '{vertex_project_env}', location '{vertex_location_env}'.")
717
750
  except FileNotFoundError:
718
751
  if verbose:
719
- rprint(f"[ERROR] Vertex credentials file not found at path specified by VERTEX_CREDENTIALS env var: '{credentials_file_path}'. LiteLLM may try ADC or fail.")
752
+ logger.error(f"[ERROR] Vertex credentials file not found at path specified by VERTEX_CREDENTIALS env var: '{credentials_file_path}'. LiteLLM may try ADC or fail.")
720
753
  except json.JSONDecodeError:
721
754
  if verbose:
722
- rprint(f"[ERROR] Failed to decode JSON from Vertex credentials file: '{credentials_file_path}'. Check file content. LiteLLM may try ADC or fail.")
755
+ logger.error(f"[ERROR] Failed to decode JSON from Vertex credentials file: '{credentials_file_path}'. Check file content. LiteLLM may try ADC or fail.")
723
756
  except Exception as e:
724
757
  if verbose:
725
- rprint(f"[ERROR] Failed to load or process Vertex credentials from '{credentials_file_path}': {e}. LiteLLM may try ADC or fail.")
758
+ logger.error(f"[ERROR] Failed to load or process Vertex credentials from '{credentials_file_path}': {e}. LiteLLM may try ADC or fail.")
726
759
  else:
727
760
  if verbose:
728
- rprint(f"[WARN] For Vertex AI (using '{api_key_name_from_csv}'): One or more required environment variables (VERTEX_CREDENTIALS, VERTEX_PROJECT, VERTEX_LOCATION) are missing.")
729
- if not credentials_file_path: rprint(f" Reason: VERTEX_CREDENTIALS (path to JSON file) env var not set or empty.")
730
- if not vertex_project_env: rprint(f" Reason: VERTEX_PROJECT env var not set or empty.")
731
- if not vertex_location_env: rprint(f" Reason: VERTEX_LOCATION env var not set or empty.")
732
- rprint(f" LiteLLM may attempt to use Application Default Credentials or the call may fail.")
761
+ logger.warning(f"[WARN] For Vertex AI (using '{api_key_name_from_csv}'): One or more required environment variables (VERTEX_CREDENTIALS, VERTEX_PROJECT, VERTEX_LOCATION) are missing.")
762
+ if not credentials_file_path: logger.warning(f" Reason: VERTEX_CREDENTIALS (path to JSON file) env var not set or empty.")
763
+ if not vertex_project_env: logger.warning(f" Reason: VERTEX_PROJECT env var not set or empty.")
764
+ if not vertex_location_env: logger.warning(f" Reason: VERTEX_LOCATION env var not set or empty.")
765
+ logger.warning(f" LiteLLM may attempt to use Application Default Credentials or the call may fail.")
733
766
 
734
767
  elif api_key_name_from_csv: # For other api_key_names specified in CSV (e.g., OPENAI_API_KEY, or a direct VERTEX_AI_API_KEY string)
735
768
  key_value = os.getenv(api_key_name_from_csv)
736
769
  if key_value:
737
770
  litellm_kwargs["api_key"] = key_value
738
771
  if verbose:
739
- rprint(f"[INFO] Explicitly passing API key from env var '{api_key_name_from_csv}' as 'api_key' parameter to LiteLLM.")
772
+ logger.info(f"[INFO] Explicitly passing API key from env var '{api_key_name_from_csv}' as 'api_key' parameter to LiteLLM.")
740
773
 
741
774
  # If this model is Vertex AI AND uses a direct API key string (not VERTEX_CREDENTIALS from CSV),
742
775
  # also pass project and location from env vars.
@@ -747,14 +780,14 @@ def llm_invoke(
747
780
  litellm_kwargs["vertex_project"] = vertex_project_env
748
781
  litellm_kwargs["vertex_location"] = vertex_location_env
749
782
  if verbose:
750
- rprint(f"[INFO] For Vertex AI model (using direct API key '{api_key_name_from_csv}'), also passing vertex_project='{vertex_project_env}' and vertex_location='{vertex_location_env}' from env vars.")
783
+ logger.info(f"[INFO] For Vertex AI model (using direct API key '{api_key_name_from_csv}'), also passing vertex_project='{vertex_project_env}' and vertex_location='{vertex_location_env}' from env vars.")
751
784
  elif verbose:
752
- rprint(f"[WARN] For Vertex AI model (using direct API key '{api_key_name_from_csv}'), VERTEX_PROJECT or VERTEX_LOCATION env vars not set. This might be required by LiteLLM.")
785
+ logger.warning(f"[WARN] For Vertex AI model (using direct API key '{api_key_name_from_csv}'), VERTEX_PROJECT or VERTEX_LOCATION env vars not set. This might be required by LiteLLM.")
753
786
  elif verbose: # api_key_name_from_csv was in CSV, but corresponding env var was not set/empty
754
- rprint(f"[WARN] API key name '{api_key_name_from_csv}' found in CSV, but the environment variable '{api_key_name_from_csv}' is not set or empty. LiteLLM will use default authentication if applicable (e.g., other standard env vars or ADC).")
787
+ logger.warning(f"[WARN] API key name '{api_key_name_from_csv}' found in CSV, but the environment variable '{api_key_name_from_csv}' is not set or empty. LiteLLM will use default authentication if applicable (e.g., other standard env vars or ADC).")
755
788
 
756
789
  elif verbose: # No api_key_name_from_csv in CSV for this model
757
- rprint(f"[INFO] No API key name specified in CSV for model '{model_name_litellm}'. LiteLLM will use its default authentication mechanisms (e.g., standard provider env vars or ADC for Vertex AI).")
790
+ logger.info(f"[INFO] No API key name specified in CSV for model '{model_name_litellm}'. LiteLLM will use its default authentication mechanisms (e.g., standard provider env vars or ADC for Vertex AI).")
758
791
 
759
792
  # Add api_base if present in CSV
760
793
  api_base = model_info.get('base_url')
@@ -772,7 +805,7 @@ def llm_invoke(
772
805
 
773
806
  if supports_structured:
774
807
  if verbose:
775
- rprint(f"[INFO] Requesting structured output (Pydantic: {output_pydantic.__name__}) for {model_name_litellm}")
808
+ logger.info(f"[INFO] Requesting structured output (Pydantic: {output_pydantic.__name__}) for {model_name_litellm}")
776
809
  # Pass the Pydantic model directly if supported, else use json_object
777
810
  # LiteLLM handles passing Pydantic models for supported providers
778
811
  litellm_kwargs["response_format"] = output_pydantic
@@ -782,7 +815,7 @@ def llm_invoke(
782
815
  # litellm.enable_json_schema_validation = True # Enable globally if needed
783
816
  else:
784
817
  if verbose:
785
- rprint(f"[WARN] Model {model_name_litellm} does not support structured output via CSV flag. Output might not be valid {output_pydantic.__name__}.")
818
+ logger.warning(f"[WARN] Model {model_name_litellm} does not support structured output via CSV flag. Output might not be valid {output_pydantic.__name__}.")
786
819
  # Proceed without forcing JSON mode, parsing will be attempted later
787
820
 
788
821
  # --- NEW REASONING LOGIC ---
@@ -799,15 +832,15 @@ def llm_invoke(
799
832
  if provider == 'anthropic': # Check provider column instead of model prefix
800
833
  litellm_kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget}
801
834
  if verbose:
802
- rprint(f"[INFO] Requesting Anthropic thinking (budget type) with budget: {budget} tokens for {model_name_litellm}")
835
+ logger.info(f"[INFO] Requesting Anthropic thinking (budget type) with budget: {budget} tokens for {model_name_litellm}")
803
836
  else:
804
837
  # If other providers adopt a budget param recognized by LiteLLM, add here
805
838
  if verbose:
806
- rprint(f"[WARN] Reasoning type is 'budget' for {model_name_litellm}, but no specific LiteLLM budget parameter known for this provider. Parameter not sent.")
839
+ logger.warning(f"[WARN] Reasoning type is 'budget' for {model_name_litellm}, but no specific LiteLLM budget parameter known for this provider. Parameter not sent.")
807
840
  elif verbose:
808
- rprint(f"[INFO] Calculated reasoning budget is 0 for {model_name_litellm}, skipping reasoning parameter.")
841
+ logger.info(f"[INFO] Calculated reasoning budget is 0 for {model_name_litellm}, skipping reasoning parameter.")
809
842
  elif verbose:
810
- rprint(f"[WARN] Reasoning type is 'budget' for {model_name_litellm}, but 'max_reasoning_tokens' is missing or zero in CSV. Reasoning parameter not sent.")
843
+ logger.warning(f"[WARN] Reasoning type is 'budget' for {model_name_litellm}, but 'max_reasoning_tokens' is missing or zero in CSV. Reasoning parameter not sent.")
811
844
 
812
845
  elif reasoning_type == 'effort':
813
846
  effort = "low"
@@ -818,15 +851,15 @@ def llm_invoke(
818
851
  # Use the common 'reasoning_effort' param LiteLLM provides
819
852
  litellm_kwargs["reasoning_effort"] = effort
820
853
  if verbose:
821
- rprint(f"[INFO] Requesting reasoning_effort='{effort}' (effort type) for {model_name_litellm} based on time={time}")
854
+ logger.info(f"[INFO] Requesting reasoning_effort='{effort}' (effort type) for {model_name_litellm} based on time={time}")
822
855
 
823
856
  elif reasoning_type == 'none':
824
857
  if verbose:
825
- rprint(f"[INFO] Model {model_name_litellm} has reasoning_type='none'. No reasoning parameter sent.")
858
+ logger.info(f"[INFO] Model {model_name_litellm} has reasoning_type='none'. No reasoning parameter sent.")
826
859
 
827
860
  else: # Unknown reasoning_type in CSV
828
861
  if verbose:
829
- rprint(f"[WARN] Unknown reasoning_type '{reasoning_type}' for model {model_name_litellm} in CSV. No reasoning parameter sent.")
862
+ logger.warning(f"[WARN] Unknown reasoning_type '{reasoning_type}' for model {model_name_litellm} in CSV. No reasoning parameter sent.")
830
863
 
831
864
  # --- END NEW REASONING LOGIC ---
832
865
 
@@ -837,35 +870,34 @@ def llm_invoke(
837
870
  try:
838
871
  start_time = time_module.time()
839
872
 
840
- # --- ADDED CACHE STATUS DEBUGGING (NOW UNCONDITIONAL) ---
841
- print(f"[DEBUG llm_invoke] Cache Check: litellm.cache is None: {litellm.cache is None}") # MODIFIED: unconditional print
873
+ # Log cache status with proper logging
874
+ logger.debug(f"Cache Check: litellm.cache is None: {litellm.cache is None}")
842
875
  if litellm.cache is not None:
843
- print(f"[DEBUG llm_invoke] litellm.cache type: {type(litellm.cache)}, ID: {id(litellm.cache)}") # MODIFIED: unconditional print
844
- # --- END ADDED CACHE STATUS DEBUGGING ---
876
+ logger.debug(f"litellm.cache type: {type(litellm.cache)}, ID: {id(litellm.cache)}")
845
877
 
846
- # <<< EXPLICITLY ENABLE CACHING >>>
847
878
  # Only add if litellm.cache is configured
848
879
  if litellm.cache is not None:
849
880
  litellm_kwargs["caching"] = True
850
- else: # MODIFIED: unconditional print for this path too
851
- print(f"[DEBUG llm_invoke] NOT ENABLING CACHING: litellm.cache is None at call time.")
881
+ logger.debug("Caching enabled for this request")
882
+ else:
883
+ logger.debug("NOT ENABLING CACHING: litellm.cache is None at call time")
852
884
 
853
885
 
854
886
  if use_batch_mode:
855
887
  if verbose:
856
- rprint(f"[INFO] Calling litellm.batch_completion for {model_name_litellm}...")
888
+ logger.info(f"[INFO] Calling litellm.batch_completion for {model_name_litellm}...")
857
889
  response = litellm.batch_completion(**litellm_kwargs)
858
890
 
859
891
 
860
892
  else:
861
893
  if verbose:
862
- rprint(f"[INFO] Calling litellm.completion for {model_name_litellm}...")
894
+ logger.info(f"[INFO] Calling litellm.completion for {model_name_litellm}...")
863
895
  response = litellm.completion(**litellm_kwargs)
864
896
 
865
897
  end_time = time_module.time()
866
898
 
867
899
  if verbose:
868
- rprint(f"[SUCCESS] Invocation successful for {model_name_litellm} (took {end_time - start_time:.2f}s)")
900
+ logger.info(f"[SUCCESS] Invocation successful for {model_name_litellm} (took {end_time - start_time:.2f}s)")
869
901
 
870
902
  # --- 7. Process Response ---
871
903
  results = []
@@ -883,17 +915,17 @@ def llm_invoke(
883
915
  if hasattr(resp_item, '_hidden_params') and resp_item._hidden_params and 'thinking' in resp_item._hidden_params:
884
916
  thinking = resp_item._hidden_params['thinking']
885
917
  if verbose:
886
- rprint("[DEBUG] Extracted thinking output from response._hidden_params['thinking']")
918
+ logger.debug("[DEBUG] Extracted thinking output from response._hidden_params['thinking']")
887
919
  # Attempt 2: Fallback to reasoning_content in message
888
920
  # Use .get() for safer access
889
921
  elif hasattr(resp_item, 'choices') and resp_item.choices and hasattr(resp_item.choices[0], 'message') and hasattr(resp_item.choices[0].message, 'get') and resp_item.choices[0].message.get('reasoning_content'):
890
922
  thinking = resp_item.choices[0].message.get('reasoning_content')
891
923
  if verbose:
892
- rprint("[DEBUG] Extracted thinking output from response.choices[0].message.get('reasoning_content')")
924
+ logger.debug("[DEBUG] Extracted thinking output from response.choices[0].message.get('reasoning_content')")
893
925
 
894
926
  except (AttributeError, IndexError, KeyError, TypeError):
895
927
  if verbose:
896
- rprint("[DEBUG] Failed to extract thinking output from known locations.")
928
+ logger.debug("[DEBUG] Failed to extract thinking output from known locations.")
897
929
  pass # Ignore if structure doesn't match or errors occur
898
930
  thinking_outputs.append(thinking)
899
931
 
@@ -909,13 +941,13 @@ def llm_invoke(
909
941
  if isinstance(raw_result, output_pydantic):
910
942
  parsed_result = raw_result
911
943
  if verbose:
912
- rprint("[DEBUG] Pydantic object received directly from LiteLLM.")
944
+ logger.debug("[DEBUG] Pydantic object received directly from LiteLLM.")
913
945
 
914
946
  # Attempt 2: Check if raw_result is dict-like and validate
915
947
  elif isinstance(raw_result, dict):
916
948
  parsed_result = output_pydantic.model_validate(raw_result)
917
949
  if verbose:
918
- rprint("[DEBUG] Validated dictionary-like object directly.")
950
+ logger.debug("[DEBUG] Validated dictionary-like object directly.")
919
951
 
920
952
  # Attempt 3: Process as string (if not already parsed/validated)
921
953
  elif isinstance(raw_result, str):
@@ -929,7 +961,7 @@ def llm_invoke(
929
961
  # Basic check if it looks like JSON
930
962
  if potential_json.strip().startswith('{') and potential_json.strip().endswith('}'):
931
963
  if verbose:
932
- rprint(f"[DEBUG] Attempting to parse extracted JSON block: '{potential_json}'")
964
+ logger.debug(f"[DEBUG] Attempting to parse extracted JSON block: '{potential_json}'")
933
965
  parsed_result = output_pydantic.model_validate_json(potential_json)
934
966
  else:
935
967
  # If block extraction fails, try cleaning markdown next
@@ -939,7 +971,7 @@ def llm_invoke(
939
971
  raise ValueError("Could not find enclosing {}")
940
972
  except (json.JSONDecodeError, ValidationError, ValueError) as extraction_error:
941
973
  if verbose:
942
- rprint(f"[DEBUG] JSON block extraction/validation failed ('{extraction_error}'). Trying markdown cleaning.")
974
+ logger.debug(f"[DEBUG] JSON block extraction/validation failed ('{extraction_error}'). Trying markdown cleaning.")
943
975
  # Fallback: Clean markdown fences and retry JSON validation
944
976
  cleaned_result_str = raw_result.strip()
945
977
  if cleaned_result_str.startswith("```json"):
@@ -952,7 +984,7 @@ def llm_invoke(
952
984
  # Check again if it looks like JSON before parsing
953
985
  if cleaned_result_str.startswith('{') and cleaned_result_str.endswith('}'):
954
986
  if verbose:
955
- rprint(f"[DEBUG] Attempting parse after cleaning markdown fences. Cleaned string: '{cleaned_result_str}'")
987
+ logger.debug(f"[DEBUG] Attempting parse after cleaning markdown fences. Cleaned string: '{cleaned_result_str}'")
956
988
  json_string_to_parse = cleaned_result_str # Update string for error reporting
957
989
  parsed_result = output_pydantic.model_validate_json(json_string_to_parse)
958
990
  else:
@@ -966,10 +998,10 @@ def llm_invoke(
966
998
  raise TypeError(f"Raw result type {type(raw_result)} or content could not be validated/parsed against {output_pydantic.__name__}.")
967
999
 
968
1000
  except (ValidationError, json.JSONDecodeError, TypeError, ValueError) as parse_error:
969
- rprint(f"[ERROR] Failed to parse response into Pydantic model {output_pydantic.__name__} for item {i}: {parse_error}")
1001
+ logger.error(f"[ERROR] Failed to parse response into Pydantic model {output_pydantic.__name__} for item {i}: {parse_error}")
970
1002
  # Use the string that was last attempted for parsing in the error message
971
1003
  error_content = json_string_to_parse if json_string_to_parse is not None else raw_result
972
- rprint("[ERROR] Content attempted for parsing:", repr(error_content)) # Use repr for clarity
1004
+ logger.error("[ERROR] Content attempted for parsing: %s", repr(error_content)) # CORRECTED (or use f-string)
973
1005
  results.append(f"ERROR: Failed to parse Pydantic. Raw: {repr(raw_result)}")
974
1006
  continue # Skip appending result below if parsing failed
975
1007
 
@@ -981,7 +1013,7 @@ def llm_invoke(
981
1013
  results.append(raw_result)
982
1014
 
983
1015
  except (AttributeError, IndexError) as e:
984
- rprint(f"[ERROR] Could not extract result content from response item {i}: {e}")
1016
+ logger.error(f"[ERROR] Could not extract result content from response item {i}: {e}")
985
1017
  results.append(f"ERROR: Could not extract result content. Response: {resp_item}")
986
1018
 
987
1019
  # --- Retrieve Cost from Callback Data --- (Reinstated)
@@ -1002,24 +1034,24 @@ def llm_invoke(
1002
1034
  cost_input_pm = model_info.get('input', 0.0) if pd.notna(model_info.get('input')) else 0.0
1003
1035
  cost_output_pm = model_info.get('output', 0.0) if pd.notna(model_info.get('output')) else 0.0
1004
1036
 
1005
- rprint(f"[RESULT] Model Used: {model_name_litellm}")
1006
- rprint(f"[RESULT] Cost (Input): ${cost_input_pm:.2f}/M tokens")
1007
- rprint(f"[RESULT] Cost (Output): ${cost_output_pm:.2f}/M tokens")
1008
- rprint(f"[RESULT] Tokens (Prompt): {input_tokens}")
1009
- rprint(f"[RESULT] Tokens (Completion): {output_tokens}")
1037
+ logger.info(f"[RESULT] Model Used: {model_name_litellm}")
1038
+ logger.info(f"[RESULT] Cost (Input): ${cost_input_pm:.2f}/M tokens")
1039
+ logger.info(f"[RESULT] Cost (Output): ${cost_output_pm:.2f}/M tokens")
1040
+ logger.info(f"[RESULT] Tokens (Prompt): {input_tokens}")
1041
+ logger.info(f"[RESULT] Tokens (Completion): {output_tokens}")
1010
1042
  # Display the cost captured by the callback
1011
- rprint(f"[RESULT] Total Cost (from callback): ${total_cost:.6g}") # Renamed label for clarity
1012
- rprint("[RESULT] Max Completion Tokens: Provider Default") # Indicate default limit
1043
+ logger.info(f"[RESULT] Total Cost (from callback): ${total_cost:.6g}") # Renamed label for clarity
1044
+ logger.info("[RESULT] Max Completion Tokens: Provider Default") # Indicate default limit
1013
1045
  if final_thinking:
1014
- rprint("[RESULT] Thinking Output:")
1015
- rprint(final_thinking) # Rich print should handle the thinking output format
1046
+ logger.info("[RESULT] Thinking Output:")
1047
+ logger.info(final_thinking)
1016
1048
 
1017
1049
  # --- Print raw output before returning if verbose ---
1018
1050
  if verbose:
1019
- rprint("[DEBUG] Raw output before return:")
1020
- print(f" Raw Result (repr): {repr(final_result)}")
1021
- print(f" Raw Thinking (repr): {repr(final_thinking)}")
1022
- rprint("-" * 20) # Separator
1051
+ logger.debug("[DEBUG] Raw output before return:")
1052
+ logger.debug(f" Raw Result (repr): {repr(final_result)}")
1053
+ logger.debug(f" Raw Thinking (repr): {repr(final_thinking)}")
1054
+ logger.debug("-" * 20) # Separator
1023
1055
 
1024
1056
  # --- Return Success ---
1025
1057
  return {
@@ -1033,7 +1065,7 @@ def llm_invoke(
1033
1065
  except openai.AuthenticationError as e:
1034
1066
  last_exception = e
1035
1067
  if newly_acquired_keys.get(api_key_name):
1036
- rprint(f"[AUTH ERROR] Authentication failed for {model_name_litellm} with the newly provided key for '{api_key_name}'. Please check the key and try again.")
1068
+ logger.warning(f"[AUTH ERROR] Authentication failed for {model_name_litellm} with the newly provided key for '{api_key_name}'. Please check the key and try again.")
1037
1069
  # Invalidate the key in env for this session to force re-prompt on retry
1038
1070
  if api_key_name in os.environ:
1039
1071
  del os.environ[api_key_name]
@@ -1042,19 +1074,19 @@ def llm_invoke(
1042
1074
  retry_with_same_model = True # Set flag to retry the same model after re-prompt
1043
1075
  # Go back to the start of the 'while retry_with_same_model' loop
1044
1076
  else:
1045
- rprint(f"[AUTH ERROR] Authentication failed for {model_name_litellm} using existing key '{api_key_name}'. Trying next model.")
1077
+ logger.warning(f"[AUTH ERROR] Authentication failed for {model_name_litellm} using existing key '{api_key_name}'. Trying next model.")
1046
1078
  break # Break inner loop, try next model candidate
1047
1079
 
1048
1080
  except (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError,
1049
1081
  openai.APIStatusError, openai.BadRequestError, openai.InternalServerError,
1050
- Exception) as e:
1082
+ Exception) as e: # Catch generic Exception last
1051
1083
  last_exception = e
1052
1084
  error_type = type(e).__name__
1053
- rprint(f"[ERROR] Invocation failed for {model_name_litellm} ({error_type}): {e}. Trying next model.")
1085
+ logger.error(f"[ERROR] Invocation failed for {model_name_litellm} ({error_type}): {e}. Trying next model.")
1054
1086
  # Log more details in verbose mode
1055
1087
  if verbose:
1056
- import traceback
1057
- traceback.print_exc()
1088
+ # import traceback # Not needed if using exc_info=True
1089
+ logger.debug(f"Detailed exception traceback for {model_name_litellm}:", exc_info=True)
1058
1090
  break # Break inner loop, try next model candidate
1059
1091
 
1060
1092
  # If the inner loop was broken (not by success), continue to the next candidate model
@@ -1064,7 +1096,7 @@ def llm_invoke(
1064
1096
  error_message = "All candidate models failed."
1065
1097
  if last_exception:
1066
1098
  error_message += f" Last error ({type(last_exception).__name__}): {last_exception}"
1067
- rprint(f"[FATAL] {error_message}")
1099
+ logger.error(f"[FATAL] {error_message}")
1068
1100
  raise RuntimeError(error_message) from last_exception
1069
1101
 
1070
1102
  # --- Example Usage (Optional) ---
@@ -1076,7 +1108,7 @@ if __name__ == "__main__":
1076
1108
  # os.environ["PDD_DEBUG_SELECTOR"] = "1"
1077
1109
 
1078
1110
  # Example 1: Simple text generation
1079
- print("\n--- Example 1: Simple Text Generation (Strength 0.5) ---")
1111
+ logger.info("\n--- Example 1: Simple Text Generation (Strength 0.5) ---")
1080
1112
  try:
1081
1113
  response = llm_invoke(
1082
1114
  prompt="Tell me a short joke about {topic}.",
@@ -1085,13 +1117,13 @@ if __name__ == "__main__":
1085
1117
  temperature=0.7,
1086
1118
  verbose=True
1087
1119
  )
1088
- rprint("\nExample 1 Response:")
1089
- rprint(response)
1120
+ logger.info("\nExample 1 Response:")
1121
+ logger.info(response)
1090
1122
  except Exception as e:
1091
- rprint(f"\nExample 1 Failed: {e}")
1123
+ logger.error(f"\nExample 1 Failed: {e}", exc_info=True)
1092
1124
 
1093
1125
  # Example 1b: Simple text generation (Strength 0.3)
1094
- print("\n--- Example 1b: Simple Text Generation (Strength 0.3) ---")
1126
+ logger.info("\n--- Example 1b: Simple Text Generation (Strength 0.3) ---")
1095
1127
  try:
1096
1128
  response = llm_invoke(
1097
1129
  prompt="Tell me a short joke about {topic}.",
@@ -1100,13 +1132,13 @@ if __name__ == "__main__":
1100
1132
  temperature=0.7,
1101
1133
  verbose=True
1102
1134
  )
1103
- rprint("\nExample 1b Response:")
1104
- rprint(response)
1135
+ logger.info("\nExample 1b Response:")
1136
+ logger.info(response)
1105
1137
  except Exception as e:
1106
- rprint(f"\nExample 1b Failed: {e}")
1138
+ logger.error(f"\nExample 1b Failed: {e}", exc_info=True)
1107
1139
 
1108
1140
  # Example 2: Structured output (requires a Pydantic model)
1109
- print("\n--- Example 2: Structured Output (Pydantic, Strength 0.8) ---")
1141
+ logger.info("\n--- Example 2: Structured Output (Pydantic, Strength 0.8) ---")
1110
1142
  class JokeStructure(BaseModel):
1111
1143
  setup: str
1112
1144
  punchline: str
@@ -1123,19 +1155,19 @@ if __name__ == "__main__":
1123
1155
  output_pydantic=JokeStructure,
1124
1156
  verbose=True
1125
1157
  )
1126
- rprint("\nExample 2 Response:")
1127
- rprint(response_structured)
1158
+ logger.info("\nExample 2 Response:")
1159
+ logger.info(response_structured)
1128
1160
  if isinstance(response_structured.get('result'), JokeStructure):
1129
- rprint("\nPydantic object received successfully:", response_structured['result'].model_dump())
1161
+ logger.info("\nPydantic object received successfully: %s", response_structured['result'].model_dump())
1130
1162
  else:
1131
- rprint("\nResult was not the expected Pydantic object:", response_structured.get('result'))
1163
+ logger.info("\nResult was not the expected Pydantic object: %s", response_structured.get('result'))
1132
1164
 
1133
1165
  except Exception as e:
1134
- rprint(f"\nExample 2 Failed: {e}")
1166
+ logger.error(f"\nExample 2 Failed: {e}", exc_info=True)
1135
1167
 
1136
1168
 
1137
1169
  # Example 3: Batch processing
1138
- print("\n--- Example 3: Batch Processing (Strength 0.3) ---")
1170
+ logger.info("\n--- Example 3: Batch Processing (Strength 0.3) ---")
1139
1171
  try:
1140
1172
  batch_input = [
1141
1173
  {"animal": "cat", "adjective": "lazy"},
@@ -1150,13 +1182,13 @@ if __name__ == "__main__":
1150
1182
  use_batch_mode=True,
1151
1183
  verbose=True
1152
1184
  )
1153
- rprint("\nExample 3 Response:")
1154
- rprint(response_batch)
1185
+ logger.info("\nExample 3 Response:")
1186
+ logger.info(response_batch)
1155
1187
  except Exception as e:
1156
- rprint(f"\nExample 3 Failed: {e}")
1188
+ logger.error(f"\nExample 3 Failed: {e}", exc_info=True)
1157
1189
 
1158
1190
  # Example 4: Using 'messages' input
1159
- print("\n--- Example 4: Using 'messages' input (Strength 0.5) ---")
1191
+ logger.info("\n--- Example 4: Using 'messages' input (Strength 0.5) ---")
1160
1192
  try:
1161
1193
  custom_messages = [
1162
1194
  {"role": "system", "content": "You are a helpful assistant."},
@@ -1169,13 +1201,13 @@ if __name__ == "__main__":
1169
1201
  temperature=0.1,
1170
1202
  verbose=True
1171
1203
  )
1172
- rprint("\nExample 4 Response:")
1173
- rprint(response_messages)
1204
+ logger.info("\nExample 4 Response:")
1205
+ logger.info(response_messages)
1174
1206
  except Exception as e:
1175
- rprint(f"\nExample 4 Failed: {e}")
1207
+ logger.error(f"\nExample 4 Failed: {e}", exc_info=True)
1176
1208
 
1177
1209
  # Example 5: Requesting thinking time (e.g., for Anthropic)
1178
- print("\n--- Example 5: Requesting Thinking Time (Strength 1.0, Time 0.5) ---")
1210
+ logger.info("\n--- Example 5: Requesting Thinking Time (Strength 1.0, Time 0.5) ---")
1179
1211
  try:
1180
1212
  # Ensure your CSV has max_reasoning_tokens for an Anthropic model
1181
1213
  # Strength 1.0 should select claude-3 (highest ELO)
@@ -1188,15 +1220,15 @@ if __name__ == "__main__":
1188
1220
  time=0.5, # Request moderate thinking time
1189
1221
  verbose=True
1190
1222
  )
1191
- rprint("\nExample 5 Response:")
1192
- rprint(response_thinking)
1223
+ logger.info("\nExample 5 Response:")
1224
+ logger.info(response_thinking)
1193
1225
  except Exception as e:
1194
- rprint(f"\nExample 5 Failed: {e}")
1226
+ logger.error(f"\nExample 5 Failed: {e}", exc_info=True)
1195
1227
 
1196
1228
  # Example 6: Pydantic Fallback Parsing (Strength 0.3)
1197
- print("\n--- Example 6: Pydantic Fallback Parsing (Strength 0.3) ---")
1229
+ logger.info("\n--- Example 6: Pydantic Fallback Parsing (Strength 0.3) ---")
1198
1230
  # This requires mocking litellm.completion to return a JSON string
1199
1231
  # even when gemini-pro (which supports structured output) is selected.
1200
1232
  # This is hard to demonstrate cleanly in the __main__ block without mocks.
1201
1233
  # The unit test test_llm_invoke_output_pydantic_unsupported_parses covers this.
1202
- print("(Covered by unit tests)")
1234
+ logger.info("(Covered by unit tests)")