evalring 0.2.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.
- EvalRing/__init__.py +106 -0
- EvalRing/agent/__init__.py +33 -0
- EvalRing/agent/base.py +122 -0
- EvalRing/agent/classification.py +191 -0
- EvalRing/agent/errors.py +109 -0
- EvalRing/agent/implementations.py +575 -0
- EvalRing/agent/multi_role.py +305 -0
- EvalRing/cli.py +284 -0
- EvalRing/config.py +216 -0
- EvalRing/dataset/__init__.py +8 -0
- EvalRing/dataset/ai_data_generator.py +217 -0
- EvalRing/dataset/base.py +238 -0
- EvalRing/dataset/implementations.py +179 -0
- EvalRing/evaluator/__init__.py +36 -0
- EvalRing/evaluator/base.py +165 -0
- EvalRing/evaluator/implementations.py +404 -0
- EvalRing/evaluator/llm_judge/__init__.py +32 -0
- EvalRing/evaluator/llm_judge/evaluator.py +588 -0
- EvalRing/evaluator/llm_judge/judge.py +138 -0
- EvalRing/evaluator/llm_judge/metric.py +194 -0
- EvalRing/evaluator/llm_judge/rubric.py +160 -0
- EvalRing/evaluator/llm_judge/schema.py +147 -0
- EvalRing/evaluator/llm_judge/template.py +144 -0
- EvalRing/logging_utils.py +66 -0
- EvalRing/py.typed +0 -0
- EvalRing/utils/__init__.py +16 -0
- EvalRing/utils/generate_model_list.py +155 -0
- EvalRing/utils/global_cache.py +531 -0
- EvalRing/utils/suite_runner.py +836 -0
- EvalRing/utils/timeout.py +48 -0
- EvalRing/utils/visualizations.py +560 -0
- evalring-0.2.0.dist-info/METADATA +454 -0
- evalring-0.2.0.dist-info/RECORD +37 -0
- evalring-0.2.0.dist-info/WHEEL +5 -0
- evalring-0.2.0.dist-info/entry_points.txt +2 -0
- evalring-0.2.0.dist-info/licenses/LICENSE +21 -0
- evalring-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Concrete implementations of datasets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .base import BaseDataset, DataSample
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JSONDataset(BaseDataset):
|
|
14
|
+
"""Dataset loaded from a JSON file."""
|
|
15
|
+
|
|
16
|
+
def load_data(
|
|
17
|
+
self,
|
|
18
|
+
source: str | Path | pd.DataFrame,
|
|
19
|
+
text_field: str = "text",
|
|
20
|
+
label_field: str = "label",
|
|
21
|
+
id_field: str | None = "id",
|
|
22
|
+
**kwargs,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Load data from a JSON file.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
source: Path to JSON file
|
|
29
|
+
text_field: Key for input text
|
|
30
|
+
label_field: Key for target output
|
|
31
|
+
id_field: Key for sample ID (auto-generated if None)
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(source, pd.DataFrame):
|
|
34
|
+
raise TypeError("JSONDataset expects a file path; use DataFrameDataset instead.")
|
|
35
|
+
|
|
36
|
+
filepath = Path(source)
|
|
37
|
+
if not filepath.exists():
|
|
38
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
39
|
+
|
|
40
|
+
with open(filepath) as f:
|
|
41
|
+
data = json.load(f)
|
|
42
|
+
|
|
43
|
+
if not isinstance(data, list):
|
|
44
|
+
raise ValueError("JSON data must be a list of objects")
|
|
45
|
+
|
|
46
|
+
for i, item in enumerate(data):
|
|
47
|
+
sample_id = str(item.get(id_field, i)) if id_field else str(i)
|
|
48
|
+
|
|
49
|
+
# Extract metadata (everything except text and label)
|
|
50
|
+
metadata = {
|
|
51
|
+
k: v for k, v in item.items() if k not in [text_field, label_field, id_field]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
sample = DataSample(
|
|
55
|
+
id=sample_id,
|
|
56
|
+
input_text=item.get(text_field, ""),
|
|
57
|
+
target_output=item.get(label_field),
|
|
58
|
+
metadata=metadata,
|
|
59
|
+
)
|
|
60
|
+
self.add_sample(sample)
|
|
61
|
+
|
|
62
|
+
def validate_data(self) -> bool:
|
|
63
|
+
"""Validate that all samples have input text and target output."""
|
|
64
|
+
for sample in self._samples:
|
|
65
|
+
if not sample.input_text or sample.target_output is None:
|
|
66
|
+
return False
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CSVDataset(BaseDataset):
|
|
71
|
+
"""Dataset loaded from a CSV file."""
|
|
72
|
+
|
|
73
|
+
def load_data(
|
|
74
|
+
self,
|
|
75
|
+
source: str | Path | pd.DataFrame,
|
|
76
|
+
text_field: str = "text",
|
|
77
|
+
label_field: str = "label",
|
|
78
|
+
id_field: str | None = None,
|
|
79
|
+
**kwargs,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Load data from a CSV file.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
source: Path to CSV file
|
|
86
|
+
text_field: Column name for input text
|
|
87
|
+
label_field: Column name for target output
|
|
88
|
+
id_field: Column name for sample ID (auto-generated if None)
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(source, pd.DataFrame):
|
|
91
|
+
raise TypeError("CSVDataset expects a file path; use DataFrameDataset instead.")
|
|
92
|
+
|
|
93
|
+
filepath = Path(source)
|
|
94
|
+
if not filepath.exists():
|
|
95
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
96
|
+
|
|
97
|
+
df = pd.read_csv(filepath, **kwargs)
|
|
98
|
+
|
|
99
|
+
if text_field not in df.columns:
|
|
100
|
+
raise ValueError(f"Text field '{text_field}' not found in CSV columns")
|
|
101
|
+
|
|
102
|
+
if label_field not in df.columns:
|
|
103
|
+
raise ValueError(f"Label field '{label_field}' not found in CSV columns")
|
|
104
|
+
|
|
105
|
+
for i, row in df.iterrows():
|
|
106
|
+
sample_id = str(row[id_field]) if id_field and id_field in df.columns else str(i)
|
|
107
|
+
|
|
108
|
+
# Extract metadata
|
|
109
|
+
metadata_cols = [c for c in df.columns if c not in [text_field, label_field, id_field]]
|
|
110
|
+
metadata = {col: row[col] for col in metadata_cols}
|
|
111
|
+
|
|
112
|
+
sample = DataSample(
|
|
113
|
+
id=sample_id,
|
|
114
|
+
input_text=str(row[text_field]),
|
|
115
|
+
target_output=row[label_field],
|
|
116
|
+
metadata=metadata,
|
|
117
|
+
)
|
|
118
|
+
self.add_sample(sample)
|
|
119
|
+
|
|
120
|
+
def validate_data(self) -> bool:
|
|
121
|
+
"""Validate that all samples have input text and target output."""
|
|
122
|
+
for sample in self._samples:
|
|
123
|
+
if not sample.input_text or pd.isna(sample.target_output):
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DataFrameDataset(BaseDataset):
|
|
129
|
+
"""Dataset loaded from a pandas DataFrame."""
|
|
130
|
+
|
|
131
|
+
def load_data(
|
|
132
|
+
self,
|
|
133
|
+
source: str | Path | pd.DataFrame,
|
|
134
|
+
text_field: str = "text",
|
|
135
|
+
label_field: str = "label",
|
|
136
|
+
id_field: str | None = None,
|
|
137
|
+
**kwargs,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Load data from a pandas DataFrame.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
source: pandas DataFrame
|
|
144
|
+
text_field: Column name for input text
|
|
145
|
+
label_field: Column name for target output
|
|
146
|
+
id_field: Column name for sample ID (auto-generated if None)
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(source, pd.DataFrame):
|
|
149
|
+
raise ValueError("Source must be a pandas DataFrame")
|
|
150
|
+
|
|
151
|
+
df = source
|
|
152
|
+
|
|
153
|
+
if text_field not in df.columns:
|
|
154
|
+
raise ValueError(f"Text field '{text_field}' not found in DataFrame columns")
|
|
155
|
+
|
|
156
|
+
if label_field not in df.columns:
|
|
157
|
+
raise ValueError(f"Label field '{label_field}' not found in DataFrame columns")
|
|
158
|
+
|
|
159
|
+
for i, row in df.iterrows():
|
|
160
|
+
sample_id = str(row[id_field]) if id_field and id_field in df.columns else str(i)
|
|
161
|
+
|
|
162
|
+
# Extract metadata
|
|
163
|
+
metadata_cols = [c for c in df.columns if c not in [text_field, label_field, id_field]]
|
|
164
|
+
metadata = {col: row[col] for col in metadata_cols}
|
|
165
|
+
|
|
166
|
+
sample = DataSample(
|
|
167
|
+
id=sample_id,
|
|
168
|
+
input_text=str(row[text_field]),
|
|
169
|
+
target_output=row[label_field],
|
|
170
|
+
metadata=metadata,
|
|
171
|
+
)
|
|
172
|
+
self.add_sample(sample)
|
|
173
|
+
|
|
174
|
+
def validate_data(self) -> bool:
|
|
175
|
+
"""Validate that all samples have input text and target output."""
|
|
176
|
+
for sample in self._samples:
|
|
177
|
+
if not sample.input_text or pd.isna(sample.target_output):
|
|
178
|
+
return False
|
|
179
|
+
return True
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Evaluator module for EvalRing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import BaseEvaluator, EvaluationMetrics, EvaluationResult
|
|
6
|
+
from .implementations import ClassificationEvaluator
|
|
7
|
+
from .llm_judge import (
|
|
8
|
+
EvalSteps,
|
|
9
|
+
JudgeMetric,
|
|
10
|
+
JudgeTemplate,
|
|
11
|
+
JudgeVerdict,
|
|
12
|
+
LLMJudge,
|
|
13
|
+
LLMJudgeEvaluator,
|
|
14
|
+
OpenAIJudge,
|
|
15
|
+
Rubric,
|
|
16
|
+
RubricLevel,
|
|
17
|
+
ScoringCriteria,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseEvaluator",
|
|
22
|
+
"EvaluationResult",
|
|
23
|
+
"EvaluationMetrics",
|
|
24
|
+
"ClassificationEvaluator",
|
|
25
|
+
# LLM-as-a-Judge
|
|
26
|
+
"Rubric",
|
|
27
|
+
"RubricLevel",
|
|
28
|
+
"ScoringCriteria",
|
|
29
|
+
"JudgeVerdict",
|
|
30
|
+
"EvalSteps",
|
|
31
|
+
"JudgeTemplate",
|
|
32
|
+
"JudgeMetric",
|
|
33
|
+
"LLMJudge",
|
|
34
|
+
"OpenAIJudge",
|
|
35
|
+
"LLMJudgeEvaluator",
|
|
36
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for evaluators in the EvalRing framework.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..agent.base import BaseAgent
|
|
13
|
+
from ..dataset.base import BaseDataset
|
|
14
|
+
from ..utils.global_cache import GlobalCache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EvaluationMetrics:
|
|
19
|
+
"""Container for evaluation metrics."""
|
|
20
|
+
|
|
21
|
+
metrics: dict[str, float] = field(default_factory=dict)
|
|
22
|
+
per_sample_metrics: list[dict[str, float]] = field(default_factory=list)
|
|
23
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
def add_metric(self, name: str, value: float) -> None:
|
|
26
|
+
"""Add a metric."""
|
|
27
|
+
self.metrics[name] = value
|
|
28
|
+
|
|
29
|
+
def get_metric(self, name: str, default: float | None = None) -> float | None:
|
|
30
|
+
"""Get a metric by name."""
|
|
31
|
+
return self.metrics.get(name, default)
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict[str, Any]:
|
|
34
|
+
"""Convert to dictionary."""
|
|
35
|
+
return {
|
|
36
|
+
"metrics": self.metrics,
|
|
37
|
+
"per_sample_metrics": self.per_sample_metrics,
|
|
38
|
+
"metadata": self.metadata,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class EvaluationResult:
|
|
44
|
+
"""Container for evaluation results."""
|
|
45
|
+
|
|
46
|
+
agent_name: str
|
|
47
|
+
dataset_name: str
|
|
48
|
+
metrics: EvaluationMetrics
|
|
49
|
+
duration: float
|
|
50
|
+
timestamp: datetime
|
|
51
|
+
task_name: str
|
|
52
|
+
version: str = "1.0"
|
|
53
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
"""Convert to dictionary."""
|
|
57
|
+
return {
|
|
58
|
+
"agent_name": self.agent_name,
|
|
59
|
+
"dataset_name": self.dataset_name,
|
|
60
|
+
"metrics": self.metrics.to_dict(),
|
|
61
|
+
"duration": self.duration,
|
|
62
|
+
"timestamp": self.timestamp.isoformat(),
|
|
63
|
+
"task_name": self.task_name,
|
|
64
|
+
"version": self.version,
|
|
65
|
+
"metadata": self.metadata,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def save(self, filepath: str | Path) -> None:
|
|
69
|
+
"""Save evaluation results to file."""
|
|
70
|
+
with open(filepath, "w") as f:
|
|
71
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BaseEvaluator(ABC):
|
|
75
|
+
"""
|
|
76
|
+
Abstract base class for evaluators in EvalRing.
|
|
77
|
+
|
|
78
|
+
This class orchestrates the evaluation process, managing agents,
|
|
79
|
+
datasets, and metric calculation.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, name: str = "evaluator", output_dir: str | Path | None = None, **kwargs):
|
|
83
|
+
self.name = name
|
|
84
|
+
self.output_dir = Path(output_dir) if output_dir else Path("./results")
|
|
85
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
self._metadata = kwargs
|
|
87
|
+
|
|
88
|
+
# Initialize global cache
|
|
89
|
+
cache_mode = kwargs.get("cache_mode", "both")
|
|
90
|
+
self.global_cache = GlobalCache(mode=cache_mode)
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def evaluate(
|
|
94
|
+
self, agent: BaseAgent, dataset: BaseDataset, task_name: str, version: str = "1.0", **kwargs
|
|
95
|
+
) -> EvaluationResult:
|
|
96
|
+
"""
|
|
97
|
+
Run evaluation on the given agent and dataset.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
agent: The agent to evaluate
|
|
101
|
+
dataset: The dataset to evaluate on
|
|
102
|
+
task_name: Name of the evaluation task
|
|
103
|
+
version: Version of the evaluation
|
|
104
|
+
**kwargs: Additional evaluation parameters
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
EvaluationResult containing metrics and metadata
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def validate_inputs(self, agent: BaseAgent, dataset: BaseDataset) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Validate agent and dataset before evaluation.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
agent: Agent to validate
|
|
117
|
+
dataset: Dataset to validate
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if inputs are valid, False otherwise
|
|
121
|
+
"""
|
|
122
|
+
if not isinstance(agent, BaseAgent):
|
|
123
|
+
raise ValueError("Agent must inherit from BaseAgent")
|
|
124
|
+
|
|
125
|
+
if not isinstance(dataset, BaseDataset):
|
|
126
|
+
raise ValueError("Dataset must inherit from BaseDataset")
|
|
127
|
+
|
|
128
|
+
if len(dataset) == 0:
|
|
129
|
+
raise ValueError("Dataset cannot be empty")
|
|
130
|
+
|
|
131
|
+
# Validate data integrity and enforce unique per-sample IDs.
|
|
132
|
+
if hasattr(dataset, "validate_data") and not dataset.validate_data():
|
|
133
|
+
raise ValueError("Dataset validation failed")
|
|
134
|
+
if hasattr(dataset, "assert_unique_ids"):
|
|
135
|
+
dataset.assert_unique_ids(context="evaluator.validate_inputs")
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def save_results(self, result: EvaluationResult, filename: str | None = None) -> Path:
|
|
140
|
+
"""
|
|
141
|
+
Save evaluation results to file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
result: Evaluation result to save
|
|
145
|
+
filename: Optional filename (auto-generated if not provided)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Path to the saved file
|
|
149
|
+
"""
|
|
150
|
+
if filename is None:
|
|
151
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
152
|
+
filename = f"evaluation_{result.agent_name}_{result.task_name}_{timestamp}.json"
|
|
153
|
+
|
|
154
|
+
filepath = self.output_dir / filename
|
|
155
|
+
result.save(filepath)
|
|
156
|
+
return filepath
|
|
157
|
+
|
|
158
|
+
def get_info(self) -> dict[str, Any]:
|
|
159
|
+
"""Get evaluator information."""
|
|
160
|
+
return {
|
|
161
|
+
"name": self.name,
|
|
162
|
+
"type": self.__class__.__name__,
|
|
163
|
+
"output_dir": str(self.output_dir),
|
|
164
|
+
"metadata": self._metadata,
|
|
165
|
+
}
|