meshagent-openai 0.18.0__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.
@@ -0,0 +1,253 @@
1
+ def validate_response_format(response_format) -> str | None:
2
+ """
3
+ Validates a response format according to the OpenAI Structured Outputs specification.
4
+
5
+ See https://platform.openai.com/docs/guides/structured-outputs for details.
6
+
7
+ Note: This code is up to date as of January 21, 2024
8
+ """
9
+
10
+ # Check that response_format is a dictionary
11
+ if not isinstance(response_format, dict):
12
+ return "Error: Response format must be a dictionary."
13
+
14
+ # Check that response_format contains exactly "type" and "json_schema" keys
15
+ if set(response_format.keys()) != {"type", "json_schema"}:
16
+ return (
17
+ "Error: Response format must contain exactly 'type' and 'json_schema' keys."
18
+ )
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(
26
+ response_format["json_schema"], dict
27
+ ):
28
+ return "Error: 'json_schema' key must be a dictionary."
29
+
30
+ # Check that "json_schema" contains exactly "name" and "schema" keys, and optionally "description" and "strict" keys
31
+ required_keys = {"name", "schema"}
32
+ optional_keys = {"description", "strict"}
33
+ if set(response_format["json_schema"].keys()) != required_keys.union(optional_keys):
34
+ return "Error: 'json_schema' key must contain exactly 'name', 'schema', and optionally 'description' and 'strict' keys."
35
+
36
+ # Check that "json_schema" contains a "name" string
37
+ if "name" not in response_format["json_schema"] or not isinstance(
38
+ response_format["json_schema"]["name"], str
39
+ ):
40
+ return "Error: 'name' key must be a string."
41
+
42
+ # Check that "description" is a string if present
43
+ if "description" in response_format["json_schema"] and not isinstance(
44
+ response_format["json_schema"]["description"], str
45
+ ):
46
+ return "Error: 'description' key must be a string."
47
+
48
+ # Check that "json_schema" contains a "schema" dict
49
+ if "schema" not in response_format["json_schema"] or not isinstance(
50
+ response_format["json_schema"]["schema"], dict
51
+ ):
52
+ return "Error: 'schema' key must be a dictionary."
53
+
54
+ # Check that "strict" is a bool if present
55
+ if "strict" in response_format["json_schema"] and not isinstance(
56
+ response_format["json_schema"]["strict"], bool
57
+ ):
58
+ return "Error: 'strict' key must be a boolean."
59
+
60
+ return validate_schema(response_format["json_schema"]["schema"])
61
+
62
+
63
+ def validate_schema(schema, path="root", depth=0, stats=None):
64
+ """
65
+ Validates a JSON schema according to the OpenAI Structured Outputs specification.
66
+
67
+ See https://platform.openai.com/docs/guides/structured-outputs for details.
68
+
69
+ Note: This code is up to date as of January 21, 2024
70
+ """
71
+ print(f"Validating schema at {path}...")
72
+
73
+ # Initialize stats
74
+ if stats is None:
75
+ stats = {
76
+ "total_properties": 0,
77
+ "total_enum_values": 0,
78
+ "total_enum_string_length": 0,
79
+ "total_string_length": 0,
80
+ }
81
+
82
+ # Check root object type
83
+ if path == "root" and schema.get("type") != "object":
84
+ return f"Error at {path}: Root schema must be of type 'object'."
85
+
86
+ # Check for anyOf at root
87
+ if path == "root" and "anyOf" in schema:
88
+ return f"Error at {path}: Root schema must not use 'anyOf'."
89
+
90
+ # Check for required fields
91
+ if schema.get("type") == "object" and "properties" in schema:
92
+ if "required" not in schema or set(schema["required"]) != set(
93
+ schema["properties"].keys()
94
+ ):
95
+ missing_keys = set(schema["properties"].keys()) - set(
96
+ schema.get("required", [])
97
+ )
98
+ return f"Error at {path}: All object properties must be required. Missing keys: {missing_keys}."
99
+ if (
100
+ "additionalProperties" not in schema
101
+ or schema["additionalProperties"] is not False
102
+ ):
103
+ return f"Error at {path}: 'additionalProperties' must be set to false."
104
+
105
+ # Check for supported type
106
+ valid_types = {
107
+ "string",
108
+ "number",
109
+ "boolean",
110
+ "integer",
111
+ "object",
112
+ "array",
113
+ "enum",
114
+ "anyOf",
115
+ }
116
+ if "type" in schema:
117
+ schema_type = schema["type"]
118
+ if isinstance(schema_type, list):
119
+ if (
120
+ (len(schema_type) != 2)
121
+ or ("null" not in schema_type)
122
+ or not any(t in valid_types for t in schema_type if t != "null")
123
+ ):
124
+ return f"Error at {path}: Invalid type list {schema_type}. Must contain exactly one valid type and None."
125
+ null_allowed = True
126
+ elif schema_type not in valid_types:
127
+ return f"Error at {path}: Invalid type '{schema_type}'. Must be one of {valid_types}."
128
+ else:
129
+ null_allowed = False
130
+
131
+ # Check that enum matches specified type
132
+ if "enum" in schema:
133
+ for enum in schema["enum"]:
134
+ if null_allowed and enum is None:
135
+ continue
136
+ if not null_allowed and enum is None:
137
+ return f"Error at {path}: Enum value cannot be null unless type is [..., null]."
138
+
139
+ schema_type = schema.get("type")
140
+ if isinstance(schema_type, list):
141
+ valid_type = next(t for t in schema_type if t != "null")
142
+ else:
143
+ valid_type = schema_type
144
+
145
+ if valid_type == "integer" and not isinstance(enum, int):
146
+ return f"Error at {path}: Enum value '{enum}' does not match type 'integer'."
147
+ if valid_type == "number" and not isinstance(enum, (int, float)):
148
+ return f"Error at {path}: Enum value '{enum}' does not match type 'number'."
149
+ if valid_type == "string" and not isinstance(enum, str):
150
+ return f"Error at {path}: Enum value '{enum}' does not match type 'string'."
151
+ if valid_type == "boolean" and not isinstance(enum, bool):
152
+ return f"Error at {path}: Enum value '{enum}' does not match type 'boolean'."
153
+ if valid_type == "object" and not isinstance(enum, dict):
154
+ return f"Error at {path}: Enum value '{enum}' does not match type 'object'."
155
+ if valid_type == "array" and not isinstance(enum, list):
156
+ return (
157
+ f"Error at {path}: Enum value '{enum}' does not match type 'array'."
158
+ )
159
+
160
+ # Check for unsupported keywords based on type
161
+ unsupported_keywords_by_type = {
162
+ "string": ["minLength", "maxLength", "pattern", "format"],
163
+ "number": ["minimum", "maximum", "multipleOf"],
164
+ "integer": ["minimum", "maximum", "multipleOf"],
165
+ "object": [
166
+ "patternProperties",
167
+ "unevaluatedProperties",
168
+ "propertyNames",
169
+ "minProperties",
170
+ "maxProperties",
171
+ ],
172
+ "array": [
173
+ "unevaluatedItems",
174
+ "contains",
175
+ "minContains",
176
+ "maxContains",
177
+ "minItems",
178
+ "maxItems",
179
+ "uniqueItems",
180
+ ],
181
+ }
182
+
183
+ schema_type = schema.get("type")
184
+ if isinstance(schema_type, list):
185
+ schema_type = next(t for t in schema_type if t != "null")
186
+
187
+ if schema_type in unsupported_keywords_by_type:
188
+ for keyword in unsupported_keywords_by_type[schema_type]:
189
+ if keyword in schema:
190
+ return f"Error at {path}: Unsupported keyword '{keyword}' found for type '{schema_type}'."
191
+
192
+ # Check for nesting depth
193
+ if depth > 5:
194
+ return f"Error at {path}: Exceeded maximum nesting depth of 5."
195
+
196
+ # Check for total properties
197
+ if schema.get("type") == "object":
198
+ stats["total_properties"] += len(schema.get("properties", {}))
199
+ if stats["total_properties"] > 100:
200
+ return "Error: Exceeded maximum of 100 object properties."
201
+
202
+ # Check for total string length
203
+ for key in schema.get("properties", {}):
204
+ stats["total_string_length"] += len(key)
205
+ for enum in schema.get("enum", []):
206
+ stats["total_enum_values"] += 1
207
+ stats["total_enum_string_length"] += len(str(enum)) if enum is not None else 4
208
+ if stats["total_string_length"] > 15000:
209
+ return "Error: Exceeded maximum total string length of 15,000 characters."
210
+ if stats["total_enum_values"] > 500:
211
+ return "Error: Exceeded maximum of 500 enum values."
212
+ if stats["total_enum_string_length"] > 7500 and stats["total_enum_values"] > 250:
213
+ return "Error: Exceeded maximum total enum string length of 7,500 characters for more than 250 enum values."
214
+
215
+ # Recursively validate nested schemas
216
+ if "properties" in schema:
217
+ for prop, subschema in schema["properties"].items():
218
+ result = validate_schema(
219
+ subschema, path=f"{path}.{prop}", depth=depth + 1, stats=stats
220
+ )
221
+ if result:
222
+ return result
223
+
224
+ if "anyOf" in schema:
225
+ for index, subschema in enumerate(schema["anyOf"]):
226
+ result = validate_schema(
227
+ subschema, path=f"{path}.anyOf[{index}]", depth=depth + 1, stats=stats
228
+ )
229
+ if result:
230
+ return result
231
+
232
+ if "$defs" in schema:
233
+ for def_name, subschema in schema["$defs"].items():
234
+ result = validate_schema(
235
+ subschema, path=f"{path}.$defs.{def_name}", depth=depth + 1, stats=stats
236
+ )
237
+ if result:
238
+ return result
239
+
240
+ if "items" in schema:
241
+ result = validate_schema(
242
+ schema["items"], path=f"{path}.items", depth=depth + 1, stats=stats
243
+ )
244
+ if result:
245
+ return result
246
+
247
+ return None
248
+
249
+
250
+ def validate_strict_schema(schema) -> str | None:
251
+ return validate_response_format(
252
+ {"type": "json_schema", "name": "schema", "strict": True, "json_schema": schema}
253
+ )
@@ -0,0 +1,118 @@
1
+ from meshagent.tools import ToolContext, Tool, Toolkit, JsonResponse, TextResponse
2
+ from openai import AsyncOpenAI
3
+ from pydantic import BaseModel
4
+ from meshagent.openai.proxy import get_client
5
+ from typing import Optional
6
+ import io
7
+ import pathlib
8
+
9
+
10
+ async def _transcribe(
11
+ *,
12
+ client: AsyncOpenAI,
13
+ data: bytes,
14
+ model: str,
15
+ filename: str,
16
+ response_format: str,
17
+ timestamp_granularities: list[str] = None,
18
+ prompt: Optional[str] = None,
19
+ language: Optional[str] = None,
20
+ ):
21
+ buf = io.BytesIO(data)
22
+ buf.name = filename
23
+ transcript: BaseModel = await client.audio.transcriptions.create(
24
+ model=model,
25
+ response_format=response_format,
26
+ file=buf,
27
+ prompt=prompt,
28
+ language=language,
29
+ timestamp_granularities=timestamp_granularities,
30
+ stream=False,
31
+ )
32
+
33
+ if isinstance(transcript, str):
34
+ return TextResponse(text=transcript)
35
+
36
+ return JsonResponse(json=transcript.model_dump(mode="json"))
37
+
38
+
39
+ class OpenAIAudioFileSTT(Tool):
40
+ def __init__(self, *, client: Optional[AsyncOpenAI] = None):
41
+ super().__init__(
42
+ name="openai-file-stt",
43
+ input_schema={
44
+ "type": "object",
45
+ "additionalProperties": False,
46
+ "required": [
47
+ "model",
48
+ "path",
49
+ "response_format",
50
+ "timestamp_granularities",
51
+ "prompt",
52
+ ],
53
+ "properties": {
54
+ "path": {
55
+ "type": "string",
56
+ "description": "the path to a file in the room storage",
57
+ },
58
+ "prompt": {
59
+ "type": "string",
60
+ "description": "a prompt. can improve the accuracy of the transcript",
61
+ },
62
+ "model": {
63
+ "type": "string",
64
+ "enum": [
65
+ "whisper-1",
66
+ "gpt-4o-mini-transcribe",
67
+ "gpt-4o-transcribe",
68
+ ],
69
+ },
70
+ "response_format": {
71
+ "type": "string",
72
+ "description": "text and json are supported for all models, srt, verbose_json, and vtt are only supported for whisper-1",
73
+ "enum": ["text", "json", "srt", "verbose_json", "vtt"],
74
+ },
75
+ "timestamp_granularities": {
76
+ "description": "timestamp_granularities are only valid with whisper-1",
77
+ "type": "array",
78
+ "items": {"type": "string", "enum": ["word", "segment"]},
79
+ },
80
+ },
81
+ },
82
+ title="OpenAI audio file STT",
83
+ description="transcribes an audio file to text",
84
+ )
85
+ self.client = client
86
+
87
+ async def execute(
88
+ self,
89
+ context: ToolContext,
90
+ *,
91
+ model: str,
92
+ prompt: str,
93
+ path: str,
94
+ response_format: str,
95
+ timestamp_granularities: list,
96
+ ):
97
+ file_data = await context.room.storage.download(path=path)
98
+ client = self.client
99
+ if client is None:
100
+ client = get_client(room=context.room)
101
+
102
+ return await _transcribe(
103
+ client=client,
104
+ data=file_data.data,
105
+ model=model,
106
+ prompt=prompt,
107
+ filename=pathlib.Path(path).name,
108
+ response_format=response_format,
109
+ )
110
+
111
+
112
+ class OpenAISTTToolkit(Toolkit):
113
+ def __init__(self):
114
+ super().__init__(
115
+ name="openai-stt",
116
+ description="tools for speech to text using openai",
117
+ tools=[OpenAIAudioFileSTT()],
118
+ )
@@ -0,0 +1,87 @@
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
+ ################################################################################
12
+ # Fixtures
13
+ ################################################################################
14
+ @pytest.fixture(scope="session")
15
+ def client() -> AsyncOpenAI:
16
+ """Real async OpenAI client – no mocks, hits the network."""
17
+ return AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
18
+
19
+
20
+ @pytest.fixture(scope="session")
21
+ def audio_bytes() -> bytes:
22
+ """Loads the test clip only once per session."""
23
+ with open("harvard.wav", "rb") as fp:
24
+ return fp.read()
25
+
26
+
27
+ ################################################################################
28
+ # Tests – one for “text”, one for “json”. Add more if you need other formats.
29
+ ################################################################################
30
+ @pytest.mark.asyncio
31
+ async def test_transcribe_text(client, audio_bytes):
32
+ """_transcribe should return non-empty TextResponse for plain-text format."""
33
+ result = await asyncio.wait_for(
34
+ _transcribe(
35
+ client=client,
36
+ data=audio_bytes,
37
+ filename="harvard.wav",
38
+ model="gpt-4o-mini-transcribe",
39
+ prompt="",
40
+ response_format="text",
41
+ ),
42
+ timeout=90,
43
+ )
44
+
45
+ # Basic sanity checks
46
+ assert isinstance(result, TextResponse)
47
+ assert result.text.strip() != ""
48
+
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_transcribe_json(client, audio_bytes):
52
+ """_transcribe should return a well-formed JsonResponse for JSON format."""
53
+ result = await asyncio.wait_for(
54
+ _transcribe(
55
+ client=client,
56
+ data=audio_bytes,
57
+ filename="harvard.wav",
58
+ model="gpt-4o-mini-transcribe",
59
+ prompt="",
60
+ response_format="json",
61
+ ),
62
+ timeout=90,
63
+ )
64
+
65
+ # Basic sanity checks
66
+ assert isinstance(result, JsonResponse)
67
+ assert isinstance(result.json["text"], str)
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_transcribe_verbose_json(client, audio_bytes):
72
+ """_transcribe should return a well-formed JsonResponse for JSON format."""
73
+ result = await asyncio.wait_for(
74
+ _transcribe(
75
+ client=client,
76
+ data=audio_bytes,
77
+ filename="harvard.wav",
78
+ model="whisper-1",
79
+ prompt="",
80
+ response_format="verbose_json",
81
+ ),
82
+ timeout=90,
83
+ )
84
+
85
+ # Basic sanity checks
86
+ assert isinstance(result, JsonResponse)
87
+ assert isinstance(result.json["segments"], list)
@@ -0,0 +1 @@
1
+ __version__ = "0.18.0"
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: meshagent-openai
3
+ Version: 0.18.0
4
+ Summary: OpenAI Building Blocks for Meshagent
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Documentation, https://docs.meshagent.com
7
+ Project-URL: Website, https://www.meshagent.com
8
+ Project-URL: Source, https://www.meshagent.com
9
+ Requires-Python: >=3.13
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyjwt~=2.10
13
+ Requires-Dist: pytest~=8.4
14
+ Requires-Dist: pytest-asyncio~=0.26
15
+ Requires-Dist: openai~=2.6.0
16
+ Requires-Dist: meshagent-api~=0.18.0
17
+ Requires-Dist: meshagent-agents~=0.18.0
18
+ Requires-Dist: meshagent-tools~=0.18.0
19
+ Dynamic: license-file
20
+
21
+ # [Meshagent](https://www.meshagent.com)
22
+
23
+ ## MeshAgent OpenAI
24
+ The ``meshagent.openai`` package provides adapters to integrate OpenAI models with MeshAgent tools and agents.
25
+
26
+ ### Completions Adapter and Responses Adapter
27
+ MeshAgent supports both the OpenAI Chat Completions API and Responses API. It is recommended to use the Responses adapter given the newer OpenAI models and functionality use the Responses adapter.
28
+
29
+ - ``OpenAICompletionsAdapter``: wraps the OpenAI Chat Completions API. It turns Toolkit objects into OpenAI-style tool definitions and processes tool calls appropriately.
30
+ - ``OpenAIResponsesAdapter``: wraps the newer OpenAI Responses API. It collects tools, handles streaming events, and provides callbacks for advanced features like image generation or web search.
31
+
32
+ ```Python Python
33
+ from meshagent.openai import OpenAIResponsesAdapter
34
+ from openai import AsyncOpenAI
35
+
36
+ # Use an OpenAI client inside a MeshAgent LLMAdapter
37
+ adapter = OpenAIResponsesAdapter(client=AsyncOpenAI(api_key="sk-..."))
38
+ ```
39
+
40
+ ### Tool Response Adapter
41
+ The ``OpenAICompletionsToolResponseAdapter`` and ``OpenAIResponsesToolResponseAdapter``convert a tool's structured response into plain text or JSOn that can beinserted into an OpenAI chat context.
42
+
43
+ ---
44
+ ### Learn more about MeshAgent on our website or check out the docs for additional examples!
45
+
46
+ **Website**: [www.meshagent.com](https://www.meshagent.com/)
47
+
48
+ **Documentation**: [docs.meshagent.com](https://docs.meshagent.com/)
49
+
50
+ ---
@@ -0,0 +1,16 @@
1
+ meshagent/openai/__init__.py,sha256=g4RSQWfL2El6HQ8i2Aw8wwBEJVC861Z61S0GqkFnBys,369
2
+ meshagent/openai/version.py,sha256=0EHw4xygmgkGSyfwNfEoMlQyN0uHxjHtlSFF79s6120,23
3
+ meshagent/openai/proxy/__init__.py,sha256=PkOCHmUptsbuX5sNlWJk5bMxnSzyg5AZhPtooEPV7XE,54
4
+ meshagent/openai/proxy/proxy.py,sha256=iTgk6ONcYUiOGjEownWW3JeeJ-zCyX28faUZ3oFu6fM,2635
5
+ meshagent/openai/tools/__init__.py,sha256=cLXoB9CBqKbCGhZMAJTIX6-yv_UO8AxpaH8vQQ1e8VY,467
6
+ meshagent/openai/tools/apply_patch.py,sha256=iSkZpyq4jaMYHs1lLZ8pkocqmDeuhPKxgzHHCsd7euU,10195
7
+ meshagent/openai/tools/completions_adapter.py,sha256=dBRXuWxc2LiWaTpA8agMhwxhRvbxbMnggvv_9QtK-HA,15946
8
+ meshagent/openai/tools/responses_adapter.py,sha256=x4XJLXLDeVHWt9QkPqHK0eSPd21L8C2KAuJhUQpt5RY,88900
9
+ meshagent/openai/tools/schema.py,sha256=YaP0iEL9Lf2qS4xZy8VILjr1IS52XS9LEcn_cskNreo,10079
10
+ meshagent/openai/tools/stt.py,sha256=H3YusIjigJwxfEdkrK5qZ6DHbjQagaLNj7q_-fTfwy4,3845
11
+ meshagent/openai/tools/stt_test.py,sha256=XE4qZBlNeEWdJW5NjBGyaJmuCKN0ZLlJ2b_GBp7MzVk,2651
12
+ meshagent_openai-0.18.0.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
13
+ meshagent_openai-0.18.0.dist-info/METADATA,sha256=odoFXoF4tXmkqchyIO58E0gX7MWVjNKO9zPnRc2cBdw,2108
14
+ meshagent_openai-0.18.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ meshagent_openai-0.18.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
16
+ meshagent_openai-0.18.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+