synth-ai 0.1.0.dev49__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 +3 -0
  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.dev49.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.dev49.dist-info/RECORD +0 -6
  45. {synth_ai-0.1.0.dev49.dist-info → synth_ai-0.1.0.dev51.dist-info}/WHEEL +0 -0
  46. {synth_ai-0.1.0.dev49.dist-info → synth_ai-0.1.0.dev51.dist-info}/licenses/LICENSE +0 -0
  47. {synth_ai-0.1.0.dev49.dist-info → synth_ai-0.1.0.dev51.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,268 @@
1
+ from typing import Any, Dict, List, Literal, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from synth_ai.zyk.lms.core.exceptions import StructuredOutputCoercionFailureException
6
+ from synth_ai.zyk.lms.core.vendor_clients import (
7
+ anthropic_naming_regexes,
8
+ get_client,
9
+ openai_naming_regexes,
10
+ )
11
+ from synth_ai.zyk.lms.structured_outputs.handler import StructuredOutputHandler
12
+ from synth_ai.zyk.lms.vendors.base import VendorBase
13
+ from synth_ai.zyk.lms.tools.base import BaseTool
14
+ from synth_ai.zyk.lms.config import reasoning_models
15
+
16
+ def build_messages(
17
+ sys_msg: str,
18
+ user_msg: str,
19
+ images_bytes: List = [],
20
+ model_name: Optional[str] = None,
21
+ ) -> List[Dict]:
22
+ if len(images_bytes) > 0 and any(
23
+ regex.match(model_name) for regex in openai_naming_regexes
24
+ ):
25
+ return [
26
+ {"role": "system", "content": sys_msg},
27
+ {
28
+ "role": "user",
29
+ "content": [{"type": "text", "text": user_msg}]
30
+ + [
31
+ {
32
+ "type": "image_url",
33
+ "image_url": {"url": f"data:image/jpeg;base64,{image_bytes}"},
34
+ }
35
+ for image_bytes in images_bytes
36
+ ],
37
+ },
38
+ ]
39
+ elif len(images_bytes) > 0 and any(
40
+ regex.match(model_name) for regex in anthropic_naming_regexes
41
+ ):
42
+ system_info = {"role": "system", "content": sys_msg}
43
+ user_info = {
44
+ "role": "user",
45
+ "content": [{"type": "text", "text": user_msg}]
46
+ + [
47
+ {
48
+ "type": "image",
49
+ "source": {
50
+ "type": "base64",
51
+ "media_type": "image/png",
52
+ "data": image_bytes,
53
+ },
54
+ }
55
+ for image_bytes in images_bytes
56
+ ],
57
+ }
58
+ return [system_info, user_info]
59
+ elif len(images_bytes) > 0:
60
+ raise ValueError("Images are not yet supported for this model")
61
+ else:
62
+ return [
63
+ {"role": "system", "content": sys_msg},
64
+ {"role": "user", "content": user_msg},
65
+ ]
66
+
67
+
68
+ class LM:
69
+ # if str
70
+ model_name: str
71
+ client: VendorBase
72
+ lm_config: Dict[str, Any]
73
+ structured_output_handler: StructuredOutputHandler
74
+
75
+ def __init__(
76
+ self,
77
+ model_name: str,
78
+ formatting_model_name: str,
79
+ temperature: float,
80
+ max_retries: Literal["None", "Few", "Many"] = "Few",
81
+ structured_output_mode: Literal[
82
+ "stringified_json", "forced_json"
83
+ ] = "stringified_json",
84
+ synth_logging: bool = True,
85
+ ):
86
+ # print("Structured output mode", structured_output_mode)
87
+ self.client = get_client(
88
+ model_name,
89
+ with_formatting=structured_output_mode == "forced_json",
90
+ synth_logging=synth_logging,
91
+ )
92
+ # print(self.client.__class__)
93
+
94
+ formatting_client = get_client(formatting_model_name, with_formatting=True)
95
+
96
+ max_retries_dict = {"None": 0, "Few": 2, "Many": 5}
97
+ self.structured_output_handler = StructuredOutputHandler(
98
+ self.client,
99
+ formatting_client,
100
+ structured_output_mode,
101
+ {"max_retries": max_retries_dict.get(max_retries, 2)},
102
+ )
103
+ self.backup_structured_output_handler = StructuredOutputHandler(
104
+ self.client,
105
+ formatting_client,
106
+ "forced_json",
107
+ {"max_retries": max_retries_dict.get(max_retries, 2)},
108
+ )
109
+ # Override temperature to 1 for reasoning models
110
+ effective_temperature = 1.0 if model_name in reasoning_models else temperature
111
+ self.lm_config = {"temperature": effective_temperature}
112
+ self.model_name = model_name
113
+
114
+ def respond_sync(
115
+ self,
116
+ system_message: Optional[str] = None,
117
+ user_message: Optional[str] = None,
118
+ messages: Optional[List[Dict]] = None,
119
+ images_as_bytes: List[Any] = [],
120
+ response_model: Optional[BaseModel] = None,
121
+ use_ephemeral_cache_only: bool = False,
122
+ tools: Optional[List[BaseTool]] = None,
123
+ reasoning_effort: str = "low",
124
+ ):
125
+ assert (system_message is None) == (
126
+ user_message is None
127
+ ), "Must provide both system_message and user_message or neither"
128
+ assert (
129
+ (messages is None) != (system_message is None)
130
+ ), "Must provide either messages or system_message/user_message pair, but not both"
131
+ assert not (response_model and tools), "Cannot provide both response_model and tools"
132
+ if messages is None:
133
+ messages = build_messages(
134
+ system_message, user_message, images_as_bytes, self.model_name
135
+ )
136
+ result = None
137
+ if response_model:
138
+ try:
139
+ result = self.structured_output_handler.call_sync(
140
+ messages,
141
+ model=self.model_name,
142
+ lm_config=self.lm_config,
143
+ response_model=response_model,
144
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
145
+ reasoning_effort=reasoning_effort,
146
+ )
147
+ except StructuredOutputCoercionFailureException:
148
+ # print("Falling back to backup handler")
149
+ result = self.backup_structured_output_handler.call_sync(
150
+ messages,
151
+ model=self.model_name,
152
+ lm_config=self.lm_config,
153
+ response_model=response_model,
154
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
155
+ reasoning_effort=reasoning_effort,
156
+ )
157
+ else:
158
+ result = self.client._hit_api_sync(
159
+ messages=messages,
160
+ model=self.model_name,
161
+ lm_config=self.lm_config,
162
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
163
+ tools=tools,
164
+ reasoning_effort=reasoning_effort,
165
+ )
166
+ assert isinstance(result.raw_response, str), "Raw response must be a string"
167
+ assert (isinstance(result.structured_output, BaseModel) or result.structured_output is None), "Structured output must be a Pydantic model or None"
168
+ assert (isinstance(result.tool_calls, list) or result.tool_calls is None), "Tool calls must be a list or None"
169
+ return result
170
+
171
+ async def respond_async(
172
+ self,
173
+ system_message: Optional[str] = None,
174
+ user_message: Optional[str] = None,
175
+ messages: Optional[List[Dict]] = None,
176
+ images_as_bytes: List[Any] = [],
177
+ response_model: Optional[BaseModel] = None,
178
+ use_ephemeral_cache_only: bool = False,
179
+ tools: Optional[List[BaseTool]] = None,
180
+ reasoning_effort: str = "low",
181
+ ):
182
+ # "In respond_async")
183
+ assert (system_message is None) == (
184
+ user_message is None
185
+ ), "Must provide both system_message and user_message or neither"
186
+ assert (
187
+ (messages is None) != (system_message is None)
188
+ ), "Must provide either messages or system_message/user_message pair, but not both"
189
+
190
+ assert not (response_model and tools), "Cannot provide both response_model and tools"
191
+ if messages is None:
192
+ messages = build_messages(
193
+ system_message, user_message, images_as_bytes, self.model_name
194
+ )
195
+ result = None
196
+ if response_model:
197
+ try:
198
+ print("Trying structured output handler")
199
+ result = await self.structured_output_handler.call_async(
200
+ messages,
201
+ model=self.model_name,
202
+ lm_config=self.lm_config,
203
+ response_model=response_model,
204
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
205
+ reasoning_effort=reasoning_effort,
206
+ )
207
+ except StructuredOutputCoercionFailureException:
208
+ print("Falling back to backup handler")
209
+ result = await self.backup_structured_output_handler.call_async(
210
+ messages,
211
+ model=self.model_name,
212
+ lm_config=self.lm_config,
213
+ response_model=response_model,
214
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
215
+ reasoning_effort=reasoning_effort,
216
+ )
217
+ else:
218
+ #print("Calling API no response model")
219
+ result = await self.client._hit_api_async(
220
+ messages=messages,
221
+ model=self.model_name,
222
+ lm_config=self.lm_config,
223
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
224
+ tools=tools,
225
+ reasoning_effort=reasoning_effort,
226
+ )
227
+ assert isinstance(result.raw_response, str), "Raw response must be a string"
228
+ assert (isinstance(result.structured_output, BaseModel) or result.structured_output is None), "Structured output must be a Pydantic model or None"
229
+ assert (isinstance(result.tool_calls, list) or result.tool_calls is None), "Tool calls must be a list or None"
230
+ return result
231
+
232
+ if __name__ == "__main__":
233
+ import asyncio
234
+
235
+ # Update json instructions to handle nested pydantic?
236
+ class Thought(BaseModel):
237
+ argument_keys: List[str] = Field(description="The keys of the arguments")
238
+ argument_values: List[str] = Field(
239
+ description="Stringified JSON for the values of the arguments"
240
+ )
241
+
242
+ class TestModel(BaseModel):
243
+ emotion: str = Field(description="The emotion expressed")
244
+ concern: str = Field(description="The concern expressed")
245
+ action: str = Field(description="The action to be taken")
246
+ thought: Thought = Field(description="The thought process")
247
+
248
+ class Config:
249
+ schema_extra = {"required": ["thought", "emotion", "concern", "action"]}
250
+
251
+ lm = LM(
252
+ model_name="gpt-4o-mini",
253
+ formatting_model_name="gpt-4o-mini",
254
+ temperature=1,
255
+ max_retries="Few",
256
+ structured_output_mode="forced_json",
257
+ )
258
+ print(
259
+ asyncio.run(
260
+ lm.respond_async(
261
+ system_message="You are a helpful assistant ",
262
+ user_message="Hello, how are you?",
263
+ images_as_bytes=[],
264
+ response_model=TestModel,
265
+ use_ephemeral_cache_only=False,
266
+ )
267
+ )
268
+ )
@@ -0,0 +1,85 @@
1
+ import re
2
+ from typing import Any, List, Pattern
3
+
4
+ from synth_ai.zyk.lms.core.all import (
5
+ AnthropicClient,
6
+ DeepSeekClient,
7
+ GeminiClient,
8
+ GroqAPI,
9
+ MistralAPI,
10
+ # OpenAIClient,
11
+ OpenAIStructuredOutputClient,
12
+ TogetherClient,
13
+ )
14
+
15
+ openai_naming_regexes: List[Pattern] = [
16
+ re.compile(r"^(ft:)?(o[1,3,4](-.*)?|gpt-.*)$"),
17
+ ]
18
+ openai_formatting_model_regexes: List[Pattern] = [
19
+ re.compile(r"^(ft:)?gpt-4o(-.*)?$"),
20
+ ]
21
+ anthropic_naming_regexes: List[Pattern] = [
22
+ re.compile(r"^claude-.*$"),
23
+ ]
24
+ gemini_naming_regexes: List[Pattern] = [
25
+ re.compile(r"^gemini-.*$"),
26
+ re.compile(r"^gemma[2-9].*$"),
27
+ ]
28
+ deepseek_naming_regexes: List[Pattern] = [
29
+ re.compile(r"^deepseek-.*$"),
30
+ ]
31
+ together_naming_regexes: List[Pattern] = [
32
+ re.compile(r"^.*\/.*$"),
33
+ ]
34
+
35
+ groq_naming_regexes: List[Pattern] = [
36
+ re.compile(r"^llama-3.3-70b-versatile$"),
37
+ re.compile(r"^llama-3.1-8b-instant$"),
38
+ re.compile(r"^qwen-2.5-32b$"),
39
+ re.compile(r"^deepseek-r1-distill-qwen-32b$"),
40
+ re.compile(r"^deepseek-r1-distill-llama-70b-specdec$"),
41
+ re.compile(r"^deepseek-r1-distill-llama-70b$"),
42
+ re.compile(r"^llama-3.3-70b-specdec$"),
43
+ re.compile(r"^llama-3.2-1b-preview$"),
44
+ re.compile(r"^llama-3.2-3b-preview$"),
45
+ re.compile(r"^llama-3.2-11b-vision-preview$"),
46
+ re.compile(r"^llama-3.2-90b-vision-preview$"),
47
+ ]
48
+
49
+ mistral_naming_regexes: List[Pattern] = [
50
+ re.compile(r"^mistral-.*$"),
51
+ ]
52
+
53
+
54
+ def get_client(
55
+ model_name: str,
56
+ with_formatting: bool = False,
57
+ synth_logging: bool = True,
58
+ ) -> Any:
59
+ # print("With formatting", with_formatting)
60
+ if any(regex.match(model_name) for regex in openai_naming_regexes):
61
+ # print("Returning OpenAIStructuredOutputClient")
62
+ return OpenAIStructuredOutputClient(
63
+ synth_logging=synth_logging,
64
+ )
65
+ elif any(regex.match(model_name) for regex in anthropic_naming_regexes):
66
+ if with_formatting:
67
+ client = AnthropicClient()
68
+ client._hit_api_async_structured_output = OpenAIStructuredOutputClient(
69
+ synth_logging=synth_logging
70
+ )._hit_api_async
71
+ return client
72
+ else:
73
+ return AnthropicClient()
74
+ elif any(regex.match(model_name) for regex in gemini_naming_regexes):
75
+ return GeminiClient()
76
+ elif any(regex.match(model_name) for regex in deepseek_naming_regexes):
77
+ return DeepSeekClient()
78
+ elif any(regex.match(model_name) for regex in together_naming_regexes):
79
+ return TogetherClient()
80
+ elif any(regex.match(model_name) for regex in groq_naming_regexes):
81
+ return GroqAPI()
82
+ elif any(regex.match(model_name) for regex in mistral_naming_regexes):
83
+ return MistralAPI()
84
+ else:
85
+ raise ValueError(f"Invalid model name: {model_name}")
File without changes
@@ -0,0 +1 @@
1
+ #TODO
@@ -0,0 +1 @@
1
+ # Maybe some kind of ephemeral cache
File without changes