synth-ai 0.0.0.dev3__py3-none-any.whl → 0.1.0.dev6__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 synth-ai might be problematic. Click here for more details.

Files changed (48) hide show
  1. public_tests/synth_sdk.py +389 -0
  2. public_tests/test_agent.py +538 -0
  3. public_tests/test_recursive_structured_outputs.py +180 -0
  4. public_tests/test_structured_outputs.py +100 -0
  5. synth_ai/__init__.py +1 -0
  6. synth_ai/zyk/__init__.py +3 -0
  7. synth_ai/zyk/lms/__init__.py +0 -0
  8. synth_ai/zyk/lms/caching/__init__.py +0 -0
  9. synth_ai/zyk/lms/caching/constants.py +1 -0
  10. synth_ai/zyk/lms/caching/dbs.py +0 -0
  11. synth_ai/zyk/lms/caching/ephemeral.py +50 -0
  12. synth_ai/zyk/lms/caching/handler.py +92 -0
  13. synth_ai/zyk/lms/caching/initialize.py +13 -0
  14. synth_ai/zyk/lms/caching/persistent.py +55 -0
  15. synth_ai/zyk/lms/config.py +8 -0
  16. synth_ai/zyk/lms/core/__init__.py +0 -0
  17. synth_ai/zyk/lms/core/all.py +35 -0
  18. synth_ai/zyk/lms/core/exceptions.py +9 -0
  19. synth_ai/zyk/lms/core/main.py +245 -0
  20. synth_ai/zyk/lms/core/vendor_clients.py +60 -0
  21. synth_ai/zyk/lms/cost/__init__.py +0 -0
  22. synth_ai/zyk/lms/cost/monitor.py +1 -0
  23. synth_ai/zyk/lms/cost/statefulness.py +1 -0
  24. synth_ai/zyk/lms/structured_outputs/__init__.py +0 -0
  25. synth_ai/zyk/lms/structured_outputs/handler.py +388 -0
  26. synth_ai/zyk/lms/structured_outputs/inject.py +185 -0
  27. synth_ai/zyk/lms/structured_outputs/rehabilitate.py +186 -0
  28. synth_ai/zyk/lms/vendors/__init__.py +0 -0
  29. synth_ai/zyk/lms/vendors/base.py +15 -0
  30. synth_ai/zyk/lms/vendors/constants.py +5 -0
  31. synth_ai/zyk/lms/vendors/core/__init__.py +0 -0
  32. synth_ai/zyk/lms/vendors/core/anthropic_api.py +191 -0
  33. synth_ai/zyk/lms/vendors/core/gemini_api.py +146 -0
  34. synth_ai/zyk/lms/vendors/core/openai_api.py +145 -0
  35. synth_ai/zyk/lms/vendors/local/__init__.py +0 -0
  36. synth_ai/zyk/lms/vendors/local/ollama.py +0 -0
  37. synth_ai/zyk/lms/vendors/openai_standard.py +141 -0
  38. synth_ai/zyk/lms/vendors/retries.py +3 -0
  39. synth_ai/zyk/lms/vendors/supported/__init__.py +0 -0
  40. synth_ai/zyk/lms/vendors/supported/deepseek.py +18 -0
  41. synth_ai/zyk/lms/vendors/supported/together.py +11 -0
  42. {synth_ai-0.0.0.dev3.dist-info → synth_ai-0.1.0.dev6.dist-info}/METADATA +1 -1
  43. synth_ai-0.1.0.dev6.dist-info/RECORD +46 -0
  44. synth_ai-0.1.0.dev6.dist-info/top_level.txt +2 -0
  45. synth_ai-0.0.0.dev3.dist-info/RECORD +0 -6
  46. synth_ai-0.0.0.dev3.dist-info/top_level.txt +0 -1
  47. {synth_ai-0.0.0.dev3.dist-info → synth_ai-0.1.0.dev6.dist-info}/LICENSE +0 -0
  48. {synth_ai-0.0.0.dev3.dist-info → synth_ai-0.1.0.dev6.dist-info}/WHEEL +0 -0
@@ -0,0 +1,185 @@
1
+ import json
2
+ from typing import Any, Dict, List, Optional, Tuple, Type, get_type_hints, get_args, get_origin, Union
3
+ from pydantic import BaseModel
4
+ import warnings
5
+
6
+
7
+ def generate_type_map() -> Dict[Any, str]:
8
+ base_types = {
9
+ int: "int",
10
+ float: "float",
11
+ bool: "bool",
12
+ str: "str",
13
+ Any: "Any",
14
+ }
15
+
16
+ collection_types = {
17
+ List: "List",
18
+ Dict: "Dict",
19
+ Optional: "Optional",
20
+ }
21
+
22
+ type_map = {}
23
+ for base_type, name in base_types.items():
24
+ type_map[base_type] = name
25
+ for collection, collection_name in collection_types.items():
26
+ if collection is Optional:
27
+ type_map[Optional[base_type]] = name
28
+ elif collection is Dict:
29
+ # Handle generic Dict type
30
+ type_map[Dict] = "Dict[Any,Any]"
31
+ # Provide both key and value types for Dict
32
+ type_map[Dict[base_type, base_type]] = f"{collection_name}[{name},{name}]"
33
+ # Handle Dict[Any, Any] explicitly
34
+ type_map[Dict[Any, Any]] = "Dict[Any,Any]"
35
+ else:
36
+ type_map[collection[base_type]] = f"{collection_name}[{name}]"
37
+ return type_map
38
+
39
+ def generate_example_dict() -> Dict[str, Any]:
40
+ example_values = {
41
+ "str": "<Your type-str response here>",
42
+ "int": "<Your type-int response here>",
43
+ "float": "<Your type-float response here>",
44
+ "bool": "<Your type-bool response here>",
45
+ "Any": "<Your response here (infer the type from context)>",
46
+ }
47
+
48
+ example_dict = {}
49
+ for key, value in example_values.items():
50
+ example_dict[key] = value
51
+ example_dict[f"List[{key}]"] = [value]
52
+ example_dict[f"List[List[{key}]]"] = [[value]]
53
+ if key == "Dict[str,str]":
54
+ example_dict[f"Dict[str,{key.split('[')[1]}"] = {value: value}
55
+ elif key.startswith("Dict"):
56
+ example_dict[key] = {value: value}
57
+
58
+ # Add example for Dict[Any,Any]
59
+ example_dict["Dict[Any,Any]"] = {"<key>": "<value>"}
60
+
61
+ return example_dict
62
+
63
+ base_type_examples = {
64
+ int: ("int", 42),
65
+ float: ("float", 3.14),
66
+ bool: ("bool", True),
67
+ str: ("str", "example"),
68
+ Any: ("Any", "<Any value>"),
69
+ }
70
+
71
+ def get_type_string(type_hint):
72
+ origin = get_origin(type_hint)
73
+ args = get_args(type_hint)
74
+
75
+ if origin is None:
76
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
77
+ return type_hint.__name__
78
+ else:
79
+ return base_type_examples.get(type_hint, ("Unknown", "unknown"))[0]
80
+ elif origin in (list, List):
81
+ elem_type = get_type_string(args[0])
82
+ return f"List[{elem_type}]"
83
+ elif origin in (dict, Dict):
84
+ key_type = get_type_string(args[0])
85
+ value_type = get_type_string(args[1])
86
+ return f"Dict[{key_type}, {value_type}]"
87
+ elif origin is Union:
88
+ non_none_types = [t for t in args if t is not type(None)]
89
+ if len(non_none_types) == 1:
90
+ return f"Optional[{get_type_string(non_none_types[0])}]"
91
+ else:
92
+ union_types = ", ".join(get_type_string(t) for t in non_none_types)
93
+ return f"Union[{union_types}]"
94
+ else:
95
+ return "Any"
96
+
97
+ def get_example_value(type_hint):
98
+ origin = get_origin(type_hint)
99
+ args = get_args(type_hint)
100
+
101
+ if origin is None:
102
+ if isinstance(type_hint, type) and issubclass(type_hint, BaseModel):
103
+ example = {}
104
+ for field_name, field_info in type_hint.model_fields.items():
105
+ # Updated attribute from type_ to annotation
106
+ example[field_name] = get_example_value(field_info.annotation)
107
+ return example
108
+ else:
109
+ return base_type_examples.get(type_hint, ("Unknown", "unknown"))[1]
110
+ elif origin in (list, List):
111
+ return [get_example_value(args[0])]
112
+ elif origin in (dict, Dict):
113
+ if not args or len(args) < 2:
114
+ warnings.warn(
115
+ f"Dictionary type hint {type_hint} missing type arguments. "
116
+ "Defaulting to Dict[str, Any].",
117
+ UserWarning
118
+ )
119
+ return {"example_key": "<Any value>"} # Default for Dict[str, Any]
120
+ return {get_example_value(args[0]): get_example_value(args[1])}
121
+ elif origin is Union:
122
+ non_none_types = [t for t in args if t is not type(None)]
123
+ return get_example_value(non_none_types[0])
124
+ else:
125
+ return "<Unknown>"
126
+
127
+ def add_json_instructions_to_messages(
128
+ system_message,
129
+ user_message,
130
+ response_model: Optional[Type[BaseModel]] = None,
131
+ previously_failed_error_messages: List[str] = [],
132
+ ) -> Tuple[str, str]:
133
+ if response_model:
134
+ type_hints = get_type_hints(response_model)
135
+ stringified_fields = {}
136
+ for key, type_hint in type_hints.items():
137
+ example_value = get_example_value(type_hint)
138
+ field = response_model.model_fields[key] # Updated for Pydantic v2
139
+
140
+ # Adjusted for Pydantic v2
141
+ field_description = field.description if hasattr(field, 'description') else None
142
+
143
+ if field_description:
144
+ stringified_fields[key] = (example_value, field_description)
145
+ else:
146
+ stringified_fields[key] = example_value
147
+ example_json = json.dumps(
148
+ {k: v[0] if isinstance(v, tuple) else v for k, v in stringified_fields.items()},
149
+ indent=4
150
+ )
151
+ description_comments = "\n".join(
152
+ f'// {k}: {v[1]}' for k, v in stringified_fields.items() if isinstance(v, tuple)
153
+ )
154
+ system_message += f"""\n\n
155
+ Please deliver your response in the following JSON format:
156
+ ```json
157
+ {example_json}
158
+ ```
159
+ {description_comments}
160
+ """
161
+ if len(previously_failed_error_messages) != 0:
162
+ system_message += f"""\n\n
163
+ Please take special care to follow the format exactly.
164
+ Keep in mind the following:
165
+ - Always use double quotes for strings
166
+
167
+ Here are some error traces from previous attempts:
168
+ {previously_failed_error_messages}
169
+ """
170
+ return system_message, user_message
171
+
172
+
173
+ def inject_structured_output_instructions(
174
+ messages: List[Dict[str, str]],
175
+ response_model: Optional[Type[BaseModel]] = None,
176
+ previously_failed_error_messages: List[str] = [],
177
+ ) -> List[Dict[str, str]]:
178
+ prev_system_message_content = messages[0]["content"]
179
+ prev_user_message_content = messages[1]["content"]
180
+ system_message, user_message = add_json_instructions_to_messages(
181
+ prev_system_message_content, prev_user_message_content, response_model, previously_failed_error_messages
182
+ )
183
+ messages[0]["content"] = system_message
184
+ messages[1]["content"] = user_message
185
+ return messages
@@ -0,0 +1,186 @@
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
+ return final
59
+
60
+
61
+ def fix_errant_stringified_json_sync(
62
+ response_raw: str,
63
+ response_model: Type[BaseModel],
64
+ models: List[str] = ["gpt-4o-mini", "gpt-4o"],
65
+ ) -> BaseModel:
66
+ try:
67
+ return pull_out_structured_output(response_raw, response_model)
68
+ except ValueError as e:
69
+ # print("")
70
+ mini_client = OpenAIStructuredOutputClient()
71
+ messages = [
72
+ {
73
+ "role": "system",
74
+ "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.",
75
+ },
76
+ {
77
+ "role": "user",
78
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}\n# Error Message\n {str(e)}.",
79
+ },
80
+ ]
81
+ for model in models:
82
+ if model == "gpt-4o":
83
+ print(
84
+ "Warning - using gpt-4o for structured output correction - this could add up over time (latency, cost)"
85
+ )
86
+ try:
87
+ fixed_response = mini_client._hit_api_sync(
88
+ model, messages=messages, lm_config={"temperature": 0.0}
89
+ )
90
+ return pull_out_structured_output(fixed_response, response_model)
91
+ except Exception as e:
92
+ pass
93
+ raise ValueError("Failed to fix response using any model")
94
+
95
+
96
+ async def fix_errant_stringified_json_async(
97
+ response_raw: str,
98
+ response_model: Type[BaseModel],
99
+ models: List[str] = ["gpt-4o-mini", "gpt-4o"],
100
+ ) -> BaseModel:
101
+ try:
102
+ return pull_out_structured_output(response_raw, response_model)
103
+ except ValueError as e:
104
+ mini_client = OpenAIStructuredOutputClient()
105
+ messages = [
106
+ {
107
+ "role": "system",
108
+ "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.",
109
+ },
110
+ {
111
+ "role": "user",
112
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}\n# Error Message\n {str(e)}.",
113
+ },
114
+ ]
115
+ for model in models:
116
+ if model == "gpt-4o":
117
+ print(
118
+ "Warning - using gpt-4o for structured output correction - this could add up over time (latency, cost)"
119
+ )
120
+ try:
121
+ fixed_response = await mini_client._hit_api_async(
122
+ model, messages=messages, lm_config={"temperature": 0.0}
123
+ )
124
+ return pull_out_structured_output(fixed_response, response_model)
125
+ except Exception as e:
126
+ pass
127
+ raise ValueError("Failed to fix response using any model")
128
+
129
+
130
+ async def fix_errant_forced_async(
131
+ messages: List[Dict],
132
+ response_raw: str,
133
+ response_model: Type[BaseModel],
134
+ model: str,
135
+ ) -> BaseModel:
136
+ try:
137
+ return pull_out_structured_output(response_raw, response_model)
138
+ except ValueError as e:
139
+ client = OpenAIStructuredOutputClient()
140
+ messages = [
141
+ {
142
+ "role": "system",
143
+ "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.",
144
+ },
145
+ {
146
+ "role": "user",
147
+ "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)}.",
148
+ },
149
+ ]
150
+ # print("Broken response:")
151
+ # print(response_raw)
152
+ fixed_response = await client._hit_api_async_structured_output(
153
+ model=model,
154
+ messages=messages,
155
+ response_model=response_model,
156
+ temperature=0.0,
157
+ )
158
+ # print("Fixed response:")
159
+ # print(fixed_response)
160
+ return fixed_response
161
+
162
+
163
+ def fix_errant_forced_sync(
164
+ response_raw: str,
165
+ response_model: Type[BaseModel],
166
+ model: str,
167
+ ) -> BaseModel:
168
+ client = OpenAIStructuredOutputClient()
169
+ messages = [
170
+ {
171
+ "role": "system",
172
+ "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.",
173
+ },
174
+ {
175
+ "role": "user",
176
+ "content": f"# Errant Attempt\n{response_raw}\n# Response Model\n {response_model.model_json_schema()}",
177
+ },
178
+ ]
179
+ # print("Broken response:")
180
+ # print(response_raw)
181
+ fixed_response = client._hit_api_sync_structured_output(
182
+ model=model, messages=messages, response_model=response_model, temperature=0.0
183
+ )
184
+ # print("Fixed response:")
185
+ # print(fixed_response)
186
+ return fixed_response
File without changes
@@ -0,0 +1,15 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Any, Optional
3
+ from pydantic import BaseModel
4
+
5
+ class VendorBase(ABC):
6
+ used_for_structured_outputs: bool = False
7
+ exceptions_to_retry: List[Exception] = []
8
+
9
+ @abstractmethod
10
+ async def _hit_api_async(self, messages: List[Dict[str, Any]], response_model_override: Optional[BaseModel] = None) -> str:
11
+ pass
12
+
13
+ @abstractmethod
14
+ def _hit_api_sync(self, messages: List[Dict[str, Any]], response_model_override: Optional[BaseModel] = None) -> str:
15
+ pass
@@ -0,0 +1,5 @@
1
+ SPECIAL_BASE_TEMPS = {
2
+ "claude-3-5-sonnet-20240620": 0.7,
3
+ "o1-mini": 1,
4
+ "o1-preview": 1,
5
+ }
File without changes
@@ -0,0 +1,191 @@
1
+ import json
2
+ from typing import Any, Dict, List, Tuple, Type
3
+
4
+ import anthropic
5
+ import pydantic
6
+ from pydantic import BaseModel
7
+
8
+ from synth_ai.zyk.lms.caching.initialize import (
9
+ get_cache_handler,
10
+ )
11
+ from synth_ai.zyk.lms.vendors.base import VendorBase
12
+ from synth_ai.zyk.lms.vendors.constants import SPECIAL_BASE_TEMPS
13
+ from synth_ai.zyk.lms.vendors.core.openai_api import OpenAIStructuredOutputClient
14
+ from synth_ai.zyk.lms.vendors.retries import BACKOFF_TOLERANCE, backoff
15
+
16
+ ANTHROPIC_EXCEPTIONS_TO_RETRY: Tuple[Type[Exception], ...] = (anthropic.APIError,)
17
+
18
+
19
+ class AnthropicAPI(VendorBase):
20
+ used_for_structured_outputs: bool = True
21
+ exceptions_to_retry: Tuple = ANTHROPIC_EXCEPTIONS_TO_RETRY
22
+ sync_client: Any
23
+ async_client: Any
24
+
25
+ def __init__(
26
+ self,
27
+ exceptions_to_retry: Tuple[
28
+ Type[Exception], ...
29
+ ] = ANTHROPIC_EXCEPTIONS_TO_RETRY,
30
+ used_for_structured_outputs: bool = False,
31
+ ):
32
+ self.sync_client = anthropic.Anthropic()
33
+ self.async_client = anthropic.AsyncAnthropic()
34
+ self.used_for_structured_outputs = used_for_structured_outputs
35
+ self.exceptions_to_retry = exceptions_to_retry
36
+ self._openai_fallback = None
37
+
38
+ @backoff.on_exception(
39
+ backoff.expo,
40
+ exceptions_to_retry,
41
+ max_tries=BACKOFF_TOLERANCE,
42
+ on_giveup=lambda e: print(e),
43
+ )
44
+ async def _hit_api_async(
45
+ self,
46
+ model: str,
47
+ messages: List[Dict[str, Any]],
48
+ lm_config: Dict[str, Any],
49
+ use_ephemeral_cache_only: bool = False,
50
+ ) -> str:
51
+ assert (
52
+ lm_config.get("response_model", None) is None
53
+ ), "response_model is not supported for standard calls"
54
+ used_cache_handler = get_cache_handler(use_ephemeral_cache_only)
55
+ cache_result = used_cache_handler.hit_managed_cache(
56
+ model, messages, lm_config=lm_config
57
+ )
58
+ if cache_result:
59
+ return (
60
+ cache_result["response"]
61
+ if isinstance(cache_result, dict)
62
+ else cache_result
63
+ )
64
+ response = await self.async_client.messages.create(
65
+ system=messages[0]["content"],
66
+ messages=messages[1:],
67
+ model=model,
68
+ max_tokens=lm_config.get("max_tokens", 4096),
69
+ temperature=lm_config.get("temperature", SPECIAL_BASE_TEMPS.get(model, 0)),
70
+ )
71
+ api_result = response.content[0].text
72
+ used_cache_handler.add_to_managed_cache(
73
+ model, messages, lm_config=lm_config, output=api_result
74
+ )
75
+ return api_result
76
+
77
+ @backoff.on_exception(
78
+ backoff.expo,
79
+ exceptions_to_retry,
80
+ max_tries=BACKOFF_TOLERANCE,
81
+ on_giveup=lambda e: print(e),
82
+ )
83
+ def _hit_api_sync(
84
+ self,
85
+ model: str,
86
+ messages: List[Dict[str, Any]],
87
+ lm_config: Dict[str, Any],
88
+ use_ephemeral_cache_only: bool = False,
89
+ ) -> str:
90
+ assert (
91
+ lm_config.get("response_model", None) is None
92
+ ), "response_model is not supported for standard calls"
93
+ used_cache_handler = get_cache_handler(
94
+ use_ephemeral_cache_only=use_ephemeral_cache_only
95
+ )
96
+ cache_result = used_cache_handler.hit_managed_cache(
97
+ model, messages, lm_config=lm_config
98
+ )
99
+ if cache_result:
100
+ return (
101
+ cache_result["response"]
102
+ if isinstance(cache_result, dict)
103
+ else cache_result
104
+ )
105
+ # print("Calling Anthropic API")
106
+ # import time
107
+ # t = time.time()
108
+ response = self.sync_client.messages.create(
109
+ system=messages[0]["content"],
110
+ messages=messages[1:],
111
+ model=model,
112
+ max_tokens=lm_config.get("max_tokens", 4096),
113
+ temperature=lm_config.get("temperature", SPECIAL_BASE_TEMPS.get(model, 0)),
114
+ )
115
+ # print("Time taken for API call", time.time() - t)
116
+ api_result = response.content[0].text
117
+ used_cache_handler.add_to_managed_cache(
118
+ model, messages, lm_config=lm_config, output=api_result
119
+ )
120
+ return api_result
121
+
122
+ async def _hit_api_async_structured_output(
123
+ self,
124
+ model: str,
125
+ messages: List[Dict[str, Any]],
126
+ response_model: BaseModel,
127
+ temperature: float,
128
+ use_ephemeral_cache_only: bool = False,
129
+ ) -> str:
130
+ try:
131
+ # First try with Anthropic
132
+ response = await self.async_client.messages.create(
133
+ system=messages[0]["content"],
134
+ messages=messages[1:],
135
+ model=model,
136
+ max_tokens=4096,
137
+ temperature=temperature,
138
+ )
139
+ result = response.content[0].text
140
+ # Try to parse the result as JSON
141
+ parsed = json.loads(result)
142
+ return response_model(**parsed)
143
+ except (json.JSONDecodeError, pydantic.ValidationError):
144
+ # If Anthropic fails, fallback to OpenAI
145
+ if self._openai_fallback is None:
146
+ self._openai_fallback = OpenAIStructuredOutputClient()
147
+ return await self._openai_fallback._hit_api_async_structured_output(
148
+ model="gpt-4o", # Fallback to GPT-4
149
+ messages=messages,
150
+ response_model=response_model,
151
+ temperature=temperature,
152
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
153
+ )
154
+
155
+ def _hit_api_sync_structured_output(
156
+ self,
157
+ model: str,
158
+ messages: List[Dict[str, Any]],
159
+ response_model: BaseModel,
160
+ temperature: float,
161
+ use_ephemeral_cache_only: bool = False,
162
+ ) -> str:
163
+ try:
164
+ # First try with Anthropic
165
+ import time
166
+
167
+ # t = time.time()
168
+ response = self.sync_client.messages.create(
169
+ system=messages[0]["content"],
170
+ messages=messages[1:],
171
+ model=model,
172
+ max_tokens=4096,
173
+ temperature=temperature,
174
+ )
175
+ # print("Time taken for API call", time.time() - t)
176
+ result = response.content[0].text
177
+ # Try to parse the result as JSON
178
+ parsed = json.loads(result)
179
+ return response_model(**parsed)
180
+ except (json.JSONDecodeError, pydantic.ValidationError):
181
+ # If Anthropic fails, fallback to OpenAI
182
+ print("WARNING - Falling back to OpenAI - THIS IS SLOW")
183
+ if self._openai_fallback is None:
184
+ self._openai_fallback = OpenAIStructuredOutputClient()
185
+ return self._openai_fallback._hit_api_sync_structured_output(
186
+ model="gpt-4o", # Fallback to GPT-4
187
+ messages=messages,
188
+ response_model=response_model,
189
+ temperature=temperature,
190
+ use_ephemeral_cache_only=use_ephemeral_cache_only,
191
+ )