reeval-python 0.10.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.
- reeval_python/__init__.py +23 -0
- reeval_python/__main__.py +4 -0
- reeval_python/assertion.py +168 -0
- reeval_python/context.py +163 -0
- reeval_python/declare.py +263 -0
- reeval_python/errors.py +7 -0
- reeval_python/expect.py +95 -0
- reeval_python/py.typed +0 -0
- reeval_python/runner.py +190 -0
- reeval_python/types.py +97 -0
- reeval_python/wire.py +81 -0
- reeval_python-0.10.0.dist-info/METADATA +85 -0
- reeval_python-0.10.0.dist-info/RECORD +15 -0
- reeval_python-0.10.0.dist-info/WHEEL +4 -0
- reeval_python-0.10.0.dist-info/licenses/LICENSE +202 -0
reeval_python/expect.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Fluent expect() API; maps to wire matchers internally."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reeval_python.wire import MATCHER_TO_WIRE, subject_for_matcher, wire_args
|
|
9
|
+
|
|
10
|
+
SubmitFn = Any
|
|
11
|
+
|
|
12
|
+
_submit: ContextVar[SubmitFn | None] = ContextVar("_submit", default=None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def bind_expect(submit: SubmitFn) -> None:
|
|
16
|
+
_submit.set(submit)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def expect(actual: Any, label: str | None = None) -> _Expectation:
|
|
20
|
+
submit = _submit.get()
|
|
21
|
+
if submit is None:
|
|
22
|
+
raise RuntimeError("expect() must run inside an eval body")
|
|
23
|
+
return _Expectation(actual, submit, label=label)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Expectation:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
actual: Any,
|
|
30
|
+
submit: SubmitFn,
|
|
31
|
+
*,
|
|
32
|
+
label: str | None = None,
|
|
33
|
+
not_: bool = False,
|
|
34
|
+
soft: bool = False,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._actual = actual
|
|
37
|
+
self._submit = submit
|
|
38
|
+
self._label = label
|
|
39
|
+
self._not = not_
|
|
40
|
+
self._soft = soft
|
|
41
|
+
self._sync = bool(getattr(submit, "_sync_mode", False))
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def not_(self) -> _Expectation:
|
|
45
|
+
return _Expectation(
|
|
46
|
+
self._actual,
|
|
47
|
+
self._submit,
|
|
48
|
+
label=self._label,
|
|
49
|
+
not_=not self._not,
|
|
50
|
+
soft=self._soft,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __getattr__(self, name: str) -> Any:
|
|
54
|
+
if not name.startswith("to_"):
|
|
55
|
+
raise AttributeError(name)
|
|
56
|
+
wire = MATCHER_TO_WIRE.get(name)
|
|
57
|
+
if wire is None:
|
|
58
|
+
raise AttributeError(f"unknown matcher {name}")
|
|
59
|
+
|
|
60
|
+
async def _async_matcher(*args: Any) -> None:
|
|
61
|
+
subject = subject_for_matcher(self._actual, wire)
|
|
62
|
+
await self._submit(
|
|
63
|
+
subject=subject,
|
|
64
|
+
matcher=wire,
|
|
65
|
+
args=wire_args(args),
|
|
66
|
+
label=self._label,
|
|
67
|
+
not_=self._not,
|
|
68
|
+
soft=self._soft,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _sync_matcher(*args: Any) -> None:
|
|
72
|
+
subject = subject_for_matcher(self._actual, wire)
|
|
73
|
+
self._submit(
|
|
74
|
+
subject=subject,
|
|
75
|
+
matcher=wire,
|
|
76
|
+
args=wire_args(args),
|
|
77
|
+
label=self._label,
|
|
78
|
+
not_=self._not,
|
|
79
|
+
soft=self._soft,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return _sync_matcher if self._sync else _async_matcher
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class soft: # noqa: N801
|
|
86
|
+
"""expect.soft(subject, label?) — soft assertion."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, actual: Any, label: str | None = None) -> None:
|
|
89
|
+
submit = _submit.get()
|
|
90
|
+
if submit is None:
|
|
91
|
+
raise RuntimeError("expect.soft() must run inside an eval body")
|
|
92
|
+
self._inner = _Expectation(actual, submit, label=label, soft=True)
|
|
93
|
+
|
|
94
|
+
def __getattr__(self, name: str) -> Any:
|
|
95
|
+
return getattr(self._inner, name)
|
reeval_python/py.typed
ADDED
|
File without changes
|
reeval_python/runner.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Out-of-process runner: collect declaration trees and run eval bodies over attempt protocol v2."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from reeval_python.context import AttemptContext, SyncAttemptContext
|
|
16
|
+
from reeval_python.declare import collect_eval_file, get_body
|
|
17
|
+
from reeval_python.errors import ReevalAssertionError
|
|
18
|
+
|
|
19
|
+
RUNNER_PROTOCOL = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sdk_version() -> str:
|
|
23
|
+
try:
|
|
24
|
+
return version("reeval-python")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
return "0.0.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Connection:
|
|
30
|
+
def __init__(self, writer: asyncio.StreamWriter) -> None:
|
|
31
|
+
self._writer = writer
|
|
32
|
+
self._pending: dict[tuple[str, str], asyncio.Future[dict[str, Any]]] = {}
|
|
33
|
+
|
|
34
|
+
def write(self, msg: dict[str, Any]) -> None:
|
|
35
|
+
self._writer.write((json.dumps(msg) + "\n").encode())
|
|
36
|
+
|
|
37
|
+
async def drain(self) -> None:
|
|
38
|
+
await self._writer.drain()
|
|
39
|
+
|
|
40
|
+
def complete_reply(self, msg: dict[str, Any]) -> None:
|
|
41
|
+
attempt = msg.get("attempt")
|
|
42
|
+
req_id = msg.get("id")
|
|
43
|
+
if attempt is None or req_id is None:
|
|
44
|
+
return
|
|
45
|
+
key = (str(attempt), str(req_id))
|
|
46
|
+
fut = self._pending.pop(key, None)
|
|
47
|
+
if fut is not None and not fut.done():
|
|
48
|
+
fut.set_result(msg)
|
|
49
|
+
|
|
50
|
+
async def request(self, attempt_id: str, req_id: str, call: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
key = (attempt_id, req_id)
|
|
52
|
+
fut = asyncio.get_running_loop().create_future()
|
|
53
|
+
self._pending[key] = fut
|
|
54
|
+
self.write({"id": req_id, "call": call, "attempt": attempt_id})
|
|
55
|
+
await self.drain()
|
|
56
|
+
return await fut
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _run_body(conn: _Connection, attempt_id: str, body_id: str) -> None:
|
|
60
|
+
fn = get_body(body_id)
|
|
61
|
+
seq = 0
|
|
62
|
+
|
|
63
|
+
async def send_request(message: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
nonlocal seq
|
|
65
|
+
seq += 1
|
|
66
|
+
req_id = str(seq)
|
|
67
|
+
return await conn.request(attempt_id, req_id, message["call"])
|
|
68
|
+
|
|
69
|
+
loop = asyncio.get_running_loop()
|
|
70
|
+
async_ctx = AttemptContext(attempt_id, send_request)
|
|
71
|
+
error: dict[str, Any] | None = None
|
|
72
|
+
|
|
73
|
+
async def run_async_body() -> None:
|
|
74
|
+
async_ctx.bind_expect()
|
|
75
|
+
await fn(async_ctx)
|
|
76
|
+
|
|
77
|
+
async def run_sync_body() -> None:
|
|
78
|
+
sync_ctx = SyncAttemptContext(async_ctx, loop)
|
|
79
|
+
sync_ctx.bind_expect()
|
|
80
|
+
await asyncio.to_thread(fn, sync_ctx)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if inspect.iscoroutinefunction(fn):
|
|
84
|
+
await run_async_body()
|
|
85
|
+
else:
|
|
86
|
+
await run_sync_body()
|
|
87
|
+
except ReevalAssertionError as exc:
|
|
88
|
+
error = {
|
|
89
|
+
"name": "AssertionError",
|
|
90
|
+
"message": str(exc),
|
|
91
|
+
"stack": traceback.format_exc(),
|
|
92
|
+
}
|
|
93
|
+
except AssertionError as exc:
|
|
94
|
+
try:
|
|
95
|
+
await async_ctx.record_plain_assert(exc)
|
|
96
|
+
except ReevalAssertionError as recorded:
|
|
97
|
+
error = {
|
|
98
|
+
"name": "AssertionError",
|
|
99
|
+
"message": str(recorded),
|
|
100
|
+
"stack": traceback.format_exc(),
|
|
101
|
+
}
|
|
102
|
+
except AssertionError as nested:
|
|
103
|
+
error = {
|
|
104
|
+
"name": "AssertionError",
|
|
105
|
+
"message": str(nested),
|
|
106
|
+
"stack": traceback.format_exc(),
|
|
107
|
+
}
|
|
108
|
+
except Exception as exc: # noqa: BLE001
|
|
109
|
+
error = {"name": type(exc).__name__, "message": str(exc), "stack": traceback.format_exc()}
|
|
110
|
+
conn.write({"type": "done", "attempt": attempt_id, **({"error": error} if error else {})})
|
|
111
|
+
await conn.drain()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def serve() -> None:
|
|
115
|
+
host_port = os.environ.get("REEVAL_RUNNER", "")
|
|
116
|
+
token = os.environ.get("REEVAL_RUNNER_TOKEN", "")
|
|
117
|
+
root = Path(os.environ.get("REEVAL_PROJECT_ROOT", os.getcwd()))
|
|
118
|
+
if ":" not in host_port or not token:
|
|
119
|
+
print("This is reeval's Python runner; run evals with reeval run.", file=sys.stderr)
|
|
120
|
+
sys.exit(2)
|
|
121
|
+
|
|
122
|
+
host, port_str = host_port.rsplit(":", 1)
|
|
123
|
+
reader, writer = await asyncio.open_connection(host, int(port_str))
|
|
124
|
+
conn = _Connection(writer)
|
|
125
|
+
|
|
126
|
+
hello = {
|
|
127
|
+
"t": "hello",
|
|
128
|
+
"protocol": RUNNER_PROTOCOL,
|
|
129
|
+
"token": token,
|
|
130
|
+
"sdk": "python",
|
|
131
|
+
"version": sdk_version(),
|
|
132
|
+
"runtime": sys.version.split()[0],
|
|
133
|
+
}
|
|
134
|
+
writer.write((json.dumps(hello) + "\n").encode())
|
|
135
|
+
await writer.drain()
|
|
136
|
+
|
|
137
|
+
tasks: set[asyncio.Task[None]] = set()
|
|
138
|
+
buf = b""
|
|
139
|
+
while True:
|
|
140
|
+
chunk = await reader.read(65536)
|
|
141
|
+
if chunk == b"":
|
|
142
|
+
break
|
|
143
|
+
buf += chunk
|
|
144
|
+
while True:
|
|
145
|
+
newline = buf.find(b"\n")
|
|
146
|
+
if newline < 0:
|
|
147
|
+
break
|
|
148
|
+
line = buf[:newline]
|
|
149
|
+
buf = buf[newline + 1 :]
|
|
150
|
+
if not line.strip():
|
|
151
|
+
continue
|
|
152
|
+
msg = cast(dict[str, Any], json.loads(line.decode()))
|
|
153
|
+
if msg.get("t") == "collect":
|
|
154
|
+
rel = str(msg["file"])
|
|
155
|
+
try:
|
|
156
|
+
tree = collect_eval_file(root / rel)
|
|
157
|
+
conn.write({"t": "declared", "file": rel, "tree": tree})
|
|
158
|
+
except Exception as exc: # noqa: BLE001
|
|
159
|
+
conn.write(
|
|
160
|
+
{
|
|
161
|
+
"t": "load-error",
|
|
162
|
+
"error": {
|
|
163
|
+
"name": type(exc).__name__,
|
|
164
|
+
"message": str(exc),
|
|
165
|
+
"stack": traceback.format_exc(),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
await conn.drain()
|
|
170
|
+
elif msg.get("type") == "start":
|
|
171
|
+
attempt_id = str(msg["attempt"]["id"])
|
|
172
|
+
body_id = str(msg["body"])
|
|
173
|
+
task = asyncio.create_task(_run_body(conn, attempt_id, body_id))
|
|
174
|
+
tasks.add(task)
|
|
175
|
+
task.add_done_callback(tasks.discard)
|
|
176
|
+
elif msg.get("type") == "abort":
|
|
177
|
+
for task in list(tasks):
|
|
178
|
+
task.cancel()
|
|
179
|
+
elif "id" in msg and "ok" in msg:
|
|
180
|
+
conn.complete_reply(msg)
|
|
181
|
+
|
|
182
|
+
writer.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main() -> None:
|
|
186
|
+
asyncio.run(serve())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
main()
|
reeval_python/types.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Typed results for attempt calls (public, not wire-shaped)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal, cast
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ExecResult:
|
|
11
|
+
command: str
|
|
12
|
+
exit_code: int
|
|
13
|
+
stdout: str
|
|
14
|
+
stderr: str
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def ok(self) -> bool:
|
|
18
|
+
return self.exit_code == 0
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_wire(cls, command: str, data: dict[str, Any]) -> ExecResult:
|
|
22
|
+
return cls(
|
|
23
|
+
command=command,
|
|
24
|
+
exit_code=int(data.get("exitCode", data.get("exit_code", 1))),
|
|
25
|
+
stdout=str(data.get("stdout") or ""),
|
|
26
|
+
stderr=str(data.get("stderr") or ""),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class BackgroundHandle:
|
|
32
|
+
id: str
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_wire(cls, data: dict[str, Any]) -> BackgroundHandle:
|
|
36
|
+
return cls(id=str(data.get("handle") or data.get("id") or ""))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Turn:
|
|
41
|
+
index: int
|
|
42
|
+
raw: dict[str, Any]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def prompt(self) -> str:
|
|
46
|
+
return str(self.raw.get("prompt") or "")
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def text(self) -> str:
|
|
50
|
+
return str(self.raw.get("text") or "")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def status(self) -> str:
|
|
54
|
+
return str(self.raw.get("status") or "")
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def diff(self) -> dict[str, Any] | None:
|
|
58
|
+
diff = self.raw.get("diff")
|
|
59
|
+
if isinstance(diff, dict):
|
|
60
|
+
return cast(dict[str, Any], diff)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
VerdictKind = Literal["pass", "fail", "error"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class Verdict:
|
|
69
|
+
verdict: VerdictKind
|
|
70
|
+
score: float
|
|
71
|
+
threshold: float
|
|
72
|
+
question: str
|
|
73
|
+
model: str
|
|
74
|
+
raw: dict[str, Any]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def passed(self) -> bool:
|
|
78
|
+
return self.verdict == "pass"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def reason(self) -> str | None:
|
|
82
|
+
for key in ("reason", "explanation", "rationale"):
|
|
83
|
+
value = self.raw.get(key)
|
|
84
|
+
if isinstance(value, str) and value:
|
|
85
|
+
return value
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_wire(cls, data: dict[str, Any]) -> Verdict:
|
|
90
|
+
return cls(
|
|
91
|
+
verdict=data.get("verdict", "fail"), # type: ignore[arg-type]
|
|
92
|
+
score=float(data.get("score", 0)),
|
|
93
|
+
threshold=float(data.get("threshold", 0)),
|
|
94
|
+
question=str(data.get("question") or ""),
|
|
95
|
+
model=str(data.get("model") or ""),
|
|
96
|
+
raw=dict(data),
|
|
97
|
+
)
|
reeval_python/wire.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Map public Python values to attempt-protocol wire shapes (internal)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from reeval_python.types import ExecResult, Turn, Verdict
|
|
9
|
+
|
|
10
|
+
# Python snake_case matcher method -> wire matcher name (Vitest + reeval table).
|
|
11
|
+
MATCHER_TO_WIRE: dict[str, str] = {
|
|
12
|
+
"to_succeed": "toSucceed",
|
|
13
|
+
"to_pass": "toPass",
|
|
14
|
+
"to_equal": "toEqual",
|
|
15
|
+
"to_contain": "toContain",
|
|
16
|
+
"to_be_truthy": "toBeTruthy",
|
|
17
|
+
"to_be": "toBe",
|
|
18
|
+
"to_be_null": "toBeNull",
|
|
19
|
+
"to_have_file": "toHaveFile",
|
|
20
|
+
"to_have_edited": "toHaveEdited",
|
|
21
|
+
"to_have_run": "toHaveRun",
|
|
22
|
+
"to_have_searched": "toHaveSearched",
|
|
23
|
+
"to_have_fetched": "toHaveFetched",
|
|
24
|
+
"to_have_called_skill": "toHaveCalledSkill",
|
|
25
|
+
"to_have_called_mcp": "toHaveCalledMcp",
|
|
26
|
+
"to_have_called_tool": "toHaveCalledTool",
|
|
27
|
+
"to_have_changed": "toHaveChanged",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Judge / evaluate options: snake_case kwargs -> camelCase keys on the wire.
|
|
31
|
+
OPTION_KEY_TO_WIRE: dict[str, str] = {
|
|
32
|
+
"max_retries": "maxRetries",
|
|
33
|
+
"max_tokens": "maxTokens",
|
|
34
|
+
"read_only": "readOnly",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def options_to_wire(options: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
39
|
+
if not options:
|
|
40
|
+
return None
|
|
41
|
+
out: dict[str, Any] = {}
|
|
42
|
+
for key, value in options.items():
|
|
43
|
+
wire_key = OPTION_KEY_TO_WIRE.get(key, key)
|
|
44
|
+
out[wire_key] = value
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _pattern_arg(value: re.Pattern[str]) -> Any:
|
|
49
|
+
return {"regexp": value.pattern, "flags": value.flags and int(value.flags) or None}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def wire_args(args: tuple[Any, ...]) -> list[Any]:
|
|
53
|
+
out: list[Any] = []
|
|
54
|
+
for a in args:
|
|
55
|
+
if isinstance(a, re.Pattern):
|
|
56
|
+
out.append(_pattern_arg(cast(re.Pattern[str], a)))
|
|
57
|
+
else:
|
|
58
|
+
out.append(a)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def subject_for_matcher(actual: Any, matcher: str) -> dict[str, Any]:
|
|
63
|
+
if isinstance(actual, ExecResult) and matcher == "toSucceed":
|
|
64
|
+
return {"exec": actual.command}
|
|
65
|
+
if isinstance(actual, ExecResult):
|
|
66
|
+
return {
|
|
67
|
+
"value": {
|
|
68
|
+
"exitCode": actual.exit_code,
|
|
69
|
+
"stdout": actual.stdout,
|
|
70
|
+
"stderr": actual.stderr,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if isinstance(actual, Verdict):
|
|
74
|
+
return {"value": actual.raw}
|
|
75
|
+
if isinstance(actual, Turn):
|
|
76
|
+
return {"turn": actual.index}
|
|
77
|
+
if isinstance(actual, dict):
|
|
78
|
+
wire_keys = ("exec", "turn", "attempt", "readFile", "value")
|
|
79
|
+
if any(k in actual for k in wire_keys):
|
|
80
|
+
return cast(dict[str, Any], actual)
|
|
81
|
+
return {"value": actual}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: reeval-python
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: Python authoring SDK for reeval (*.eval.py eval files)
|
|
5
|
+
Project-URL: Homepage, https://reeval.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/invertase/reeval
|
|
7
|
+
Project-URL: Documentation, https://github.com/invertase/reeval/tree/main/packages/reeval-python#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/invertase/reeval/issues
|
|
9
|
+
Author: Invertase
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# reeval-python
|
|
25
|
+
|
|
26
|
+
Python authoring SDK for [reeval](https://github.com/invertase/reeval). Write `*.eval.py` files,
|
|
27
|
+
declare evals with the same options as TypeScript, and run them through reeval's language runner
|
|
28
|
+
(loopback TCP, attempt protocol v2).
|
|
29
|
+
|
|
30
|
+
Import as `reeval_python` (`from reeval_python import evaluate`). The PyPI distribution is
|
|
31
|
+
**reeval-python** (the name `reeval` is already taken on PyPI by another project).
|
|
32
|
+
|
|
33
|
+
With reeval installed, default discovery includes `evals/**/*.eval.py`. When your project has
|
|
34
|
+
`pyproject.toml` or `uv.lock` and [uv](https://docs.astral.sh/uv/) is on `PATH`, reeval starts the
|
|
35
|
+
runner with `uv run --project <root> python -m reeval_python`. Otherwise it uses `<root>/.venv/bin/python`
|
|
36
|
+
when present, then `python3`. Set `REEVAL_PYTHON` to override the interpreter.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add reeval-python
|
|
42
|
+
# or: pip install reeval-python
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Monorepo development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd packages/reeval-python
|
|
49
|
+
uv sync --dev
|
|
50
|
+
uv run pytest
|
|
51
|
+
uv run ruff check .
|
|
52
|
+
uv run pyright
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Declare trees are checked against `packages/reeval/test/conformance/declare/` goldens. From the
|
|
56
|
+
reeval package, `test/python-sdk.test.ts` runs the same suite in CI.
|
|
57
|
+
|
|
58
|
+
## Writing evals
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from reeval_python import evaluate, expect
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@evaluate("lands")
|
|
65
|
+
def lands(c):
|
|
66
|
+
c.send("Create greet.py that prints hi")
|
|
67
|
+
assert c.exec("test -f greet.py").ok
|
|
68
|
+
expect(c.judge("Does greet.py print hi?")).to_pass()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Plain `assert` is first-class: failures become hard checks in the run record (same shape as
|
|
72
|
+
TypeScript). Use typed results (`ExecResult.ok`, `Verdict.passed`, `Turn.text`, …) and `c.file(path)`
|
|
73
|
+
for quick checks. Keep `expect` for reeval matchers (`to_have_file`, `to_succeed`, soft checks,
|
|
74
|
+
labels) — Playwright-style.
|
|
75
|
+
|
|
76
|
+
Use snake_case on the context (`read_file`, `write_file`, `file`) and on matchers (`to_succeed`,
|
|
77
|
+
`to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`. Async bodies use
|
|
78
|
+
`async def`, `await c.send(...)`, and `await expect(await c.exec(...)).to_succeed()`; sync bodies
|
|
79
|
+
use plain `def` and block on the runner loop.
|
|
80
|
+
|
|
81
|
+
## Examples
|
|
82
|
+
|
|
83
|
+
- Feature smoke: `packages/reeval/test/features/python-sdk/hello/` (uv project with a project dependency)
|
|
84
|
+
- Plan case: `packages/reeval/test/plan/python/`
|
|
85
|
+
- Contributor guide: `.agents/skills/add-a-language/SKILL.md` in the reeval repo
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
reeval_python/__init__.py,sha256=Mzv45KSuKaWmcKO7yZ-jzk3DcnYDrvzavbxZzJsf6AM,655
|
|
2
|
+
reeval_python/__main__.py,sha256=0AezY7FdEV8RqwkKhR0twWdvhaiJkt02ps7U1TEujzs,77
|
|
3
|
+
reeval_python/assertion.py,sha256=AnRa_8KAWBw-RHnZFG2NuO2qimLvGS3ZuxsBq79Oqv8,5077
|
|
4
|
+
reeval_python/context.py,sha256=i0__DQIZ0KBCqRgbhjjZ-VZGd_9O5NBWIPYRQT2grQA,5818
|
|
5
|
+
reeval_python/declare.py,sha256=7ceRmccd2GrvX_yVQfqfzU5o3W4v3_SjEwQvkEgE8OY,7090
|
|
6
|
+
reeval_python/errors.py,sha256=q6xm8zwEzS4LewZf1h5oKYfnXHc2SvuKghew0ryia4A,208
|
|
7
|
+
reeval_python/expect.py,sha256=giZTP3h2xavXow2GOe4N9QaxLXG1wV6IeblA0Otaym0,2777
|
|
8
|
+
reeval_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
reeval_python/runner.py,sha256=-9-QSh5cPr-xCbYLEoa2D4CtVWT7z1CCam4mnMeEx4Q,6230
|
|
10
|
+
reeval_python/types.py,sha256=qcZA1u0c7uxu4hI5tATXylH0ru0_V65QuWja7B3UeXI,2453
|
|
11
|
+
reeval_python/wire.py,sha256=ECX1t8ETFigEOFHGhXmSn7YnYYU-27tNCJMyh-eUJG4,2564
|
|
12
|
+
reeval_python-0.10.0.dist-info/METADATA,sha256=vVKz67mwpgt_x3KKjFFrSZY4KF7m6zUFfXBd0ldArTA,3308
|
|
13
|
+
reeval_python-0.10.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
14
|
+
reeval_python-0.10.0.dist-info/licenses/LICENSE,sha256=ksfhGdGZnBIw2o77d0o71LHzgK-bRGUx1qS6nzYMjK4,11348
|
|
15
|
+
reeval_python-0.10.0.dist-info/RECORD,,
|