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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: evalport-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for EvalPort — The Open Evaluation Standard
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.8
@@ -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,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: evalport-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for EvalPort — The Open Evaluation Standard
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.8
@@ -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,3 @@
1
+ dist
2
+ openeval
3
+ tests
@@ -0,0 +1,3 @@
1
+ from .types import *
2
+ from .validate import *
3
+ from .convert import *
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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