meshagent-openai 0.0.30__py3-none-any.whl → 0.0.31__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 meshagent-openai might be problematic. Click here for more details.

@@ -0,0 +1,202 @@
1
+
2
+
3
+ def validate_response_format(response_format) -> str | None:
4
+ """
5
+ Validates a response format according to the OpenAI Structured Outputs specification.
6
+
7
+ See https://platform.openai.com/docs/guides/structured-outputs for details.
8
+
9
+ Note: This code is up to date as of January 21, 2024
10
+ """
11
+
12
+ # Check that response_format is a dictionary
13
+ if not isinstance(response_format, dict):
14
+ return "Error: Response format must be a dictionary."
15
+
16
+ # Check that response_format contains exactly "type" and "json_schema" keys
17
+ if set(response_format.keys()) != {"type", "json_schema"}:
18
+ return "Error: Response format must contain exactly 'type' and 'json_schema' keys."
19
+
20
+ # Check that response format has type=json_schema
21
+ if "type" not in response_format or response_format["type"] != "json_schema":
22
+ return "Error: Response format must have type 'json_schema'."
23
+
24
+ # Check that the "json_schema" is a dict
25
+ if "json_schema" not in response_format or not isinstance(response_format["json_schema"], dict):
26
+ return "Error: 'json_schema' key must be a dictionary."
27
+
28
+ # Check that "json_schema" contains exactly "name" and "schema" keys, and optionally "description" and "strict" keys
29
+ required_keys = {"name", "schema"}
30
+ optional_keys = {"description", "strict"}
31
+ if set(response_format["json_schema"].keys()) != required_keys.union(optional_keys):
32
+ return "Error: 'json_schema' key must contain exactly 'name', 'schema', and optionally 'description' and 'strict' keys."
33
+
34
+ # Check that "json_schema" contains a "name" string
35
+ if "name" not in response_format["json_schema"] or not isinstance(response_format["json_schema"]["name"], str):
36
+ return "Error: 'name' key must be a string."
37
+
38
+ # Check that "description" is a string if present
39
+ if "description" in response_format["json_schema"] and not isinstance(response_format["json_schema"]["description"], str):
40
+ return "Error: 'description' key must be a string."
41
+
42
+ # Check that "json_schema" contains a "schema" dict
43
+ if "schema" not in response_format["json_schema"] or not isinstance(response_format["json_schema"]["schema"], dict):
44
+ return "Error: 'schema' key must be a dictionary."
45
+
46
+ # Check that "strict" is a bool if present
47
+ if "strict" in response_format["json_schema"] and not isinstance(response_format["json_schema"]["strict"], bool):
48
+ return "Error: 'strict' key must be a boolean."
49
+
50
+ return validate_schema(response_format["json_schema"]["schema"])
51
+
52
+ def validate_schema(schema, path="root", depth=0, stats=None):
53
+ """
54
+ Validates a JSON schema according to the OpenAI Structured Outputs specification.
55
+
56
+ See https://platform.openai.com/docs/guides/structured-outputs for details.
57
+
58
+ Note: This code is up to date as of January 21, 2024
59
+ """
60
+ print(f"Validating schema at {path}...")
61
+
62
+ # Initialize stats
63
+ if stats is None:
64
+ stats = {
65
+ "total_properties": 0,
66
+ "total_enum_values": 0,
67
+ "total_enum_string_length": 0,
68
+ "total_string_length": 0
69
+ }
70
+
71
+ # Check root object type
72
+ if path == "root" and schema.get("type") != "object":
73
+ return f"Error at {path}: Root schema must be of type 'object'."
74
+
75
+ # Check for anyOf at root
76
+ if path == "root" and "anyOf" in schema:
77
+ return f"Error at {path}: Root schema must not use 'anyOf'."
78
+
79
+ # Check for required fields
80
+ if schema.get("type") == "object" and "properties" in schema:
81
+ if "required" not in schema or set(schema["required"]) != set(schema["properties"].keys()):
82
+ missing_keys = set(schema["properties"].keys()) - set(schema.get("required", []))
83
+ return f"Error at {path}: All object properties must be required. Missing keys: {missing_keys}."
84
+ if "additionalProperties" not in schema or schema["additionalProperties"] is not False:
85
+ return f"Error at {path}: 'additionalProperties' must be set to false."
86
+
87
+ # Check for supported type
88
+ valid_types = {"string", "number", "boolean", "integer", "object", "array", "enum", "anyOf"}
89
+ if "type" in schema:
90
+ schema_type = schema["type"]
91
+ if isinstance(schema_type, list):
92
+ if (len(schema_type) != 2) or ("null" not in schema_type) or not any(t in valid_types for t in schema_type if t != "null"):
93
+ return f"Error at {path}: Invalid type list {schema_type}. Must contain exactly one valid type and None."
94
+ null_allowed = True
95
+ elif schema_type not in valid_types:
96
+ return f"Error at {path}: Invalid type '{schema_type}'. Must be one of {valid_types}."
97
+ else:
98
+ null_allowed = False
99
+
100
+ # Check that enum matches specified type
101
+ if "enum" in schema:
102
+ for enum in schema["enum"]:
103
+ if null_allowed and enum is None:
104
+ continue
105
+ if not null_allowed and enum is None:
106
+ return f"Error at {path}: Enum value cannot be null unless type is [..., null]."
107
+
108
+ schema_type = schema.get("type")
109
+ if isinstance(schema_type, list):
110
+ valid_type = next(t for t in schema_type if t != "null")
111
+ else:
112
+ valid_type = schema_type
113
+
114
+ if valid_type == "integer" and not isinstance(enum, int):
115
+ return f"Error at {path}: Enum value '{enum}' does not match type 'integer'."
116
+ if valid_type == "number" and not isinstance(enum, (int, float)):
117
+ return f"Error at {path}: Enum value '{enum}' does not match type 'number'."
118
+ if valid_type == "string" and not isinstance(enum, str):
119
+ return f"Error at {path}: Enum value '{enum}' does not match type 'string'."
120
+ if valid_type == "boolean" and not isinstance(enum, bool):
121
+ return f"Error at {path}: Enum value '{enum}' does not match type 'boolean'."
122
+ if valid_type == "object" and not isinstance(enum, dict):
123
+ return f"Error at {path}: Enum value '{enum}' does not match type 'object'."
124
+ if valid_type == "array" and not isinstance(enum, list):
125
+ return f"Error at {path}: Enum value '{enum}' does not match type 'array'."
126
+
127
+ # Check for unsupported keywords based on type
128
+ unsupported_keywords_by_type = {
129
+ "string": ["minLength", "maxLength", "pattern", "format"],
130
+ "number": ["minimum", "maximum", "multipleOf"],
131
+ "integer": ["minimum", "maximum", "multipleOf"],
132
+ "object": ["patternProperties", "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties"],
133
+ "array": ["unevaluatedItems", "contains", "minContains", "maxContains", "minItems", "maxItems", "uniqueItems"]
134
+ }
135
+
136
+ schema_type = schema.get("type")
137
+ if isinstance(schema_type, list):
138
+ schema_type = next(t for t in schema_type if t != "null")
139
+
140
+ if schema_type in unsupported_keywords_by_type:
141
+ for keyword in unsupported_keywords_by_type[schema_type]:
142
+ if keyword in schema:
143
+ return f"Error at {path}: Unsupported keyword '{keyword}' found for type '{schema_type}'."
144
+
145
+ # Check for nesting depth
146
+ if depth > 5:
147
+ return f"Error at {path}: Exceeded maximum nesting depth of 5."
148
+
149
+ # Check for total properties
150
+ if schema.get("type") == "object":
151
+ stats["total_properties"] += len(schema.get("properties", {}))
152
+ if stats["total_properties"] > 100:
153
+ return f"Error: Exceeded maximum of 100 object properties."
154
+
155
+ # Check for total string length
156
+ for key in schema.get("properties", {}):
157
+ stats["total_string_length"] += len(key)
158
+ for enum in schema.get("enum", []):
159
+ stats["total_enum_values"] += 1
160
+ stats["total_enum_string_length"] += len(str(enum)) if enum is not None else 4
161
+ if stats["total_string_length"] > 15000:
162
+ return f"Error: Exceeded maximum total string length of 15,000 characters."
163
+ if stats["total_enum_values"] > 500:
164
+ return f"Error: Exceeded maximum of 500 enum values."
165
+ if stats["total_enum_string_length"] > 7500 and stats["total_enum_values"] > 250:
166
+ return f"Error: Exceeded maximum total enum string length of 7,500 characters for more than 250 enum values."
167
+
168
+ # Recursively validate nested schemas
169
+ if "properties" in schema:
170
+ for prop, subschema in schema["properties"].items():
171
+ result = validate_schema(subschema, path=f"{path}.{prop}", depth=depth + 1, stats=stats)
172
+ if result:
173
+ return result
174
+
175
+ if "anyOf" in schema:
176
+ for index, subschema in enumerate(schema["anyOf"]):
177
+ result = validate_schema(subschema, path=f"{path}.anyOf[{index}]", depth=depth + 1, stats=stats)
178
+ if result:
179
+ return result
180
+
181
+ if "$defs" in schema:
182
+ for def_name, subschema in schema["$defs"].items():
183
+ result = validate_schema(subschema, path=f"{path}.$defs.{def_name}", depth=depth + 1, stats=stats)
184
+ if result:
185
+ return result
186
+
187
+ if "items" in schema:
188
+ result = validate_schema(schema["items"], path=f"{path}.items", depth=depth + 1, stats=stats)
189
+ if result:
190
+ return result
191
+
192
+ return None
193
+
194
+
195
+ def validate_strict_schema(schema) -> str | None:
196
+
197
+ return validate_response_format({
198
+ "type" : "json_schema",
199
+ "name" : "schema",
200
+ "strict" : True,
201
+ "json_schema" : schema
202
+ })
@@ -5,8 +5,6 @@ from typing import Optional
5
5
  import io
6
6
  import pathlib
7
7
 
8
- from meshagent.openai.proxy import get_client
9
-
10
8
  async def _transcribe(*, client: AsyncOpenAI, data: bytes, model: str, filename: str, response_format: str, timestamp_granularities: list[str] = None, prompt: Optional[str] = None, language: Optional[str] = None):
11
9
 
12
10
  buf = io.BytesIO(data)
@@ -0,0 +1,85 @@
1
+ import os
2
+ import asyncio
3
+ import pytest
4
+
5
+ from openai import AsyncOpenAI
6
+ from meshagent.tools import JsonResponse, TextResponse
7
+
8
+ from .tts import _transcribe
9
+
10
+ ################################################################################
11
+ # Fixtures
12
+ ################################################################################
13
+ @pytest.fixture(scope="session")
14
+ def client() -> AsyncOpenAI:
15
+ """Real async OpenAI client – no mocks, hits the network."""
16
+ return AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
17
+
18
+
19
+ @pytest.fixture(scope="session")
20
+ def audio_bytes() -> bytes:
21
+ """Loads the test clip only once per session."""
22
+ with open("harvard.wav", "rb") as fp:
23
+ return fp.read()
24
+
25
+
26
+ ################################################################################
27
+ # Tests – one for “text”, one for “json”. Add more if you need other formats.
28
+ ################################################################################
29
+ @pytest.mark.asyncio
30
+ async def test_transcribe_text(client, audio_bytes):
31
+ """_transcribe should return non-empty TextResponse for plain-text format."""
32
+ result = await asyncio.wait_for(
33
+ _transcribe(
34
+ client=client,
35
+ data=audio_bytes,
36
+ filename="harvard.wav",
37
+ model="gpt-4o-mini-transcribe",
38
+ prompt="",
39
+ response_format="text",
40
+ ),
41
+ timeout=90,
42
+ )
43
+
44
+ # Basic sanity checks
45
+ assert isinstance(result, TextResponse)
46
+ assert result.text.strip() != ""
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_transcribe_json(client, audio_bytes):
50
+ """_transcribe should return a well-formed JsonResponse for JSON format."""
51
+ result = await asyncio.wait_for(
52
+ _transcribe(
53
+ client=client,
54
+ data=audio_bytes,
55
+ filename="harvard.wav",
56
+ model="gpt-4o-mini-transcribe",
57
+ prompt="",
58
+ response_format="json"
59
+ ),
60
+ timeout=90,
61
+ )
62
+
63
+ # Basic sanity checks
64
+ assert isinstance(result, JsonResponse)
65
+ assert isinstance(result.json["text"], str)
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_transcribe_verbose_json(client, audio_bytes):
69
+ """_transcribe should return a well-formed JsonResponse for JSON format."""
70
+ result = await asyncio.wait_for(
71
+ _transcribe(
72
+ client=client,
73
+ data=audio_bytes,
74
+ filename="harvard.wav",
75
+ model="whisper-1",
76
+ prompt="",
77
+ response_format="verbose_json"
78
+ ),
79
+ timeout=90,
80
+ )
81
+
82
+ # Basic sanity checks
83
+ assert isinstance(result, JsonResponse)
84
+ assert isinstance(result.json["segments"], list)
85
+
@@ -1 +1 @@
1
- __version__ = "0.0.30"
1
+ __version__ = "0.0.31"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-openai
3
- Version: 0.0.30
3
+ Version: 0.0.31
4
4
  Summary: OpenAI Building Blocks for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -12,10 +12,10 @@ License-File: LICENSE
12
12
  Requires-Dist: pyjwt~=2.10.1
13
13
  Requires-Dist: pytest~=8.3.5
14
14
  Requires-Dist: pytest-asyncio~=0.26.0
15
- Requires-Dist: openai~=1.70.0
16
- Requires-Dist: meshagent-api~=0.0.30
17
- Requires-Dist: meshagent-agents~=0.0.30
18
- Requires-Dist: meshagent-tools~=0.0.30
15
+ Requires-Dist: openai~=1.86.0
16
+ Requires-Dist: meshagent-api~=0.0.31
17
+ Requires-Dist: meshagent-agents~=0.0.31
18
+ Requires-Dist: meshagent-tools~=0.0.31
19
19
  Dynamic: license-file
20
20
 
21
21
  ### Meshagent OpenAI
@@ -0,0 +1,15 @@
1
+ meshagent/openai/__init__.py,sha256=4JRby-ltGfJzrNYhJkMNIpVc2ml2zL_JkkFC0T1_8Vk,174
2
+ meshagent/openai/version.py,sha256=JkmidIUbQFwwAhFKnz2l-8dxIwGERIblSCC4CXcpX08,22
3
+ meshagent/openai/proxy/__init__.py,sha256=SqoueAmMXHbDKd8O4EeqGkI0gEiC3xLTLlpESGxySPU,30
4
+ meshagent/openai/proxy/proxy.py,sha256=FBlFYVwBVorxif1hbYAG3fz-1J3_XPogpBIHS_OGRW0,822
5
+ meshagent/openai/tools/__init__.py,sha256=SRJpWc_L9jv1c8aBLULflDg8co1kaw2Ffnr6hDkYEwg,240
6
+ meshagent/openai/tools/completions_adapter.py,sha256=M8PpyaLu02QwrYkLB3c1h72J3wlmrK3UdfNKx6yUDJk,14483
7
+ meshagent/openai/tools/responses_adapter.py,sha256=KIE68oPssJkh3r6NJcXIJXfAd5cG4KzDFeO5tfkS91A,53116
8
+ meshagent/openai/tools/schema.py,sha256=7WvWFWK65G123G6ADxR27wA8vVpB_Twc3ZXlrYulMZg,9572
9
+ meshagent/openai/tools/stt.py,sha256=08QcfIcdUZgGRhgK-mwrkabKApE7uwhe4fG5invYSh0,3565
10
+ meshagent/openai/tools/stt_test.py,sha256=FCTWZ7bI0vUnTRjRivO_5QEZqHaTE0ehNp1QQkx8iJ0,2651
11
+ meshagent_openai-0.0.31.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
12
+ meshagent_openai-0.0.31.dist-info/METADATA,sha256=TfnNgH14lTJEYYW1TJG2OzTy32X84ntB7xtdcauiZyM,660
13
+ meshagent_openai-0.0.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ meshagent_openai-0.0.31.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
15
+ meshagent_openai-0.0.31.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- meshagent/openai/__init__.py,sha256=4JRby-ltGfJzrNYhJkMNIpVc2ml2zL_JkkFC0T1_8Vk,174
2
- meshagent/openai/version.py,sha256=Ji8g_yOyFfpoV8jQQPNpvlINEqW1-F23R31bqwShK9o,22
3
- meshagent/openai/proxy/__init__.py,sha256=SqoueAmMXHbDKd8O4EeqGkI0gEiC3xLTLlpESGxySPU,30
4
- meshagent/openai/proxy/proxy.py,sha256=FBlFYVwBVorxif1hbYAG3fz-1J3_XPogpBIHS_OGRW0,822
5
- meshagent/openai/tools/__init__.py,sha256=RBU_J4qRDuBaaUdi6jpgpuMlIbvT30QmTBrZrYLwsUU,185
6
- meshagent/openai/tools/completions_adapter.py,sha256=4xEfs57O5GrIRnz1_yVRd3nJpC521lMKFO7RANczRnw,15191
7
- meshagent/openai/tools/responses_adapter.py,sha256=QdTnnSvsGK0nMgceEX-kwo1lAc59ot7G1CYre-n6aFM,23390
8
- meshagent/openai/tools/stt.py,sha256=qMuzkOss7Z5zRVsxmZRASugfgsvuWMG3_6toezq68s8,3612
9
- meshagent_openai-0.0.30.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
10
- meshagent_openai-0.0.30.dist-info/METADATA,sha256=XQb4qd9uTDHuHGJnuvt2Cp3oJ8lgx0xJZWWvrdhFlUg,660
11
- meshagent_openai-0.0.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- meshagent_openai-0.0.30.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
13
- meshagent_openai-0.0.30.dist-info/RECORD,,