Skip to content

Commit 1cf6313

Browse files
committed
feat: add Inspect AI integration package (learning-commons-inspect-scorers)
Adds integrations/inspect-python with InspectModelAdapter (wraps Inspect's get_model() to satisfy LLMGeneratorProtocol) and gla_scorer() — an Inspect scorer for grade-level appropriateness, wired to the GLA evaluator via the injected-provider protocol from the SDK. - inspect-ai>=0.3.214 (Score.unscored() lower bound) - registered for independent release via release-please Split out from the combined integrations PR so the Inspect path — which has a real consumer (edu-panda-skill-harness) — can be validated and merged on its own.
1 parent 7d75e0d commit 1cf6313

12 files changed

Lines changed: 737 additions & 1 deletion

File tree

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"evals/prompts": "1.5.0",
33
"sdks/python": "0.2.0",
4-
"sdks/typescript": "0.7.0"
4+
"sdks/typescript": "0.7.0",
5+
"integrations/inspect-python": "0.1.0"
56
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.egg-info/
2+
dist/
3+
build/
4+
__pycache__/
5+
.pytest_cache/
6+
.mypy_cache/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# learning-commons-inspect-scorers
2+
3+
[Inspect AI](https://inspect.aisi.org.uk/) scorer wrappers for the [Learning Commons evaluators](https://github.com/learning-commons-org/evaluators) SDK.
4+
5+
## Installation
6+
7+
```bash
8+
pip install learning-commons-inspect-scorers
9+
```
10+
11+
> **Note:** Requires `learning-commons-evaluators>=0.2.0`. During local development
12+
> install the SDK from the repo root first:
13+
> ```bash
14+
> pip install -e sdks/python
15+
> pip install -e integrations/inspect-python
16+
> ```
17+
18+
## Usage
19+
20+
### Grade Level Appropriateness scorer
21+
22+
Evaluates whether model output (or generated artifact files) is written at the
23+
appropriate reading level for a target K-12 grade band.
24+
25+
```python
26+
from inspect_ai import Task, task
27+
from inspect_ai.dataset import csv_dataset, FieldSpec
28+
from inspect_ai.solver import generate
29+
from learning_commons_inspect_scorers import gla_scorer
30+
31+
@task
32+
def my_eval():
33+
return Task(
34+
dataset=csv_dataset("samples.csv"), # requires target_grade column
35+
solver=[generate()],
36+
scorer=gla_scorer(),
37+
)
38+
```
39+
40+
The dataset CSV must include a `target_grade` metadata column with one of:
41+
`K-1`, `2-3`, `4-5`, `6-8`, `9-10`, `11-CCR`.
42+
43+
### Scoring artifact files (edu-panda-skill-harness)
44+
45+
```python
46+
scorer=gla_scorer(text_source="artifacts")
47+
```
48+
49+
### Re-scoring an existing log from the CLI
50+
51+
Once installed, scorers are registered via Inspect's entry point system:
52+
53+
```bash
54+
inspect score logs/my-eval.eval --scorer learning_commons_inspect_scorers/gla_scorer
55+
```
56+
57+
## Configuration
58+
59+
| Parameter | Default | Description |
60+
|---|---|---|
61+
| `grader_model` | `"anthropic/claude-opus-4-8"` | Inspect model string for the grading LLM. Uses Inspect's model system — no separate API key configuration needed. |
62+
| `text_source` | `"completion"` | `"completion"` scores `state.output.completion`; `"artifacts"` joins `state.metadata["artifacts"]` file contents. |
63+
| `target_grade_key` | `"target_grade"` | Metadata key holding the expected grade band. |
64+
| `allow_adjacent` | `True` | If `True`, the one grade band above or below the target also passes. |
65+
66+
## Development
67+
68+
```bash
69+
# From repo root
70+
pip install -e sdks/python
71+
pip install -e "integrations/inspect-python[dev]"
72+
pytest integrations/inspect-python/tests/
73+
```
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
[build-system]
2+
requires = ["setuptools>=61", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "learning-commons-inspect-scorers"
7+
version = "0.1.0"
8+
description = "Inspect AI scorer wrappers for Learning Commons evaluators"
9+
readme = "README.md"
10+
license = { text = "MIT" }
11+
requires-python = ">=3.10"
12+
authors = [{ name = "Learning Commons" }]
13+
keywords = ["education", "evaluators", "inspect", "evals", "scoring"]
14+
classifiers = [
15+
"Development Status :: 3 - Alpha",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.10",
20+
"Programming Language :: Python :: 3.11",
21+
"Programming Language :: Python :: 3.12",
22+
"Programming Language :: Python :: 3.13",
23+
"Topic :: Education",
24+
]
25+
dependencies = [
26+
"learning-commons-evaluators>=0.2.0",
27+
# Score.unscored() — used in every skip/error path of gla_scorer — was added in inspect-ai 0.3.214.
28+
"inspect-ai>=0.3.214",
29+
]
30+
31+
[project.optional-dependencies]
32+
dev = [
33+
"pytest>=7.0.0",
34+
"pytest-asyncio>=0.21.0",
35+
"ruff>=0.9.0",
36+
"mypy>=1.14.0",
37+
]
38+
39+
[project.urls]
40+
Homepage = "https://github.com/learning-commons-org/evaluators"
41+
Repository = "https://github.com/learning-commons-org/evaluators/tree/main/integrations/inspect-python"
42+
Documentation = "https://docs.learningcommons.org/evaluators"
43+
"Bug Tracker" = "https://github.com/learning-commons-org/evaluators/issues"
44+
45+
# Registers scorers with Inspect's component discovery via setuptools entry points.
46+
# Once installed, scorers are accessible as e.g. `learning_commons_inspect_scorers/gla_scorer`
47+
# from the CLI: inspect score log.eval --scorer learning_commons_inspect_scorers/gla_scorer
48+
[project.entry-points.inspect_ai]
49+
learning_commons_inspect_scorers = "learning_commons_inspect_scorers._registry"
50+
51+
[tool.setuptools.packages.find]
52+
where = ["src"]
53+
54+
[tool.setuptools.package-data]
55+
learning_commons_inspect_scorers = ["py.typed"]
56+
57+
[tool.pytest.ini_options]
58+
asyncio_mode = "auto"
59+
testpaths = ["tests"]
60+
61+
[tool.ruff]
62+
target-version = "py310"
63+
line-length = 100
64+
65+
[tool.ruff.lint]
66+
select = ["E", "W", "F", "I", "UP", "B", "SIM"]
67+
ignore = ["E501"]
68+
69+
[tool.mypy]
70+
python_version = "3.10"
71+
mypy_path = ["src", "tests"]
72+
explicit_package_bases = true
73+
plugins = ["pydantic.mypy"]
74+
warn_unused_configs = true
75+
show_error_codes = true
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Learning Commons Inspect scorers — Inspect AI wrappers for LC evaluators."""
2+
3+
from learning_commons_inspect_scorers.adapter import InspectModelAdapter
4+
from learning_commons_inspect_scorers.gla import gla_scorer
5+
6+
__all__ = ["InspectModelAdapter", "gla_scorer"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Entry point registry — imported by Inspect via the inspect_ai setuptools entry point.
2+
3+
Importing this module registers all scorers with Inspect's component system,
4+
making them accessible by name (e.g. learning_commons_inspect_scorers/gla_scorer)
5+
from both the Python API and the CLI.
6+
"""
7+
8+
from learning_commons_inspect_scorers.gla import (
9+
gla_scorer, # noqa: F401 — import triggers @scorer registry side-effect
10+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Inspect AI model adapter implementing LLMGeneratorProtocol.
2+
3+
This is a separate package (``learning-commons-inspect-scorers``) rather than
4+
part of ``learning-commons-evaluators`` because it introduces ``inspect-ai`` as
5+
a hard dependency — a heavy framework that not all SDK users need.
6+
7+
**Versioning contract**: this package requires ``learning-commons-evaluators>=0.2.0``
8+
where ``LLMGeneratorProtocol`` was introduced. If a new method is added to the
9+
protocol, bump the lower bound here and update this adapter.
10+
11+
**Building a new integration** (e.g. ``integrations/langsmith-python``):
12+
implement ``LLMGeneratorProtocol`` — a single async ``generate()`` method that
13+
calls your framework's model and returns ``LLMResponse`` — then inject it into
14+
any evaluator via ``GradeLevelAppropriatenessEvaluator(config=..., llm_provider=adapter)``.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
from inspect_ai.model import (
20+
ChatMessageSystem,
21+
ChatMessageUser,
22+
get_model,
23+
)
24+
from inspect_ai.model import (
25+
GenerateConfig as InspectGenConfig,
26+
)
27+
from learning_commons_evaluators.schemas.llm_provider import GenerateConfig, LLMResponse
28+
29+
30+
class InspectModelAdapter:
31+
"""Wraps Inspect's get_model() to satisfy LLMGeneratorProtocol.
32+
33+
Pass a model string in the same form accepted by Inspect's ``--model`` flag,
34+
for example ``"anthropic/claude-opus-4-8"`` or ``"openai/gpt-4o"``.
35+
36+
Example::
37+
38+
adapter = InspectModelAdapter("anthropic/claude-opus-4-8")
39+
evaluator = GradeLevelAppropriatenessEvaluator(
40+
config=create_config_no_telemetry(),
41+
llm_provider=adapter,
42+
)
43+
"""
44+
45+
def __init__(self, model_name: str) -> None:
46+
self._model_name = model_name
47+
48+
async def generate(
49+
self,
50+
*,
51+
system: str,
52+
human: str,
53+
config: GenerateConfig | None = None,
54+
) -> LLMResponse:
55+
# get_model() is memoized by Inspect — repeated calls with the same string
56+
# return the cached Model object without reconstruction.
57+
inspect_model = get_model(self._model_name)
58+
inspect_config = InspectGenConfig(
59+
temperature=config.temperature if config is not None else None,
60+
max_tokens=config.max_tokens if config is not None else None,
61+
)
62+
output = await inspect_model.generate(
63+
[ChatMessageSystem(content=system), ChatMessageUser(content=human)],
64+
config=inspect_config,
65+
)
66+
return LLMResponse(
67+
content=output.completion,
68+
model=self._model_name,
69+
input_tokens=getattr(output.usage, "input_tokens", None),
70+
output_tokens=getattr(output.usage, "output_tokens", None),
71+
)

0 commit comments

Comments
 (0)