Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
.venv/
__pycache__/
*.pyc
*.egg-info/
dist/
build/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Inspect AI eval logs
logs/

# Jupyter Notebook
.ipynb_checkpoints/
Expand Down
5 changes: 4 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"evals/prompts": "1.5.0",
"sdks/python": "0.2.0",
"sdks/typescript": "0.7.0"
"sdks/typescript": "0.7.0",
"integrations/langfuse-python": "0.1.0",
"integrations/arize-python": "0.1.0",
"integrations/braintrust-python": "0.1.0"
}
6 changes: 6 additions & 0 deletions integrations/arize-python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.egg-info/
dist/
build/
__pycache__/
.pytest_cache/
.mypy_cache/
1 change: 1 addition & 0 deletions integrations/arize-python/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
45 changes: 45 additions & 0 deletions integrations/arize-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# learning-commons-arize-scorers

[Arize/Phoenix](https://phoenix.arize.com/) OTel tracing adapter for the [Learning Commons evaluators](https://github.com/learning-commons-org/evaluators) SDK.

Wraps any `LLMGeneratorProtocol` adapter and emits [OpenInference](https://github.com/Arize-ai/openinference) spans compatible with Arize Phoenix and any OTel backend.

## Installation

```bash
pip install learning-commons-arize-scorers
```

## Usage

```python
from learning_commons_arize_scorers import PhoenixTracingAdapter
from learning_commons_inspect_scorers.adapter import InspectModelAdapter
from learning_commons_evaluators import GradeLevelAppropriatenessEvaluator
from learning_commons_evaluators.config import create_config_no_telemetry

adapter = PhoenixTracingAdapter(
InspectModelAdapter("anthropic/claude-opus-4-8"),
capture_message_content=False, # False by default — K-12 privacy
)
evaluator = GradeLevelAppropriatenessEvaluator(
config=create_config_no_telemetry(),
llm_provider=adapter,
)
```

## Configuration

| Parameter | Default | Description |
|---|---|---|
| `inner` | required | Any `LLMGeneratorProtocol` adapter to wrap. |
| `tracer` | auto | OTel `Tracer`. Defaults to `trace.get_tracer("learning_commons_arize_scorers")`. |
| `capture_message_content` | `False` | Set `True` to include prompt/response text in spans. Off by default — student data may be sensitive. |

## Development

```bash
pip install -e sdks/python
pip install -e "integrations/arize-python[dev]"
pytest integrations/arize-python/tests/
```
68 changes: 68 additions & 0 deletions integrations/arize-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "learning-commons-arize-scorers"
version = "0.1.0"
description = "Arize/Phoenix OTel tracing adapter for Learning Commons evaluators"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"
authors = [{ name = "Learning Commons" }]
keywords = ["education", "evaluators", "arize", "phoenix", "opentelemetry", "tracing"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Education",
]
dependencies = [
"learning-commons-evaluators>=0.2.0",
"opentelemetry-api>=1.0.0",
]

[project.optional-dependencies]
dev = [
"opentelemetry-sdk>=1.0.0",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"ruff>=0.9.0",
"mypy>=1.14.0",
]

[project.urls]
Homepage = "https://github.com/learning-commons-org/evaluators"
Repository = "https://github.com/learning-commons-org/evaluators/tree/main/integrations/arize-python"
Documentation = "https://docs.learningcommons.org/evaluators"
"Bug Tracker" = "https://github.com/learning-commons-org/evaluators/issues"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
learning_commons_arize_scorers = ["py.typed"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

[tool.ruff]
target-version = "py310"
line-length = 100

[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "SIM"]
ignore = ["E501"]

[tool.mypy]
python_version = "3.10"
mypy_path = ["src", "tests"]
explicit_package_bases = true
warn_unused_configs = true
show_error_codes = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Learning Commons Arize scorers — OpenInference OTel tracing adapter for LC evaluators."""

from learning_commons_arize_scorers.adapter import PhoenixTracingAdapter

__all__ = ["PhoenixTracingAdapter"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""PhoenixTracingAdapter — decorates any LLMGeneratorProtocol with OpenInference OTel spans."""

from __future__ import annotations

from opentelemetry import trace
from opentelemetry.trace import Tracer
from opentelemetry.trace.status import Status, StatusCode

from learning_commons_evaluators.schemas.llm_provider import (
GenerateConfig,
LLMGeneratorProtocol,
LLMResponse,
)


class PhoenixTracingAdapter:
"""Decorator adapter: wraps any LLMGeneratorProtocol, emits OpenInference OTel spans.

Composes with any other adapter::

from learning_commons_arize_scorers import PhoenixTracingAdapter
from learning_commons_inspect_scorers.adapter import InspectModelAdapter

adapter = PhoenixTracingAdapter(InspectModelAdapter("anthropic/claude-opus-4-8"))
evaluator = GradeLevelAppropriatenessEvaluator(config=..., llm_provider=adapter)

Args:
inner: The underlying adapter to delegate generation to.
tracer: OTel Tracer instance. Defaults to a tracer named
``"learning_commons_arize_scorers"``.
capture_message_content: If ``True``, writes system and human prompt text
and the model response into span attributes. Defaults to ``False``.

.. warning::
Enabling this may capture student-submitted text and other PII
into your observability backend. Ensure your data handling
controls (FERPA, COPPA for K-12) permit this before enabling.
"""

def __init__(
self,
inner: LLMGeneratorProtocol,
tracer: Tracer | None = None,
*,
capture_message_content: bool = False,
) -> None:
self._inner = inner
self._tracer = tracer or trace.get_tracer("learning_commons_arize_scorers")
self._capture_message_content = capture_message_content

async def generate(
self, *, system: str, human: str, config: GenerateConfig | None = None
) -> LLMResponse:
with self._tracer.start_as_current_span("llm.generate") as span:
span.set_attribute("openinference.span.kind", "LLM")
span.set_attribute("gen_ai.operation.name", "chat")
if self._capture_message_content:
span.set_attribute("llm.input_messages.0.message.role", "system")
span.set_attribute("llm.input_messages.0.message.content", system)
span.set_attribute("llm.input_messages.1.message.role", "user")
span.set_attribute("llm.input_messages.1.message.content", human)
try:
response = await self._inner.generate(system=system, human=human, config=config)
span.set_attribute("gen_ai.response.model", response.model)
span.set_attribute("llm.model_name", response.model)
if response.input_tokens is not None:
span.set_attribute("gen_ai.usage.input_tokens", response.input_tokens)
span.set_attribute("llm.token_count.prompt", response.input_tokens)
if response.output_tokens is not None:
span.set_attribute("gen_ai.usage.output_tokens", response.output_tokens)
span.set_attribute("llm.token_count.completion", response.output_tokens)
if self._capture_message_content:
span.set_attribute("llm.output_messages.0.message.role", "assistant")
span.set_attribute("llm.output_messages.0.message.content", response.content)
return response
except Exception as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR))
raise
Empty file.
Loading