learning-commons-evaluators 0.1.0__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.
- learning_commons_evaluators/__init__.py +154 -0
- learning_commons_evaluators/config.py +27 -0
- learning_commons_evaluators/errors.py +31 -0
- learning_commons_evaluators/evaluators/__init__.py +29 -0
- learning_commons_evaluators/evaluators/base.py +434 -0
- learning_commons_evaluators/evaluators/conventionality.py +117 -0
- learning_commons_evaluators/evaluators/vocabulary.py +195 -0
- learning_commons_evaluators/logger.py +99 -0
- learning_commons_evaluators/providers/__init__.py +15 -0
- learning_commons_evaluators/providers/langchain.py +108 -0
- learning_commons_evaluators/py.typed +0 -0
- learning_commons_evaluators/schemas/__init__.py +75 -0
- learning_commons_evaluators/schemas/common_inputs.py +101 -0
- learning_commons_evaluators/schemas/config.py +166 -0
- learning_commons_evaluators/schemas/conventionality.py +32 -0
- learning_commons_evaluators/schemas/errors.py +592 -0
- learning_commons_evaluators/schemas/evaluator.py +220 -0
- learning_commons_evaluators/schemas/input_specs.py +131 -0
- learning_commons_evaluators/schemas/metadata.py +132 -0
- learning_commons_evaluators/schemas/text_complexity.py +48 -0
- learning_commons_evaluators/schemas/vocabulary.py +68 -0
- learning_commons_evaluators/settings/__init__.py +9 -0
- learning_commons_evaluators/settings/_generated_conventionality_settings.py +101 -0
- learning_commons_evaluators/settings/_generated_vocabulary_settings.py +404 -0
- learning_commons_evaluators/settings/conventionality/__init__.py +0 -0
- learning_commons_evaluators/settings/conventionality/contracts.toml +130 -0
- learning_commons_evaluators/settings/load_settings.py +196 -0
- learning_commons_evaluators/settings/vocabulary/__init__.py +0 -0
- learning_commons_evaluators/settings/vocabulary/contracts.toml +641 -0
- learning_commons_evaluators/version.py +10 -0
- learning_commons_evaluators-0.1.0.dist-info/METADATA +93 -0
- learning_commons_evaluators-0.1.0.dist-info/RECORD +35 -0
- learning_commons_evaluators-0.1.0.dist-info/WHEEL +5 -0
- learning_commons_evaluators-0.1.0.dist-info/licenses/LICENSE +21 -0
- learning_commons_evaluators-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Learning Commons Evaluators – Python SDK for educational text evaluators."""
|
|
2
|
+
|
|
3
|
+
# Bind __version__ first so consumers reading it during package init never see AttributeError.
|
|
4
|
+
from learning_commons_evaluators.version import __description__, __version__ # noqa: I001
|
|
5
|
+
|
|
6
|
+
# Config
|
|
7
|
+
from learning_commons_evaluators.config import (
|
|
8
|
+
AnthropicLLMProviderConfig,
|
|
9
|
+
EvaluatorConfig,
|
|
10
|
+
GoogleLLMProviderConfig,
|
|
11
|
+
LLMProviderConfig,
|
|
12
|
+
OpenAILLMProviderConfig,
|
|
13
|
+
PromptSettings,
|
|
14
|
+
TelemetryConfig,
|
|
15
|
+
create_config,
|
|
16
|
+
create_config_no_telemetry,
|
|
17
|
+
create_config_telemetry_with_full_input,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Errors
|
|
21
|
+
from learning_commons_evaluators.errors import (
|
|
22
|
+
APIError,
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
ConfigurationError,
|
|
25
|
+
EvaluatorError,
|
|
26
|
+
InputValidationError,
|
|
27
|
+
NetworkError,
|
|
28
|
+
OutputValidationError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
RequestTimeoutError,
|
|
31
|
+
wrap_provider_error,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Evaluators
|
|
35
|
+
from learning_commons_evaluators.evaluators import (
|
|
36
|
+
BaseEvaluator,
|
|
37
|
+
ConventionalityEvaluator,
|
|
38
|
+
InputT,
|
|
39
|
+
OutputT,
|
|
40
|
+
VocabularyEvaluationInput,
|
|
41
|
+
VocabularyEvaluator,
|
|
42
|
+
)
|
|
43
|
+
from learning_commons_evaluators.evaluators.conventionality import (
|
|
44
|
+
ConventionalityEvaluationInput,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Logger (uses Python standard logging)
|
|
48
|
+
from learning_commons_evaluators.logger import (
|
|
49
|
+
SDK_LOGGER_NAME,
|
|
50
|
+
Logger,
|
|
51
|
+
create_logger,
|
|
52
|
+
create_silent_logger,
|
|
53
|
+
get_logger,
|
|
54
|
+
)
|
|
55
|
+
from learning_commons_evaluators.schemas.common_inputs import (
|
|
56
|
+
GradeInputField,
|
|
57
|
+
TextInputField,
|
|
58
|
+
)
|
|
59
|
+
from learning_commons_evaluators.schemas.config import EvaluationSettings, LLMProvider
|
|
60
|
+
from learning_commons_evaluators.schemas.conventionality import (
|
|
61
|
+
ConventionalityEvaluationSettings,
|
|
62
|
+
ConventionalityOutput,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Schemas (core)
|
|
66
|
+
from learning_commons_evaluators.schemas.evaluator import (
|
|
67
|
+
EvaluationAnswer,
|
|
68
|
+
EvaluationExplanation,
|
|
69
|
+
EvaluationInput,
|
|
70
|
+
EvaluationResult,
|
|
71
|
+
InputField,
|
|
72
|
+
)
|
|
73
|
+
from learning_commons_evaluators.schemas.input_specs import (
|
|
74
|
+
AnyInputSpec,
|
|
75
|
+
GradeInputSpec,
|
|
76
|
+
InputSpec,
|
|
77
|
+
TextInputSpec,
|
|
78
|
+
)
|
|
79
|
+
from learning_commons_evaluators.schemas.metadata import (
|
|
80
|
+
EvaluationMetadata,
|
|
81
|
+
EvaluatorMaturity,
|
|
82
|
+
EvaluatorMetadata,
|
|
83
|
+
Status,
|
|
84
|
+
TokenUsage,
|
|
85
|
+
)
|
|
86
|
+
from learning_commons_evaluators.schemas.text_complexity import (
|
|
87
|
+
TextComplexityEvaluationInput,
|
|
88
|
+
)
|
|
89
|
+
from learning_commons_evaluators.schemas.vocabulary import (
|
|
90
|
+
VocabularyComplexityOutput,
|
|
91
|
+
VocabularyEvaluationSettings,
|
|
92
|
+
normalize_complexity_output,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"__description__",
|
|
97
|
+
"__version__",
|
|
98
|
+
"APIError",
|
|
99
|
+
"AnthropicLLMProviderConfig",
|
|
100
|
+
"AuthenticationError",
|
|
101
|
+
"BaseEvaluator",
|
|
102
|
+
"ConfigurationError",
|
|
103
|
+
"ConventionalityEvaluationInput",
|
|
104
|
+
"ConventionalityEvaluationSettings",
|
|
105
|
+
"ConventionalityEvaluator",
|
|
106
|
+
"ConventionalityOutput",
|
|
107
|
+
"EvaluationAnswer",
|
|
108
|
+
"EvaluationExplanation",
|
|
109
|
+
"EvaluationInput",
|
|
110
|
+
"EvaluationMetadata",
|
|
111
|
+
"EvaluationResult",
|
|
112
|
+
"EvaluationSettings",
|
|
113
|
+
"EvaluatorConfig",
|
|
114
|
+
"EvaluatorError",
|
|
115
|
+
"EvaluatorMaturity",
|
|
116
|
+
"EvaluatorMetadata",
|
|
117
|
+
"GoogleLLMProviderConfig",
|
|
118
|
+
"AnyInputSpec",
|
|
119
|
+
"GradeInputField",
|
|
120
|
+
"GradeInputSpec",
|
|
121
|
+
"InputField",
|
|
122
|
+
"InputSpec",
|
|
123
|
+
"InputT",
|
|
124
|
+
"InputValidationError",
|
|
125
|
+
"TextInputSpec",
|
|
126
|
+
"LLMProvider",
|
|
127
|
+
"LLMProviderConfig",
|
|
128
|
+
"Logger",
|
|
129
|
+
"NetworkError",
|
|
130
|
+
"OpenAILLMProviderConfig",
|
|
131
|
+
"OutputT",
|
|
132
|
+
"OutputValidationError",
|
|
133
|
+
"PromptSettings",
|
|
134
|
+
"RateLimitError",
|
|
135
|
+
"RequestTimeoutError",
|
|
136
|
+
"SDK_LOGGER_NAME",
|
|
137
|
+
"Status",
|
|
138
|
+
"TelemetryConfig",
|
|
139
|
+
"TextComplexityEvaluationInput",
|
|
140
|
+
"TextInputField",
|
|
141
|
+
"TokenUsage",
|
|
142
|
+
"VocabularyEvaluationInput",
|
|
143
|
+
"VocabularyEvaluationSettings",
|
|
144
|
+
"VocabularyEvaluator",
|
|
145
|
+
"VocabularyComplexityOutput",
|
|
146
|
+
"normalize_complexity_output",
|
|
147
|
+
"create_config",
|
|
148
|
+
"create_config_no_telemetry",
|
|
149
|
+
"create_config_telemetry_with_full_input",
|
|
150
|
+
"create_logger",
|
|
151
|
+
"create_silent_logger",
|
|
152
|
+
"get_logger",
|
|
153
|
+
"wrap_provider_error",
|
|
154
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Re-export evaluator config from schemas.config for package-level imports."""
|
|
2
|
+
|
|
3
|
+
from learning_commons_evaluators.schemas.config import (
|
|
4
|
+
AnthropicLLMProviderConfig,
|
|
5
|
+
EvaluatorConfig,
|
|
6
|
+
GoogleLLMProviderConfig,
|
|
7
|
+
LLMProviderConfig,
|
|
8
|
+
OpenAILLMProviderConfig,
|
|
9
|
+
PromptSettings,
|
|
10
|
+
TelemetryConfig,
|
|
11
|
+
create_config,
|
|
12
|
+
create_config_no_telemetry,
|
|
13
|
+
create_config_telemetry_with_full_input,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AnthropicLLMProviderConfig",
|
|
18
|
+
"EvaluatorConfig",
|
|
19
|
+
"GoogleLLMProviderConfig",
|
|
20
|
+
"OpenAILLMProviderConfig",
|
|
21
|
+
"LLMProviderConfig",
|
|
22
|
+
"PromptSettings",
|
|
23
|
+
"TelemetryConfig",
|
|
24
|
+
"create_config",
|
|
25
|
+
"create_config_no_telemetry",
|
|
26
|
+
"create_config_telemetry_with_full_input",
|
|
27
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Re-export error types from schemas.errors for package-level imports."""
|
|
2
|
+
|
|
3
|
+
from learning_commons_evaluators.schemas.errors import (
|
|
4
|
+
APIError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
ConfigurationError,
|
|
7
|
+
EvaluatorError,
|
|
8
|
+
InputValidationError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
OutputValidationError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
RequestTimeoutError,
|
|
13
|
+
format_error_for_metadata,
|
|
14
|
+
sanitize_pydantic_errors,
|
|
15
|
+
wrap_provider_error,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"APIError",
|
|
20
|
+
"AuthenticationError",
|
|
21
|
+
"ConfigurationError",
|
|
22
|
+
"EvaluatorError",
|
|
23
|
+
"InputValidationError",
|
|
24
|
+
"NetworkError",
|
|
25
|
+
"OutputValidationError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"RequestTimeoutError",
|
|
28
|
+
"format_error_for_metadata",
|
|
29
|
+
"sanitize_pydantic_errors",
|
|
30
|
+
"wrap_provider_error",
|
|
31
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Evaluator implementations.
|
|
2
|
+
|
|
3
|
+
Config types (``EvaluatorConfig``, ``GoogleLLMProviderConfig``, etc.) are
|
|
4
|
+
exported from the top-level :mod:`learning_commons_evaluators` package, not
|
|
5
|
+
from here. Import evaluator classes directly from this sub-package only when
|
|
6
|
+
you want to be explicit about the source.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from learning_commons_evaluators.evaluators.base import BaseEvaluator, InputT, OutputT
|
|
10
|
+
from learning_commons_evaluators.evaluators.conventionality import (
|
|
11
|
+
ConventionalityEvaluationInput,
|
|
12
|
+
ConventionalityEvaluator,
|
|
13
|
+
)
|
|
14
|
+
from learning_commons_evaluators.evaluators.vocabulary import (
|
|
15
|
+
VocabularyEvaluationInput,
|
|
16
|
+
VocabularyEvaluator,
|
|
17
|
+
)
|
|
18
|
+
from learning_commons_evaluators.schemas.vocabulary import normalize_complexity_output
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseEvaluator",
|
|
22
|
+
"ConventionalityEvaluationInput",
|
|
23
|
+
"ConventionalityEvaluator",
|
|
24
|
+
"InputT",
|
|
25
|
+
"OutputT",
|
|
26
|
+
"VocabularyEvaluationInput",
|
|
27
|
+
"VocabularyEvaluator",
|
|
28
|
+
"normalize_complexity_output",
|
|
29
|
+
]
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Base evaluator class. Config types live in schemas.config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any, Generic, TypeVar, overload
|
|
10
|
+
|
|
11
|
+
from langchain_core.exceptions import OutputParserException
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
14
|
+
|
|
15
|
+
from learning_commons_evaluators.providers import (
|
|
16
|
+
create_provider,
|
|
17
|
+
token_usage_from_aimessage,
|
|
18
|
+
)
|
|
19
|
+
from learning_commons_evaluators.schemas.config import (
|
|
20
|
+
EvaluationSettings,
|
|
21
|
+
EvaluatorConfig,
|
|
22
|
+
PromptSettings,
|
|
23
|
+
)
|
|
24
|
+
from learning_commons_evaluators.schemas.errors import (
|
|
25
|
+
EvaluatorError,
|
|
26
|
+
OutputValidationError,
|
|
27
|
+
format_error_for_metadata,
|
|
28
|
+
sanitize_pydantic_errors,
|
|
29
|
+
wrap_provider_error,
|
|
30
|
+
)
|
|
31
|
+
from learning_commons_evaluators.schemas.evaluator import (
|
|
32
|
+
EvaluationInput,
|
|
33
|
+
EvaluationResult,
|
|
34
|
+
)
|
|
35
|
+
from learning_commons_evaluators.schemas.metadata import (
|
|
36
|
+
PROMPT_STEP_EXTRA_PROMPT_SETTINGS,
|
|
37
|
+
PROMPT_STEP_EXTRA_TOKEN_USAGE,
|
|
38
|
+
EvaluationMetadata,
|
|
39
|
+
EvaluatorMetadata,
|
|
40
|
+
Status,
|
|
41
|
+
StepMetadata,
|
|
42
|
+
TokenUsage,
|
|
43
|
+
prompt_settings_to_extras_value,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
InputT = TypeVar("InputT", bound=EvaluationInput)
|
|
47
|
+
OutputT = TypeVar("OutputT", bound=EvaluationResult)
|
|
48
|
+
SettingsT = TypeVar("SettingsT", bound=EvaluationSettings)
|
|
49
|
+
StepResultT = TypeVar("StepResultT")
|
|
50
|
+
ParsedT = TypeVar("ParsedT", bound=BaseModel)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BaseEvaluator(ABC, Generic[InputT, OutputT, SettingsT]):
|
|
54
|
+
"""
|
|
55
|
+
Abstract base class for all evaluators.
|
|
56
|
+
Subclasses must set ``metadata``, ``default_evaluation_settings``, and implement
|
|
57
|
+
:meth:`evaluate_impl`.
|
|
58
|
+
|
|
59
|
+
Callers run an evaluation with :meth:`evaluate` (async: ``await evaluator.evaluate(...)``)
|
|
60
|
+
or :meth:`evaluate_sync` from synchronous code (uses :func:`asyncio.run`). If a loop is
|
|
61
|
+
already running on this thread, :meth:`evaluate_sync` raises :exc:`RuntimeError`; use
|
|
62
|
+
``await`` :meth:`evaluate` in that case.
|
|
63
|
+
|
|
64
|
+
Pass ``default_evaluation_settings`` at construction to override the class-level
|
|
65
|
+
defaults for that instance (used when :meth:`evaluate` is called without
|
|
66
|
+
``evaluation_settings``).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
config: EvaluatorConfig
|
|
70
|
+
metadata: EvaluatorMetadata
|
|
71
|
+
default_evaluation_settings: SettingsT
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
config: EvaluatorConfig,
|
|
76
|
+
*,
|
|
77
|
+
default_evaluation_settings: SettingsT | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.config = config
|
|
80
|
+
if default_evaluation_settings is not None:
|
|
81
|
+
self.default_evaluation_settings = default_evaluation_settings
|
|
82
|
+
# TODO: validate config
|
|
83
|
+
|
|
84
|
+
async def evaluate(
|
|
85
|
+
self,
|
|
86
|
+
input: InputT,
|
|
87
|
+
evaluation_settings: SettingsT | None = None,
|
|
88
|
+
) -> OutputT:
|
|
89
|
+
"""Evaluate the given input and return a structured result.
|
|
90
|
+
|
|
91
|
+
Validates the input, delegates to :meth:`evaluate_impl`, records timing
|
|
92
|
+
and status on the returned metadata, and logs start/end events via the
|
|
93
|
+
configured logger. If ``evaluation_settings`` is ``None``, a deep copy of
|
|
94
|
+
the instance's :attr:`default_evaluation_settings` is used (from the
|
|
95
|
+
constructor keyword when given, otherwise the subclass class attribute).
|
|
96
|
+
|
|
97
|
+
The ``finally`` block always runs so ``processing_time_ms`` and the end log
|
|
98
|
+
line are emitted even when validation or the implementation raises; telemetry
|
|
99
|
+
hooks remain TODOs here until wired.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
input: Typed input for this evaluator.
|
|
103
|
+
evaluation_settings: Optional override for evaluation settings.
|
|
104
|
+
Defaults to :attr:`default_evaluation_settings` (constructor or class).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A typed result whose ``metadata.status`` is
|
|
108
|
+
:attr:`~learning_commons_evaluators.schemas.metadata.Status.succeeded`
|
|
109
|
+
on success.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
InputValidationError: Input fails validation.
|
|
113
|
+
ConfigurationError: No provider config for the required LLM provider.
|
|
114
|
+
APIError (or subclasses): The LLM API call failed.
|
|
115
|
+
|
|
116
|
+
On failure, :attr:`~learning_commons_evaluators.schemas.metadata.EvaluationMetadata.status`
|
|
117
|
+
and ``error_details`` are set on the in-memory metadata object and included in the
|
|
118
|
+
evaluation end log record; no result object is returned because this method re-raises.
|
|
119
|
+
"""
|
|
120
|
+
if evaluation_settings is None:
|
|
121
|
+
evaluation_settings = self.default_evaluation_settings.model_copy(deep=True)
|
|
122
|
+
start = time.perf_counter()
|
|
123
|
+
evaluation_metadata = EvaluationMetadata(
|
|
124
|
+
evaluator_metadata=self.metadata,
|
|
125
|
+
evaluation_settings=evaluation_settings,
|
|
126
|
+
input_metadata=input.input_metadata(),
|
|
127
|
+
)
|
|
128
|
+
self.config.logger.info(
|
|
129
|
+
"evaluation start",
|
|
130
|
+
extra={"evaluation_metadata": evaluation_metadata},
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
input.validate()
|
|
134
|
+
result = await self.evaluate_impl(input, evaluation_settings, evaluation_metadata)
|
|
135
|
+
evaluation_metadata.status = Status.succeeded
|
|
136
|
+
result.metadata = evaluation_metadata
|
|
137
|
+
return result
|
|
138
|
+
except Exception as e:
|
|
139
|
+
evaluation_metadata.status = Status.failed
|
|
140
|
+
evaluation_metadata.error_details = format_error_for_metadata(e)
|
|
141
|
+
raise
|
|
142
|
+
finally:
|
|
143
|
+
evaluation_metadata.processing_time_ms = (time.perf_counter() - start) * 1000
|
|
144
|
+
self.config.logger.info(
|
|
145
|
+
"evaluation end",
|
|
146
|
+
extra={"evaluation_metadata": evaluation_metadata},
|
|
147
|
+
)
|
|
148
|
+
# TODO: add full input to telemetry if enabled
|
|
149
|
+
# TODO: send_telemetry(evaluation_metadata)
|
|
150
|
+
|
|
151
|
+
def evaluate_sync(
|
|
152
|
+
self,
|
|
153
|
+
input: InputT,
|
|
154
|
+
evaluation_settings: SettingsT | None = None,
|
|
155
|
+
) -> OutputT:
|
|
156
|
+
"""Run :meth:`evaluate` to completion from synchronous code.
|
|
157
|
+
|
|
158
|
+
This is a thin wrapper around :func:`asyncio.run` over :meth:`evaluate`. Use it
|
|
159
|
+
from scripts, REPLs, or tests that are not already inside an asyncio event loop.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
input: Same as :meth:`evaluate`.
|
|
163
|
+
evaluation_settings: Same as :meth:`evaluate`.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Same typed result as :meth:`evaluate` on success.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
Same exceptions as :meth:`evaluate`.
|
|
170
|
+
RuntimeError: If this thread already has a running asyncio event loop; use
|
|
171
|
+
``await evaluator.evaluate(...)`` instead of :meth:`evaluate_sync`.
|
|
172
|
+
|
|
173
|
+
Note:
|
|
174
|
+
Do not call this method when a running event loop is active (for example
|
|
175
|
+
from inside an ``async def`` without nesting another ``asyncio.run``); prefer
|
|
176
|
+
awaiting :meth:`evaluate` instead.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
asyncio.get_running_loop()
|
|
180
|
+
except RuntimeError:
|
|
181
|
+
pass
|
|
182
|
+
else:
|
|
183
|
+
raise RuntimeError(
|
|
184
|
+
"evaluate_sync() cannot be used while an asyncio event loop is running in "
|
|
185
|
+
"this thread; use await evaluator.evaluate(...) from async code instead."
|
|
186
|
+
) from None
|
|
187
|
+
return asyncio.run(self.evaluate(input, evaluation_settings))
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
async def evaluate_impl(
|
|
191
|
+
self,
|
|
192
|
+
input: InputT,
|
|
193
|
+
evaluation_settings: SettingsT,
|
|
194
|
+
evaluation_metadata: EvaluationMetadata,
|
|
195
|
+
) -> OutputT:
|
|
196
|
+
"""Implement the evaluator-specific logic for one run.
|
|
197
|
+
|
|
198
|
+
Subclasses perform prompt steps (often via :meth:`execute_prompt_chain_step`),
|
|
199
|
+
assemble the typed result, and return it. The base :meth:`evaluate` assigns
|
|
200
|
+
``evaluation_metadata`` onto ``result.metadata`` after a successful return.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
input: Validated evaluation input.
|
|
204
|
+
evaluation_settings: Resolved settings for this run (already deep-copied
|
|
205
|
+
when the caller omitted overrides).
|
|
206
|
+
evaluation_metadata: Run metadata; populate ``step_details`` and related
|
|
207
|
+
fields as steps execute.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
A fully constructed result object (``metadata`` may still point at the
|
|
211
|
+
same ``evaluation_metadata`` instance; the base layer sets status and timing).
|
|
212
|
+
"""
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
async def execute_step(
|
|
216
|
+
self,
|
|
217
|
+
step_name: str,
|
|
218
|
+
evaluation_metadata: EvaluationMetadata,
|
|
219
|
+
implementation_function: Callable[[], Awaitable[StepResultT]],
|
|
220
|
+
*,
|
|
221
|
+
extras: dict[str, Any] | None = None,
|
|
222
|
+
) -> StepResultT:
|
|
223
|
+
"""Await ``implementation_function`` and record step metadata on ``evaluation_metadata``.
|
|
224
|
+
|
|
225
|
+
``step_name`` is always the step id. Optional ``extras`` is copied into
|
|
226
|
+
:attr:`StepMetadata.extras` (merged with any updates made during the step, e.g. token usage).
|
|
227
|
+
|
|
228
|
+
``implementation_function`` must be a zero-argument callable that returns an
|
|
229
|
+
awaitable (typically an ``async def`` with no parameters, or a lambda that
|
|
230
|
+
returns one). The awaited value may be any type (e.g. a Pydantic model, a plain
|
|
231
|
+
``str``, or ``None``); the same type is returned to the caller.
|
|
232
|
+
"""
|
|
233
|
+
start = time.perf_counter()
|
|
234
|
+
step_extras = dict(extras) if extras is not None else {}
|
|
235
|
+
step_metadata = StepMetadata(step_id=step_name, extras=step_extras)
|
|
236
|
+
self.config.logger.info("step start", extra={"step_metadata": step_metadata})
|
|
237
|
+
try:
|
|
238
|
+
result = await implementation_function()
|
|
239
|
+
step_metadata.status = Status.succeeded
|
|
240
|
+
return result
|
|
241
|
+
except Exception as e:
|
|
242
|
+
step_metadata.status = Status.failed
|
|
243
|
+
step_metadata.error_details = format_error_for_metadata(e)
|
|
244
|
+
raise
|
|
245
|
+
finally:
|
|
246
|
+
step_metadata.processing_time_ms = (time.perf_counter() - start) * 1000
|
|
247
|
+
self.config.logger.info("step end", extra={"step_metadata": step_metadata})
|
|
248
|
+
evaluation_metadata.step_details[step_name] = step_metadata
|
|
249
|
+
|
|
250
|
+
@overload
|
|
251
|
+
async def execute_prompt_chain_step(
|
|
252
|
+
self,
|
|
253
|
+
step_name: str,
|
|
254
|
+
prompt_settings: PromptSettings,
|
|
255
|
+
evaluation_metadata: EvaluationMetadata,
|
|
256
|
+
template: Any,
|
|
257
|
+
chain_inputs: dict[str, Any],
|
|
258
|
+
parser_output_type: None = None,
|
|
259
|
+
) -> str: ...
|
|
260
|
+
|
|
261
|
+
@overload
|
|
262
|
+
async def execute_prompt_chain_step(
|
|
263
|
+
self,
|
|
264
|
+
step_name: str,
|
|
265
|
+
prompt_settings: PromptSettings,
|
|
266
|
+
evaluation_metadata: EvaluationMetadata,
|
|
267
|
+
template: Any,
|
|
268
|
+
chain_inputs: dict[str, Any],
|
|
269
|
+
parser_output_type: type[ParsedT],
|
|
270
|
+
json_dict_normalizer: Callable[[dict], dict] | None = None,
|
|
271
|
+
) -> ParsedT: ...
|
|
272
|
+
|
|
273
|
+
async def execute_prompt_chain_step(
|
|
274
|
+
self,
|
|
275
|
+
step_name: str,
|
|
276
|
+
prompt_settings: PromptSettings,
|
|
277
|
+
evaluation_metadata: EvaluationMetadata,
|
|
278
|
+
template: Any,
|
|
279
|
+
chain_inputs: dict[str, Any],
|
|
280
|
+
parser_output_type: type[BaseModel] | None = None,
|
|
281
|
+
json_dict_normalizer: Callable[[dict], dict] | None = None,
|
|
282
|
+
) -> BaseModel | str:
|
|
283
|
+
"""Run a prompt chain (template | LLM) with ``ainvoke``, record metadata, and return the result.
|
|
284
|
+
|
|
285
|
+
The LangChain runnable ``template | provider`` is invoked asynchronously via
|
|
286
|
+
:meth:`~langchain_core.runnables.base.Runnable.ainvoke`. When ``parser_output_type``
|
|
287
|
+
is a Pydantic model class, the LLM response is parsed as JSON and returned as an
|
|
288
|
+
instance of that class. When it is ``None`` (the default), the raw response content
|
|
289
|
+
is returned as a plain ``str`` (no JSON parser) — use that for steps that produce
|
|
290
|
+
unstructured prose (e.g. a background-knowledge assumption).
|
|
291
|
+
|
|
292
|
+
Provider config (e.g. API key) is resolved from ``self.config`` by
|
|
293
|
+
``prompt_settings.provider_type``.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
step_name: Identifier for this step in ``evaluation_metadata.step_details``.
|
|
297
|
+
prompt_settings: Provider type, model, and temperature for the LLM call.
|
|
298
|
+
evaluation_metadata: Metadata for the full evaluation; step metadata and
|
|
299
|
+
token usage are updated in place.
|
|
300
|
+
template: A LangChain prompt template.
|
|
301
|
+
chain_inputs: Variables to format the template and invoke the chain.
|
|
302
|
+
parser_output_type: Pydantic model class for JSON parsing, or ``None``
|
|
303
|
+
to return the raw text response.
|
|
304
|
+
json_dict_normalizer: When set with ``parser_output_type``, parse the
|
|
305
|
+
model response as JSON into a plain dict (no Pydantic parse),
|
|
306
|
+
apply this function (e.g. notebook-style ``normalize_complexity_output``),
|
|
307
|
+
then validate with ``parser_output_type``. Format instructions for the
|
|
308
|
+
prompt should still be built from the same ``parser_output_type`` via
|
|
309
|
+
:class:`~langchain_core.output_parsers.JsonOutputParser`.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Parsed instance of ``parser_output_type`` when it is a model class; plain
|
|
313
|
+
``str`` when ``parser_output_type`` is omitted or ``None``.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
ConfigurationError: No provider config for ``prompt_settings.provider_type``.
|
|
317
|
+
OutputValidationError: The LLM response didn't satisfy the expected
|
|
318
|
+
output schema — invalid JSON (LangChain ``OutputParserException``),
|
|
319
|
+
JSON that didn't match the Pydantic model (``pydantic.ValidationError``),
|
|
320
|
+
a non-object JSON value when using ``json_dict_normalizer``, or
|
|
321
|
+
``TypeError`` / ``ValueError`` from ``json_dict_normalizer`` itself.
|
|
322
|
+
The original exception is reachable via ``__cause__``.
|
|
323
|
+
APIError (or other ``EvaluatorError`` subclasses):
|
|
324
|
+
:func:`~learning_commons_evaluators.schemas.errors.wrap_provider_error`
|
|
325
|
+
output for LangChain or HTTP failures from the LLM provider.
|
|
326
|
+
ValueError: If ``json_dict_normalizer`` is set but ``parser_output_type`` is omitted.
|
|
327
|
+
"""
|
|
328
|
+
if json_dict_normalizer is not None and parser_output_type is None:
|
|
329
|
+
raise ValueError("json_dict_normalizer requires parser_output_type to be set")
|
|
330
|
+
# Populated after a successful LLM invoke so we can attach usage even if parsing fails.
|
|
331
|
+
token_usage: TokenUsage | None = None
|
|
332
|
+
|
|
333
|
+
async def _run_chain() -> BaseModel | str:
|
|
334
|
+
nonlocal token_usage
|
|
335
|
+
try:
|
|
336
|
+
provider = create_provider(prompt_settings, self.config)
|
|
337
|
+
llm_chain: Any = template | provider
|
|
338
|
+
ai_message = await llm_chain.ainvoke(chain_inputs)
|
|
339
|
+
token_usage = token_usage_from_aimessage(ai_message, prompt_settings)
|
|
340
|
+
if parser_output_type is None:
|
|
341
|
+
return str(ai_message.content)
|
|
342
|
+
from langchain_core.output_parsers.json import JsonOutputParser
|
|
343
|
+
|
|
344
|
+
if json_dict_normalizer is not None:
|
|
345
|
+
loose = JsonOutputParser()
|
|
346
|
+
parsed_dict = await loose.ainvoke(ai_message)
|
|
347
|
+
if not isinstance(parsed_dict, dict):
|
|
348
|
+
# JSON parsed cleanly but the top-level value isn't an object
|
|
349
|
+
# (e.g. the LLM returned a JSON array or scalar). That's an
|
|
350
|
+
# output-shape failure, not a parse failure — surface it as
|
|
351
|
+
# OutputValidationError so callers can treat it consistently
|
|
352
|
+
# with schema-mismatch errors, and avoid the TypeError that
|
|
353
|
+
# ``dict(parsed_dict)`` would raise on a non-dict.
|
|
354
|
+
raise OutputValidationError(
|
|
355
|
+
"Model output is not a JSON object",
|
|
356
|
+
provider=prompt_settings.provider_type,
|
|
357
|
+
model=prompt_settings.model,
|
|
358
|
+
)
|
|
359
|
+
try:
|
|
360
|
+
normalized = json_dict_normalizer(parsed_dict)
|
|
361
|
+
except (TypeError, ValueError) as norm_err:
|
|
362
|
+
raise OutputValidationError(
|
|
363
|
+
"Model output could not be normalized before validation",
|
|
364
|
+
provider=prompt_settings.provider_type,
|
|
365
|
+
model=prompt_settings.model,
|
|
366
|
+
) from norm_err
|
|
367
|
+
return parser_output_type.model_validate(normalized)
|
|
368
|
+
|
|
369
|
+
parser = JsonOutputParser(pydantic_object=parser_output_type)
|
|
370
|
+
raw = await parser.ainvoke(ai_message)
|
|
371
|
+
if isinstance(raw, parser_output_type):
|
|
372
|
+
return raw
|
|
373
|
+
return parser_output_type.model_validate(raw)
|
|
374
|
+
except EvaluatorError:
|
|
375
|
+
raise
|
|
376
|
+
except (PydanticValidationError, OutputParserException) as e:
|
|
377
|
+
# The provider returned a response that didn't match the expected schema.
|
|
378
|
+
# This covers both Pydantic schema-validation failures and LangChain
|
|
379
|
+
# JSON-parse failures (``OutputParserException``). Wrap so callers can
|
|
380
|
+
# discriminate output-parse failures from other API errors and so the
|
|
381
|
+
# message that lands in ``EvaluationMetadata.error_details`` stays
|
|
382
|
+
# sanitized — the original error (which may include LLM output snippets)
|
|
383
|
+
# is reachable via ``__cause__`` for debugging.
|
|
384
|
+
validation_errors = (
|
|
385
|
+
sanitize_pydantic_errors(e.errors())
|
|
386
|
+
if isinstance(e, PydanticValidationError)
|
|
387
|
+
else None
|
|
388
|
+
)
|
|
389
|
+
raise OutputValidationError(
|
|
390
|
+
provider=prompt_settings.provider_type,
|
|
391
|
+
model=prompt_settings.model,
|
|
392
|
+
validation_errors=validation_errors,
|
|
393
|
+
) from e
|
|
394
|
+
except (KeyboardInterrupt, SystemExit):
|
|
395
|
+
raise
|
|
396
|
+
except Exception as e:
|
|
397
|
+
raise wrap_provider_error(
|
|
398
|
+
e,
|
|
399
|
+
provider=prompt_settings.provider_type,
|
|
400
|
+
model=prompt_settings.model,
|
|
401
|
+
) from e
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
return await self.execute_step(
|
|
405
|
+
step_name,
|
|
406
|
+
evaluation_metadata,
|
|
407
|
+
_run_chain,
|
|
408
|
+
extras={
|
|
409
|
+
PROMPT_STEP_EXTRA_PROMPT_SETTINGS: prompt_settings_to_extras_value(
|
|
410
|
+
prompt_settings
|
|
411
|
+
),
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
finally:
|
|
415
|
+
if token_usage is not None:
|
|
416
|
+
self.update_total_token_usage(token_usage, evaluation_metadata)
|
|
417
|
+
step = evaluation_metadata.step_details.get(step_name)
|
|
418
|
+
if step is not None:
|
|
419
|
+
step.extras[PROMPT_STEP_EXTRA_TOKEN_USAGE] = token_usage.model_dump(mode="json")
|
|
420
|
+
|
|
421
|
+
def update_total_token_usage(
|
|
422
|
+
self, token_usage: TokenUsage, evaluation_metadata: EvaluationMetadata
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Record token usage for the current run."""
|
|
425
|
+
if token_usage.provider_type not in evaluation_metadata.total_token_usage:
|
|
426
|
+
evaluation_metadata.total_token_usage[token_usage.provider_type] = token_usage
|
|
427
|
+
else:
|
|
428
|
+
current_total = evaluation_metadata.total_token_usage[token_usage.provider_type]
|
|
429
|
+
evaluation_metadata.total_token_usage[token_usage.provider_type] = TokenUsage(
|
|
430
|
+
provider_type=current_total.provider_type,
|
|
431
|
+
model=current_total.model,
|
|
432
|
+
input_tokens=current_total.input_tokens + token_usage.input_tokens,
|
|
433
|
+
output_tokens=current_total.output_tokens + token_usage.output_tokens,
|
|
434
|
+
)
|