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,117 @@
|
|
|
1
|
+
"""Conventionality evaluator: evaluates text for conventionality of language."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
import textstat # type: ignore[import-untyped]
|
|
8
|
+
from langchain_core.output_parsers import JsonOutputParser
|
|
9
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from learning_commons_evaluators.evaluators.base import BaseEvaluator
|
|
13
|
+
from learning_commons_evaluators.schemas.common_inputs import (
|
|
14
|
+
GradeInputField,
|
|
15
|
+
TextInputField,
|
|
16
|
+
)
|
|
17
|
+
from learning_commons_evaluators.schemas.conventionality import (
|
|
18
|
+
ConventionalityEvaluationSettings,
|
|
19
|
+
ConventionalityOutput,
|
|
20
|
+
)
|
|
21
|
+
from learning_commons_evaluators.schemas.evaluator import (
|
|
22
|
+
EvaluationExplanation,
|
|
23
|
+
EvaluationInput,
|
|
24
|
+
)
|
|
25
|
+
from learning_commons_evaluators.schemas.metadata import (
|
|
26
|
+
EvaluationMetadata,
|
|
27
|
+
EvaluatorMetadata,
|
|
28
|
+
)
|
|
29
|
+
from learning_commons_evaluators.schemas.text_complexity import (
|
|
30
|
+
TextComplexityAnswer,
|
|
31
|
+
TextComplexityResult,
|
|
32
|
+
)
|
|
33
|
+
from learning_commons_evaluators.settings._generated_conventionality_settings import (
|
|
34
|
+
CONFIG as _CONVENTIONALITY_CONFIG,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_INPUT_SETTINGS = _CONVENTIONALITY_CONFIG.evaluator_metadata.inputs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ConventionalityEvaluationInput(EvaluationInput):
|
|
41
|
+
"""Input for a conventionality evaluation.
|
|
42
|
+
|
|
43
|
+
Constraints (min/max text length, allowed grades) are sourced from
|
|
44
|
+
``[[evaluator_metadata.inputs]]`` in evaluator settings and applied
|
|
45
|
+
automatically — callers supply raw values, not field objects.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
inp = ConventionalityEvaluationInput(text="The quick brown fox...", grade=5)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
_input_settings: ClassVar[dict] = _INPUT_SETTINGS
|
|
53
|
+
|
|
54
|
+
text: TextInputField = Field(description="The text to evaluate.")
|
|
55
|
+
grade: GradeInputField = Field(description="The grade level of the text.")
|
|
56
|
+
|
|
57
|
+
def __init__(self, *, text: str, grade: int, **kwargs):
|
|
58
|
+
super().__init__(text=text, grade=grade, **kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConventionalityEvaluator(
|
|
62
|
+
BaseEvaluator[
|
|
63
|
+
ConventionalityEvaluationInput,
|
|
64
|
+
TextComplexityResult,
|
|
65
|
+
ConventionalityEvaluationSettings,
|
|
66
|
+
]
|
|
67
|
+
):
|
|
68
|
+
"""Evaluates text for conventionality (idioms, metaphors, implied meaning) relative to grade."""
|
|
69
|
+
|
|
70
|
+
metadata: EvaluatorMetadata = _CONVENTIONALITY_CONFIG.evaluator_metadata
|
|
71
|
+
default_evaluation_settings: ConventionalityEvaluationSettings = (
|
|
72
|
+
_CONVENTIONALITY_CONFIG.evaluation_settings
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def evaluate_impl(
|
|
76
|
+
self,
|
|
77
|
+
input: ConventionalityEvaluationInput,
|
|
78
|
+
evaluation_settings: ConventionalityEvaluationSettings,
|
|
79
|
+
evaluation_metadata: EvaluationMetadata,
|
|
80
|
+
) -> TextComplexityResult:
|
|
81
|
+
"""Run conventionality evaluation. Returns TextComplexityResult with answer, explanation, metadata."""
|
|
82
|
+
step_prompt_settings = evaluation_settings.prompt_settings_step_conventionality_evaluation
|
|
83
|
+
|
|
84
|
+
fk_score = round(textstat.flesch_kincaid_grade(input.text.value), 2)
|
|
85
|
+
prompt_inputs = input.input_values()
|
|
86
|
+
prompt_inputs["fk_score"] = fk_score
|
|
87
|
+
|
|
88
|
+
parser = JsonOutputParser(pydantic_object=ConventionalityOutput)
|
|
89
|
+
prompts = _CONVENTIONALITY_CONFIG.prompts
|
|
90
|
+
template = ChatPromptTemplate.from_messages(
|
|
91
|
+
[
|
|
92
|
+
("system", prompts["system_prompt"]),
|
|
93
|
+
("human", prompts["human_prompt"]),
|
|
94
|
+
]
|
|
95
|
+
).partial(format_instructions=parser.get_format_instructions())
|
|
96
|
+
conventionality_output = await self.execute_prompt_chain_step(
|
|
97
|
+
step_name="conventionality_evaluation",
|
|
98
|
+
prompt_settings=step_prompt_settings,
|
|
99
|
+
evaluation_metadata=evaluation_metadata,
|
|
100
|
+
template=template,
|
|
101
|
+
chain_inputs=prompt_inputs,
|
|
102
|
+
parser_output_type=ConventionalityOutput,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
answer = TextComplexityAnswer.from_score(conventionality_output.complexity_score)
|
|
106
|
+
return TextComplexityResult(
|
|
107
|
+
answer=answer,
|
|
108
|
+
explanation=EvaluationExplanation(
|
|
109
|
+
summary=conventionality_output.reasoning,
|
|
110
|
+
details={
|
|
111
|
+
"conventionality_features": conventionality_output.conventionality_features,
|
|
112
|
+
"grade_context": conventionality_output.grade_context,
|
|
113
|
+
"instructional_insights": conventionality_output.instructional_insights,
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
metadata=evaluation_metadata,
|
|
117
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Vocabulary evaluator: evaluates text for vocabulary complexity relative to grade level."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
|
+
|
|
7
|
+
import textstat # type: ignore[import-untyped]
|
|
8
|
+
from langchain_core.output_parsers import JsonOutputParser
|
|
9
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from learning_commons_evaluators.evaluators.base import BaseEvaluator
|
|
13
|
+
from learning_commons_evaluators.schemas.common_inputs import (
|
|
14
|
+
GradeInputField,
|
|
15
|
+
TextInputField,
|
|
16
|
+
)
|
|
17
|
+
from learning_commons_evaluators.schemas.config import PromptSettings
|
|
18
|
+
from learning_commons_evaluators.schemas.evaluator import (
|
|
19
|
+
EvaluationExplanation,
|
|
20
|
+
EvaluationInput,
|
|
21
|
+
)
|
|
22
|
+
from learning_commons_evaluators.schemas.metadata import (
|
|
23
|
+
EvaluationMetadata,
|
|
24
|
+
EvaluatorMetadata,
|
|
25
|
+
)
|
|
26
|
+
from learning_commons_evaluators.schemas.text_complexity import (
|
|
27
|
+
TextComplexityAnswer,
|
|
28
|
+
TextComplexityResult,
|
|
29
|
+
)
|
|
30
|
+
from learning_commons_evaluators.schemas.vocabulary import (
|
|
31
|
+
VocabularyComplexityOutput,
|
|
32
|
+
VocabularyEvaluationSettings,
|
|
33
|
+
normalize_complexity_output,
|
|
34
|
+
)
|
|
35
|
+
from learning_commons_evaluators.settings._generated_vocabulary_settings import (
|
|
36
|
+
CONFIG as _VOCABULARY_CONFIG,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_INPUT_SETTINGS = _VOCABULARY_CONFIG.evaluator_metadata.inputs
|
|
40
|
+
|
|
41
|
+
# Grades 3–4 use the Gemini-based rubric prompt; grades 5–12 use the GPT-based annotation guide prompt.
|
|
42
|
+
_GRADES_3_4 = frozenset({3, 4})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VocabularyEvaluationInput(EvaluationInput):
|
|
46
|
+
"""Input for a vocabulary evaluation.
|
|
47
|
+
|
|
48
|
+
Constraints (allowed grade levels) are sourced from
|
|
49
|
+
``[[evaluator_metadata.inputs]]`` in evaluator settings and applied
|
|
50
|
+
automatically — callers supply raw values, not field objects.
|
|
51
|
+
|
|
52
|
+
Example::
|
|
53
|
+
|
|
54
|
+
inp = VocabularyEvaluationInput(text="The quick brown fox...", grade=5)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_input_settings: ClassVar[dict] = _INPUT_SETTINGS
|
|
58
|
+
|
|
59
|
+
text: TextInputField = Field(description="The text to evaluate.")
|
|
60
|
+
grade: GradeInputField = Field(description="The grade level of the text (3–12).")
|
|
61
|
+
|
|
62
|
+
def __init__(self, *, text: str, grade: int, **kwargs):
|
|
63
|
+
super().__init__(text=text, grade=grade, **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class VocabularyEvaluator(
|
|
67
|
+
BaseEvaluator[VocabularyEvaluationInput, TextComplexityResult, VocabularyEvaluationSettings]
|
|
68
|
+
):
|
|
69
|
+
"""Evaluates text for vocabulary complexity relative to the target grade level.
|
|
70
|
+
|
|
71
|
+
The evaluation runs in two steps:
|
|
72
|
+
1. **Background knowledge** – the model generates a short assumption about what
|
|
73
|
+
students at the target grade already know about the text's topic. This
|
|
74
|
+
provides context that keeps the complexity rating from penalising familiar
|
|
75
|
+
domain words.
|
|
76
|
+
2. **Vocabulary complexity** – a grade-specific prompt + model produces JSON
|
|
77
|
+
matching the notebook ``Output`` shape. Raw JSON is passed through
|
|
78
|
+
:func:`~learning_commons_evaluators.schemas.vocabulary.normalize_complexity_output`
|
|
79
|
+
(same behaviour as the notebook), then validated. Grades 3–4 use Gemini;
|
|
80
|
+
grades 5–12 use GPT. Scores are normalised to :class:`TextComplexityAnswer`.
|
|
81
|
+
|
|
82
|
+
Supported grades: 3–12.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
metadata: EvaluatorMetadata = _VOCABULARY_CONFIG.evaluator_metadata
|
|
86
|
+
default_evaluation_settings: VocabularyEvaluationSettings = (
|
|
87
|
+
_VOCABULARY_CONFIG.evaluation_settings
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def evaluate_impl(
|
|
91
|
+
self,
|
|
92
|
+
input: VocabularyEvaluationInput,
|
|
93
|
+
evaluation_settings: VocabularyEvaluationSettings,
|
|
94
|
+
evaluation_metadata: EvaluationMetadata,
|
|
95
|
+
) -> TextComplexityResult:
|
|
96
|
+
"""Run the two-step vocabulary evaluation and return a TextComplexityResult.
|
|
97
|
+
|
|
98
|
+
Grade validation is handled by the framework before this method is called:
|
|
99
|
+
``VocabularyEvaluationInput`` automatically constrains ``grade`` to the
|
|
100
|
+
evaluator's ``allowed_grades`` from settings (3–12), so
|
|
101
|
+
``BaseEvaluator.evaluate`` / ``evaluate_sync`` raise before reaching here for unsupported grades.
|
|
102
|
+
"""
|
|
103
|
+
ps_bk = evaluation_settings.prompt_settings_step_background_knowledge
|
|
104
|
+
ps_34 = evaluation_settings.prompt_settings_step_vocab_grades_3_4
|
|
105
|
+
ps_og = evaluation_settings.prompt_settings_step_vocab_other_grades
|
|
106
|
+
|
|
107
|
+
grade = input.grade.value
|
|
108
|
+
text = input.text.value
|
|
109
|
+
fk_score = round(textstat.flesch_kincaid_grade(text), 2)
|
|
110
|
+
prompts = _VOCABULARY_CONFIG.prompts
|
|
111
|
+
|
|
112
|
+
# ── Step 1: background knowledge ──────────────────────────────────────
|
|
113
|
+
bk_template = ChatPromptTemplate.from_messages(
|
|
114
|
+
[("human", prompts["background_knowledge_prompt"])]
|
|
115
|
+
)
|
|
116
|
+
background_knowledge: str = await self.execute_prompt_chain_step(
|
|
117
|
+
step_name="background_knowledge",
|
|
118
|
+
prompt_settings=ps_bk,
|
|
119
|
+
evaluation_metadata=evaluation_metadata,
|
|
120
|
+
template=bk_template,
|
|
121
|
+
chain_inputs={"text": text, "grade": grade},
|
|
122
|
+
parser_output_type=None,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ── Step 2: vocabulary complexity (grade-specific prompts, shared Output shape)
|
|
126
|
+
chain_inputs: dict[str, Any] = {
|
|
127
|
+
"text": input.text.value,
|
|
128
|
+
"student_grade_level": grade,
|
|
129
|
+
"student_background_knowledge": background_knowledge,
|
|
130
|
+
}
|
|
131
|
+
if grade in _GRADES_3_4:
|
|
132
|
+
chain_inputs["fk_level"] = fk_score
|
|
133
|
+
answer, explanation = await self._run_vocab_complexity_chain(
|
|
134
|
+
chain_inputs=chain_inputs,
|
|
135
|
+
evaluation_metadata=evaluation_metadata,
|
|
136
|
+
prompt_settings_vocab=ps_34,
|
|
137
|
+
system_prompt=prompts["vocab_grades_3_4_system_prompt"],
|
|
138
|
+
user_prompt_template=prompts["vocab_grades_3_4_user_prompt"],
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
answer, explanation = await self._run_vocab_complexity_chain(
|
|
142
|
+
chain_inputs=chain_inputs,
|
|
143
|
+
evaluation_metadata=evaluation_metadata,
|
|
144
|
+
prompt_settings_vocab=ps_og,
|
|
145
|
+
system_prompt=prompts["vocab_other_grades_system_prompt"],
|
|
146
|
+
user_prompt_template=prompts["vocab_other_grades_user_prompt"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return TextComplexityResult(
|
|
150
|
+
answer=answer,
|
|
151
|
+
explanation=explanation,
|
|
152
|
+
metadata=evaluation_metadata,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def _run_vocab_complexity_chain(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
chain_inputs: dict[str, Any],
|
|
159
|
+
evaluation_metadata: EvaluationMetadata,
|
|
160
|
+
prompt_settings_vocab: PromptSettings,
|
|
161
|
+
system_prompt: str,
|
|
162
|
+
user_prompt_template: str,
|
|
163
|
+
) -> tuple[TextComplexityAnswer, EvaluationExplanation]:
|
|
164
|
+
parser = JsonOutputParser(pydantic_object=VocabularyComplexityOutput)
|
|
165
|
+
template = ChatPromptTemplate.from_messages(
|
|
166
|
+
[
|
|
167
|
+
("system", system_prompt),
|
|
168
|
+
("human", user_prompt_template),
|
|
169
|
+
]
|
|
170
|
+
).partial(format_instructions=parser.get_format_instructions())
|
|
171
|
+
|
|
172
|
+
output = await self.execute_prompt_chain_step(
|
|
173
|
+
step_name="vocab_complexity",
|
|
174
|
+
prompt_settings=prompt_settings_vocab,
|
|
175
|
+
evaluation_metadata=evaluation_metadata,
|
|
176
|
+
template=template,
|
|
177
|
+
chain_inputs=chain_inputs,
|
|
178
|
+
parser_output_type=VocabularyComplexityOutput,
|
|
179
|
+
json_dict_normalizer=normalize_complexity_output,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Normalise the score string: the prompt may return spaces ("very complex")
|
|
183
|
+
# but TextComplexityAnswer expects underscores ("very_complex").
|
|
184
|
+
score = output.complexity_score.lower().replace(" ", "_")
|
|
185
|
+
answer = TextComplexityAnswer.from_score(score)
|
|
186
|
+
explanation = EvaluationExplanation(
|
|
187
|
+
summary=output.reasoning,
|
|
188
|
+
details={
|
|
189
|
+
"tier_2_words": output.tier_2_words,
|
|
190
|
+
"tier_3_words": output.tier_3_words,
|
|
191
|
+
"archaic_words": output.archaic_words,
|
|
192
|
+
"other_complex_words": output.other_complex_words,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
return answer, explanation
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Logging utilities for the Learning Commons Evaluators SDK.
|
|
2
|
+
|
|
3
|
+
Follows the "Configuring logging for a library" section of the Python logging HOWTO:
|
|
4
|
+
|
|
5
|
+
- A :class:`logging.NullHandler` is attached once to the logger named
|
|
6
|
+
``learning_commons_evaluators`` when this module is imported, so optional log
|
|
7
|
+
calls never trigger "no handler" warnings if the host app does not configure
|
|
8
|
+
logging.
|
|
9
|
+
- The library does not call :func:`logging.basicConfig` or otherwise configure
|
|
10
|
+
the root logger.
|
|
11
|
+
- By default, :class:`~learning_commons_evaluators.schemas.config.EvaluatorConfig`
|
|
12
|
+
uses the package logger ``learning_commons_evaluators`` so messages propagate
|
|
13
|
+
to the root like typical libraries. Use :func:`create_silent_logger` when
|
|
14
|
+
passing ``logger=`` if you need to discard SDK log lines entirely.
|
|
15
|
+
- Applications attach handlers to the root or to ``learning_commons_evaluators``,
|
|
16
|
+
adjust levels (e.g. suppress the SDK while the app is at DEBUG), pass a custom
|
|
17
|
+
:class:`~logging.Logger` through config, or use :func:`create_logger` for a
|
|
18
|
+
small stream-handler helper.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
Logger = logging.Logger
|
|
26
|
+
|
|
27
|
+
SDK_LOGGER_NAME = "learning_commons_evaluators"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _has_non_null_handler(logger: logging.Logger) -> bool:
|
|
31
|
+
return any(not isinstance(h, logging.NullHandler) for h in logger.handlers)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _install_library_null_handler() -> None:
|
|
35
|
+
"""Attach NullHandler to the SDK root logger once (stdlib library pattern)."""
|
|
36
|
+
root = logging.getLogger(SDK_LOGGER_NAME)
|
|
37
|
+
if not any(isinstance(h, logging.NullHandler) for h in root.handlers):
|
|
38
|
+
root.addHandler(logging.NullHandler())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_install_library_null_handler()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
45
|
+
"""Return the SDK root logger or a child (``learning_commons_evaluators.<name>``).
|
|
46
|
+
|
|
47
|
+
Does not add handlers or change levels; configuration is left to the
|
|
48
|
+
application or to :func:`create_logger`.
|
|
49
|
+
"""
|
|
50
|
+
if name is None:
|
|
51
|
+
return logging.getLogger(SDK_LOGGER_NAME)
|
|
52
|
+
return logging.getLogger(f"{SDK_LOGGER_NAME}.{name}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_logger(
|
|
56
|
+
name: str | None = None,
|
|
57
|
+
level: int = logging.WARNING,
|
|
58
|
+
handler: logging.Handler | None = None,
|
|
59
|
+
*,
|
|
60
|
+
use_logger: logging.Logger | None = None,
|
|
61
|
+
) -> logging.Logger:
|
|
62
|
+
"""Return ``use_logger`` unchanged, or configure an SDK subtree logger.
|
|
63
|
+
|
|
64
|
+
When no ``use_logger`` is given, sets *level* and attaches *handler*, or a
|
|
65
|
+
:class:`logging.StreamHandler` if there is no non-:class:`~logging.NullHandler`
|
|
66
|
+
handler yet (the import-time NullHandler alone does not count).
|
|
67
|
+
|
|
68
|
+
Intended for applications and quick debugging, not for internal SDK calls.
|
|
69
|
+
"""
|
|
70
|
+
if use_logger is not None:
|
|
71
|
+
return use_logger
|
|
72
|
+
|
|
73
|
+
logger = get_logger(name)
|
|
74
|
+
logger.setLevel(level)
|
|
75
|
+
|
|
76
|
+
if handler is not None:
|
|
77
|
+
logger.addHandler(handler)
|
|
78
|
+
elif not _has_non_null_handler(logger):
|
|
79
|
+
stream_handler = logging.StreamHandler()
|
|
80
|
+
stream_handler.setLevel(level)
|
|
81
|
+
stream_handler.setFormatter(logging.Formatter("[%(levelname)s] %(name)s: %(message)s"))
|
|
82
|
+
logger.addHandler(stream_handler)
|
|
83
|
+
|
|
84
|
+
return logger
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_silent_logger() -> logging.Logger:
|
|
88
|
+
"""Logger that discards all records (no propagation). Use for ``logger=`` opt-out."""
|
|
89
|
+
logger = logging.getLogger(f"{SDK_LOGGER_NAME}.silent")
|
|
90
|
+
for h in list(logger.handlers):
|
|
91
|
+
logger.removeHandler(h)
|
|
92
|
+
logger.addHandler(logging.NullHandler())
|
|
93
|
+
logger.propagate = False
|
|
94
|
+
return logger
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_error(error: BaseException) -> str:
|
|
98
|
+
"""Format an exception for logging."""
|
|
99
|
+
return f"{type(error).__name__}: {error}"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""LLM providers for use in chains (e.g. LangChain).
|
|
2
|
+
|
|
3
|
+
create_provider() returns a langchain_core BaseChatModel (OpenAI, Google, Anthropic).
|
|
4
|
+
|
|
5
|
+
Config types (LLMProviderConfig and provider-specific configs:
|
|
6
|
+
AnthropicLLMProviderConfig, GoogleLLMProviderConfig, OpenAILLMProviderConfig)
|
|
7
|
+
live in learning_commons_evaluators.schemas.config.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from learning_commons_evaluators.providers.langchain import (
|
|
11
|
+
create_provider,
|
|
12
|
+
token_usage_from_aimessage,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = ["create_provider", "token_usage_from_aimessage"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""LangChain chat model factory.
|
|
2
|
+
|
|
3
|
+
create_provider(prompt_settings, evaluator_config) builds a LangChain chat model
|
|
4
|
+
(OpenAI, Google, Anthropic) using PromptSettings and EvaluatorConfig.
|
|
5
|
+
Set model and temperature when building the chain, e.g. llm.bind(model="gpt-4o", temperature=0.7).
|
|
6
|
+
|
|
7
|
+
token_usage_from_aimessage() extracts TokenUsage from a LangChain AIMessage.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from learning_commons_evaluators.errors import ConfigurationError
|
|
15
|
+
from learning_commons_evaluators.schemas.config import (
|
|
16
|
+
EvaluatorConfig,
|
|
17
|
+
LLMProvider,
|
|
18
|
+
PromptSettings,
|
|
19
|
+
)
|
|
20
|
+
from learning_commons_evaluators.schemas.metadata import TokenUsage
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def token_usage_from_aimessage(message: Any, prompt_settings: PromptSettings) -> TokenUsage:
|
|
27
|
+
"""Extract TokenUsage from a LangChain AIMessage and prompt settings."""
|
|
28
|
+
usage = getattr(message, "usage_metadata", None) or getattr(
|
|
29
|
+
message, "response_metadata", {}
|
|
30
|
+
).get("usage_metadata")
|
|
31
|
+
if not usage:
|
|
32
|
+
return TokenUsage(
|
|
33
|
+
provider_type=prompt_settings.provider_type,
|
|
34
|
+
model=prompt_settings.model,
|
|
35
|
+
input_tokens=0,
|
|
36
|
+
output_tokens=0,
|
|
37
|
+
)
|
|
38
|
+
return TokenUsage(
|
|
39
|
+
provider_type=prompt_settings.provider_type,
|
|
40
|
+
model=prompt_settings.model,
|
|
41
|
+
input_tokens=usage.get("input_tokens", 0),
|
|
42
|
+
output_tokens=usage.get("output_tokens", 0),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _create_openai_llm(
|
|
47
|
+
prompt_settings: PromptSettings, evaluator_config: EvaluatorConfig
|
|
48
|
+
) -> BaseChatModel:
|
|
49
|
+
from langchain_openai import ChatOpenAI
|
|
50
|
+
|
|
51
|
+
openai_config = evaluator_config.openai_llm_provider_config
|
|
52
|
+
if openai_config is None:
|
|
53
|
+
raise ConfigurationError("OpenAI provider config is not set on EvaluatorConfig")
|
|
54
|
+
kwargs: dict[str, Any] = {
|
|
55
|
+
"api_key": openai_config.api_key,
|
|
56
|
+
"model": prompt_settings.model,
|
|
57
|
+
"temperature": prompt_settings.temperature,
|
|
58
|
+
}
|
|
59
|
+
return ChatOpenAI(**kwargs)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _create_google_llm(
|
|
63
|
+
prompt_settings: PromptSettings, evaluator_config: EvaluatorConfig
|
|
64
|
+
) -> BaseChatModel:
|
|
65
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
66
|
+
|
|
67
|
+
google_config = evaluator_config.google_llm_provider_config
|
|
68
|
+
if google_config is None:
|
|
69
|
+
raise ConfigurationError("Google provider config is not set on EvaluatorConfig")
|
|
70
|
+
return ChatGoogleGenerativeAI(
|
|
71
|
+
google_api_key=google_config.api_key,
|
|
72
|
+
model=prompt_settings.model,
|
|
73
|
+
temperature=prompt_settings.temperature,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _create_anthropic_llm(
|
|
78
|
+
prompt_settings: PromptSettings, evaluator_config: EvaluatorConfig
|
|
79
|
+
) -> BaseChatModel:
|
|
80
|
+
from langchain_anthropic import ChatAnthropic
|
|
81
|
+
|
|
82
|
+
anthropic_config = evaluator_config.anthropic_llm_provider_config
|
|
83
|
+
if anthropic_config is None:
|
|
84
|
+
raise ConfigurationError("Anthropic provider config is not set on EvaluatorConfig")
|
|
85
|
+
return ChatAnthropic(
|
|
86
|
+
api_key=anthropic_config.api_key,
|
|
87
|
+
model=prompt_settings.model,
|
|
88
|
+
temperature=prompt_settings.temperature,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_provider(
|
|
93
|
+
prompt_settings: PromptSettings, evaluator_config: EvaluatorConfig
|
|
94
|
+
) -> BaseChatModel:
|
|
95
|
+
"""Create a LangChain chat model from a PromptSettings for use in a chain."""
|
|
96
|
+
if prompt_settings.provider_type == LLMProvider.OPENAI:
|
|
97
|
+
return _create_openai_llm(prompt_settings, evaluator_config)
|
|
98
|
+
if prompt_settings.provider_type == LLMProvider.GOOGLE:
|
|
99
|
+
return _create_google_llm(prompt_settings, evaluator_config)
|
|
100
|
+
if prompt_settings.provider_type == LLMProvider.ANTHROPIC:
|
|
101
|
+
return _create_anthropic_llm(prompt_settings, evaluator_config)
|
|
102
|
+
raise ConfigurationError(
|
|
103
|
+
f"Unsupported LLM provider type: {prompt_settings.provider_type!r}. "
|
|
104
|
+
"Expected openai, google, or anthropic."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = ["create_provider", "token_usage_from_aimessage"]
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Schema and config types. Import from submodules or from learning_commons_evaluators."""
|
|
2
|
+
|
|
3
|
+
from learning_commons_evaluators.schemas.common_inputs import (
|
|
4
|
+
GradeInputField,
|
|
5
|
+
TextInputField,
|
|
6
|
+
)
|
|
7
|
+
from learning_commons_evaluators.schemas.config import (
|
|
8
|
+
EvaluationSettings,
|
|
9
|
+
LLMProvider,
|
|
10
|
+
PromptSettings,
|
|
11
|
+
)
|
|
12
|
+
from learning_commons_evaluators.schemas.conventionality import (
|
|
13
|
+
ConventionalityEvaluationSettings,
|
|
14
|
+
ConventionalityOutput,
|
|
15
|
+
)
|
|
16
|
+
from learning_commons_evaluators.schemas.errors import InputValidationError
|
|
17
|
+
from learning_commons_evaluators.schemas.evaluator import (
|
|
18
|
+
EvaluationAnswer,
|
|
19
|
+
EvaluationExplanation,
|
|
20
|
+
EvaluationInput,
|
|
21
|
+
EvaluationResult,
|
|
22
|
+
InputField,
|
|
23
|
+
)
|
|
24
|
+
from learning_commons_evaluators.schemas.input_specs import (
|
|
25
|
+
AnyInputSpec,
|
|
26
|
+
GradeInputSpec,
|
|
27
|
+
InputSpec,
|
|
28
|
+
TextInputSpec,
|
|
29
|
+
)
|
|
30
|
+
from learning_commons_evaluators.schemas.metadata import (
|
|
31
|
+
PROMPT_STEP_EXTRA_PROMPT_SETTINGS,
|
|
32
|
+
PROMPT_STEP_EXTRA_TOKEN_USAGE,
|
|
33
|
+
EvaluationMetadata,
|
|
34
|
+
EvaluatorMaturity,
|
|
35
|
+
EvaluatorMetadata,
|
|
36
|
+
InputMetadata,
|
|
37
|
+
Status,
|
|
38
|
+
StepMetadata,
|
|
39
|
+
TokenUsage,
|
|
40
|
+
prompt_settings_to_extras_value,
|
|
41
|
+
)
|
|
42
|
+
from learning_commons_evaluators.schemas.text_complexity import (
|
|
43
|
+
TextComplexityEvaluationInput,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"AnyInputSpec",
|
|
48
|
+
"ConventionalityEvaluationSettings",
|
|
49
|
+
"ConventionalityOutput",
|
|
50
|
+
"GradeInputSpec",
|
|
51
|
+
"InputSpec",
|
|
52
|
+
"TextInputSpec",
|
|
53
|
+
"EvaluationAnswer",
|
|
54
|
+
"EvaluationExplanation",
|
|
55
|
+
"EvaluationInput",
|
|
56
|
+
"EvaluationMetadata",
|
|
57
|
+
"EvaluationResult",
|
|
58
|
+
"EvaluationSettings",
|
|
59
|
+
"EvaluatorMetadata",
|
|
60
|
+
"EvaluatorMaturity",
|
|
61
|
+
"GradeInputField",
|
|
62
|
+
"InputField",
|
|
63
|
+
"InputMetadata",
|
|
64
|
+
"LLMProvider",
|
|
65
|
+
"PromptSettings",
|
|
66
|
+
"PROMPT_STEP_EXTRA_PROMPT_SETTINGS",
|
|
67
|
+
"PROMPT_STEP_EXTRA_TOKEN_USAGE",
|
|
68
|
+
"Status",
|
|
69
|
+
"StepMetadata",
|
|
70
|
+
"TextComplexityEvaluationInput",
|
|
71
|
+
"TextInputField",
|
|
72
|
+
"TokenUsage",
|
|
73
|
+
"InputValidationError",
|
|
74
|
+
"prompt_settings_to_extras_value",
|
|
75
|
+
]
|