osmosis-ai 0.1.8__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 +2 -16
- 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 +284 -89
- 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.8.dist-info → osmosis_ai-0.2.4.dist-info}/licenses/LICENSE +1 -1
- osmosis_ai/adapters/__init__.py +0 -9
- osmosis_ai/adapters/anthropic.py +0 -502
- osmosis_ai/adapters/langchain.py +0 -674
- osmosis_ai/adapters/langchain_anthropic.py +0 -338
- osmosis_ai/adapters/langchain_openai.py +0 -596
- osmosis_ai/adapters/openai.py +0 -900
- osmosis_ai/logger.py +0 -77
- osmosis_ai-0.1.8.dist-info/METADATA +0 -281
- osmosis_ai-0.1.8.dist-info/RECORD +0 -15
- {osmosis_ai-0.1.8.dist-info → osmosis_ai-0.2.4.dist-info}/WHEEL +0 -0
- {osmosis_ai-0.1.8.dist-info → osmosis_ai-0.2.4.dist-info}/top_level.txt +0 -0
osmosis_ai/utils.py
CHANGED
|
@@ -1,120 +1,315 @@
|
|
|
1
|
-
|
|
2
|
-
Utility functions for osmosisadapters
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
-
from typing import Any, Dict, Callable
|
|
8
|
-
import xxhash
|
|
1
|
+
|
|
9
2
|
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import types
|
|
5
|
+
from typing import Any, Callable, Mapping, Union, get_args, get_origin, get_type_hints
|
|
10
6
|
|
|
11
|
-
# Import constants
|
|
12
|
-
from .consts import osmosis_api_url
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
def osmosis_reward(func: Callable) -> Callable:
|
|
9
|
+
"""
|
|
10
|
+
Decorator for reward functions that enforces the signature:
|
|
11
|
+
(solution_str: str, ground_truth: str, extra_info: dict = None) -> float
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
osmosis_api_key = None # Will be set by init()
|
|
20
|
-
_initialized = False
|
|
13
|
+
Args:
|
|
14
|
+
func: The reward function to be wrapped
|
|
21
15
|
|
|
16
|
+
Returns:
|
|
17
|
+
The wrapped function
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Initialize osmosiswith the OSMOSIS API key.
|
|
19
|
+
Raises:
|
|
20
|
+
TypeError: If the function doesn't have the required signature or doesn't return a float
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
Example:
|
|
23
|
+
@osmosis_reward
|
|
24
|
+
def calculate_reward(solution_str: str, ground_truth: str, extra_info: dict = None) -> float:
|
|
25
|
+
return some_calculation(solution_str, ground_truth)
|
|
29
26
|
"""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
# Validate function signature
|
|
28
|
+
sig = inspect.signature(func)
|
|
29
|
+
params = list(sig.parameters.values())
|
|
33
30
|
|
|
31
|
+
if len(params) < 3:
|
|
32
|
+
raise TypeError(f"Function {func.__name__} must have at least 3 parameters, got {len(params)}")
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
# Check first parameter: solution_str: str
|
|
35
|
+
if params[0].name != 'solution_str':
|
|
36
|
+
raise TypeError(f"First parameter must be named 'solution_str', got '{params[0].name}'")
|
|
37
|
+
if params[0].annotation != str:
|
|
38
|
+
raise TypeError(f"First parameter 'solution_str' must be annotated as str, got {params[0].annotation}")
|
|
38
39
|
|
|
40
|
+
# Check second parameter: ground_truth: str
|
|
41
|
+
if params[1].name != 'ground_truth':
|
|
42
|
+
raise TypeError(f"Second parameter must be named 'ground_truth', got '{params[1].name}'")
|
|
43
|
+
if params[1].annotation != str:
|
|
44
|
+
raise TypeError(f"Second parameter 'ground_truth' must be annotated as str, got {params[1].annotation}")
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
# Check third parameter if present: extra_info: dict = None
|
|
47
|
+
if len(params) >= 3:
|
|
48
|
+
if params[2].name != 'extra_info':
|
|
49
|
+
raise TypeError(f"Third parameter must be named 'extra_info', got '{params[2].name}'")
|
|
50
|
+
if params[2].annotation != dict:
|
|
51
|
+
raise TypeError(f"Third parameter 'extra_info' must be annotated as dict, got {params[2].annotation}")
|
|
52
|
+
if params[2].default is inspect.Parameter.empty:
|
|
53
|
+
raise TypeError("Third parameter 'extra_info' must have a default value of None")
|
|
43
54
|
|
|
55
|
+
@functools.wraps(func)
|
|
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
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
query: Dict[str, Any], response: Dict[str, Any], status: int = 200
|
|
47
|
-
) -> None:
|
|
48
|
-
"""
|
|
49
|
-
Send query and response data to the OSMOSIS API using AWS Firehose.
|
|
63
|
+
return wrapper
|
|
50
64
|
|
|
51
|
-
Args:
|
|
52
|
-
query: The query/request data
|
|
53
|
-
response: The response data
|
|
54
|
-
status: The HTTP status code (default: 200)
|
|
55
|
-
"""
|
|
56
|
-
if not enabled or not osmosis_api_key:
|
|
57
|
-
return
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
logger.warning("osmosisnot initialized. Call osmosis_ai.init(api_key) first.")
|
|
61
|
-
return
|
|
66
|
+
ALLOWED_ROLES = {"user", "system", "assistant", "developer", "tool", "function"}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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[")
|
|
86
125
|
)
|
|
126
|
+
origin = get_origin(annotation)
|
|
127
|
+
return origin is list
|
|
87
128
|
|
|
88
|
-
if response_data.status_code != 200:
|
|
89
|
-
logger.warning(
|
|
90
|
-
f"OSMOSIS API returned status {response_data.status_code} for data with error: {response_data.text}"
|
|
91
|
-
)
|
|
92
129
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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[")
|
|
96
164
|
)
|
|
97
|
-
|
|
98
|
-
logger.warning(f"Failed to send data to OSMOSIS API: {str(e)}")
|
|
165
|
+
return False
|
|
99
166
|
|
|
100
167
|
|
|
101
|
-
def
|
|
168
|
+
def osmosis_rubric(func: Callable) -> Callable:
|
|
102
169
|
"""
|
|
103
|
-
Decorator for
|
|
104
|
-
|
|
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
|
+
|
|
105
176
|
Args:
|
|
106
|
-
func: The
|
|
107
|
-
|
|
177
|
+
func: The rubric function to be wrapped.
|
|
178
|
+
|
|
108
179
|
Returns:
|
|
109
|
-
The wrapped function
|
|
110
|
-
|
|
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
|
+
|
|
111
185
|
Example:
|
|
112
|
-
@
|
|
113
|
-
def
|
|
114
|
-
|
|
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)
|
|
115
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
|
+
|
|
116
239
|
@functools.wraps(func)
|
|
117
240
|
def wrapper(*args, **kwargs):
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
|
|
310
|
+
result = func(*args, **kwargs)
|
|
311
|
+
if not isinstance(result, float):
|
|
312
|
+
raise TypeError(f"Function {func.__name__} must return a float, got {type(result).__name__}")
|
|
313
|
+
return result
|
|
314
|
+
|
|
120
315
|
return wrapper
|