osmosis-ai 0.1.9__py3-none-any.whl → 0.2.4__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.
Potentially problematic release.
This version of osmosis-ai might be problematic. Click here for more details.
- osmosis_ai/__init__.py +19 -132
- osmosis_ai/cli.py +50 -0
- osmosis_ai/cli_commands.py +181 -0
- osmosis_ai/cli_services/__init__.py +60 -0
- osmosis_ai/cli_services/config.py +410 -0
- osmosis_ai/cli_services/dataset.py +175 -0
- osmosis_ai/cli_services/engine.py +421 -0
- osmosis_ai/cli_services/errors.py +7 -0
- osmosis_ai/cli_services/reporting.py +307 -0
- osmosis_ai/cli_services/session.py +174 -0
- osmosis_ai/cli_services/shared.py +209 -0
- osmosis_ai/consts.py +1 -1
- osmosis_ai/providers/__init__.py +36 -0
- osmosis_ai/providers/anthropic_provider.py +85 -0
- osmosis_ai/providers/base.py +60 -0
- osmosis_ai/providers/gemini_provider.py +314 -0
- osmosis_ai/providers/openai_family.py +607 -0
- osmosis_ai/providers/shared.py +92 -0
- osmosis_ai/rubric_eval.py +356 -0
- osmosis_ai/rubric_types.py +49 -0
- osmosis_ai/utils.py +258 -5
- osmosis_ai-0.2.4.dist-info/METADATA +314 -0
- osmosis_ai-0.2.4.dist-info/RECORD +27 -0
- osmosis_ai-0.2.4.dist-info/entry_points.txt +4 -0
- osmosis_ai-0.1.9.dist-info/METADATA +0 -143
- osmosis_ai-0.1.9.dist-info/RECORD +0 -8
- {osmosis_ai-0.1.9.dist-info → osmosis_ai-0.2.4.dist-info}/WHEEL +0 -0
- {osmosis_ai-0.1.9.dist-info → osmosis_ai-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {osmosis_ai-0.1.9.dist-info → osmosis_ai-0.2.4.dist-info}/top_level.txt +0 -0
osmosis_ai/utils.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import functools
|
|
3
3
|
import inspect
|
|
4
|
-
|
|
4
|
+
import types
|
|
5
|
+
from typing import Any, Callable, Mapping, Union, get_args, get_origin, get_type_hints
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def osmosis_reward(func: Callable) -> Callable:
|
|
@@ -27,9 +28,8 @@ def osmosis_reward(func: Callable) -> Callable:
|
|
|
27
28
|
sig = inspect.signature(func)
|
|
28
29
|
params = list(sig.parameters.values())
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise TypeError(f"Function {func.__name__} must have 2-3 parameters, got {len(params)}")
|
|
31
|
+
if len(params) < 3:
|
|
32
|
+
raise TypeError(f"Function {func.__name__} must have at least 3 parameters, got {len(params)}")
|
|
33
33
|
|
|
34
34
|
# Check first parameter: solution_str: str
|
|
35
35
|
if params[0].name != 'solution_str':
|
|
@@ -44,7 +44,7 @@ def osmosis_reward(func: Callable) -> Callable:
|
|
|
44
44
|
raise TypeError(f"Second parameter 'ground_truth' must be annotated as str, got {params[1].annotation}")
|
|
45
45
|
|
|
46
46
|
# Check third parameter if present: extra_info: dict = None
|
|
47
|
-
if len(params)
|
|
47
|
+
if len(params) >= 3:
|
|
48
48
|
if params[2].name != 'extra_info':
|
|
49
49
|
raise TypeError(f"Third parameter must be named 'extra_info', got '{params[2].name}'")
|
|
50
50
|
if params[2].annotation != dict:
|
|
@@ -54,6 +54,259 @@ def osmosis_reward(func: Callable) -> Callable:
|
|
|
54
54
|
|
|
55
55
|
@functools.wraps(func)
|
|
56
56
|
def wrapper(*args, **kwargs):
|
|
57
|
+
kwargs.pop("data_source", None)
|
|
58
|
+
result = func(*args, **kwargs)
|
|
59
|
+
if not isinstance(result, float):
|
|
60
|
+
raise TypeError(f"Function {func.__name__} must return a float, got {type(result).__name__}")
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
return wrapper
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
ALLOWED_ROLES = {"user", "system", "assistant", "developer", "tool", "function"}
|
|
67
|
+
|
|
68
|
+
_UNION_TYPES = {Union}
|
|
69
|
+
_types_union_type = getattr(types, "UnionType", None)
|
|
70
|
+
if _types_union_type is not None:
|
|
71
|
+
_UNION_TYPES.add(_types_union_type)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_str_annotation(annotation: Any) -> bool:
|
|
75
|
+
if annotation is inspect.Parameter.empty:
|
|
76
|
+
return False
|
|
77
|
+
if annotation is str:
|
|
78
|
+
return True
|
|
79
|
+
if isinstance(annotation, str):
|
|
80
|
+
return annotation in {"str", "builtins.str"}
|
|
81
|
+
if isinstance(annotation, type):
|
|
82
|
+
try:
|
|
83
|
+
return issubclass(annotation, str)
|
|
84
|
+
except TypeError:
|
|
85
|
+
return False
|
|
86
|
+
forward_arg = getattr(annotation, "__forward_arg__", None)
|
|
87
|
+
if isinstance(forward_arg, str):
|
|
88
|
+
return forward_arg in {"str", "builtins.str"}
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_optional_str(annotation: Any) -> bool:
|
|
93
|
+
if _is_str_annotation(annotation):
|
|
94
|
+
return True
|
|
95
|
+
if isinstance(annotation, str):
|
|
96
|
+
normalized = annotation.replace(" ", "")
|
|
97
|
+
if normalized in {
|
|
98
|
+
"Optional[str]",
|
|
99
|
+
"typing.Optional[str]",
|
|
100
|
+
"Str|None",
|
|
101
|
+
"str|None",
|
|
102
|
+
"builtins.str|None",
|
|
103
|
+
"None|str",
|
|
104
|
+
"None|builtins.str",
|
|
105
|
+
}:
|
|
106
|
+
return True
|
|
107
|
+
origin = get_origin(annotation)
|
|
108
|
+
if origin in _UNION_TYPES:
|
|
109
|
+
args = tuple(arg for arg in get_args(annotation) if arg is not type(None)) # noqa: E721
|
|
110
|
+
return len(args) == 1 and _is_str_annotation(args[0])
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_list_annotation(annotation: Any) -> bool:
|
|
115
|
+
if annotation is list:
|
|
116
|
+
return True
|
|
117
|
+
if isinstance(annotation, str):
|
|
118
|
+
normalized = annotation.replace(" ", "")
|
|
119
|
+
return (
|
|
120
|
+
normalized in {"list", "builtins.list", "typing.List", "List"}
|
|
121
|
+
or normalized.startswith("list[")
|
|
122
|
+
or normalized.startswith("builtins.list[")
|
|
123
|
+
or normalized.startswith("typing.List[")
|
|
124
|
+
or normalized.startswith("List[")
|
|
125
|
+
)
|
|
126
|
+
origin = get_origin(annotation)
|
|
127
|
+
return origin is list
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _is_float_annotation(annotation: Any) -> bool:
|
|
131
|
+
if annotation in {inspect.Parameter.empty, float}:
|
|
132
|
+
return True
|
|
133
|
+
if isinstance(annotation, str):
|
|
134
|
+
return annotation in {"float", "builtins.float"}
|
|
135
|
+
origin = get_origin(annotation)
|
|
136
|
+
return origin is float
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_numeric(value: Any) -> bool:
|
|
140
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _is_dict_annotation(annotation: Any) -> bool:
|
|
144
|
+
if annotation in {dict, Mapping}:
|
|
145
|
+
return True
|
|
146
|
+
origin = get_origin(annotation)
|
|
147
|
+
if origin in {dict, Mapping}:
|
|
148
|
+
return True
|
|
149
|
+
if isinstance(annotation, type):
|
|
150
|
+
try:
|
|
151
|
+
return issubclass(annotation, dict)
|
|
152
|
+
except TypeError:
|
|
153
|
+
return False
|
|
154
|
+
if isinstance(annotation, str):
|
|
155
|
+
normalized = annotation.replace(" ", "")
|
|
156
|
+
return (
|
|
157
|
+
normalized in {"dict", "builtins.dict", "typing.Mapping", "collections.abc.Mapping", "Mapping"}
|
|
158
|
+
or normalized.startswith("dict[")
|
|
159
|
+
or normalized.startswith("builtins.dict[")
|
|
160
|
+
or normalized.startswith("typing.Dict[")
|
|
161
|
+
or normalized.startswith("Dict[")
|
|
162
|
+
or normalized.startswith("typing.Mapping[")
|
|
163
|
+
or normalized.startswith("Mapping[")
|
|
164
|
+
)
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def osmosis_rubric(func: Callable) -> Callable:
|
|
169
|
+
"""
|
|
170
|
+
Decorator for rubric functions that enforces the signature:
|
|
171
|
+
(solution_str: str, ground_truth: str, extra_info: dict) -> float
|
|
172
|
+
|
|
173
|
+
The `extra_info` mapping must include the current `provider`, `model`, and `rubric`
|
|
174
|
+
values, and may optionally provide `system_prompt`, `score_min`, and `score_max`.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
func: The rubric function to be wrapped.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The wrapped function.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
TypeError: If the function doesn't have the required signature or doesn't return a float.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
@osmosis_rubric
|
|
187
|
+
def evaluate_response(
|
|
188
|
+
solution_str: str,
|
|
189
|
+
ground_truth: str,
|
|
190
|
+
extra_info: dict,
|
|
191
|
+
) -> float:
|
|
192
|
+
return some_evaluation(solution_str, ground_truth, extra_info)
|
|
193
|
+
"""
|
|
194
|
+
sig = inspect.signature(func)
|
|
195
|
+
params = list(sig.parameters.values())
|
|
196
|
+
try:
|
|
197
|
+
resolved_annotations = get_type_hints(
|
|
198
|
+
func,
|
|
199
|
+
globalns=getattr(func, "__globals__", {}),
|
|
200
|
+
include_extras=True,
|
|
201
|
+
)
|
|
202
|
+
except Exception: # pragma: no cover - best effort for forward refs
|
|
203
|
+
resolved_annotations = {}
|
|
204
|
+
|
|
205
|
+
if len(params) < 3:
|
|
206
|
+
raise TypeError(f"Function {func.__name__} must have at least 3 parameters, got {len(params)}")
|
|
207
|
+
|
|
208
|
+
solution_param = params[0]
|
|
209
|
+
if solution_param.name != "solution_str":
|
|
210
|
+
raise TypeError(f"First parameter must be named 'solution_str', got '{solution_param.name}'")
|
|
211
|
+
solution_annotation = resolved_annotations.get(solution_param.name, solution_param.annotation)
|
|
212
|
+
if not _is_str_annotation(solution_annotation):
|
|
213
|
+
raise TypeError(f"First parameter 'solution_str' must be annotated as str, got {solution_annotation}")
|
|
214
|
+
if solution_param.default is not inspect.Parameter.empty:
|
|
215
|
+
raise TypeError("First parameter 'solution_str' cannot have a default value")
|
|
216
|
+
|
|
217
|
+
ground_truth_param = params[1]
|
|
218
|
+
if ground_truth_param.name != "ground_truth":
|
|
219
|
+
raise TypeError(f"Second parameter must be named 'ground_truth', got '{ground_truth_param.name}'")
|
|
220
|
+
ground_truth_annotation = resolved_annotations.get(ground_truth_param.name, ground_truth_param.annotation)
|
|
221
|
+
if not _is_optional_str(ground_truth_annotation):
|
|
222
|
+
raise TypeError(
|
|
223
|
+
f"Second parameter 'ground_truth' must be annotated as str or Optional[str], got {ground_truth_annotation}"
|
|
224
|
+
)
|
|
225
|
+
if ground_truth_param.default is not inspect.Parameter.empty:
|
|
226
|
+
raise TypeError("Second parameter 'ground_truth' cannot have a default value")
|
|
227
|
+
|
|
228
|
+
extra_info_param = params[2]
|
|
229
|
+
if extra_info_param.name != "extra_info":
|
|
230
|
+
raise TypeError(f"Third parameter must be named 'extra_info', got '{extra_info_param.name}'")
|
|
231
|
+
extra_info_annotation = resolved_annotations.get(extra_info_param.name, extra_info_param.annotation)
|
|
232
|
+
if not _is_dict_annotation(extra_info_annotation):
|
|
233
|
+
raise TypeError(
|
|
234
|
+
f"Third parameter 'extra_info' must be annotated as a dict or mapping, got {extra_info_annotation}"
|
|
235
|
+
)
|
|
236
|
+
if extra_info_param.default is not inspect.Parameter.empty:
|
|
237
|
+
raise TypeError("Third parameter 'extra_info' cannot have a default value")
|
|
238
|
+
|
|
239
|
+
@functools.wraps(func)
|
|
240
|
+
def wrapper(*args, **kwargs):
|
|
241
|
+
kwargs.pop("data_source", None)
|
|
242
|
+
bound = sig.bind_partial(*args, **kwargs)
|
|
243
|
+
bound.apply_defaults()
|
|
244
|
+
|
|
245
|
+
if "solution_str" not in bound.arguments:
|
|
246
|
+
raise TypeError("'solution_str' argument is required")
|
|
247
|
+
solution_value = bound.arguments["solution_str"]
|
|
248
|
+
if not isinstance(solution_value, str):
|
|
249
|
+
raise TypeError(f"'solution_str' must be a string, got {type(solution_value).__name__}")
|
|
250
|
+
|
|
251
|
+
if "ground_truth" not in bound.arguments:
|
|
252
|
+
raise TypeError("'ground_truth' argument is required")
|
|
253
|
+
ground_truth_value = bound.arguments["ground_truth"]
|
|
254
|
+
if ground_truth_value is not None and not isinstance(ground_truth_value, str):
|
|
255
|
+
raise TypeError(
|
|
256
|
+
f"'ground_truth' must be a string or None, got {type(ground_truth_value).__name__}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if "extra_info" not in bound.arguments:
|
|
260
|
+
raise TypeError("'extra_info' argument is required")
|
|
261
|
+
extra_info_value = bound.arguments["extra_info"]
|
|
262
|
+
if not isinstance(extra_info_value, Mapping):
|
|
263
|
+
raise TypeError(f"'extra_info' must be a mapping, got {type(extra_info_value).__name__}")
|
|
264
|
+
|
|
265
|
+
provider_value = extra_info_value.get("provider")
|
|
266
|
+
if not isinstance(provider_value, str) or not provider_value.strip():
|
|
267
|
+
raise TypeError("'extra_info[\"provider\"]' must be a non-empty string")
|
|
268
|
+
|
|
269
|
+
model_value = extra_info_value.get("model")
|
|
270
|
+
if not isinstance(model_value, str) or not model_value.strip():
|
|
271
|
+
raise TypeError("'extra_info[\"model\"]' must be a non-empty string")
|
|
272
|
+
|
|
273
|
+
if "rubric" not in extra_info_value:
|
|
274
|
+
raise TypeError("'extra_info' must include a 'rubric' string")
|
|
275
|
+
rubric_value = extra_info_value["rubric"]
|
|
276
|
+
if not isinstance(rubric_value, str):
|
|
277
|
+
raise TypeError(f"'extra_info[\"rubric\"]' must be a string, got {type(rubric_value).__name__}")
|
|
278
|
+
|
|
279
|
+
api_key_value = extra_info_value.get("api_key")
|
|
280
|
+
api_key_env_value = extra_info_value.get("api_key_env")
|
|
281
|
+
has_api_key = isinstance(api_key_value, str) and bool(api_key_value.strip())
|
|
282
|
+
has_api_key_env = isinstance(api_key_env_value, str) and bool(api_key_env_value.strip())
|
|
283
|
+
if not (has_api_key or has_api_key_env):
|
|
284
|
+
raise TypeError(
|
|
285
|
+
"'extra_info' must include either a non-empty 'api_key' or 'api_key_env' string"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
system_prompt_value = extra_info_value.get("system_prompt")
|
|
289
|
+
if system_prompt_value is not None and not isinstance(system_prompt_value, str):
|
|
290
|
+
raise TypeError(
|
|
291
|
+
f"'extra_info[\"system_prompt\"]' must be a string or None, got {type(system_prompt_value).__name__}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
score_min_value = extra_info_value.get("score_min")
|
|
295
|
+
if score_min_value is not None and not _is_numeric(score_min_value):
|
|
296
|
+
raise TypeError(
|
|
297
|
+
f"'extra_info[\"score_min\"]' must be numeric, got {type(score_min_value).__name__}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
score_max_value = extra_info_value.get("score_max")
|
|
301
|
+
if score_max_value is not None and not _is_numeric(score_max_value):
|
|
302
|
+
raise TypeError(
|
|
303
|
+
f"'extra_info[\"score_max\"]' must be numeric, got {type(score_max_value).__name__}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if score_min_value is not None and score_max_value is not None:
|
|
307
|
+
if float(score_max_value) <= float(score_min_value):
|
|
308
|
+
raise ValueError("'extra_info[\"score_max\"]' must be greater than 'extra_info[\"score_min\"]'")
|
|
309
|
+
|
|
57
310
|
result = func(*args, **kwargs)
|
|
58
311
|
if not isinstance(result, float):
|
|
59
312
|
raise TypeError(f"Function {func.__name__} must return a float, got {type(result).__name__}")
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osmosis-ai
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: A Python library for reward function validation with strict type enforcement.
|
|
5
|
+
Author-email: Osmosis AI <jake@osmosis.ai>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Gulp AI
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/Osmosis-AI/osmosis-sdk-python
|
|
28
|
+
Project-URL: Issues, https://github.com/Osmosis-AI/osmosis-sdk-python/issues
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: PyYAML<7.0,>=6.0
|
|
36
|
+
Requires-Dist: python-dotenv<2.0.0,>=0.1.0
|
|
37
|
+
Requires-Dist: requests<3.0.0,>=2.0.0
|
|
38
|
+
Requires-Dist: xxhash<4.0.0,>=3.0.0
|
|
39
|
+
Requires-Dist: anthropic<0.50.0,>=0.36.0
|
|
40
|
+
Requires-Dist: openai>=2.0.0
|
|
41
|
+
Requires-Dist: google-genai>=1.0.0
|
|
42
|
+
Requires-Dist: xai-sdk>=1.2.0
|
|
43
|
+
Requires-Dist: tqdm<5.0.0,>=4.0.0
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# osmosis-ai
|
|
47
|
+
|
|
48
|
+
A Python library that provides reward and rubric validation helpers for LLM applications with strict type enforcement.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install osmosis-ai
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Python 3.9 or newer.
|
|
57
|
+
|
|
58
|
+
This installs the Osmosis CLI and pulls in the required provider SDKs (`openai`, `anthropic`, `google-genai`, `xai-sdk`) along with supporting utilities such as `PyYAML`, `python-dotenv`, `requests`, and `xxhash`.
|
|
59
|
+
|
|
60
|
+
For development:
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/Osmosis-AI/osmosis-sdk-python
|
|
63
|
+
cd osmosis-sdk-python
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from osmosis_ai import osmosis_reward
|
|
71
|
+
|
|
72
|
+
@osmosis_reward
|
|
73
|
+
def simple_reward(solution_str: str, ground_truth: str, extra_info: dict = None) -> float:
|
|
74
|
+
"""Basic exact match reward function."""
|
|
75
|
+
return 1.0 if solution_str.strip() == ground_truth.strip() else 0.0
|
|
76
|
+
|
|
77
|
+
# Use the reward function
|
|
78
|
+
score = simple_reward("hello world", "hello world") # Returns 1.0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from osmosis_ai import evaluate_rubric
|
|
83
|
+
|
|
84
|
+
solution = "The capital of France is Paris."
|
|
85
|
+
|
|
86
|
+
# Export OPENAI_API_KEY in your shell before running this snippet.
|
|
87
|
+
rubric_score = evaluate_rubric(
|
|
88
|
+
rubric="Assistant must mention the verified capital city.",
|
|
89
|
+
solution_str=solution,
|
|
90
|
+
model_info={
|
|
91
|
+
"provider": "openai",
|
|
92
|
+
"model": "gpt-5",
|
|
93
|
+
"api_key_env": "OPENAI_API_KEY",
|
|
94
|
+
},
|
|
95
|
+
ground_truth="Paris",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
print(rubric_score) # -> 1.0 (full payload available via return_details=True)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Remote Rubric Evaluation
|
|
102
|
+
|
|
103
|
+
`evaluate_rubric` talks to each provider through its official Python SDK while enforcing the same JSON schema everywhere:
|
|
104
|
+
|
|
105
|
+
- **OpenAI / xAI** – Uses `OpenAI(...).responses.create` (or `chat.completions.create`) with `response_format={"type": "json_schema"}` and falls back to `json_object` when needed.
|
|
106
|
+
- **Anthropic** – Forces a tool call with a JSON schema via `Anthropic(...).messages.create`, extracting the returned tool arguments.
|
|
107
|
+
- **Google Gemini** – Invokes `google.genai.Client(...).models.generate_content` with `response_mime_type="application/json"` and `response_schema`.
|
|
108
|
+
|
|
109
|
+
Every provider therefore returns a strict JSON object with `{"score": number, "explanation": string}`. The helper clamps the score into your configured range, validates the structure, and exposes the raw payload when `return_details=True`.
|
|
110
|
+
|
|
111
|
+
Credentials are resolved from environment variables by default:
|
|
112
|
+
|
|
113
|
+
- `OPENAI_API_KEY` for OpenAI
|
|
114
|
+
- `ANTHROPIC_API_KEY` for Anthropic
|
|
115
|
+
- `GOOGLE_API_KEY` for Google Gemini
|
|
116
|
+
- `XAI_API_KEY` for xAI
|
|
117
|
+
|
|
118
|
+
Override the environment variable name with `model_info={"api_key_env": "CUSTOM_ENV_NAME"}` when needed, or supply an inline secret with `model_info={"api_key": "sk-..."}` for ephemeral credentials. Missing API keys raise a `MissingAPIKeyError` that explains how to export the secret before trying again.
|
|
119
|
+
|
|
120
|
+
`api_key` and `api_key_env` are mutually exclusive ways to provide the same credential. When `api_key` is present and non-empty it is used directly, skipping any environment lookup. Otherwise the resolver falls back to `api_key_env` (or the provider default) and pulls the value from your local environment with `os.getenv`.
|
|
121
|
+
|
|
122
|
+
`model_info` accepts additional rubric-specific knobs:
|
|
123
|
+
|
|
124
|
+
- `score_min` / `score_max` – change the default `[0.0, 1.0]` scoring bounds.
|
|
125
|
+
- `system_prompt` / `original_input` – provide optional context strings that will be quoted in the judging prompt.
|
|
126
|
+
- `timeout` – customise the provider timeout in seconds.
|
|
127
|
+
|
|
128
|
+
Pass `metadata={...}` to `evaluate_rubric` when you need structured context quoted in the judge prompt, and set `return_details=True` to receive the full `RewardRubricRunResult` payload (including the provider’s raw response).
|
|
129
|
+
|
|
130
|
+
Remote failures surface as `ProviderRequestError` instances, with `ModelNotFoundError` reserved for missing model identifiers so you can retry with a new snapshot.
|
|
131
|
+
|
|
132
|
+
> Older SDK versions that lack schema parameters automatically fall back to instruction-only JSON; the helper still validates the response payload before returning.
|
|
133
|
+
> Provider model snapshot names change frequently. Check each vendor's dashboard for the latest identifier if you encounter a “model not found” error.
|
|
134
|
+
|
|
135
|
+
### Provider Architecture
|
|
136
|
+
|
|
137
|
+
All remote integrations live in `osmosis_ai/providers/` and implement the `RubricProvider` interface. At import time the default registry registers OpenAI, xAI, Anthropic, and Google Gemini so `evaluate_rubric` can route requests without additional configuration. The request/response plumbing is encapsulated in each provider module, keeping `evaluate_rubric` focused on prompt construction, payload validation, and credential resolution.
|
|
138
|
+
|
|
139
|
+
Add your own provider by subclassing `RubricProvider`, implementing `run()` with the vendor SDK, and calling `register_provider()` during start-up. A step-by-step guide is available in [`osmosis_ai/providers/README.md`](osmosis_ai/providers/README.md).
|
|
140
|
+
|
|
141
|
+
## Required Function Signature
|
|
142
|
+
|
|
143
|
+
All functions decorated with `@osmosis_reward` must have exactly this signature:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
@osmosis_reward
|
|
147
|
+
def your_function(solution_str: str, ground_truth: str, extra_info: dict = None) -> float:
|
|
148
|
+
# Your reward logic here
|
|
149
|
+
return float_score
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Parameters
|
|
153
|
+
|
|
154
|
+
- **`solution_str: str`** - The solution string to evaluate (required)
|
|
155
|
+
- **`ground_truth: str`** - The correct/expected answer (required)
|
|
156
|
+
- **`extra_info: dict = None`** - Optional dictionary for additional configuration
|
|
157
|
+
|
|
158
|
+
### Return Value
|
|
159
|
+
|
|
160
|
+
- **`-> float`** - Must return a float value representing the reward score
|
|
161
|
+
|
|
162
|
+
The decorator will raise a `TypeError` if the function doesn't match this exact signature or doesn't return a float.
|
|
163
|
+
|
|
164
|
+
## Rubric Function Signature
|
|
165
|
+
|
|
166
|
+
Rubric functions decorated with `@osmosis_rubric` must match this signature:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
@osmosis_rubric
|
|
170
|
+
def your_rubric(solution_str: str, ground_truth: str | None, extra_info: dict) -> float:
|
|
171
|
+
# Your rubric logic here
|
|
172
|
+
return float_score
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> The runtime forwards `None` for `ground_truth` when no reference answer exists. Annotate the parameter as `Optional[str]` (or handle `None` explicitly) if your rubric logic expects to run in that scenario.
|
|
176
|
+
|
|
177
|
+
### Required `extra_info` fields
|
|
178
|
+
|
|
179
|
+
- **`provider`** – Non-empty string identifying the judge provider.
|
|
180
|
+
- **`model`** – Non-empty string naming the provider model to call.
|
|
181
|
+
- **`rubric`** – Natural-language rubric instructions for the judge model.
|
|
182
|
+
- **`api_key` / `api_key_env`** – Supply either the raw key or the environment variable name that exposes it.
|
|
183
|
+
|
|
184
|
+
### Optional `extra_info` fields
|
|
185
|
+
|
|
186
|
+
- **`system_prompt`** – Optional string prepended to the provider’s base system prompt when invoking the judge; include it inside `extra_info` rather than as a separate argument.
|
|
187
|
+
- **`score_min` / `score_max`** – Optional numeric overrides for the expected score range.
|
|
188
|
+
- **`model_info_overrides`** – Optional dict merged into the provider configuration passed to the judge.
|
|
189
|
+
|
|
190
|
+
Additional keys are passthrough and can be used for custom configuration. If you need to extend the provider payload (for example adding `api_key_env`), add a dict under `model_info_overrides` and it will be merged with the required `provider`/`model` pair before invoking `evaluate_rubric`. The decorator enforces the parameter names/annotations, validates the embedded configuration at call time, and ensures the wrapped function returns a `float`.
|
|
191
|
+
|
|
192
|
+
> Annotation quirk: `extra_info` must be annotated as `dict` **without** a default value, unlike `@osmosis_reward`.
|
|
193
|
+
|
|
194
|
+
> Tip: When delegating to `evaluate_rubric`, pass the raw `solution_str` directly and include any extra context inside the `metadata` payload.
|
|
195
|
+
|
|
196
|
+
## Examples
|
|
197
|
+
|
|
198
|
+
See the [`examples/`](examples/) directory for complete examples:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
@osmosis_reward
|
|
202
|
+
def case_insensitive_match(solution_str: str, ground_truth: str, extra_info: dict = None) -> float:
|
|
203
|
+
"""Case-insensitive string matching with partial credit."""
|
|
204
|
+
match = solution_str.lower().strip() == ground_truth.lower().strip()
|
|
205
|
+
|
|
206
|
+
if extra_info and 'partial_credit' in extra_info:
|
|
207
|
+
if not match and extra_info['partial_credit']:
|
|
208
|
+
len_diff = abs(len(solution_str) - len(ground_truth))
|
|
209
|
+
if len_diff <= 2:
|
|
210
|
+
return 0.5
|
|
211
|
+
|
|
212
|
+
return 1.0 if match else 0.0
|
|
213
|
+
|
|
214
|
+
@osmosis_reward
|
|
215
|
+
def numeric_tolerance(solution_str: str, ground_truth: str, extra_info: dict = None) -> float:
|
|
216
|
+
"""Numeric comparison with configurable tolerance."""
|
|
217
|
+
try:
|
|
218
|
+
solution_num = float(solution_str.strip())
|
|
219
|
+
truth_num = float(ground_truth.strip())
|
|
220
|
+
|
|
221
|
+
tolerance = extra_info.get('tolerance', 0.01) if extra_info else 0.01
|
|
222
|
+
return 1.0 if abs(solution_num - truth_num) <= tolerance else 0.0
|
|
223
|
+
except ValueError:
|
|
224
|
+
return 0.0
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `examples/rubric_functions.py` demonstrates `evaluate_rubric` with OpenAI, Anthropic, Gemini, and xAI using the schema-enforced SDK integrations.
|
|
228
|
+
- `examples/reward_functions.py` keeps local reward helpers that showcase the decorator contract without external calls.
|
|
229
|
+
- `examples/rubric_configs.yaml` bundles two rubric definitions with provider configuration and scoring bounds.
|
|
230
|
+
- `examples/sample_data.jsonl` contains two rubric-aligned solution strings so you can trial dataset validation.
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
# examples/rubric_configs.yaml (excerpt)
|
|
234
|
+
version: 1
|
|
235
|
+
rubrics:
|
|
236
|
+
- id: support_followup
|
|
237
|
+
model_info:
|
|
238
|
+
provider: openai
|
|
239
|
+
model: gpt-5-mini
|
|
240
|
+
api_key_env: OPENAI_API_KEY
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```jsonl
|
|
244
|
+
{"conversation_id": "ticket-001", "rubric_id": "support_followup", "original_input": "...", "solution_str": "..."}
|
|
245
|
+
{"conversation_id": "ticket-047", "rubric_id": "policy_grounding", "original_input": "...", "solution_str": "..."}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## CLI Tools
|
|
249
|
+
|
|
250
|
+
Installing the SDK also provides a lightweight CLI available as `osmosis` (aliases: `osmosis_ai`, `osmosis-ai`) for inspecting rubric YAML files and JSONL test payloads.
|
|
251
|
+
|
|
252
|
+
Preview a rubric file and print every configuration discovered, including nested entries:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
osmosis preview --path path/to/rubric.yaml
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Preview a dataset of rubric-scored solutions stored as JSONL:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
osmosis preview --path path/to/data.jsonl
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Evaluate a dataset against a hosted rubric configuration and print the returned scores:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
osmosis eval --rubric support_followup --data examples/sample_data.jsonl
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- Supply the dataset with `-d`/`--data path/to/data.jsonl`; the path is resolved relative to the current working directory.
|
|
271
|
+
- Use `--config path/to/rubric_configs.yaml` when the rubric definitions are not located alongside the dataset.
|
|
272
|
+
- Pass `-n`/`--number` to sample the provider multiple times per record; the CLI prints every run along with aggregate statistics (average, variance, standard deviation, and min/max).
|
|
273
|
+
- Provide `--output path/to/dir` to create the directory (if needed) and emit `rubric_eval_result_<unix_timestamp>.json`, or supply a full file path (any extension) to control the filename; each file captures every run, provider payloads, timestamps, and aggregate statistics for downstream analysis.
|
|
274
|
+
- Skip `--output` to collect results under `~/.cache/osmosis/eval_result/<rubric_id>/rubric_eval_result_<identifier>.json`; the CLI writes this JSON whether the evaluation finishes cleanly or hits provider/runtime errors so you can inspect failures later (only a manual Ctrl+C interrupt leaves no file behind).
|
|
275
|
+
- Dataset rows whose `rubric_id` does not match the requested rubric are skipped automatically.
|
|
276
|
+
- Each dataset record must provide a non-empty `solution_str`; optional fields such as `original_input`, `ground_truth`, and `extra_info` travel with the record and are forwarded to the evaluator when present.
|
|
277
|
+
- When delegating to a custom `@osmosis_rubric` function, the CLI enriches `extra_info` with the active `provider`, `model`, `rubric`, score bounds, any configured `system_prompt`, the resolved `original_input`, and the record’s metadata/extra fields so the decorator’s required entries are always present.
|
|
278
|
+
- Rubric configuration files intentionally reject `extra_info`; provide per-example context through the dataset instead.
|
|
279
|
+
|
|
280
|
+
Both commands validate the file, echo a short summary (`Loaded <n> ...`), and pretty-print the parsed records so you can confirm that new rubrics or test fixtures look correct before committing them. Invalid files raise a descriptive error and exit with a non-zero status code.
|
|
281
|
+
|
|
282
|
+
## Running Examples
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
PYTHONPATH=. python examples/reward_functions.py
|
|
286
|
+
PYTHONPATH=. python examples/rubric_functions.py # Uncomment the provider you need before running
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Testing
|
|
290
|
+
|
|
291
|
+
Run `python -m pytest` (or any subset under `tests/`) to exercise the updated helpers:
|
|
292
|
+
|
|
293
|
+
- `tests/test_rubric_eval.py` covers prompt construction for `solution_str` evaluations.
|
|
294
|
+
- `tests/test_cli_services.py` validates dataset parsing, extra-info enrichment, and engine interactions.
|
|
295
|
+
- `tests/test_cli.py` ensures the CLI pathways surface the new fields end to end.
|
|
296
|
+
|
|
297
|
+
Add additional tests under `tests/` as you extend the library.
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
302
|
+
|
|
303
|
+
## Contributing
|
|
304
|
+
|
|
305
|
+
1. Fork the repository
|
|
306
|
+
2. Create a feature branch
|
|
307
|
+
3. Make your changes
|
|
308
|
+
4. Run tests and examples
|
|
309
|
+
5. Submit a pull request
|
|
310
|
+
|
|
311
|
+
## Links
|
|
312
|
+
|
|
313
|
+
- [Homepage](https://github.com/Osmosis-AI/osmosis-sdk-python)
|
|
314
|
+
- [Issues](https://github.com/Osmosis-AI/osmosis-sdk-python/issues)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
osmosis_ai/__init__.py,sha256=2_qXxu18Yc7UicqxFZds8PjR4q0mTY1Xt17iR38OFbw,725
|
|
2
|
+
osmosis_ai/cli.py,sha256=EPCttBnj1TEqQuO2gmS9iHadYcudiizVM38jACztRFE,1320
|
|
3
|
+
osmosis_ai/cli_commands.py,sha256=CmTcb5N3delW7z3fwucss89xw5MHgIrJJ2Z5xdAuIeU,6165
|
|
4
|
+
osmosis_ai/consts.py,sha256=dAC-7yKjt7CIBpExhm1TKOhZ-1U5XyQD_tRiCewcse0,73
|
|
5
|
+
osmosis_ai/rubric_eval.py,sha256=wsrHU4OUWx3jy0kjyMZh2TYaJjbbz3hG4QvbajV_4Dk,12196
|
|
6
|
+
osmosis_ai/rubric_types.py,sha256=kJvNAjLd3Y-1Q-_Re9HLTprLAUO3qtwR-IWOBeMkFI8,1279
|
|
7
|
+
osmosis_ai/utils.py,sha256=G8xU2tKFZfbUpznp42tHBW7UNP0zCZzoWQ9EP79yDLE,12960
|
|
8
|
+
osmosis_ai/cli_services/__init__.py,sha256=nfbzDMXTJQfNUt7npIuO5JwUWTp6WWnuWMRWwntJXQo,1461
|
|
9
|
+
osmosis_ai/cli_services/config.py,sha256=pHMU7EY3J_t51ojJeBUVBFnw_jIYllpyUDTpt5hCq4o,13873
|
|
10
|
+
osmosis_ai/cli_services/dataset.py,sha256=oA1n7Nfqkn0xVJxls6Aflpk85rEx625T9TnuZcoM0Ig,6651
|
|
11
|
+
osmosis_ai/cli_services/engine.py,sha256=DaXHAPXpiqHnWoqWXKIGGZOIbn6X7t63qtQlwbP9e40,15866
|
|
12
|
+
osmosis_ai/cli_services/errors.py,sha256=nI6jlICyA4MMNKmwDHQBwyJVah5PVwstmra1HpGkVLE,136
|
|
13
|
+
osmosis_ai/cli_services/reporting.py,sha256=H2g0BmEE2stVey4RmurQM713VowH8984a9r7oDstSkA,12499
|
|
14
|
+
osmosis_ai/cli_services/session.py,sha256=Ru3HA80eqRYZGD1e38N8yd96FiAY8cIYpJvEOHKakM0,6597
|
|
15
|
+
osmosis_ai/cli_services/shared.py,sha256=PilPfW5oDvNL5VG8oObSq2ZL35QPFmhBDf0V4gfd2Ro,5942
|
|
16
|
+
osmosis_ai/providers/__init__.py,sha256=yLSExLbJToZ8AUOVxt4LDplxtIuwv-etSJJyZOcOE2Q,927
|
|
17
|
+
osmosis_ai/providers/anthropic_provider.py,sha256=zrWCVP8co4v8xhcJDFLASwvwEADKN-1p34cY_GH4q5M,3758
|
|
18
|
+
osmosis_ai/providers/base.py,sha256=fN5cnWXYAHN53RR_x6ykbUkM4bictNPDj4U8yd4b2a0,1492
|
|
19
|
+
osmosis_ai/providers/gemini_provider.py,sha256=QANSCmkKungpkpDP2RClmKYnwNVrGv3MKxJwkh68IhY,12045
|
|
20
|
+
osmosis_ai/providers/openai_family.py,sha256=DeQWPMcafEvG4xcI97m3AADTKP2pYw9KwcQTcQg-h_4,26078
|
|
21
|
+
osmosis_ai/providers/shared.py,sha256=dmVe8JDgafPmo6HkP-Kl0aWfffhAT6u3ElV_wLlYD34,2957
|
|
22
|
+
osmosis_ai-0.2.4.dist-info/licenses/LICENSE,sha256=FV2ZmyhdCYinoLLvU_ci-7pZ3DeNYY9XqZjVjOd3h94,1064
|
|
23
|
+
osmosis_ai-0.2.4.dist-info/METADATA,sha256=kyx1K0U5C2Jz9cW7c_zqqEVkAMeeUxjtrE1GfrezicI,15440
|
|
24
|
+
osmosis_ai-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
osmosis_ai-0.2.4.dist-info/entry_points.txt,sha256=aF1CR36a9I9_vcF7nlK9JnK1Iqu614vPy2_jh4QU26A,114
|
|
26
|
+
osmosis_ai-0.2.4.dist-info/top_level.txt,sha256=UPNRTKIBSrxsJVNxwXnLCqSoBS4bAiL_3jMtjvf5zEY,11
|
|
27
|
+
osmosis_ai-0.2.4.dist-info/RECORD,,
|