Skip to content

Commit daf50cb

Browse files
committed
Remove braintrust_core dep
`autoevals` is the only package that depends on it, so we can just move the class definitions into the autoevals package.
1 parent 00278c6 commit daf50cb

16 files changed

Lines changed: 206 additions & 18 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repos:
1111
files: ./
1212
- repo: https://github.com/astral-sh/ruff-pre-commit
1313
# Ruff version.
14-
rev: v0.0.282
14+
rev: v0.12.7
1515
hooks:
1616
- id: ruff
1717
args: [--fix, --exit-non-zero-on-fix]

py/autoevals/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,13 @@ async def evaluate_qa():
120120
See individual module documentation for detailed usage and options.
121121
"""
122122

123-
from braintrust_core.score import Score, Scorer
124-
125123
from .json import *
126124
from .list import *
127125
from .llm import *
128126
from .moderation import *
129127
from .number import *
130128
from .oai import init
131129
from .ragas import *
130+
from .score import Score, Scorer, SerializableDataClass
132131
from .string import *
133132
from .value import ExactMatch

py/autoevals/json.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515

1616
import json
1717

18-
from braintrust_core.score import Score, Scorer
1918
from jsonschema import ValidationError, validate
2019

2120
from autoevals.partial import ScorerWithPartial
2221

2322
from .number import NumericDiff
23+
from .score import Score, Scorer
2424
from .string import Levenshtein
2525

2626

py/autoevals/list.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import sys
22

3-
from braintrust_core.score import Score
4-
53
from autoevals.partial import ScorerWithPartial
64

5+
from .score import Score
76
from .string import Levenshtein
87

98

py/autoevals/llm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@
5353

5454
import chevron
5555
import yaml
56-
from braintrust_core.score import Score
5756

5857
from autoevals.partial import ScorerWithPartial
5958

6059
from .oai import Client, arun_cached_request, run_cached_request
60+
from .score import Score
6161

6262
# Disable HTML escaping in chevron.
6363
chevron.renderer._html_escape = lambda x: x # type: ignore[attr-defined]

py/autoevals/moderation.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from typing import Optional
22

3-
from braintrust_core.score import Score
4-
53
from autoevals.llm import OpenAIScorer
64

75
from .oai import Client, arun_cached_request, run_cached_request
6+
from .score import Score
87

98
REQUEST_TYPE = "moderation"
109

py/autoevals/number.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
- Suitable for both small and large number comparisons
1212
"""
1313

14-
from braintrust_core.score import Score
15-
1614
from autoevals.partial import ScorerWithPartial
1715

16+
from .score import Score
17+
1818

1919
class NumericDiff(ScorerWithPartial):
2020
"""Numeric similarity scorer using normalized difference.

py/autoevals/partial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from braintrust_core.score import Scorer
1+
from .score import Scorer
22

33

44
class ScorerWithPartial(Scorer):

py/autoevals/score.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import dataclasses
2+
import sys
3+
from abc import ABC, abstractmethod
4+
from typing import Any, Dict, Optional
5+
6+
from .serializable_data_class import SerializableDataClass
7+
8+
9+
@dataclasses.dataclass
10+
class Score(SerializableDataClass):
11+
"""A score for an evaluation. The score is a float between 0 and 1."""
12+
13+
name: str
14+
"""The name of the score. This should be a unique name for the scorer."""
15+
16+
score: Optional[float] = None
17+
"""The score for the evaluation. This should be a float between 0 and 1. If the score is None, the evaluation is considered to be skipped."""
18+
19+
metadata: Dict[str, Any] = dataclasses.field(default_factory=dict)
20+
"""Metadata for the score. This can be used to store additional information about the score."""
21+
22+
# DEPRECATION_NOTICE: this field is deprecated, as errors are propagated up to the caller.
23+
error: Optional[Exception] = None
24+
"""Deprecated: The error field is deprecated, as errors are now propagated to the caller. The field will be removed in a future version of the library."""
25+
26+
def as_dict(self):
27+
return {
28+
"score": self.score,
29+
"metadata": self.metadata,
30+
}
31+
32+
def __post_init__(self):
33+
if self.score is not None and (self.score < 0 or self.score > 1):
34+
raise ValueError(f"score ({self.score}) must be between 0 and 1")
35+
if self.error is not None:
36+
print(
37+
"The error field is deprecated, as errors are now propagated to the caller. The field will be removed in a future version of the library",
38+
sys.stderr,
39+
)
40+
41+
42+
class Scorer(ABC):
43+
async def eval_async(self, output: Any, expected: Any = None, **kwargs: Any) -> Score:
44+
return await self._run_eval_async(output, expected, **kwargs)
45+
46+
def eval(self, output: Any, expected: Any = None, **kwargs: Any) -> Score:
47+
return self._run_eval_sync(output, expected, **kwargs)
48+
49+
def __call__(self, output: Any, expected: Any = None, **kwargs: Any) -> Score:
50+
return self.eval(output, expected, **kwargs)
51+
52+
async def _run_eval_async(self, output: Any, expected: Any = None, **kwargs: Any) -> Score:
53+
# By default we just run the sync version in a thread
54+
return self._run_eval_sync(output, expected, **kwargs)
55+
56+
def _name(self) -> str:
57+
return self.__class__.__name__
58+
59+
@abstractmethod
60+
def _run_eval_sync(self, output: Any, expected: Any = None, **kwargs: Any) -> Score:
61+
...
62+
63+
64+
__all__ = ["Score", "Scorer"]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import dataclasses
2+
import json
3+
from typing import Dict, Union, get_origin
4+
5+
6+
class SerializableDataClass:
7+
def as_dict(self):
8+
"""Serialize the object to a dictionary."""
9+
return dataclasses.asdict(self)
10+
11+
def as_json(self, **kwargs):
12+
"""Serialize the object to JSON."""
13+
return json.dumps(self.as_dict(), **kwargs)
14+
15+
def __getitem__(self, item: str):
16+
return getattr(self, item)
17+
18+
@classmethod
19+
def from_dict(cls, d: Dict):
20+
"""Deserialize the object from a dictionary. This method
21+
is shallow and will not call from_dict() on nested objects."""
22+
fields = set(f.name for f in dataclasses.fields(cls))
23+
filtered = {k: v for k, v in d.items() if k in fields}
24+
return cls(**filtered)
25+
26+
@classmethod
27+
def from_dict_deep(cls, d: Dict):
28+
"""Deserialize the object from a dictionary. This method
29+
is deep and will call from_dict_deep() on nested objects."""
30+
fields = {f.name: f for f in dataclasses.fields(cls)}
31+
filtered = {}
32+
for k, v in d.items():
33+
if k not in fields:
34+
continue
35+
36+
if (
37+
isinstance(v, dict)
38+
and isinstance(fields[k].type, type)
39+
and issubclass(fields[k].type, SerializableDataClass)
40+
):
41+
filtered[k] = fields[k].type.from_dict_deep(v)
42+
elif get_origin(fields[k].type) == Union:
43+
for t in fields[k].type.__args__:
44+
if t == type(None) and v is None:
45+
filtered[k] = None
46+
break
47+
if isinstance(t, type) and issubclass(t, SerializableDataClass) and v is not None:
48+
try:
49+
filtered[k] = t.from_dict_deep(v)
50+
break
51+
except TypeError:
52+
pass
53+
else:
54+
filtered[k] = v
55+
elif (
56+
isinstance(v, list)
57+
and get_origin(fields[k].type) == list
58+
and len(fields[k].type.__args__) == 1
59+
and isinstance(fields[k].type.__args__[0], type)
60+
and issubclass(fields[k].type.__args__[0], SerializableDataClass)
61+
):
62+
filtered[k] = [fields[k].type.__args__[0].from_dict_deep(i) for i in v]
63+
else:
64+
filtered[k] = v
65+
return cls(**filtered)

0 commit comments

Comments
 (0)