Skip to content

Commit 6d83875

Browse files
authored
feat(llma): add typed offline evaluations SDK (#1005)
* feat(llma): add typed offline evaluations sdk * fix(llma): reject integer scores that lose precision
1 parent 5622d3a commit 6d83875

21 files changed

Lines changed: 4283 additions & 8 deletions

‎.github/scripts/check_public_api.py‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ def _is_public_alias(obj: object) -> bool:
5959
return _is_public_posthog_path(target_path)
6060

6161

62+
def _is_private_sdk_alias(obj: object) -> bool:
63+
target = _alias_target_path(obj)
64+
return (
65+
target.startswith("posthog.")
66+
and not _is_excluded_path(target)
67+
and not _is_public_posthog_path(target)
68+
)
69+
70+
6271
def _module_exports(module: object) -> set[str] | None:
6372
exports = getattr(module, "exports", None)
6473
if exports is None:
@@ -98,7 +107,9 @@ def _is_public_member(parent: object, name: str, member: object) -> bool:
98107
return False
99108

100109
if _is_alias(member):
101-
return _is_public_alias(member)
110+
return _is_public_alias(member) or (
111+
exports is not None and _is_private_sdk_alias(member)
112+
)
102113

103114
if _is_module(member):
104115
return _is_public_posthog_path(_object_path(member))
@@ -119,7 +130,7 @@ def _signature(obj: object) -> str | None:
119130

120131
def _signature_for_path(obj: object) -> str | None:
121132
signature = _signature(obj)
122-
if signature is None:
133+
if signature is None or (not signature and _is_alias(obj)):
123134
return None
124135

125136
name = str(getattr(obj, "name", ""))
@@ -144,7 +155,7 @@ def _attribute_details(obj: object) -> str:
144155

145156

146157
def _record(obj: object) -> str:
147-
if _is_alias(obj):
158+
if _is_alias(obj) and not _is_private_sdk_alias(obj):
148159
return f"alias {_object_path(obj)} -> {_alias_target_path(obj)}"
149160

150161
if _is_module(obj):
@@ -169,15 +180,21 @@ def _record(obj: object) -> str:
169180
return f"{getattr(obj, 'kind', 'object')} {_object_path(obj)}"
170181

171182

172-
def _iter_class_members(cls: object) -> Iterable[object]:
183+
def _iter_class_members(
184+
cls: object, *, exported_alias: bool = False
185+
) -> Iterable[object]:
173186
members = getattr(cls, "members", {})
174187
for name, member in sorted(members.items()):
175-
if not _is_public_member(cls, str(name), member):
188+
if not (
189+
_is_public_name(str(name))
190+
if exported_alias
191+
else _is_public_member(cls, str(name), member)
192+
):
176193
continue
177194

178195
yield member
179-
if not _is_alias(member) and _is_class(member):
180-
yield from _iter_class_members(member)
196+
if _is_class(member) and (exported_alias or not _is_alias(member)):
197+
yield from _iter_class_members(member, exported_alias=exported_alias)
181198

182199

183200
def _iter_module_members(module: object) -> Iterable[object]:
@@ -188,6 +205,8 @@ def _iter_module_members(module: object) -> Iterable[object]:
188205
if _is_alias(member):
189206
if _is_public_member(module, str(name), member):
190207
yield member
208+
if _is_private_sdk_alias(member) and _is_class(member):
209+
yield from _iter_class_members(member, exported_alias=True)
191210
continue
192211

193212
if _is_module(member):

‎.github/scripts/check_strict_types.sh‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ import atexit
1515
1616
import posthog
1717
from posthog import FeatureFlagEvaluations, FlagValue, Posthog
18+
from posthog.ai.evaluations import (
19+
AsyncOfflineEvaluations,
20+
BooleanScorerConfig,
21+
EvaluationItem,
22+
NumericPassingRule,
23+
NumericScorerConfig,
24+
OfflineEvaluations,
25+
)
1826
1927
client = Posthog("phc_test")
2028
atexit.register(client.shutdown)
@@ -31,6 +39,40 @@ active: posthog.Span | None = posthog.get_active_span()
3139
span.end()
3240
3341
_ = (flag_value, all_flags, enabled, payload, evaluations, active)
42+
43+
44+
async def offline_evaluation_types(
45+
sync_client: OfflineEvaluations, async_client: AsyncOfflineEvaluations
46+
) -> None:
47+
numeric = sync_client.scorers.create(
48+
name="Relevance",
49+
kind="numeric",
50+
config=NumericScorerConfig(
51+
min=0, max=1,
52+
passing_rule=NumericPassingRule(operator="gte", threshold=0.8),
53+
),
54+
)
55+
config: NumericScorerConfig = numeric.config
56+
experiment = sync_client.create_experiment(name="baseline", run_source="ci")
57+
item = EvaluationItem(input="question", output="answer")
58+
experiment.upload_result(item=item, scorer_version_id=numeric.current_version_id, value=0.9)
59+
boolean = await async_client.scorers.create(
60+
name="Toxicity", kind="boolean", config=BooleanScorerConfig(true_is_failure=True)
61+
)
62+
async_experiment = await async_client.create_experiment(name="async run")
63+
await async_experiment.upload_result(item=item, scorer_version_id=boolean.current_version_id, value=False)
64+
_ = config
65+
PY
66+
67+
cat > "$tmp/invalid_offline_types.py" <<'PY'
68+
# pyright: strict
69+
from posthog.ai.evaluations import BooleanScorerConfig, CategoricalScorerConfig, NumericPassingRule, NumericScorerConfig, OfflineEvaluations
70+
71+
client = OfflineEvaluations(project_id=123, secret_key="phx_example")
72+
NumericPassingRule(operator="gt", threshold=0.8) # expected-type-error
73+
NumericScorerConfig(unknown_setting=True) # expected-type-error
74+
CategoricalScorerConfig(selection_mode="single") # expected-type-error
75+
client.scorers.create(name="Bad config", kind="numeric", config=BooleanScorerConfig(true_is_failure=True)) # expected-type-error
3476
PY
3577

3678
"$tmp/.venv/bin/python" - <<'PY' > "$tmp/public_api_access.py"
@@ -73,3 +115,21 @@ JSON
73115

74116
cd "$tmp"
75117
"$tmp/.venv/bin/python" -m pyright strict_posthog_types.py public_api_access.py
118+
119+
if "$tmp/.venv/bin/python" -m pyright --outputjson invalid_offline_types.py > invalid_types.json; then
120+
echo "Invalid offline evaluation configurations unexpectedly passed type checking."
121+
exit 1
122+
fi
123+
"$tmp/.venv/bin/python" - <<'PY'
124+
import json
125+
from pathlib import Path
126+
127+
expected = {
128+
index for index, line in enumerate(Path("invalid_offline_types.py").read_text().splitlines())
129+
if "expected-type-error" in line
130+
}
131+
diagnostics = json.loads(Path("invalid_types.json").read_text())["generalDiagnostics"]
132+
actual = {item["range"]["start"]["line"] for item in diagnostics if item["severity"] == "error"}
133+
assert actual == expected, diagnostics
134+
print("Offline evaluation configuration types reject invalid inputs.")
135+
PY

‎.github/scripts/test_check_public_api.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import importlib.util
77
from pathlib import Path
88
from types import SimpleNamespace
9+
from tempfile import TemporaryDirectory
910

1011

1112
SCRIPT_PATH = Path(__file__).with_name("check_public_api.py")
@@ -42,8 +43,41 @@ def test_attribute_details_uses_placeholder_values() -> None:
4243
)
4344

4445

46+
def test_explicit_exports_from_private_modules_include_signatures() -> None:
47+
import griffe
48+
49+
check_public_api = load_check_public_api()
50+
with TemporaryDirectory() as directory:
51+
package = Path(directory) / "posthog"
52+
package.mkdir()
53+
(package / "__init__.py").write_text(
54+
'from ._implementation import Client, Hidden\n__all__ = ["Client"]\n'
55+
)
56+
(package / "_implementation.py").write_text(
57+
"class Client:\n"
58+
" def __init__(self, *, key: str): pass\n"
59+
" def upload(self, value: float) -> bool: return True\n"
60+
" def _internal(self): pass\n"
61+
"class Hidden: pass\n"
62+
)
63+
module = griffe.load(
64+
"posthog",
65+
search_paths=[directory],
66+
allow_inspection=False,
67+
try_relative_path=False,
68+
)
69+
records = [
70+
check_public_api._record(obj)
71+
for obj in check_public_api._iter_module_members(module)
72+
]
73+
assert "class posthog.Client(*, key: str)" in records
74+
assert "method posthog.Client.upload(value: float)" in records
75+
assert not any("Hidden" in record or "_internal" in record for record in records)
76+
77+
4578
def main() -> int:
4679
test_attribute_details_uses_placeholder_values()
80+
test_explicit_exports_from_private_modules_include_signatures()
4781
print("check_public_api tests passed.")
4882
return 0
4983

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: minor
3+
---
4+
5+
Add typed synchronous and asynchronous offline evaluation clients with reusable items, acknowledged bulk uploads, resumable identities, and explicit experiment completion. Manage scorers and immutable versions through the SDK, including boolean, numeric, and categorical polarity configuration.

0 commit comments

Comments
 (0)