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.

Files changed (36) hide show
  1. osmosis_ai/__init__.py +19 -132
  2. osmosis_ai/cli.py +50 -0
  3. osmosis_ai/cli_commands.py +181 -0
  4. osmosis_ai/cli_services/__init__.py +60 -0
  5. osmosis_ai/cli_services/config.py +410 -0
  6. osmosis_ai/cli_services/dataset.py +175 -0
  7. osmosis_ai/cli_services/engine.py +421 -0
  8. osmosis_ai/cli_services/errors.py +7 -0
  9. osmosis_ai/cli_services/reporting.py +307 -0
  10. osmosis_ai/cli_services/session.py +174 -0
  11. osmosis_ai/cli_services/shared.py +209 -0
  12. osmosis_ai/consts.py +2 -16
  13. osmosis_ai/providers/__init__.py +36 -0
  14. osmosis_ai/providers/anthropic_provider.py +85 -0
  15. osmosis_ai/providers/base.py +60 -0
  16. osmosis_ai/providers/gemini_provider.py +314 -0
  17. osmosis_ai/providers/openai_family.py +607 -0
  18. osmosis_ai/providers/shared.py +92 -0
  19. osmosis_ai/rubric_eval.py +356 -0
  20. osmosis_ai/rubric_types.py +49 -0
  21. osmosis_ai/utils.py +284 -89
  22. osmosis_ai-0.2.4.dist-info/METADATA +314 -0
  23. osmosis_ai-0.2.4.dist-info/RECORD +27 -0
  24. osmosis_ai-0.2.4.dist-info/entry_points.txt +4 -0
  25. {osmosis_ai-0.1.8.dist-info → osmosis_ai-0.2.4.dist-info}/licenses/LICENSE +1 -1
  26. osmosis_ai/adapters/__init__.py +0 -9
  27. osmosis_ai/adapters/anthropic.py +0 -502
  28. osmosis_ai/adapters/langchain.py +0 -674
  29. osmosis_ai/adapters/langchain_anthropic.py +0 -338
  30. osmosis_ai/adapters/langchain_openai.py +0 -596
  31. osmosis_ai/adapters/openai.py +0 -900
  32. osmosis_ai/logger.py +0 -77
  33. osmosis_ai-0.1.8.dist-info/METADATA +0 -281
  34. osmosis_ai-0.1.8.dist-info/RECORD +0 -15
  35. {osmosis_ai-0.1.8.dist-info → osmosis_ai-0.2.4.dist-info}/WHEEL +0 -0
  36. {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
- # Import logger
15
- from .logger import logger
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
- # Global configuration
18
- enabled = True
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
- def init(api_key: str) -> None:
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
- Args:
28
- api_key: The OSMOSIS API key for logging LLM usage
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
- global osmosis_api_key, _initialized
31
- osmosis_api_key = api_key
32
- _initialized = True
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
- def disable_osmosis() -> None:
36
- global enabled
37
- enabled = False
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
- def enable_osmosis() -> None:
41
- global enabled
42
- enabled = True
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
- def send_to_osmosis(
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
- if not _initialized:
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
- try:
64
- # Import requests only when needed
65
- import requests
66
-
67
- # Create headers
68
- headers = {"Content-Type": "application/json", "x-api-key": osmosis_api_key}
69
-
70
- # Prepare main data payload
71
- data = {
72
- "owner": xxhash.xxh32(osmosis_api_key.encode("utf-8")).hexdigest(),
73
- "date": int(datetime.now(timezone.utc).timestamp()),
74
- "query": query,
75
- "response": response,
76
- "status": status,
77
- }
78
-
79
- logger.info(f"Sending data to OSMOSIS API: {data}")
80
-
81
- # Send main data payload
82
- response_data = requests.post(
83
- f"{osmosis_api_url}/ingest",
84
- headers=headers,
85
- data=json.dumps(data).replace("\n", "") + "\n",
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
- except ImportError:
94
- logger.warning(
95
- "Requests library not installed. Please install it with 'pip install requests'."
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
- except Exception as e:
98
- logger.warning(f"Failed to send data to OSMOSIS API: {str(e)}")
165
+ return False
99
166
 
100
167
 
101
- def osmosis_reward(func: Callable) -> Callable:
168
+ def osmosis_rubric(func: Callable) -> Callable:
102
169
  """
103
- Decorator for reward functions.
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 reward function to be wrapped
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
- @osmosis_reward
113
- def calculate_reward(state, action):
114
- return state.score + action.value
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
- return func(*args, **kwargs)
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