synth-ai 0.1.0.dev50__py3-none-any.whl → 0.1.0.dev51__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.
Files changed (47) hide show
  1. synth_ai/__init__.py +1 -1
  2. synth_ai/zyk/__init__.py +3 -0
  3. synth_ai/zyk/lms/__init__.py +0 -0
  4. synth_ai/zyk/lms/caching/__init__.py +0 -0
  5. synth_ai/zyk/lms/caching/constants.py +1 -0
  6. synth_ai/zyk/lms/caching/dbs.py +0 -0
  7. synth_ai/zyk/lms/caching/ephemeral.py +72 -0
  8. synth_ai/zyk/lms/caching/handler.py +137 -0
  9. synth_ai/zyk/lms/caching/initialize.py +13 -0
  10. synth_ai/zyk/lms/caching/persistent.py +83 -0
  11. synth_ai/zyk/lms/config.py +10 -0
  12. synth_ai/zyk/lms/constants.py +22 -0
  13. synth_ai/zyk/lms/core/__init__.py +0 -0
  14. synth_ai/zyk/lms/core/all.py +47 -0
  15. synth_ai/zyk/lms/core/exceptions.py +9 -0
  16. synth_ai/zyk/lms/core/main.py +268 -0
  17. synth_ai/zyk/lms/core/vendor_clients.py +85 -0
  18. synth_ai/zyk/lms/cost/__init__.py +0 -0
  19. synth_ai/zyk/lms/cost/monitor.py +1 -0
  20. synth_ai/zyk/lms/cost/statefulness.py +1 -0
  21. synth_ai/zyk/lms/structured_outputs/__init__.py +0 -0
  22. synth_ai/zyk/lms/structured_outputs/handler.py +441 -0
  23. synth_ai/zyk/lms/structured_outputs/inject.py +314 -0
  24. synth_ai/zyk/lms/structured_outputs/rehabilitate.py +187 -0
  25. synth_ai/zyk/lms/tools/base.py +118 -0
  26. synth_ai/zyk/lms/vendors/__init__.py +0 -0
  27. synth_ai/zyk/lms/vendors/base.py +31 -0
  28. synth_ai/zyk/lms/vendors/core/__init__.py +0 -0
  29. synth_ai/zyk/lms/vendors/core/anthropic_api.py +365 -0
  30. synth_ai/zyk/lms/vendors/core/gemini_api.py +282 -0
  31. synth_ai/zyk/lms/vendors/core/mistral_api.py +331 -0
  32. synth_ai/zyk/lms/vendors/core/openai_api.py +187 -0
  33. synth_ai/zyk/lms/vendors/local/__init__.py +0 -0
  34. synth_ai/zyk/lms/vendors/local/ollama.py +0 -0
  35. synth_ai/zyk/lms/vendors/openai_standard.py +345 -0
  36. synth_ai/zyk/lms/vendors/retries.py +3 -0
  37. synth_ai/zyk/lms/vendors/supported/__init__.py +0 -0
  38. synth_ai/zyk/lms/vendors/supported/deepseek.py +73 -0
  39. synth_ai/zyk/lms/vendors/supported/groq.py +16 -0
  40. synth_ai/zyk/lms/vendors/supported/ollama.py +14 -0
  41. synth_ai/zyk/lms/vendors/supported/together.py +11 -0
  42. {synth_ai-0.1.0.dev50.dist-info → synth_ai-0.1.0.dev51.dist-info}/METADATA +1 -1
  43. synth_ai-0.1.0.dev51.dist-info/RECORD +46 -0
  44. synth_ai-0.1.0.dev50.dist-info/RECORD +0 -6
  45. {synth_ai-0.1.0.dev50.dist-info → synth_ai-0.1.0.dev51.dist-info}/WHEEL +0 -0
  46. {synth_ai-0.1.0.dev50.dist-info → synth_ai-0.1.0.dev51.dist-info}/licenses/LICENSE +0 -0
  47. {synth_ai-0.1.0.dev50.dist-info → synth_ai-0.1.0.dev51.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,314 @@
1
+ import json
2
+ from typing import (
3
+ Any,
4
+ Dict,
5
+ List,
6
+ Optional,
7
+ Tuple,
8
+ Type,
9
+ get_type_hints,
10
+ get_args,
11
+ get_origin,
12
+ Union,
13
+ Literal,
14
+ )
15
+ from pydantic import BaseModel
16
+ import warnings
17
+
18
+
19
+ def generate_type_map() -> Dict[Any, str]:
20
+ base_types = {
21
+ int: "int",
22
+ float: "float",
23
+ bool: "bool",
24
+ str: "str",
25
+ Any: "Any",
26
+ }
27
+
28
+ collection_types = {
29
+ List: "List",
30
+ Dict: "Dict",
31
+ Optional: "Optional",
32
+ }
33
+
34
+ type_map = {}
35
+ for base_type, name in base_types.items():
36
+ type_map[base_type] = name
37
+ for collection, collection_name in collection_types.items():
38
+ if collection is Optional:
39
+ type_map[Optional[base_type]] = name
40
+ elif collection is Dict:
41
+ # Handle generic Dict type
42
+ type_map[Dict] = "Dict[Any,Any]"
43
+ # Provide both key and value types for Dict
44
+ type_map[Dict[base_type, base_type]] = (
45
+ f"{collection_name}[{name},{name}]"
46
+ )
47
+ # Handle Dict[Any, Any] explicitly
48
+ type_map[Dict[Any, Any]] = "Dict[Any,Any]"
49
+ else:
50
+ type_map[collection[base_type]] = f"{collection_name}[{name}]"
51
+ return type_map
52
+
53
+
54
+ def generate_example_dict() -> Dict[str, Any]:
55
+ example_values = {
56
+ "str": "<Your type-str response here>",
57
+ "int": "<Your type-int response here>",
58
+ "float": "<Your type-float response here>",
59
+ "bool": "<Your type-bool response here>",
60
+ "Any": "<Your response here (infer the type from context)>",
61
+ }
62
+
63
+ example_dict = {}
64
+ for key, value in example_values.items():
65
+ example_dict[key] = value
66
+ example_dict[f"List[{key}]"] = [value]
67
+ example_dict[f"List[List[{key}]]"] = [[value]]
68
+ if key == "Dict[str,str]":
69
+ example_dict[f"Dict[str,{key.split('[')[1]}"] = {value: value}
70
+ elif key.startswith("Dict"):
71
+ example_dict[key] = {value: value}
72
+
73
+ # Add example for Dict[Any,Any]
74
+ example_dict["Dict[Any,Any]"] = {"<key>": "<value>"}
75
+
76
+ return example_dict
77
+
78
+
79
+ base_type_examples = {
80
+ int: ("int", 42),
81
+ float: ("float", 3.14),
82
+ bool: ("bool", True),
83
+ str: ("str", "example"),
84
+ Any: ("Any", "<Any value>"),
85
+ }
86
+
87
+
88
+ def get_type_string(type_hint):
89
+ origin = get_origin(type_hint)
90
+ args = get_args(type_hint)
91
+
92
+ if origin is None:
93
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
94
+ # For Pydantic models, create a dictionary of field types
95
+ field_types = {}
96
+ for field_name, field_info in type_hint.model_fields.items():
97
+ field_type = get_type_string(field_info.annotation)
98
+ # Check for Literal type by looking at the origin
99
+ if get_origin(field_info.annotation) is Literal:
100
+ literal_args = get_args(field_info.annotation)
101
+ field_type = f"Literal[{repr(literal_args[0])}]"
102
+ field_types[field_name] = field_type
103
+ return f"{type_hint.__name__}({', '.join(f'{k}: {v}' for k, v in field_types.items())})"
104
+ else:
105
+ return base_type_examples.get(type_hint, ("Unknown", "unknown"))[0]
106
+ elif origin in (list, List):
107
+ elem_type = get_type_string(args[0])
108
+ return f"List[{elem_type}]"
109
+ elif origin in (dict, Dict):
110
+ key_type = get_type_string(args[0])
111
+ value_type = get_type_string(args[1])
112
+ return f"Dict[{key_type}, {value_type}]"
113
+ elif origin is Union:
114
+ non_none_types = [t for t in args if t is not type(None)]
115
+ if len(non_none_types) == 1:
116
+ return f"Optional[{get_type_string(non_none_types[0])}]"
117
+ else:
118
+ # For unions of Pydantic models (like tool calls), show each variant
119
+ union_types = []
120
+ for t in non_none_types:
121
+ if isinstance(t, type) and issubclass(t, BaseModel):
122
+ # Include discriminator field if present
123
+ discriminator = None
124
+ for field_name, field_info in t.model_fields.items():
125
+ if get_origin(field_info.annotation) is Literal:
126
+ literal_args = get_args(field_info.annotation)
127
+ discriminator = f"{field_name}={repr(literal_args[0])}"
128
+ break
129
+ type_str = t.__name__
130
+ if discriminator:
131
+ type_str += f"({discriminator})"
132
+ union_types.append(type_str)
133
+ else:
134
+ union_types.append(get_type_string(t))
135
+ return f"Union[{', '.join(union_types)}]"
136
+ elif origin is Literal:
137
+ # Handle Literal type directly
138
+ return f"Literal[{repr(args[0])}]"
139
+ else:
140
+ return "Any"
141
+
142
+
143
+ def get_example_value(type_hint):
144
+ origin = get_origin(type_hint)
145
+ args = get_args(type_hint)
146
+
147
+ if origin is None:
148
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
149
+ example = {}
150
+ union_docs = []
151
+ for field_name, field_info in type_hint.model_fields.items():
152
+ field_value, field_docs = get_example_value(field_info.annotation)
153
+ if field_docs:
154
+ union_docs.extend(field_docs)
155
+
156
+ # Handle literal fields by checking origin
157
+ if get_origin(field_info.annotation) is Literal:
158
+ literal_args = get_args(field_info.annotation)
159
+ field_value = literal_args[0]
160
+
161
+ # Include field description if available
162
+ if field_info.description:
163
+ example[field_name] = {
164
+ "value": field_value,
165
+ "description": field_info.description,
166
+ }
167
+ else:
168
+ example[field_name] = field_value
169
+ return example, union_docs
170
+ else:
171
+ return base_type_examples.get(type_hint, ("Unknown", "unknown"))[1], []
172
+ elif origin in (list, List):
173
+ value, docs = get_example_value(args[0])
174
+ return [value], docs
175
+ elif origin in (dict, Dict):
176
+ if not args or len(args) < 2:
177
+ warnings.warn(
178
+ f"Dictionary type hint {type_hint} missing type arguments. "
179
+ "Defaulting to Dict[str, Any].",
180
+ UserWarning,
181
+ )
182
+ return {"example_key": "<Any value>"}, [] # Default for Dict[str, Any]
183
+ key_value, key_docs = get_example_value(args[0])
184
+ value_value, value_docs = get_example_value(args[1])
185
+ return {key_value: value_value}, key_docs + value_docs
186
+ elif origin is Union:
187
+ non_none_types = [t for t in args if t is not type(None)]
188
+ # For unions of tool calls, use the first one but preserve the discriminator
189
+ first_type = non_none_types[0]
190
+ union_docs = []
191
+
192
+ if all(
193
+ isinstance(t, type) and issubclass(t, BaseModel) for t in non_none_types
194
+ ):
195
+ # Generate examples for all union variants
196
+ for t in non_none_types:
197
+ example = {}
198
+ for field_name, field_info in t.model_fields.items():
199
+ field_value, _ = get_example_value(field_info.annotation)
200
+ if get_origin(field_info.annotation) is Literal:
201
+ literal_args = get_args(field_info.annotation)
202
+ field_value = literal_args[0]
203
+ example[field_name] = field_value
204
+ union_docs.append(f"\nExample {t.__name__}:")
205
+ union_docs.append(json.dumps(example, indent=2))
206
+
207
+ # Return first type as main example
208
+ if isinstance(first_type, type) and issubclass(first_type, BaseModel):
209
+ example, _ = get_example_value(first_type)
210
+ # Ensure tool_type or other discriminator is preserved
211
+ for field_name, field_info in first_type.model_fields.items():
212
+ if get_origin(field_info.annotation) is Literal:
213
+ literal_args = get_args(field_info.annotation)
214
+ if (
215
+ isinstance(example[field_name], dict)
216
+ and "value" in example[field_name]
217
+ ):
218
+ example[field_name]["value"] = literal_args[0]
219
+ else:
220
+ example[field_name] = literal_args[0]
221
+ return example, union_docs
222
+ main_example, docs = get_example_value(first_type)
223
+ return main_example, docs + union_docs
224
+ elif origin is Literal:
225
+ # Handle Literal type directly
226
+ return args[0], []
227
+ else:
228
+ return "<Unknown>", []
229
+
230
+
231
+ def add_json_instructions_to_messages(
232
+ system_message,
233
+ user_message,
234
+ response_model: Optional[Type[BaseModel]] = None,
235
+ previously_failed_error_messages: List[str] = [],
236
+ ) -> Tuple[str, str]:
237
+ if response_model:
238
+ type_hints = get_type_hints(response_model)
239
+ # print("Type hints", type_hints)
240
+ stringified_fields = {}
241
+ union_docs = []
242
+ for key, type_hint in type_hints.items():
243
+ # print("Key", key, "Type hint", type_hint)
244
+ example_value, docs = get_example_value(type_hint)
245
+ union_docs.extend(docs)
246
+ field = response_model.model_fields[key] # Updated for Pydantic v2
247
+
248
+ # Adjusted for Pydantic v2
249
+ field_description = (
250
+ field.description if hasattr(field, "description") else None
251
+ )
252
+
253
+ if field_description:
254
+ stringified_fields[key] = (example_value, field_description)
255
+ else:
256
+ stringified_fields[key] = example_value
257
+ example_json = json.dumps(
258
+ {
259
+ k: v[0] if isinstance(v, tuple) else v
260
+ for k, v in stringified_fields.items()
261
+ },
262
+ indent=4,
263
+ )
264
+ description_comments = "\n".join(
265
+ f"// {k}: {v[1]}"
266
+ for k, v in stringified_fields.items()
267
+ if isinstance(v, tuple)
268
+ )
269
+
270
+ # print("Example JSON", example_json)
271
+ # print("Description comments", description_comments)
272
+ # print("Union documentation", "\n".join(union_docs))
273
+ # raise Exception("Stop here")
274
+
275
+ system_message += f"""\n\n
276
+ Please deliver your response in the following JSON format:
277
+ ```json
278
+ {example_json}
279
+ ```
280
+ {description_comments}
281
+ """
282
+ if len(union_docs) != 0:
283
+ system_message += f"""\n\n
284
+ NOTE - the above example included a union type. Here are some additional examples for that union type:
285
+ {chr(10).join(union_docs)}
286
+ """
287
+ if len(previously_failed_error_messages) != 0:
288
+ system_message += f"""\n\n
289
+ Please take special care to follow the format exactly.
290
+ Keep in mind the following:
291
+ - Always use double quotes for strings
292
+
293
+ Here are some error traces from previous attempts:
294
+ {previously_failed_error_messages}
295
+ """
296
+ return system_message, user_message
297
+
298
+
299
+ def inject_structured_output_instructions(
300
+ messages: List[Dict[str, str]],
301
+ response_model: Optional[Type[BaseModel]] = None,
302
+ previously_failed_error_messages: List[str] = [],
303
+ ) -> List[Dict[str, str]]:
304
+ prev_system_message_content = messages[0]["content"]
305
+ prev_user_message_content = messages[1]["content"]
306
+ system_message, user_message = add_json_instructions_to_messages(
307
+ prev_system_message_content,
308
+ prev_user_message_content,
309
+ response_model,
310
+ previously_failed_error_messages,
311
+ )
312
+ messages[0]["content"] = system_message
313
+ messages[1]["content"] = user_message
314
+ return messages
@@ -0,0 +1,187 @@
1
+ import ast
2
+ import json
3
+ import logging
4
+ import re
5
+ from typing import Dict, List, Type, Union
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from synth_ai.zyk.lms.vendors.base import VendorBase
10
+ from synth_ai.zyk.lms.vendors.core.openai_api import OpenAIStructuredOutputClient
11
+
12
+
13
+ def pull_out_structured_output(
14
+ response_raw: str, response_model: Type[BaseModel]
15
+ ) -> BaseModel:
16
+ logger = logging.getLogger(__name__)
17
+ #logger.debug(f"Raw response received: {response_raw}")
18
+
19
+ assert isinstance(
20
+ response_raw, str
21
+ ), f"Response raw is not a string: {type(response_raw)}"
22
+
23
+ # Use regex to extract JSON content within ```json ... ```
24
+ json_pattern = re.compile(r"```json\s*(\{.*\})\s*```", re.DOTALL)
25
+ match = json_pattern.search(response_raw)
26
+ if match:
27
+ response_prepared = match.group(1).strip()
28
+ else:
29
+ # Fallback to existing parsing if no code fencing is found
30
+ if "```" in response_raw:
31
+ response_prepared = response_raw.split("```")[1].strip()
32
+ else:
33
+ response_prepared = response_raw.strip()
34
+
35
+ # Replace "null" with '"None"' if needed (ensure this aligns with your data)
36
+ response_prepared = response_prepared.replace("null", '"None"')
37
+
38
+ logger.debug(f"Prepared response for JSON parsing: {response_prepared}")
39
+
40
+ try:
41
+ response = json.loads(response_prepared)
42
+ final = response_model(**response)
43
+ except json.JSONDecodeError as e:
44
+ # Attempt to parse using ast.literal_eval as a fallback
45
+ response_prepared = response_prepared.replace("\n", "").replace("\\n", "")
46
+ response_prepared = response_prepared.replace('\\"', '"')
47
+ try:
48
+ response = ast.literal_eval(response_prepared)
49
+ final = response_model(**response)
50
+ except Exception as inner_e:
51
+ raise ValueError(
52
+ f"Failed to parse response as {response_model}: {inner_e} - {response_prepared}"
53
+ )
54
+ except Exception as e:
55
+ raise ValueError(
56
+ f"Failed to parse response as {response_model}: {e} - {response_prepared}"
57
+ )
58
+ assert isinstance(final, BaseModel), "Structured output must be a Pydantic model"
59
+ return final
60
+
61
+
62
+ def fix_errant_stringified_json_sync(
63
+ response_raw: str,
64
+ response_model: Type[BaseModel],
65
+ models: List[str] = ["gpt-4o-mini", "gpt-4o"],
66
+ ) -> BaseModel:
67
+ try:
68
+ return pull_out_structured_output(response_raw, response_model)
69
+ except ValueError as e:
70
+ # print("")
71
+ mini_client = OpenAIStructuredOutputClient()
72
+ messages = [
73
+ {
74
+ "role": "system",
75
+ "content": "An AI attempted to generate stringified json that could be extracted into the provided Pydantic model. This json cannot be extracted, and an error message is provided to elucidate why. Please review the information and return a corrected response. Do not materially change the content of the response, just formatting where necessary.",
76
+ },
77
+ {
78
+ "role": "user",
79
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}\n# Error Message\n {str(e)}.",
80
+ },
81
+ ]
82
+ for model in models:
83
+ if model == "gpt-4o":
84
+ print(
85
+ "Warning - using gpt-4o for structured output correction - this could add up over time (latency, cost)"
86
+ )
87
+ try:
88
+ fixed_response = mini_client._hit_api_sync(
89
+ model, messages=messages, lm_config={"temperature": 0.0}
90
+ )
91
+ return pull_out_structured_output(fixed_response, response_model)
92
+ except Exception as e:
93
+ pass
94
+ raise ValueError("Failed to fix response using any model")
95
+
96
+
97
+ async def fix_errant_stringified_json_async(
98
+ response_raw: str,
99
+ response_model: Type[BaseModel],
100
+ models: List[str] = ["gpt-4o-mini", "gpt-4o"],
101
+ ) -> BaseModel:
102
+ try:
103
+ return pull_out_structured_output(response_raw, response_model)
104
+ except ValueError as e:
105
+ mini_client = OpenAIStructuredOutputClient()
106
+ messages = [
107
+ {
108
+ "role": "system",
109
+ "content": "An AI attempted to generate stringified json that could be extracted into the provided Pydantic model. This json cannot be extracted, and an error message is provided to elucidate why. Please review the information and return a corrected response. Do not materially change the content of the response, just formatting where necessary.",
110
+ },
111
+ {
112
+ "role": "user",
113
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}\n# Error Message\n {str(e)}.",
114
+ },
115
+ ]
116
+ for model in models:
117
+ if model == "gpt-4o":
118
+ print(
119
+ "Warning - using gpt-4o for structured output correction - this could add up over time (latency, cost)"
120
+ )
121
+ try:
122
+ fixed_response = await mini_client._hit_api_async(
123
+ model, messages=messages, lm_config={"temperature": 0.0}
124
+ )
125
+ return pull_out_structured_output(fixed_response, response_model)
126
+ except Exception as e:
127
+ pass
128
+ raise ValueError("Failed to fix response using any model")
129
+
130
+
131
+ async def fix_errant_forced_async(
132
+ messages: List[Dict],
133
+ response_raw: str,
134
+ response_model: Type[BaseModel],
135
+ model: str,
136
+ ) -> BaseModel:
137
+ try:
138
+ return pull_out_structured_output(response_raw, response_model)
139
+ except ValueError as e:
140
+ client = OpenAIStructuredOutputClient()
141
+ messages = [
142
+ {
143
+ "role": "system",
144
+ "content": "An AI attempted to generate stringified json that could be extracted into the provided Pydantic model. This json cannot be extracted, and an error message is provided to elucidate why. Please review the information and return a corrected response. Do not materially change the content of the response, just formatting where necessary.",
145
+ },
146
+ {
147
+ "role": "user",
148
+ "content": f"<previous messages>\n<system_message>{messages[0]['content']}</system_message>\n<user_message>{messages[1]['content']}</user_message></previous messages> # Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}\n# Error Message\n {str(e)}.",
149
+ },
150
+ ]
151
+ # print("Broken response:")
152
+ # print(response_raw)
153
+ fixed_response = await client._hit_api_async_structured_output(
154
+ model=model,
155
+ messages=messages,
156
+ response_model=response_model,
157
+ temperature=0.0,
158
+ )
159
+ # print("Fixed response:")
160
+ # print(fixed_response)
161
+ return fixed_response.structured_output
162
+
163
+
164
+ def fix_errant_forced_sync(
165
+ response_raw: str,
166
+ response_model: Type[BaseModel],
167
+ model: str,
168
+ ) -> BaseModel:
169
+ client = OpenAIStructuredOutputClient()
170
+ messages = [
171
+ {
172
+ "role": "system",
173
+ "content": "An AI attempted to generate a response that could be extracted into the provided Pydantic model. This response cannot be extracted. Please review the information and return a corrected response.",
174
+ },
175
+ {
176
+ "role": "user",
177
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}",
178
+ },
179
+ ]
180
+ # print("Broken response:")
181
+ # print(response_raw)
182
+ fixed_response = client._hit_api_sync_structured_output(
183
+ model=model, messages=messages, response_model=response_model, temperature=0.0
184
+ )
185
+ # print("Fixed response:")
186
+ # print(fixed_response)
187
+ return fixed_response.structured_output
@@ -0,0 +1,118 @@
1
+ from typing import Type
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class BaseTool(BaseModel):
7
+ name: str
8
+ arguments: Type[BaseModel]
9
+ description: str = ""
10
+ strict: bool = True
11
+
12
+ def to_openai_tool(self):
13
+ schema = self.arguments.model_json_schema()
14
+ schema["additionalProperties"] = False
15
+
16
+ if "properties" in schema:
17
+ for prop_name, prop_schema in schema["properties"].items():
18
+ if prop_schema.get("type") == "array":
19
+ items_schema = prop_schema.get("items", {})
20
+ if not isinstance(items_schema, dict) or not items_schema.get("type"):
21
+ prop_schema["items"] = {"type": "string"}
22
+
23
+ elif "anyOf" in prop_schema:
24
+ for sub_schema in prop_schema["anyOf"]:
25
+ if isinstance(sub_schema, dict) and sub_schema.get("type") == "array":
26
+ items_schema = sub_schema.get("items", {})
27
+ if not isinstance(items_schema, dict) or not items_schema.get("type"):
28
+ sub_schema["items"] = {"type": "string"}
29
+
30
+ return {
31
+ "type": "function",
32
+ "function": {
33
+ "name": self.name,
34
+ "description": self.description,
35
+ "parameters": schema,
36
+ "strict": self.strict,
37
+ },
38
+ }
39
+
40
+ def to_anthropic_tool(self):
41
+ schema = self.arguments.model_json_schema()
42
+ schema["additionalProperties"] = False
43
+
44
+ return {
45
+ "name": self.name,
46
+ "description": self.description,
47
+ "input_schema": {
48
+ "type": "object",
49
+ "properties": schema["properties"],
50
+ "required": schema.get("required", []),
51
+ },
52
+ }
53
+
54
+ def to_mistral_tool(self):
55
+ schema = self.arguments.model_json_schema()
56
+ properties = {}
57
+ for prop_name, prop in schema.get("properties", {}).items():
58
+ prop_type = prop["type"]
59
+ if prop_type == "array" and "items" in prop:
60
+ properties[prop_name] = {
61
+ "type": "array",
62
+ "items": prop["items"],
63
+ "description": prop.get("description", ""),
64
+ }
65
+ continue
66
+
67
+ properties[prop_name] = {
68
+ "type": prop_type,
69
+ "description": prop.get("description", ""),
70
+ }
71
+ if "enum" in prop:
72
+ properties[prop_name]["enum"] = prop["enum"]
73
+
74
+ parameters = {
75
+ "type": "object",
76
+ "properties": properties,
77
+ "required": schema.get("required", []),
78
+ "additionalProperties": False,
79
+ }
80
+ return {
81
+ "type": "function",
82
+ "function": {
83
+ "name": self.name,
84
+ "description": self.description,
85
+ "parameters": parameters,
86
+ },
87
+ }
88
+
89
+ def to_gemini_tool(self):
90
+ schema = self.arguments.model_json_schema()
91
+ # Convert Pydantic schema types to Gemini schema types
92
+ properties = {}
93
+ for name, prop in schema["properties"].items():
94
+ prop_type = prop.get("type", "string")
95
+ if prop_type == "array" and "items" in prop:
96
+ properties[name] = {
97
+ "type": "array",
98
+ "items": prop["items"],
99
+ "description": prop.get("description", ""),
100
+ }
101
+ continue
102
+
103
+ properties[name] = {
104
+ "type": prop_type,
105
+ "description": prop.get("description", ""),
106
+ }
107
+ if "enum" in prop:
108
+ properties[name]["enum"] = prop["enum"]
109
+
110
+ return {
111
+ "name": self.name,
112
+ "description": self.description,
113
+ "parameters": {
114
+ "type": "object",
115
+ "properties": properties,
116
+ "required": schema.get("required", []),
117
+ },
118
+ }
File without changes
@@ -0,0 +1,31 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class BaseLMResponse(BaseModel):
8
+ raw_response: str
9
+ structured_output: Optional[BaseModel] = None
10
+ tool_calls: Optional[List[Dict]] = None
11
+
12
+
13
+ class VendorBase(ABC):
14
+ used_for_structured_outputs: bool = False
15
+ exceptions_to_retry: List[Exception] = []
16
+
17
+ @abstractmethod
18
+ async def _hit_api_async(
19
+ self,
20
+ messages: List[Dict[str, Any]],
21
+ response_model_override: Optional[BaseModel] = None,
22
+ ) -> str:
23
+ pass
24
+
25
+ @abstractmethod
26
+ def _hit_api_sync(
27
+ self,
28
+ messages: List[Dict[str, Any]],
29
+ response_model_override: Optional[BaseModel] = None,
30
+ ) -> str:
31
+ pass
File without changes