pdd-cli 0.0.26__py3-none-any.whl → 0.0.28__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/__init__.py +1 -1
- pdd/cli.py +9 -0
- pdd/cli_1_0_1_0_20250510_005101.py +1054 -0
- pdd/fix_verification_errors.py +2 -2
- pdd/fix_verification_errors_loop.py +5 -1
- pdd/fix_verification_main.py +21 -1
- pdd/generate_output_paths.py +43 -2
- pdd/llm_invoke.py +208 -176
- pdd/prompts/find_verification_errors_LLM.prompt +18 -7
- pdd/prompts/fix_verification_errors_LLM.prompt +18 -6
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/METADATA +4 -3
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/RECORD +16 -15
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.26.dist-info → pdd_cli-0.0.28.dist-info}/top_level.txt +0 -0
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
|
|
10
|
-
|
|
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
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
logger.debug(f"Loaded .env file from: {ENV_PATH}")
|
|
121
152
|
else:
|
|
122
|
-
#
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
647
|
-
|
|
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
|
-
|
|
682
|
+
logger.info("Batch mode enabled.")
|
|
650
683
|
if output_pydantic:
|
|
651
|
-
|
|
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
|
-
|
|
656
|
-
|
|
688
|
+
logger.info("Input JSON:")
|
|
689
|
+
logger.info(input_json)
|
|
657
690
|
else:
|
|
658
|
-
|
|
691
|
+
logger.info("Input: Using pre-formatted 'messages'.")
|
|
659
692
|
except Exception:
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
729
|
-
if not credentials_file_path:
|
|
730
|
-
if not vertex_project_env:
|
|
731
|
-
if not vertex_location_env:
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
841
|
+
logger.info(f"[INFO] Calculated reasoning budget is 0 for {model_name_litellm}, skipping reasoning parameter.")
|
|
809
842
|
elif verbose:
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
841
|
-
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1120
|
+
logger.info("\nExample 1 Response:")
|
|
1121
|
+
logger.info(response)
|
|
1090
1122
|
except Exception as e:
|
|
1091
|
-
|
|
1123
|
+
logger.error(f"\nExample 1 Failed: {e}", exc_info=True)
|
|
1092
1124
|
|
|
1093
1125
|
# Example 1b: Simple text generation (Strength 0.3)
|
|
1094
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1135
|
+
logger.info("\nExample 1b Response:")
|
|
1136
|
+
logger.info(response)
|
|
1105
1137
|
except Exception as e:
|
|
1106
|
-
|
|
1138
|
+
logger.error(f"\nExample 1b Failed: {e}", exc_info=True)
|
|
1107
1139
|
|
|
1108
1140
|
# Example 2: Structured output (requires a Pydantic model)
|
|
1109
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1158
|
+
logger.info("\nExample 2 Response:")
|
|
1159
|
+
logger.info(response_structured)
|
|
1128
1160
|
if isinstance(response_structured.get('result'), JokeStructure):
|
|
1129
|
-
|
|
1161
|
+
logger.info("\nPydantic object received successfully: %s", response_structured['result'].model_dump())
|
|
1130
1162
|
else:
|
|
1131
|
-
|
|
1163
|
+
logger.info("\nResult was not the expected Pydantic object: %s", response_structured.get('result'))
|
|
1132
1164
|
|
|
1133
1165
|
except Exception as e:
|
|
1134
|
-
|
|
1166
|
+
logger.error(f"\nExample 2 Failed: {e}", exc_info=True)
|
|
1135
1167
|
|
|
1136
1168
|
|
|
1137
1169
|
# Example 3: Batch processing
|
|
1138
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1185
|
+
logger.info("\nExample 3 Response:")
|
|
1186
|
+
logger.info(response_batch)
|
|
1155
1187
|
except Exception as e:
|
|
1156
|
-
|
|
1188
|
+
logger.error(f"\nExample 3 Failed: {e}", exc_info=True)
|
|
1157
1189
|
|
|
1158
1190
|
# Example 4: Using 'messages' input
|
|
1159
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
1204
|
+
logger.info("\nExample 4 Response:")
|
|
1205
|
+
logger.info(response_messages)
|
|
1174
1206
|
except Exception as e:
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1223
|
+
logger.info("\nExample 5 Response:")
|
|
1224
|
+
logger.info(response_thinking)
|
|
1193
1225
|
except Exception as e:
|
|
1194
|
-
|
|
1226
|
+
logger.error(f"\nExample 5 Failed: {e}", exc_info=True)
|
|
1195
1227
|
|
|
1196
1228
|
# Example 6: Pydantic Fallback Parsing (Strength 0.3)
|
|
1197
|
-
|
|
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
|
-
|
|
1234
|
+
logger.info("(Covered by unit tests)")
|