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,305 @@
|
|
|
1
|
+
"""Reusable multi-role host orchestration for classification tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .classification import (
|
|
10
|
+
normalize_probability_distribution,
|
|
11
|
+
parse_json_object,
|
|
12
|
+
resolve_classification_prediction,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RoleConfig:
|
|
18
|
+
"""Configuration for one specialist role in the multi-role system."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
persona: str
|
|
22
|
+
model_name: str
|
|
23
|
+
temperature: float = 0.0
|
|
24
|
+
max_completion_tokens: int = 400
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MultiRoleHostOrchestrator:
|
|
28
|
+
"""Orchestrates role-by-role reviews plus bounded host-led questioning."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
client: Any,
|
|
34
|
+
labels: list[str],
|
|
35
|
+
task_name: str,
|
|
36
|
+
task_instructions: str,
|
|
37
|
+
host_model_name: str,
|
|
38
|
+
role_configs: list[RoleConfig],
|
|
39
|
+
host_temperature: float = 0.0,
|
|
40
|
+
host_max_completion_tokens: int = 500,
|
|
41
|
+
max_iterations: int = 10,
|
|
42
|
+
):
|
|
43
|
+
if max_iterations < 1:
|
|
44
|
+
raise ValueError("max_iterations must be >= 1")
|
|
45
|
+
if not role_configs:
|
|
46
|
+
raise ValueError("role_configs cannot be empty")
|
|
47
|
+
|
|
48
|
+
self.client = client
|
|
49
|
+
self.labels = labels
|
|
50
|
+
self.task_name = task_name
|
|
51
|
+
self.task_instructions = task_instructions
|
|
52
|
+
self.host_model_name = host_model_name
|
|
53
|
+
self.role_configs = role_configs
|
|
54
|
+
self.role_map = {role.name: role for role in role_configs}
|
|
55
|
+
self.host_temperature = host_temperature
|
|
56
|
+
self.host_max_completion_tokens = host_max_completion_tokens
|
|
57
|
+
self.max_iterations = max_iterations
|
|
58
|
+
|
|
59
|
+
def _call_json(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
model_name: str,
|
|
63
|
+
messages: list[dict[str, str]],
|
|
64
|
+
temperature: float,
|
|
65
|
+
max_completion_tokens: int,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
response = self.client.chat.completions.create(
|
|
68
|
+
model=model_name,
|
|
69
|
+
messages=messages,
|
|
70
|
+
temperature=temperature,
|
|
71
|
+
max_completion_tokens=max_completion_tokens,
|
|
72
|
+
stream=False,
|
|
73
|
+
)
|
|
74
|
+
raw_output = response.choices[0].message.content or ""
|
|
75
|
+
parsed = parse_json_object(raw_output) or {}
|
|
76
|
+
|
|
77
|
+
usage = getattr(response, "usage", None)
|
|
78
|
+
token_usage = {
|
|
79
|
+
"prompt_tokens": int(getattr(usage, "prompt_tokens", 0) or 0),
|
|
80
|
+
"completion_tokens": int(getattr(usage, "completion_tokens", 0) or 0),
|
|
81
|
+
"total_tokens": int(getattr(usage, "total_tokens", 0) or 0),
|
|
82
|
+
}
|
|
83
|
+
return {"raw_output": raw_output, "parsed": parsed, "token_usage": token_usage}
|
|
84
|
+
|
|
85
|
+
def _one_hot(self, label: str) -> dict[str, float]:
|
|
86
|
+
return {candidate: (1.0 if candidate == label else 0.0) for candidate in self.labels}
|
|
87
|
+
|
|
88
|
+
def _normalize_role_judgment(self, parsed: Mapping[str, Any]) -> dict[str, Any]:
|
|
89
|
+
class_scores = parsed.get("class_scores")
|
|
90
|
+
conclusion = str(parsed.get("conclusion", "")).strip()
|
|
91
|
+
|
|
92
|
+
if isinstance(class_scores, Mapping):
|
|
93
|
+
prediction = resolve_classification_prediction(class_scores, valid_labels=self.labels)
|
|
94
|
+
label = prediction.label or (self.labels[0] if self.labels else "")
|
|
95
|
+
scores = normalize_probability_distribution(
|
|
96
|
+
{k: float(v) for k, v in prediction.class_scores.items()}
|
|
97
|
+
if prediction.class_scores
|
|
98
|
+
else self._one_hot(label)
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
prediction = resolve_classification_prediction(conclusion, valid_labels=self.labels)
|
|
102
|
+
label = prediction.label or (self.labels[0] if self.labels else "")
|
|
103
|
+
scores = self._one_hot(label)
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"analysis": str(parsed.get("analysis", "")).strip(),
|
|
107
|
+
"conclusion": label,
|
|
108
|
+
"class_scores": scores,
|
|
109
|
+
"confidence": parsed.get("confidence", None),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
def _aggregate_scores(self, role_reviews: list[dict[str, Any]]) -> dict[str, float]:
|
|
113
|
+
sums = dict.fromkeys(self.labels, 0.0)
|
|
114
|
+
if not role_reviews:
|
|
115
|
+
return sums
|
|
116
|
+
|
|
117
|
+
for review in role_reviews:
|
|
118
|
+
scores = review.get("class_scores", {})
|
|
119
|
+
for label in self.labels:
|
|
120
|
+
sums[label] += float(scores.get(label, 0.0))
|
|
121
|
+
|
|
122
|
+
return normalize_probability_distribution(sums)
|
|
123
|
+
|
|
124
|
+
def run_case(self, input_text: str) -> dict[str, Any]:
|
|
125
|
+
role_reviews: list[dict[str, Any]] = []
|
|
126
|
+
host_dialogue: list[dict[str, Any]] = []
|
|
127
|
+
total_prompt_tokens = 0
|
|
128
|
+
total_completion_tokens = 0
|
|
129
|
+
total_tokens = 0
|
|
130
|
+
|
|
131
|
+
labels_text = ", ".join(self.labels)
|
|
132
|
+
|
|
133
|
+
# 1) Independent role evaluations.
|
|
134
|
+
for role in self.role_configs:
|
|
135
|
+
call = self._call_json(
|
|
136
|
+
model_name=role.model_name,
|
|
137
|
+
messages=[
|
|
138
|
+
{
|
|
139
|
+
"role": "system",
|
|
140
|
+
"content": (
|
|
141
|
+
f"You are role: {role.name}. Persona: {role.persona}\n"
|
|
142
|
+
f"Task: {self.task_name}\n"
|
|
143
|
+
f"Allowed labels: {labels_text}\n"
|
|
144
|
+
f"Instructions: {self.task_instructions}\n"
|
|
145
|
+
"Return ONLY JSON with keys: analysis, conclusion, class_scores, confidence."
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
{"role": "user", "content": f"Case:\n\n{input_text}"},
|
|
149
|
+
],
|
|
150
|
+
temperature=role.temperature,
|
|
151
|
+
max_completion_tokens=role.max_completion_tokens,
|
|
152
|
+
)
|
|
153
|
+
normalized = self._normalize_role_judgment(call["parsed"])
|
|
154
|
+
role_reviews.append(
|
|
155
|
+
{
|
|
156
|
+
"role": role.name,
|
|
157
|
+
"model": role.model_name,
|
|
158
|
+
"raw_output": call["raw_output"],
|
|
159
|
+
**normalized,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
usage = call["token_usage"]
|
|
164
|
+
total_prompt_tokens += usage["prompt_tokens"]
|
|
165
|
+
total_completion_tokens += usage["completion_tokens"]
|
|
166
|
+
total_tokens += usage["total_tokens"]
|
|
167
|
+
|
|
168
|
+
# 2) Host iterative questioning (bounded).
|
|
169
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
170
|
+
host_call = self._call_json(
|
|
171
|
+
model_name=self.host_model_name,
|
|
172
|
+
messages=[
|
|
173
|
+
{
|
|
174
|
+
"role": "system",
|
|
175
|
+
"content": (
|
|
176
|
+
"You are the host coordinator for a multi-role classifier.\n"
|
|
177
|
+
f"Allowed labels: {labels_text}.\n"
|
|
178
|
+
"Decide either:\n"
|
|
179
|
+
"1) ask one role one follow-up question, or\n"
|
|
180
|
+
"2) finalize with a conclusion.\n"
|
|
181
|
+
"Return ONLY JSON with either:\n"
|
|
182
|
+
'- {"action":"ask","target_role":"...","question":"..."}\n'
|
|
183
|
+
"or\n"
|
|
184
|
+
'- {"action":"finalize","conclusion":"...","class_scores":{...},"rationale":"..."}.'
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"role": "user",
|
|
189
|
+
"content": (
|
|
190
|
+
f"Case:\n{input_text}\n\n"
|
|
191
|
+
f"Role reviews JSON:\n{role_reviews}\n\n"
|
|
192
|
+
f"Dialogue so far JSON:\n{host_dialogue}\n\n"
|
|
193
|
+
f"Iteration: {iteration}/{self.max_iterations}"
|
|
194
|
+
),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
temperature=self.host_temperature,
|
|
198
|
+
max_completion_tokens=self.host_max_completion_tokens,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
usage = host_call["token_usage"]
|
|
202
|
+
total_prompt_tokens += usage["prompt_tokens"]
|
|
203
|
+
total_completion_tokens += usage["completion_tokens"]
|
|
204
|
+
total_tokens += usage["total_tokens"]
|
|
205
|
+
|
|
206
|
+
action = str(host_call["parsed"].get("action", "")).strip().lower()
|
|
207
|
+
if action == "finalize":
|
|
208
|
+
final_scores_raw = host_call["parsed"].get("class_scores")
|
|
209
|
+
if isinstance(final_scores_raw, Mapping):
|
|
210
|
+
normalized_scores = normalize_probability_distribution(
|
|
211
|
+
{label: float(final_scores_raw.get(label, 0.0)) for label in self.labels}
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
label = (
|
|
215
|
+
resolve_classification_prediction(
|
|
216
|
+
host_call["parsed"].get("conclusion", ""),
|
|
217
|
+
valid_labels=self.labels,
|
|
218
|
+
).label
|
|
219
|
+
or self.labels[0]
|
|
220
|
+
)
|
|
221
|
+
normalized_scores = self._one_hot(label)
|
|
222
|
+
|
|
223
|
+
final_prediction = resolve_classification_prediction(
|
|
224
|
+
normalized_scores, valid_labels=self.labels
|
|
225
|
+
)
|
|
226
|
+
return {
|
|
227
|
+
"final_label": final_prediction.label,
|
|
228
|
+
"class_scores": normalized_scores,
|
|
229
|
+
"rationale": str(host_call["parsed"].get("rationale", "")).strip(),
|
|
230
|
+
"iterations_used": iteration,
|
|
231
|
+
"role_reviews": role_reviews,
|
|
232
|
+
"host_dialogue": host_dialogue,
|
|
233
|
+
"token_usage": {
|
|
234
|
+
"prompt_tokens": total_prompt_tokens,
|
|
235
|
+
"completion_tokens": total_completion_tokens,
|
|
236
|
+
"total_tokens": total_tokens,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if action != "ask":
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
target_role = str(host_call["parsed"].get("target_role", "")).strip()
|
|
244
|
+
question = str(host_call["parsed"].get("question", "")).strip()
|
|
245
|
+
if not question or target_role not in self.role_map:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
target_cfg = self.role_map[target_role]
|
|
249
|
+
role_reply = self._call_json(
|
|
250
|
+
model_name=target_cfg.model_name,
|
|
251
|
+
messages=[
|
|
252
|
+
{
|
|
253
|
+
"role": "system",
|
|
254
|
+
"content": (
|
|
255
|
+
f"You are role: {target_cfg.name}. Persona: {target_cfg.persona}\n"
|
|
256
|
+
f"Allowed labels: {labels_text}\n"
|
|
257
|
+
"Answer host questions. Return ONLY JSON with keys: answer, updated_conclusion, class_scores."
|
|
258
|
+
),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"role": "user",
|
|
262
|
+
"content": (
|
|
263
|
+
f"Case:\n{input_text}\n\n"
|
|
264
|
+
f"Host question:\n{question}\n\n"
|
|
265
|
+
f"Your original review context:\n{role_reviews}"
|
|
266
|
+
),
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
temperature=target_cfg.temperature,
|
|
270
|
+
max_completion_tokens=target_cfg.max_completion_tokens,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
usage = role_reply["token_usage"]
|
|
274
|
+
total_prompt_tokens += usage["prompt_tokens"]
|
|
275
|
+
total_completion_tokens += usage["completion_tokens"]
|
|
276
|
+
total_tokens += usage["total_tokens"]
|
|
277
|
+
|
|
278
|
+
host_dialogue.append(
|
|
279
|
+
{
|
|
280
|
+
"iteration": iteration,
|
|
281
|
+
"target_role": target_role,
|
|
282
|
+
"question": question,
|
|
283
|
+
"answer": role_reply["parsed"],
|
|
284
|
+
"raw_answer": role_reply["raw_output"],
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# 3) Fallback finalize if host never finalized.
|
|
289
|
+
fallback_scores = self._aggregate_scores(role_reviews)
|
|
290
|
+
fallback_prediction = resolve_classification_prediction(
|
|
291
|
+
fallback_scores, valid_labels=self.labels
|
|
292
|
+
)
|
|
293
|
+
return {
|
|
294
|
+
"final_label": fallback_prediction.label,
|
|
295
|
+
"class_scores": fallback_scores,
|
|
296
|
+
"rationale": "Fallback finalize from averaged role distributions.",
|
|
297
|
+
"iterations_used": self.max_iterations,
|
|
298
|
+
"role_reviews": role_reviews,
|
|
299
|
+
"host_dialogue": host_dialogue,
|
|
300
|
+
"token_usage": {
|
|
301
|
+
"prompt_tokens": total_prompt_tokens,
|
|
302
|
+
"completion_tokens": total_completion_tokens,
|
|
303
|
+
"total_tokens": total_tokens,
|
|
304
|
+
},
|
|
305
|
+
}
|
EvalRing/cli.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for EvalRing.
|
|
3
|
+
|
|
4
|
+
Installed as the ``evalring`` console script. Every subcommand is a thin
|
|
5
|
+
wrapper over the public Python API, so anything reachable here is also
|
|
6
|
+
reachable from code.
|
|
7
|
+
|
|
8
|
+
Run ``evalring --help`` for the full listing, or see ``docs/CLI.md``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import __version__
|
|
22
|
+
from .config import CREDENTIAL_ENV_VARS, resolve_credentials, resolve_model_name
|
|
23
|
+
from .dataset.base import BaseDataset
|
|
24
|
+
from .logging_utils import configure_logging, get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
SUPPORTED_DATASET_SUFFIXES = (".json", ".csv")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _load_dataset(
|
|
32
|
+
path: Path,
|
|
33
|
+
text_field: str,
|
|
34
|
+
label_field: str,
|
|
35
|
+
id_field: str | None,
|
|
36
|
+
) -> BaseDataset:
|
|
37
|
+
"""Load a dataset from disk, picking the reader from the file extension.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
path: Path to a ``.json`` or ``.csv`` dataset file.
|
|
41
|
+
text_field: Column or key holding the model input.
|
|
42
|
+
label_field: Column or key holding the ground-truth label.
|
|
43
|
+
id_field: Column or key holding a unique sample ID, or ``None`` to
|
|
44
|
+
fall back to the row index.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A loaded :class:`~EvalRing.dataset.base.BaseDataset` subclass instance.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the file extension has no registered reader.
|
|
51
|
+
"""
|
|
52
|
+
from .dataset import CSVDataset, JSONDataset
|
|
53
|
+
|
|
54
|
+
suffix = path.suffix.lower()
|
|
55
|
+
dataset: BaseDataset
|
|
56
|
+
if suffix == ".json":
|
|
57
|
+
dataset = JSONDataset(name=path.stem)
|
|
58
|
+
elif suffix == ".csv":
|
|
59
|
+
dataset = CSVDataset(name=path.stem)
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unsupported dataset extension {suffix!r}. "
|
|
63
|
+
f"Supported: {', '.join(SUPPORTED_DATASET_SUFFIXES)}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
dataset.load_data(
|
|
67
|
+
path,
|
|
68
|
+
text_field=text_field,
|
|
69
|
+
label_field=label_field,
|
|
70
|
+
id_field=id_field,
|
|
71
|
+
)
|
|
72
|
+
return dataset
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _cmd_info(args: argparse.Namespace) -> int:
|
|
76
|
+
"""Print dataset statistics as JSON.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
args: Parsed arguments for the ``info`` subcommand.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Process exit code: 0 on success, 1 when the dataset cannot be read.
|
|
83
|
+
"""
|
|
84
|
+
path = Path(args.dataset)
|
|
85
|
+
if not path.exists():
|
|
86
|
+
logger.error("Dataset not found: %s", path)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
dataset = _load_dataset(path, args.text_field, args.label_field, args.id_field)
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
logger.error("Failed to load %s: %s", path, exc)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
stats = dataset.get_statistics()
|
|
96
|
+
stats["valid"] = dataset.validate_data()
|
|
97
|
+
stats["path"] = str(path)
|
|
98
|
+
print(json.dumps(stats, indent=2, default=str))
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cmd_check(args: argparse.Namespace) -> int:
|
|
103
|
+
"""Report the resolved provider configuration without revealing secrets.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
args: Parsed arguments for the ``check`` subcommand.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Process exit code: 0 when a credential was resolved, 1 otherwise.
|
|
110
|
+
"""
|
|
111
|
+
credentials = resolve_credentials()
|
|
112
|
+
report = {
|
|
113
|
+
"evalring_version": __version__,
|
|
114
|
+
"python": sys.version.split()[0],
|
|
115
|
+
"api_key_found": bool(credentials.api_key),
|
|
116
|
+
"api_key_source": credentials.source,
|
|
117
|
+
"provider": credentials.provider,
|
|
118
|
+
"base_url": credentials.base_url or "<provider default>",
|
|
119
|
+
"model": resolve_model_name(default="<unset>"),
|
|
120
|
+
"recognized_key_variables": list(CREDENTIAL_ENV_VARS),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# find_spec only inspects the import system, so this stays fast even when
|
|
124
|
+
# a heavyweight optional package such as litellm is installed.
|
|
125
|
+
report["optional_packages"] = {
|
|
126
|
+
module: importlib.util.find_spec(module) is not None
|
|
127
|
+
for module in ("litellm", "openai", "matplotlib", "nest_asyncio")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
print(json.dumps(report, indent=2))
|
|
131
|
+
if not credentials.api_key:
|
|
132
|
+
logger.warning("No API key resolved. Set one of: %s", ", ".join(CREDENTIAL_ENV_VARS))
|
|
133
|
+
return 1
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cmd_run_suite(args: argparse.Namespace) -> int:
|
|
138
|
+
"""Run an evaluation script across every model in a model list.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
args: Parsed arguments for the ``run-suite`` subcommand.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Process exit code propagated from
|
|
145
|
+
:func:`EvalRing.utils.suite_runner.run_suite`.
|
|
146
|
+
"""
|
|
147
|
+
from .utils.suite_runner import run_suite
|
|
148
|
+
|
|
149
|
+
return run_suite(
|
|
150
|
+
eval_script=args.eval_script,
|
|
151
|
+
model_list_path=args.models_file,
|
|
152
|
+
n_samples=args.n_samples,
|
|
153
|
+
max_workers=args.max_workers,
|
|
154
|
+
seed=args.seed,
|
|
155
|
+
agent_mode=args.agent_mode,
|
|
156
|
+
base_class=args.base_class,
|
|
157
|
+
host_model=args.host_model,
|
|
158
|
+
role_models_json=args.role_models_json,
|
|
159
|
+
max_host_iterations=args.max_host_iterations,
|
|
160
|
+
continue_runs=args.continue_runs,
|
|
161
|
+
cache=args.cache,
|
|
162
|
+
cache_mode=args.cache_mode,
|
|
163
|
+
ignore_errors=args.ignore_errors,
|
|
164
|
+
out_dir=args.out_dir,
|
|
165
|
+
data_path=args.data_path,
|
|
166
|
+
no_confirm=args.yes,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _cmd_models(args: argparse.Namespace) -> int:
|
|
171
|
+
"""Generate a model-list JSON file from the OpenRouter catalogue.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
args: Parsed arguments for the ``models`` subcommand.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Process exit code: 0 on success, 1 on failure.
|
|
178
|
+
"""
|
|
179
|
+
from .utils.generate_model_list import generate_model_list
|
|
180
|
+
|
|
181
|
+
output = Path(args.output)
|
|
182
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
try:
|
|
184
|
+
generate_model_list(output)
|
|
185
|
+
except Exception as exc:
|
|
186
|
+
logger.error("Failed to generate model list: %s", exc)
|
|
187
|
+
return 1
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
192
|
+
"""Construct the top-level argument parser.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The fully configured :class:`argparse.ArgumentParser`.
|
|
196
|
+
"""
|
|
197
|
+
parser = argparse.ArgumentParser(
|
|
198
|
+
prog="evalring",
|
|
199
|
+
description="Evaluate agents and models across providers.",
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument("--version", action="version", version=f"evalring {__version__}")
|
|
202
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging.")
|
|
203
|
+
parser.add_argument("-q", "--quiet", action="store_true", help="Only log warnings and errors.")
|
|
204
|
+
|
|
205
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
206
|
+
|
|
207
|
+
info = sub.add_parser("info", help="Show statistics for a dataset file.")
|
|
208
|
+
info.add_argument("--dataset", required=True, help="Path to a .json or .csv dataset.")
|
|
209
|
+
info.add_argument("--text-field", default="text", help="Input text column/key.")
|
|
210
|
+
info.add_argument("--label-field", default="label", help="Ground-truth column/key.")
|
|
211
|
+
info.add_argument("--id-field", default=None, help="Unique sample ID column/key.")
|
|
212
|
+
info.set_defaults(func=_cmd_info)
|
|
213
|
+
|
|
214
|
+
check = sub.add_parser(
|
|
215
|
+
"check",
|
|
216
|
+
help="Report the resolved provider configuration (no secrets printed).",
|
|
217
|
+
)
|
|
218
|
+
check.set_defaults(func=_cmd_check)
|
|
219
|
+
|
|
220
|
+
models = sub.add_parser("models", help="Generate a model-list JSON file.")
|
|
221
|
+
models.add_argument(
|
|
222
|
+
"--output",
|
|
223
|
+
default="model_list.json",
|
|
224
|
+
help="Destination path for the model list.",
|
|
225
|
+
)
|
|
226
|
+
models.set_defaults(func=_cmd_models)
|
|
227
|
+
|
|
228
|
+
suite = sub.add_parser(
|
|
229
|
+
"run-suite",
|
|
230
|
+
help="Run an evaluation script across every model in a model list.",
|
|
231
|
+
)
|
|
232
|
+
suite.add_argument("--eval-script", required=True, help="Path to the evaluation script.")
|
|
233
|
+
suite.add_argument("--models-file", required=True, help="Path to the model list JSON.")
|
|
234
|
+
suite.add_argument("--n-samples", type=int, default=10)
|
|
235
|
+
suite.add_argument("--max-workers", type=int, default=5)
|
|
236
|
+
suite.add_argument("--seed", type=int, default=42)
|
|
237
|
+
suite.add_argument("--agent-mode", default="single-class")
|
|
238
|
+
suite.add_argument("--base-class", default=None)
|
|
239
|
+
suite.add_argument("--host-model", default=None)
|
|
240
|
+
suite.add_argument("--role-models-json", default=None)
|
|
241
|
+
suite.add_argument("--max-host-iterations", type=int, default=10)
|
|
242
|
+
suite.add_argument("--continue-runs", action="store_true")
|
|
243
|
+
suite.add_argument("--cache", default=None)
|
|
244
|
+
suite.add_argument(
|
|
245
|
+
"--cache-mode",
|
|
246
|
+
default="both",
|
|
247
|
+
choices=["runs_only", "cache_file", "both", "none"],
|
|
248
|
+
)
|
|
249
|
+
suite.add_argument("--ignore-errors", action="store_true")
|
|
250
|
+
suite.add_argument("--out-dir", default=None)
|
|
251
|
+
suite.add_argument("--data-path", default=None)
|
|
252
|
+
suite.add_argument(
|
|
253
|
+
"-y", "--yes", action="store_true", help="Skip the pre-run cache confirmation."
|
|
254
|
+
)
|
|
255
|
+
suite.set_defaults(func=_cmd_run_suite)
|
|
256
|
+
|
|
257
|
+
return parser
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
261
|
+
"""Entry point for the ``evalring`` console script.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
argv: Argument vector to parse. Defaults to ``sys.argv[1:]``.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Process exit code.
|
|
268
|
+
"""
|
|
269
|
+
parser = build_parser()
|
|
270
|
+
args = parser.parse_args(argv)
|
|
271
|
+
|
|
272
|
+
if args.verbose:
|
|
273
|
+
level = logging.DEBUG
|
|
274
|
+
elif args.quiet:
|
|
275
|
+
level = logging.WARNING
|
|
276
|
+
else:
|
|
277
|
+
level = logging.INFO
|
|
278
|
+
configure_logging(level, verbose=args.verbose)
|
|
279
|
+
|
|
280
|
+
return int(args.func(args) or 0)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__": # pragma: no cover
|
|
284
|
+
sys.exit(main())
|