Skip to content

Commit 59749df

Browse files
authored
Merge pull request lightspeed-core#187 from asamal4/consistent-config-exception
chore: consistent config exception for config data model val
2 parents 17da61a + dcd66bd commit 59749df

5 files changed

Lines changed: 34 additions & 28 deletions

File tree

src/lightspeed_evaluation/core/llm/manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from lightspeed_evaluation.core.models import LLMConfig, SystemConfig
88
from lightspeed_evaluation.core.system.env_validator import validate_provider_env
9+
from lightspeed_evaluation.core.system.exceptions import ConfigurationError
910

1011
logger = logging.getLogger(__name__)
1112

@@ -51,7 +52,7 @@ def __init__(
5152
# Create child manager without system_config to avoid recursion
5253
judge_manager = LLMManager(resolved_config, judge_id=pool_key)
5354
self.judge_managers.append(judge_manager)
54-
except ValueError as e:
55+
except ConfigurationError as e:
5556
logger.error("Failed to resolve judge panel: %s", e)
5657
raise
5758
else:

src/lightspeed_evaluation/core/metrics/geval.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from lightspeed_evaluation.core.llm.deepeval import DeepEvalLLMManager
2727
from lightspeed_evaluation.core.metrics.manager import MetricLevel, MetricManager
2828
from lightspeed_evaluation.core.models import GEvalConfig
29+
from lightspeed_evaluation.core.system.exceptions import ConfigurationError
2930

3031
logger = logging.getLogger(__name__)
3132

@@ -111,7 +112,7 @@ def evaluate( # pylint: disable=R0913,R0917
111112
# GEvalConfig (criteria, rubrics, threshold, etc.) for evaluation.
112113
try:
113114
config = GEvalConfig.from_metadata(raw_config)
114-
except (ValueError, ValidationError) as e:
115+
except (ValueError, ValidationError, ConfigurationError) as e:
115116
return None, f"Invalid GEval configuration: {e!s}"
116117

117118
# Convert validated rubrics to DeepEval Rubric objects

src/lightspeed_evaluation/core/models/system.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
model_validator,
1313
)
1414

15-
from lightspeed_evaluation.core.system.exceptions import ConfigurationError
1615
from lightspeed_evaluation.core.constants import (
1716
DEFAULT_API_BASE,
1817
DEFAULT_API_CACHE_DIR,
@@ -45,6 +44,7 @@
4544
SUPPORTED_GRAPH_TYPES,
4645
SUPPORTED_OUTPUT_TYPES,
4746
)
47+
from lightspeed_evaluation.core.system.exceptions import ConfigurationError
4848

4949

5050
class LLMConfig(BaseModel):
@@ -82,7 +82,7 @@ def validate_ssl_cert_file(self) -> "LLMConfig":
8282

8383
# Check if file exists
8484
if not os.path.isfile(cert_path):
85-
raise ValueError(
85+
raise ConfigurationError(
8686
f"SSL certificate file not found: '{cert_path}'. "
8787
f"Original path: '{self.ssl_cert_file}'. "
8888
"Please provide a valid path to a CA certificate file "
@@ -519,10 +519,10 @@ def resolve_llm_config(
519519
Fully resolved LLMConfig
520520
521521
Raises:
522-
ValueError: If model_id not found
522+
ConfigurationError: If model_id not found
523523
"""
524524
if model_id not in self.models:
525-
raise ValueError(
525+
raise ConfigurationError(
526526
f"Model '{model_id}' not found in llm_pool.models. "
527527
f"Available: {list(self.models.keys())}"
528528
)
@@ -687,7 +687,7 @@ def validate_rubrics_non_overlapping(self) -> "GEvalConfig":
687687
continue
688688
# Overlap if not (b < c or d < a)
689689
if not (b < c or d < a):
690-
raise ValueError(
690+
raise ConfigurationError(
691691
f"Rubric score ranges must not overlap: "
692692
f"[{a}, {b}] and [{c}, {d}] overlap"
693693
)
@@ -708,9 +708,9 @@ def from_metadata(cls, raw: dict[str, Any]) -> "GEvalConfig":
708708
ValueError: If raw is not a dict or criteria is missing/empty
709709
(only these pre-model_validate checks raise bare ValueError).
710710
ValidationError: If rubric or config fields fail Pydantic validation:
711-
wrong types (e.g. score_range, expected_outcome), invalid structure,
712-
or overlapping score ranges (model validator raises ValueError
713-
and Pydantic v2 wraps it as ValidationError).
711+
wrong types (e.g. score_range, expected_outcome), invalid structure.
712+
ConfigurationError: If rubric score ranges overlap (model validator
713+
raises ConfigurationError directly, bypassing Pydantic wrapping).
714714
"""
715715
if not isinstance(raw, dict):
716716
raise ValueError("GEval config must be a dict")
@@ -801,17 +801,17 @@ def validate_default_metrics_metadata_geval(
801801
802802
Raises:
803803
ConfigurationError: When a geval:* entry has invalid config (e.g.
804-
missing criteria, invalid rubric structure).
805-
Re-raised from ValueError or Pydantic ValidationError for a consistent
806-
config-failure exception type.
804+
missing criteria, invalid rubric structure, overlapping rubrics).
805+
Re-raised from ValueError, ValidationError, or ConfigurationError
806+
for a consistent config-failure exception type with metric context.
807807
"""
808808
if not v:
809809
return v
810810
for metric_id, meta in v.items():
811811
if metric_id.startswith("geval:") and isinstance(meta, dict):
812812
try:
813813
GEvalConfig.from_metadata(meta)
814-
except (ValueError, ValidationError) as e:
814+
except (ValueError, ValidationError, ConfigurationError) as e:
815815
raise ConfigurationError(
816816
f"Invalid GEval config for '{metric_id}': {e!s}"
817817
) from e

src/lightspeed_evaluation/runner/evaluation.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
# Import only lightweight modules at top level
1313
from lightspeed_evaluation.core.system import ConfigLoader
14-
from lightspeed_evaluation.core.system.exceptions import DataValidationError
14+
from lightspeed_evaluation.core.system.exceptions import (
15+
ConfigurationError,
16+
DataValidationError,
17+
)
1518

1619

1720
def _clear_caches(system_config: SystemConfig) -> None:
@@ -174,7 +177,13 @@ def run_evaluation( # pylint: disable=too-many-locals
174177
"SKIPPED": summary["SKIPPED"],
175178
}
176179

177-
except (FileNotFoundError, ValueError, RuntimeError, DataValidationError) as e:
180+
except (
181+
FileNotFoundError,
182+
ValueError,
183+
RuntimeError,
184+
ConfigurationError,
185+
DataValidationError,
186+
) as e:
178187
print(f"\n❌ Evaluation failed: {e}")
179188
traceback.print_exc()
180189
return None

tests/unit/core/models/test_system.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ def test_ssl_cert_file_handling(self, mocker: MockerFixture) -> None:
7171
os.unlink(cert_path)
7272

7373
# Non-existent file fails
74-
with pytest.raises(ValidationError, match="(?i)not found"):
74+
with pytest.raises(ConfigurationError, match="(?i)not found"):
7575
LLMConfig(ssl_cert_file="/tmp/nonexistent_cert_12345.crt")
7676

7777
# Directory fails
78-
with pytest.raises(ValidationError):
78+
with pytest.raises(ConfigurationError):
7979
LLMConfig(ssl_cert_file=tempfile.gettempdir())
8080

8181

@@ -268,7 +268,7 @@ def test_resolve_llm_config(self) -> None:
268268
assert resolved.timeout == 300
269269

270270
# Unknown model raises error
271-
with pytest.raises(ValueError, match="Model 'unknown' not found"):
271+
with pytest.raises(ConfigurationError, match="Model 'unknown' not found"):
272272
pool.resolve_llm_config("unknown")
273273

274274
def test_custom_model_id_and_ssl(self) -> None:
@@ -419,13 +419,13 @@ def test_error_branches(self) -> None:
419419
with pytest.raises(ConfigurationError, match="llm_pool.*not defined"):
420420
config.get_judge_configs()
421421

422-
# get_judge_configs with invalid judge ID raises ValueError
422+
# get_judge_configs with invalid judge ID raises error
423423
pool = LLMPoolConfig(
424424
models={"gpt-4o-mini": LLMProviderConfig(provider="openai")}
425425
)
426426
panel = JudgePanelConfig(judges=["gpt-4o-mini", "nonexistent"])
427427
config = SystemConfig(llm_pool=pool, judge_panel=panel)
428-
with pytest.raises(ValueError, match="Model 'nonexistent' not found"):
428+
with pytest.raises(ConfigurationError, match="Model 'nonexistent' not found"):
429429
config.get_judge_configs()
430430

431431

@@ -483,13 +483,8 @@ def test_geval_config_rubrics_adjacent_non_overlapping_accepted(self) -> None:
483483
assert config.rubrics[1].score_range == (4, 7)
484484

485485
def test_geval_config_rubrics_overlapping_fails(self) -> None:
486-
"""Overlapping rubric ranges fail validation.
487-
488-
validate_rubrics_non_overlapping raises ValueError, but Pydantic v2
489-
wraps it in ValidationError before from_metadata returns, so callers
490-
get ValidationError.
491-
"""
492-
with pytest.raises(ValidationError, match="overlap"):
486+
"""Overlapping rubric ranges fail validation."""
487+
with pytest.raises(ConfigurationError, match="overlap"):
493488
GEvalConfig.from_metadata(
494489
{
495490
"criteria": "Check.",

0 commit comments

Comments
 (0)