evalport-sdk 1.0.0__tar.gz
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.
- evalport_sdk-1.0.0/PKG-INFO +6 -0
- evalport_sdk-1.0.0/README.md +87 -0
- evalport_sdk-1.0.0/evalport_sdk.egg-info/PKG-INFO +6 -0
- evalport_sdk-1.0.0/evalport_sdk.egg-info/SOURCES.txt +14 -0
- evalport_sdk-1.0.0/evalport_sdk.egg-info/dependency_links.txt +1 -0
- evalport_sdk-1.0.0/evalport_sdk.egg-info/top_level.txt +3 -0
- evalport_sdk-1.0.0/openeval/__init__.py +3 -0
- evalport_sdk-1.0.0/openeval/convert.py +39 -0
- evalport_sdk-1.0.0/openeval/converters_deepeval.py +117 -0
- evalport_sdk-1.0.0/openeval/converters_inspect.py +73 -0
- evalport_sdk-1.0.0/openeval/converters_openai.py +78 -0
- evalport_sdk-1.0.0/openeval/types.py +80 -0
- evalport_sdk-1.0.0/openeval/validate.py +134 -0
- evalport_sdk-1.0.0/pyproject.toml +13 -0
- evalport_sdk-1.0.0/setup.cfg +4 -0
- evalport_sdk-1.0.0/tests/test_validate.py +18 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# openeval
|
|
2
|
+
|
|
3
|
+
Python SDK for EvalPort — The Open Evaluation Standard.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install openeval
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Validate a suite
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from openeval.validate import validate_suite
|
|
17
|
+
|
|
18
|
+
result = validate_suite({
|
|
19
|
+
"version": "1.0.0",
|
|
20
|
+
"id": "my_suite",
|
|
21
|
+
"graders": [{"id": "gr1", "type": "exact_match"}],
|
|
22
|
+
"test_cases": [{"id": "tc1", "input": "Hello", "expected_output": "Hi", "graders": ["gr1"]}]
|
|
23
|
+
})
|
|
24
|
+
print(result.valid) # True
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Convert from Promptfoo
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from openeval.convert import from_promptfoo
|
|
31
|
+
|
|
32
|
+
suite = from_promptfoo(promptfoo_config)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Convert from DeepEval
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from openeval.converters_deepeval import from_deepeval
|
|
39
|
+
|
|
40
|
+
suite = from_deepeval(deepeval_export)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Convert from Inspect AI
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from openeval.converters_inspect import from_inspect
|
|
47
|
+
|
|
48
|
+
suite = from_inspect(inspect_data)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Convert from OpenAI Evals
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from openeval.converters_openai import from_openai_evals
|
|
55
|
+
|
|
56
|
+
suite = from_openai_evals(evals_data)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Compute summary
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from openeval.convert import compute_summary, create_result_set
|
|
63
|
+
|
|
64
|
+
summary = compute_summary(results)
|
|
65
|
+
result_set = create_result_set(suite, results, "run_001")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
### Validation
|
|
71
|
+
- `validate_suite(doc)` → `ValidationResult`
|
|
72
|
+
- `validate_test_case(doc)` → `ValidationResult`
|
|
73
|
+
- `validate_grader(doc)` → `ValidationResult`
|
|
74
|
+
- `validate_result_set(doc)` → `ValidationResult`
|
|
75
|
+
- `validate_document(doc, type)` → `ValidationResult`
|
|
76
|
+
|
|
77
|
+
### Conversion
|
|
78
|
+
- `from_promptfoo(config)` → `dict`
|
|
79
|
+
- `from_deepeval(data)` → `dict` (from `converters_deepeval`)
|
|
80
|
+
- `from_inspect(data)` → `dict` (from `converters_inspect`)
|
|
81
|
+
- `from_openai_evals(data)` → `dict` (from `converters_openai`)
|
|
82
|
+
- `compute_summary(results)` → `dict`
|
|
83
|
+
- `create_result_set(suite, results, run_id)` → `dict`
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
Apache 2.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
evalport_sdk.egg-info/PKG-INFO
|
|
4
|
+
evalport_sdk.egg-info/SOURCES.txt
|
|
5
|
+
evalport_sdk.egg-info/dependency_links.txt
|
|
6
|
+
evalport_sdk.egg-info/top_level.txt
|
|
7
|
+
openeval/__init__.py
|
|
8
|
+
openeval/convert.py
|
|
9
|
+
openeval/converters_deepeval.py
|
|
10
|
+
openeval/converters_inspect.py
|
|
11
|
+
openeval/converters_openai.py
|
|
12
|
+
openeval/types.py
|
|
13
|
+
openeval/validate.py
|
|
14
|
+
tests/test_validate.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .types import OPENEVAL_VERSION
|
|
2
|
+
|
|
3
|
+
def from_promptfoo(pf):
|
|
4
|
+
tests = pf.get("tests", [])
|
|
5
|
+
providers = pf.get("providers", [])
|
|
6
|
+
graders = []
|
|
7
|
+
tcs = []
|
|
8
|
+
for i, t in enumerate(tests):
|
|
9
|
+
v = t.get("vars", {})
|
|
10
|
+
asserts = t.get("assert", [])
|
|
11
|
+
tg = []
|
|
12
|
+
for j, a in enumerate(asserts):
|
|
13
|
+
gid = f"gr_{i}_{j}"
|
|
14
|
+
at = a.get("type", "")
|
|
15
|
+
if at == "equals": graders.append({"id": gid, "type": "exact_match"})
|
|
16
|
+
elif at == "contains": graders.append({"id": gid, "type": "contains", "params": {"substring": str(a.get("value", ""))}})
|
|
17
|
+
else: graders.append({"id": gid, "type": "custom", "params": {"handler": f"promptfoo:{at}"}})
|
|
18
|
+
tg.append(gid)
|
|
19
|
+
inp = v.get("query") or v.get("prompt") or str(v)
|
|
20
|
+
tc = {"id": f"tc_{i}", "input": inp, "graders": tg if tg else ["gr_default"]}
|
|
21
|
+
if "expected" in v: tc["expected_output"] = v["expected"]
|
|
22
|
+
tcs.append(tc)
|
|
23
|
+
if not graders: graders = [{"id": "gr_default", "type": "exact_match"}]
|
|
24
|
+
cfg = {}
|
|
25
|
+
if providers and isinstance(providers[0], dict):
|
|
26
|
+
p = providers[0]
|
|
27
|
+
cfg = {"provider": {k: v for k, v in [("model", p.get("model"))] if v is not None}}
|
|
28
|
+
return {"version": OPENEVAL_VERSION, "id": "suite_promptfoo_import", "name": "Imported from Promptfoo", "graders": graders, "test_cases": tcs, "config": cfg}
|
|
29
|
+
|
|
30
|
+
def compute_summary(results):
|
|
31
|
+
total = len(results)
|
|
32
|
+
passed = sum(1 for r in results if r.get("passed"))
|
|
33
|
+
scores = [gr["score"] for r in results for gr in r.get("grader_results", []) if gr.get("score") is not None]
|
|
34
|
+
return {"total": total, "passed": passed, "failed": total - passed, "pass_rate": passed / total if total else 0, "avg_score": sum(scores) / len(scores) if scores else 0}
|
|
35
|
+
|
|
36
|
+
def create_result_set(suite, results, run_id, runner_name="evalport-sdk", runner_version="1.0.0"):
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
39
|
+
return {"version": OPENEVAL_VERSION, "suite_id": suite["id"], "suite_version": suite.get("version"), "run_id": run_id, "started_at": now, "completed_at": now, "provider": suite.get("config", {}).get("provider"), "runner": {"name": runner_name, "version": runner_version}, "results": results, "summary": compute_summary(results)}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""DeepEval to EvalPort converter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from .types import OPENEVAL_VERSION
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def from_deepeval(de: Dict) -> Dict:
|
|
8
|
+
tests = de.get("test_cases", [])
|
|
9
|
+
graders: List[Dict] = []
|
|
10
|
+
tcs: List[Dict] = []
|
|
11
|
+
|
|
12
|
+
for i, tc in enumerate(tests):
|
|
13
|
+
metrics = tc.get("metrics", [])
|
|
14
|
+
tc_graders: List[str] = []
|
|
15
|
+
|
|
16
|
+
for j, metric in enumerate(metrics):
|
|
17
|
+
gid = f"gr_{i}_{j}"
|
|
18
|
+
graders.append(_deepeval_metric_to_grader(gid, metric))
|
|
19
|
+
tc_graders.append(gid)
|
|
20
|
+
|
|
21
|
+
inp = tc.get("input", "")
|
|
22
|
+
if isinstance(inp, list):
|
|
23
|
+
inp = inp
|
|
24
|
+
|
|
25
|
+
new_tc = {
|
|
26
|
+
"id": tc.get("id", f"tc_{i}"),
|
|
27
|
+
"input": inp,
|
|
28
|
+
"graders": tc_graders if tc_graders else ["gr_default"],
|
|
29
|
+
}
|
|
30
|
+
if "expected_output" in tc:
|
|
31
|
+
new_tc["expected_output"] = tc["expected_output"]
|
|
32
|
+
if "context" in tc and isinstance(tc["context"], list):
|
|
33
|
+
new_tc["context"] = tc["context"]
|
|
34
|
+
if "retrieval_context" in tc and isinstance(tc["retrieval_context"], list):
|
|
35
|
+
new_tc["retrieval_context"] = tc["retrieval_context"]
|
|
36
|
+
if "metadata" in tc:
|
|
37
|
+
new_tc["metadata"] = tc["metadata"]
|
|
38
|
+
if "expected_tools" in tc:
|
|
39
|
+
new_tc["expected_tools"] = tc["expected_tools"]
|
|
40
|
+
|
|
41
|
+
tcs.append(new_tc)
|
|
42
|
+
|
|
43
|
+
if not graders:
|
|
44
|
+
graders = [{"id": "gr_default", "type": "exact_match"}]
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"version": OPENEVAL_VERSION,
|
|
48
|
+
"id": "suite_deepeval_import",
|
|
49
|
+
"name": "Imported from DeepEval",
|
|
50
|
+
"graders": graders,
|
|
51
|
+
"test_cases": tcs,
|
|
52
|
+
"metadata": {"openeval": {"source": "deepeval"}},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _deepeval_metric_to_grader(gid: str, metric: str) -> Dict:
|
|
57
|
+
m = metric.lower() if isinstance(metric, str) else str(metric).lower()
|
|
58
|
+
if "faithfulness" in m:
|
|
59
|
+
return {
|
|
60
|
+
"id": gid,
|
|
61
|
+
"type": "llm_judge",
|
|
62
|
+
"description": "Faithfulness (DeepEval)",
|
|
63
|
+
"params": {
|
|
64
|
+
"model": "gpt-4o",
|
|
65
|
+
"prompt": "Given context: {context}\nOutput: {output}\nIs the output faithful to the context? Return JSON: {\"score\": 0.0-1.0, \"reason\": \"...\"}",
|
|
66
|
+
"schema": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"properties": {"score": {"type": "number"}, "reason": {"type": "string"}},
|
|
69
|
+
"required": ["score", "reason"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
if "answerrelevancy" in m or "answer_relevancy" in m:
|
|
74
|
+
return {"id": gid, "type": "semantic_similarity", "params": {"threshold": 0.5}}
|
|
75
|
+
if "exactmatch" in m or "exact_match" in m:
|
|
76
|
+
return {"id": gid, "type": "exact_match"}
|
|
77
|
+
if "contextualprecision" in m or "contextual_precision" in m:
|
|
78
|
+
return {
|
|
79
|
+
"id": gid,
|
|
80
|
+
"type": "llm_judge",
|
|
81
|
+
"description": "Contextual Precision (DeepEval)",
|
|
82
|
+
"params": {
|
|
83
|
+
"model": "gpt-4o",
|
|
84
|
+
"prompt": "Given retrieval context: {context}\nExpected: {expected}\nIs the retrieved context relevant? Return JSON: {\"score\": 0.0-1.0}",
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
if "contextualrecall" in m or "contextual_recall" in m:
|
|
88
|
+
return {
|
|
89
|
+
"id": gid,
|
|
90
|
+
"type": "llm_judge",
|
|
91
|
+
"description": "Contextual Recall (DeepEval)",
|
|
92
|
+
"params": {
|
|
93
|
+
"model": "gpt-4o",
|
|
94
|
+
"prompt": "Given context: {context}\nExpected: {expected}\nIs the expected information in the context? Return JSON: {\"score\": 0.0-1.0}",
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
if "hallucination" in m:
|
|
98
|
+
return {
|
|
99
|
+
"id": gid,
|
|
100
|
+
"type": "llm_judge",
|
|
101
|
+
"description": "Hallucination Detection (DeepEval)",
|
|
102
|
+
"params": {
|
|
103
|
+
"model": "gpt-4o",
|
|
104
|
+
"prompt": "Given context: {context}\nOutput: {output}\nDoes the output contain hallucinated information? Return JSON: {\"score\": 0.0 (no hallucination) - 1.0 (full hallucination)}",
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
if "toxicity" in m:
|
|
108
|
+
return {
|
|
109
|
+
"id": gid,
|
|
110
|
+
"type": "llm_judge",
|
|
111
|
+
"description": "Toxicity Detection (DeepEval)",
|
|
112
|
+
"params": {
|
|
113
|
+
"model": "gpt-4o",
|
|
114
|
+
"prompt": "Evaluate the toxicity of: {output}. Return JSON: {\"score\": 0.0 (safe) - 1.0 (toxic)}",
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
return {"id": gid, "type": "custom", "params": {"handler": f"deepeval:{metric}"}}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Inspect AI to EvalPort converter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from .types import OPENEVAL_VERSION
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def from_inspect(data: Dict) -> Dict:
|
|
8
|
+
task_name = data.get("task", "inspect_import")
|
|
9
|
+
samples = data.get("samples", [])
|
|
10
|
+
scorers = data.get("scorers", ["exact"])
|
|
11
|
+
|
|
12
|
+
graders: List[Dict] = []
|
|
13
|
+
for i, scorer in enumerate(scorers):
|
|
14
|
+
gid = f"gr_{i}"
|
|
15
|
+
graders.append(_inspect_scorer_to_grader(gid, scorer))
|
|
16
|
+
|
|
17
|
+
if not graders:
|
|
18
|
+
graders = [{"id": "gr_0", "type": "exact_match"}]
|
|
19
|
+
|
|
20
|
+
grader_ids = [g["id"] for g in graders]
|
|
21
|
+
|
|
22
|
+
tcs: List[Dict] = []
|
|
23
|
+
for sample in samples:
|
|
24
|
+
tc = {
|
|
25
|
+
"id": sample.get("id", f"tc_{len(tcs)}"),
|
|
26
|
+
"input": sample.get("input", ""),
|
|
27
|
+
"graders": grader_ids,
|
|
28
|
+
}
|
|
29
|
+
if "target" in sample:
|
|
30
|
+
tc["expected_output"] = str(sample["target"])
|
|
31
|
+
if "context" in sample:
|
|
32
|
+
tc["context"] = sample["context"] if isinstance(sample["context"], list) else [sample["context"]]
|
|
33
|
+
if "metadata" in sample:
|
|
34
|
+
tc["metadata"] = sample["metadata"]
|
|
35
|
+
tcs.append(tc)
|
|
36
|
+
|
|
37
|
+
config = {}
|
|
38
|
+
if "model" in data:
|
|
39
|
+
config = {"provider": {"model": data["model"]}}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"version": OPENEVAL_VERSION,
|
|
43
|
+
"id": f"suite_inspect_{task_name}",
|
|
44
|
+
"name": f"Imported from Inspect AI: {task_name}",
|
|
45
|
+
"graders": graders,
|
|
46
|
+
"test_cases": tcs,
|
|
47
|
+
"config": config,
|
|
48
|
+
"metadata": {"openeval": {"source": "inspect_ai"}},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _inspect_scorer_to_grader(gid: str, scorer: str) -> Dict:
|
|
53
|
+
s = scorer.lower() if isinstance(scorer, str) else str(scorer).lower()
|
|
54
|
+
if "exact" in s:
|
|
55
|
+
return {"id": gid, "type": "exact_match"}
|
|
56
|
+
if "pattern" in s or "regex" in s:
|
|
57
|
+
return {"id": gid, "type": "regex", "params": {"pattern": ".*"}}
|
|
58
|
+
if "includes" in s or "contains" in s:
|
|
59
|
+
return {"id": gid, "type": "contains", "params": {"substring": ""}}
|
|
60
|
+
if "json" in s:
|
|
61
|
+
return {"id": gid, "type": "json_schema", "params": {"schema": {"type": "object"}}}
|
|
62
|
+
if "model_graded" in s or "modelgraded" in s or "llm" in s:
|
|
63
|
+
return {
|
|
64
|
+
"id": gid,
|
|
65
|
+
"type": "llm_judge",
|
|
66
|
+
"params": {
|
|
67
|
+
"model": "gpt-4o",
|
|
68
|
+
"prompt": "Evaluate if {output} is correct for {input}. Expected: {expected}. Return JSON: {\"score\": 0.0-1.0}",
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
if "manual" in s or "human" in s:
|
|
72
|
+
return {"id": gid, "type": "human", "params": {"instructions": "Review the output manually."}}
|
|
73
|
+
return {"id": gid, "type": "custom", "params": {"handler": f"inspect:{scorer}"}}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""OpenAI Evals to EvalPort converter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from .types import OPENEVAL_VERSION
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def from_openai_evals(data: Dict) -> Dict:
|
|
8
|
+
eval_id = data.get("id", "openai_eval_import")
|
|
9
|
+
test_data = data.get("test_data", [])
|
|
10
|
+
eval_config = data.get("config", {})
|
|
11
|
+
sampling = eval_config.get("sampling", {})
|
|
12
|
+
grader_spec = eval_config.get("grader", {})
|
|
13
|
+
|
|
14
|
+
graders: List[Dict] = []
|
|
15
|
+
if grader_spec:
|
|
16
|
+
graders.append(_openai_grader_to_openeval("gr_0", grader_spec))
|
|
17
|
+
if not graders:
|
|
18
|
+
graders = [{"id": "gr_0", "type": "exact_match"}]
|
|
19
|
+
|
|
20
|
+
grader_ids = [g["id"] for g in graders]
|
|
21
|
+
|
|
22
|
+
tcs: List[Dict] = []
|
|
23
|
+
for i, test in enumerate(test_data):
|
|
24
|
+
tc = {
|
|
25
|
+
"id": test.get("id", f"tc_{i}"),
|
|
26
|
+
"input": test.get("input", test.get("prompt", "")),
|
|
27
|
+
"graders": grader_ids,
|
|
28
|
+
}
|
|
29
|
+
if "target" in test:
|
|
30
|
+
tc["expected_output"] = str(test["target"])
|
|
31
|
+
elif "ideal" in test:
|
|
32
|
+
tc["expected_output"] = str(test["ideal"])
|
|
33
|
+
if "context" in test:
|
|
34
|
+
tc["context"] = test["context"] if isinstance(test["context"], list) else [test["context"]]
|
|
35
|
+
if "metadata" in test:
|
|
36
|
+
tc["metadata"] = test["metadata"]
|
|
37
|
+
tcs.append(tc)
|
|
38
|
+
|
|
39
|
+
config = {}
|
|
40
|
+
if "model" in sampling:
|
|
41
|
+
config = {"provider": {"model": sampling["model"]}}
|
|
42
|
+
if "temperature" in sampling:
|
|
43
|
+
config.setdefault("provider", {})["temperature"] = sampling["temperature"]
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"version": OPENEVAL_VERSION,
|
|
47
|
+
"id": f"suite_{eval_id}",
|
|
48
|
+
"name": f"Imported from OpenAI Evals: {eval_id}",
|
|
49
|
+
"graders": graders,
|
|
50
|
+
"test_cases": tcs,
|
|
51
|
+
"config": config,
|
|
52
|
+
"metadata": {"openeval": {"source": "openai_evals"}},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _openai_grader_to_openeval(gid: str, spec: Dict) -> Dict:
|
|
57
|
+
gtype = spec.get("type", spec.get("name", ""))
|
|
58
|
+
gtype_lower = gtype.lower() if isinstance(gtype, str) else str(gtype).lower()
|
|
59
|
+
|
|
60
|
+
if "exact" in gtype_lower or "match" in gtype_lower:
|
|
61
|
+
return {"id": gid, "type": "exact_match"}
|
|
62
|
+
if "includes" in gtype_lower or "contains" in gtype_lower:
|
|
63
|
+
return {"id": gid, "type": "contains", "params": {"substring": spec.get("substring", "")}}
|
|
64
|
+
if "regex" in gtype_lower or "pattern" in gtype_lower:
|
|
65
|
+
return {"id": gid, "type": "regex", "params": {"pattern": spec.get("pattern", ".*")}}
|
|
66
|
+
if "json" in gtype_lower:
|
|
67
|
+
return {"id": gid, "type": "json_schema", "params": {"schema": spec.get("schema", {"type": "object"})}}
|
|
68
|
+
if "model_graded" in gtype_lower or "modelgraded" in gtype_lower or "llm" in gtype_lower:
|
|
69
|
+
prompt = spec.get("prompt", spec.get("instructions", "Evaluate if {output} is correct. Return JSON: {\"score\": 0.0-1.0}"))
|
|
70
|
+
return {
|
|
71
|
+
"id": gid,
|
|
72
|
+
"type": "model graded",
|
|
73
|
+
"params": {
|
|
74
|
+
"model": spec.get("model", "gpt-4o"),
|
|
75
|
+
"prompt": prompt,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
return {"id": gid, "type": "custom", "params": {"handler": f"openai_evals:{gtype}"}}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Literal, Union, Optional, List, Dict
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
OPENEVAL_VERSION = "1.0.0"
|
|
6
|
+
|
|
7
|
+
GraderType = Literal["exact_match","contains","regex","semantic_similarity","llm_judge","json_schema","json_path","code","human","model graded","custom"]
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Grader:
|
|
11
|
+
id: str
|
|
12
|
+
type: GraderType
|
|
13
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
weight: float = 1.0
|
|
15
|
+
description: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TestCase:
|
|
19
|
+
id: str
|
|
20
|
+
input: Union[str, List[str]]
|
|
21
|
+
graders: List[Union[str, Grader]]
|
|
22
|
+
expected_output: Optional[str] = None
|
|
23
|
+
context: Optional[List[str]] = None
|
|
24
|
+
retrieval_context: Optional[List[str]] = None
|
|
25
|
+
tools_called: Optional[List[str]] = None
|
|
26
|
+
expected_tools: Optional[List[str]] = None
|
|
27
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
tags: List[str] = field(default_factory=list)
|
|
29
|
+
timeout_ms: Optional[int] = None
|
|
30
|
+
weight: float = 1.0
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class EvalSuite:
|
|
34
|
+
version: str
|
|
35
|
+
id: str
|
|
36
|
+
test_cases: List[TestCase]
|
|
37
|
+
name: Optional[str] = None
|
|
38
|
+
description: Optional[str] = None
|
|
39
|
+
graders: List[Grader] = field(default_factory=list)
|
|
40
|
+
config: Dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
tags: List[str] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class GraderResult:
|
|
46
|
+
grader_id: str
|
|
47
|
+
type: str
|
|
48
|
+
score: Optional[float]
|
|
49
|
+
passed: bool
|
|
50
|
+
reason: Optional[str] = None
|
|
51
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Result:
|
|
55
|
+
test_case_id: str
|
|
56
|
+
passed: bool
|
|
57
|
+
grader_results: List[GraderResult]
|
|
58
|
+
actual_output: Optional[str] = None
|
|
59
|
+
duration_ms: Optional[int] = None
|
|
60
|
+
error: Optional[Dict[str, Any]] = None
|
|
61
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ResultSet:
|
|
65
|
+
version: str
|
|
66
|
+
suite_id: str
|
|
67
|
+
run_id: str
|
|
68
|
+
started_at: str
|
|
69
|
+
results: List[Result]
|
|
70
|
+
suite_version: Optional[str] = None
|
|
71
|
+
completed_at: Optional[str] = None
|
|
72
|
+
provider: Optional[Dict[str, Any]] = None
|
|
73
|
+
runner: Optional[Dict[str, str]] = None
|
|
74
|
+
summary: Optional[Dict[str, Any]] = None
|
|
75
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ValidationResult:
|
|
79
|
+
valid: bool
|
|
80
|
+
errors: List[Dict[str, str]] = field(default_factory=list)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, List, Dict
|
|
3
|
+
from .types import ValidationResult
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
STANDARD_GRADER_TYPES = {"exact_match","contains","regex","semantic_similarity","llm_judge","json_schema","json_path","code","human","model graded","custom"}
|
|
7
|
+
|
|
8
|
+
def _err(p,m,c): return {"path":p,"message":m,"code":c}
|
|
9
|
+
|
|
10
|
+
def validate_test_case(tc):
|
|
11
|
+
errors=[]
|
|
12
|
+
if not isinstance(tc,dict): return ValidationResult(False,[_err("$","Must be object","TYPE_ERROR")])
|
|
13
|
+
if not isinstance(tc.get("id"),str) or not tc["id"]: errors.append(_err("$.id","id required","REQUIRED"))
|
|
14
|
+
inp=tc.get("input")
|
|
15
|
+
if not isinstance(inp,str) and not (isinstance(inp,list) and all(isinstance(x,str) for x in inp)): errors.append(_err("$.input","input required","REQUIRED"))
|
|
16
|
+
elif isinstance(inp,list) and not inp: errors.append(_err("$.input","empty","MIN_ITEMS"))
|
|
17
|
+
gr=tc.get("graders")
|
|
18
|
+
if not isinstance(gr,list) or not gr: errors.append(_err("$.graders","graders required","REQUIRED"))
|
|
19
|
+
else:
|
|
20
|
+
for i,g in enumerate(gr):
|
|
21
|
+
if isinstance(g,str):
|
|
22
|
+
if not g: errors.append(_err(f"$.graders[{i}]","empty","EMPTY_STRING"))
|
|
23
|
+
elif isinstance(g,dict):
|
|
24
|
+
gv=validate_grader(g)
|
|
25
|
+
if not gv.valid:
|
|
26
|
+
for e in gv.errors: errors.append(_err(f"$.graders[{i}].{e['path']}",e["message"],e["code"]))
|
|
27
|
+
else: errors.append(_err(f"$.graders[{i}]","must be string or object","TYPE_ERROR"))
|
|
28
|
+
return ValidationResult(not errors,errors)
|
|
29
|
+
|
|
30
|
+
def validate_grader(g):
|
|
31
|
+
errors=[]
|
|
32
|
+
if not isinstance(g,dict): return ValidationResult(False,[_err("$","Must be object","TYPE_ERROR")])
|
|
33
|
+
if not isinstance(g.get("id"),str) or not g["id"]: errors.append(_err("$.id","id required","REQUIRED"))
|
|
34
|
+
gt=g.get("type")
|
|
35
|
+
if not isinstance(gt,str): errors.append(_err("$.type","type required","REQUIRED"))
|
|
36
|
+
elif gt not in STANDARD_GRADER_TYPES: errors.append(_err("$.type",f"Unknown: {gt}","UNKNOWN_TYPE"))
|
|
37
|
+
else:
|
|
38
|
+
p=g.get("params") or {}
|
|
39
|
+
for e in _vp(gt,p): errors.append(_err(f"$.params.{e['path']}",e["message"],e["code"]))
|
|
40
|
+
return ValidationResult(not errors,errors)
|
|
41
|
+
|
|
42
|
+
def _vp(t,p):
|
|
43
|
+
e=[]
|
|
44
|
+
if t=="contains":
|
|
45
|
+
if not isinstance(p.get("substring"),str) or not p["substring"]: e.append(_err("substring","required","REQUIRED"))
|
|
46
|
+
elif t=="regex":
|
|
47
|
+
if not isinstance(p.get("pattern"),str) or not p["pattern"]: e.append(_err("pattern","required","REQUIRED"))
|
|
48
|
+
elif t=="semantic_similarity":
|
|
49
|
+
th=p.get("threshold")
|
|
50
|
+
if not isinstance(th,(int,float)) or th<0 or th>1: e.append(_err("threshold","0-1","OUT_OF_RANGE"))
|
|
51
|
+
elif t=="llm_judge":
|
|
52
|
+
if not isinstance(p.get("model"),str) or not p["model"]: e.append(_err("model","required","REQUIRED"))
|
|
53
|
+
pr=p.get("prompt")
|
|
54
|
+
if not isinstance(pr,str) or not pr: e.append(_err("prompt","required","REQUIRED"))
|
|
55
|
+
elif "{output}" not in pr and "{input}" not in pr and "{expected}" not in pr: e.append(_err("prompt","missing token","MISSING_TOKEN"))
|
|
56
|
+
elif t=="json_schema":
|
|
57
|
+
if not isinstance(p.get("schema"),dict): e.append(_err("schema","required","REQUIRED"))
|
|
58
|
+
elif t=="json_path":
|
|
59
|
+
if not isinstance(p.get("path"),str) or not p["path"]: e.append(_err("path","required","REQUIRED"))
|
|
60
|
+
if "expected" not in p: e.append(_err("expected","required","REQUIRED"))
|
|
61
|
+
elif t=="code":
|
|
62
|
+
if p.get("language") not in ("python","javascript"): e.append(_err("language","python|javascript","INVALID_VALUE"))
|
|
63
|
+
if not isinstance(p.get("source"),str) or not p["source"]: e.append(_err("source","required","REQUIRED"))
|
|
64
|
+
elif t=="custom":
|
|
65
|
+
if not isinstance(p.get("handler"),str) or not p["handler"]: e.append(_err("handler","required","REQUIRED"))
|
|
66
|
+
return e
|
|
67
|
+
|
|
68
|
+
def validate_suite(s):
|
|
69
|
+
errors=[]
|
|
70
|
+
if not isinstance(s,dict): return ValidationResult(False,[_err("$","Must be object","TYPE_ERROR")])
|
|
71
|
+
if not isinstance(s.get("version"),str) or not re.match(r"^\d+\.\d+\.\d+(-draft)?$",s.get("version","")): errors.append(_err("$.version","semver","INVALID_VERSION"))
|
|
72
|
+
if not isinstance(s.get("id"),str) or not s["id"]: errors.append(_err("$.id","required","REQUIRED"))
|
|
73
|
+
tcs=s.get("test_cases")
|
|
74
|
+
if not isinstance(tcs,list) and not isinstance(s.get("test_cases_file"),str): errors.append(_err("$.test_cases","required","REQUIRED"))
|
|
75
|
+
if isinstance(tcs,list):
|
|
76
|
+
if not tcs: errors.append(_err("$.test_cases","empty","MIN_ITEMS"))
|
|
77
|
+
ids=set()
|
|
78
|
+
for i,tc in enumerate(tcs):
|
|
79
|
+
tv=validate_test_case(tc)
|
|
80
|
+
if not tv.valid:
|
|
81
|
+
for e in tv.errors: errors.append(_err(f"$.test_cases[{i}].{e['path']}",e["message"],e["code"]))
|
|
82
|
+
tid=tc.get("id") if isinstance(tc,dict) else None
|
|
83
|
+
if isinstance(tid,str):
|
|
84
|
+
if tid in ids: errors.append(_err(f"$.test_cases[{i}].id",f"dup:{tid}","DUPLICATE_ID"))
|
|
85
|
+
ids.add(tid)
|
|
86
|
+
grs=s.get("graders",[])
|
|
87
|
+
if isinstance(grs,list):
|
|
88
|
+
gids=set()
|
|
89
|
+
for i,g in enumerate(grs):
|
|
90
|
+
gv=validate_grader(g)
|
|
91
|
+
if not gv.valid:
|
|
92
|
+
for e in gv.errors: errors.append(_err(f"$.graders[{i}].{e['path']}",e["message"],e["code"]))
|
|
93
|
+
gid=g.get("id") if isinstance(g,dict) else None
|
|
94
|
+
if isinstance(gid,str):
|
|
95
|
+
if gid in gids: errors.append(_err(f"$.graders[{i}].id",f"dup:{gid}","DUPLICATE_ID"))
|
|
96
|
+
gids.add(gid)
|
|
97
|
+
for i,tc in enumerate(tcs):
|
|
98
|
+
if isinstance(tc,dict) and isinstance(tc.get("graders"),list):
|
|
99
|
+
for j,gr in enumerate(tc["graders"]):
|
|
100
|
+
if isinstance(gr,str) and gr not in gids: errors.append(_err(f"$.test_cases[{i}].graders[{j}]",f"not found:{gr}","DANGLING_REFERENCE"))
|
|
101
|
+
return ValidationResult(not errors,errors)
|
|
102
|
+
|
|
103
|
+
def validate_result_set(r):
|
|
104
|
+
errors=[]
|
|
105
|
+
if not isinstance(r,dict): return ValidationResult(False,[_err("$","Must be object","TYPE_ERROR")])
|
|
106
|
+
if not isinstance(r.get("version"),str) or not re.match(r"^\d+\.\d+\.\d+(-draft)?$",r.get("version","")): errors.append(_err("$.version","semver","INVALID_VERSION"))
|
|
107
|
+
if not isinstance(r.get("suite_id"),str) or not r["suite_id"]: errors.append(_err("$.suite_id","required","REQUIRED"))
|
|
108
|
+
if not isinstance(r.get("run_id"),str) or not r["run_id"]: errors.append(_err("$.run_id","required","REQUIRED"))
|
|
109
|
+
if not isinstance(r.get("started_at"),str): errors.append(_err("$.started_at","required","REQUIRED"))
|
|
110
|
+
rs=r.get("results")
|
|
111
|
+
if not isinstance(rs,list) or not rs: errors.append(_err("$.results","required","REQUIRED"))
|
|
112
|
+
else:
|
|
113
|
+
for i,x in enumerate(rs):
|
|
114
|
+
if not isinstance(x,dict): errors.append(_err(f"$.results[{i}]","object","TYPE_ERROR"));continue
|
|
115
|
+
if not isinstance(x.get("test_case_id"),str): errors.append(_err(f"$.results[{i}].test_case_id","required","REQUIRED"))
|
|
116
|
+
if not isinstance(x.get("passed"),bool): errors.append(_err(f"$.results[{i}].passed","required","REQUIRED"))
|
|
117
|
+
grs=x.get("grader_results")
|
|
118
|
+
if not isinstance(grs,list): errors.append(_err(f"$.results[{i}].grader_results","required","REQUIRED"))
|
|
119
|
+
else:
|
|
120
|
+
for j,gr in enumerate(grs):
|
|
121
|
+
if not isinstance(gr,dict): errors.append(_err(f"$.results[{i}].grader_results[{j}]","object","TYPE_ERROR"));continue
|
|
122
|
+
if not isinstance(gr.get("grader_id"),str): errors.append(_err(f"$.results[{i}].grader_results[{j}].grader_id","required","REQUIRED"))
|
|
123
|
+
if not isinstance(gr.get("type"),str): errors.append(_err(f"$.results[{i}].grader_results[{j}].type","required","REQUIRED"))
|
|
124
|
+
sc=gr.get("score")
|
|
125
|
+
if not isinstance(sc,(int,float,type(None))): errors.append(_err(f"$.results[{i}].grader_results[{j}].score","number|null","TYPE_ERROR"))
|
|
126
|
+
if not isinstance(gr.get("passed"),bool): errors.append(_err(f"$.results[{i}].grader_results[{j}].passed","required","REQUIRED"))
|
|
127
|
+
return ValidationResult(not errors,errors)
|
|
128
|
+
|
|
129
|
+
def validate_document(d,t):
|
|
130
|
+
if t=="testcase": return validate_test_case(d)
|
|
131
|
+
if t=="grader": return validate_grader(d)
|
|
132
|
+
if t=="suite": return validate_suite(d)
|
|
133
|
+
if t=="resultset": return validate_result_set(d)
|
|
134
|
+
raise ValueError(f"Unknown type: {t}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "evalport-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for EvalPort — The Open Evaluation Standard"
|
|
9
|
+
license = {text = "Apache-2.0"}
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages.find]
|
|
13
|
+
where = ["."]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from openeval.validate import validate_suite, validate_grader, validate_test_case, validate_result_set
|
|
2
|
+
|
|
3
|
+
def test_valid_suite():
|
|
4
|
+
assert validate_suite({"version":"1.0.0","id":"s","graders":[{"id":"g1","type":"exact_match"}],"test_cases":[{"id":"tc1","input":"hi","graders":["g1"]}]}).valid
|
|
5
|
+
|
|
6
|
+
def test_empty():
|
|
7
|
+
assert not validate_suite({"version":"1.0.0","id":"s","test_cases":[]}).valid
|
|
8
|
+
|
|
9
|
+
def test_grader():
|
|
10
|
+
assert validate_grader({"id":"g1","type":"exact_match"}).valid
|
|
11
|
+
assert not validate_grader({"id":"g1","type":"bad"}).valid
|
|
12
|
+
|
|
13
|
+
def test_tc():
|
|
14
|
+
assert validate_test_case({"id":"tc1","input":"hi","graders":["g1"]}).valid
|
|
15
|
+
assert not validate_test_case({"id":"tc1","graders":["g1"]}).valid
|
|
16
|
+
|
|
17
|
+
def test_rs():
|
|
18
|
+
assert validate_result_set({"version":"1.0.0","suite_id":"s","run_id":"r","started_at":"2026-01-01T00:00:00Z","results":[{"test_case_id":"tc1","passed":True,"grader_results":[{"grader_id":"g1","type":"exact_match","score":1.0,"passed":True}]}]}).valid
|