amsdal_ml 0.1.4__py3-none-any.whl → 0.2.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.
- amsdal_ml/Third-Party Materials - AMSDAL Dependencies - License Notices.md +617 -0
- amsdal_ml/__about__.py +1 -1
- amsdal_ml/agents/__init__.py +13 -0
- amsdal_ml/agents/agent.py +5 -7
- amsdal_ml/agents/default_qa_agent.py +108 -143
- amsdal_ml/agents/functional_calling_agent.py +233 -0
- amsdal_ml/agents/mcp_client_tool.py +46 -0
- amsdal_ml/agents/python_tool.py +86 -0
- amsdal_ml/agents/retriever_tool.py +5 -6
- amsdal_ml/agents/tool_adapters.py +98 -0
- amsdal_ml/fileio/base_loader.py +7 -5
- amsdal_ml/fileio/openai_loader.py +16 -17
- amsdal_ml/mcp_client/base.py +2 -0
- amsdal_ml/mcp_client/http_client.py +7 -1
- amsdal_ml/mcp_client/stdio_client.py +19 -16
- amsdal_ml/mcp_server/server_retriever_stdio.py +8 -11
- amsdal_ml/ml_ingesting/__init__.py +29 -0
- amsdal_ml/ml_ingesting/default_ingesting.py +49 -51
- amsdal_ml/ml_ingesting/embedders/__init__.py +4 -0
- amsdal_ml/ml_ingesting/embedders/embedder.py +12 -0
- amsdal_ml/ml_ingesting/embedders/openai_embedder.py +30 -0
- amsdal_ml/ml_ingesting/embedding_data.py +3 -0
- amsdal_ml/ml_ingesting/loaders/__init__.py +6 -0
- amsdal_ml/ml_ingesting/loaders/folder_loader.py +52 -0
- amsdal_ml/ml_ingesting/loaders/loader.py +28 -0
- amsdal_ml/ml_ingesting/loaders/pdf_loader.py +136 -0
- amsdal_ml/ml_ingesting/loaders/text_loader.py +44 -0
- amsdal_ml/ml_ingesting/model_ingester.py +278 -0
- amsdal_ml/ml_ingesting/pipeline.py +131 -0
- amsdal_ml/ml_ingesting/pipeline_interface.py +31 -0
- amsdal_ml/ml_ingesting/processors/__init__.py +4 -0
- amsdal_ml/ml_ingesting/processors/cleaner.py +14 -0
- amsdal_ml/ml_ingesting/processors/text_cleaner.py +42 -0
- amsdal_ml/ml_ingesting/splitters/__init__.py +4 -0
- amsdal_ml/ml_ingesting/splitters/splitter.py +15 -0
- amsdal_ml/ml_ingesting/splitters/token_splitter.py +85 -0
- amsdal_ml/ml_ingesting/stores/__init__.py +4 -0
- amsdal_ml/ml_ingesting/stores/embedding_data.py +63 -0
- amsdal_ml/ml_ingesting/stores/store.py +22 -0
- amsdal_ml/ml_ingesting/types.py +40 -0
- amsdal_ml/ml_models/models.py +96 -4
- amsdal_ml/ml_models/openai_model.py +430 -122
- amsdal_ml/ml_models/utils.py +7 -0
- amsdal_ml/ml_retrievers/__init__.py +17 -0
- amsdal_ml/ml_retrievers/adapters.py +93 -0
- amsdal_ml/ml_retrievers/default_retriever.py +11 -1
- amsdal_ml/ml_retrievers/openai_retriever.py +27 -7
- amsdal_ml/ml_retrievers/query_retriever.py +487 -0
- amsdal_ml/ml_retrievers/retriever.py +12 -0
- amsdal_ml/models/embedding_model.py +7 -7
- amsdal_ml/prompts/__init__.py +77 -0
- amsdal_ml/prompts/database_query_agent.prompt +14 -0
- amsdal_ml/prompts/functional_calling_agent_base.prompt +9 -0
- amsdal_ml/prompts/nl_query_filter.prompt +318 -0
- amsdal_ml/{agents/promts → prompts}/react_chat.prompt +17 -8
- amsdal_ml/utils/__init__.py +5 -0
- amsdal_ml/utils/query_utils.py +189 -0
- {amsdal_ml-0.1.4.dist-info → amsdal_ml-0.2.0.dist-info}/METADATA +59 -1
- amsdal_ml-0.2.0.dist-info/RECORD +72 -0
- {amsdal_ml-0.1.4.dist-info → amsdal_ml-0.2.0.dist-info}/WHEEL +1 -1
- amsdal_ml/agents/promts/__init__.py +0 -58
- amsdal_ml-0.1.4.dist-info/RECORD +0 -39
|
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import AsyncIterator
|
|
6
7
|
from collections.abc import Iterator
|
|
8
|
+
from collections.abc import Sequence
|
|
7
9
|
from typing import Any
|
|
8
10
|
from typing import Optional
|
|
9
11
|
from typing import cast
|
|
@@ -18,29 +20,91 @@ from amsdal_ml.fileio.base_loader import FILE_ID
|
|
|
18
20
|
from amsdal_ml.fileio.base_loader import PLAIN_TEXT
|
|
19
21
|
from amsdal_ml.fileio.base_loader import FileAttachment
|
|
20
22
|
from amsdal_ml.ml_config import ml_config
|
|
23
|
+
from amsdal_ml.ml_models.models import LLModelInput
|
|
21
24
|
from amsdal_ml.ml_models.models import MLModel
|
|
22
25
|
from amsdal_ml.ml_models.models import ModelAPIError
|
|
23
26
|
from amsdal_ml.ml_models.models import ModelConnectionError
|
|
24
27
|
from amsdal_ml.ml_models.models import ModelError
|
|
25
28
|
from amsdal_ml.ml_models.models import ModelRateLimitError
|
|
29
|
+
from amsdal_ml.ml_models.models import StructuredMessage
|
|
30
|
+
from amsdal_ml.ml_models.utils import ResponseFormat
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
class OpenAIModel(MLModel):
|
|
29
34
|
"""OpenAI LLM wrapper using a single Responses API pathway for all modes."""
|
|
30
35
|
|
|
31
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
model_name: Optional[str] = None,
|
|
40
|
+
temperature: Optional[float] = None,
|
|
41
|
+
) -> None:
|
|
32
42
|
self.client: Optional[OpenAI | AsyncOpenAI] = None
|
|
33
43
|
self.async_mode: bool = bool(ml_config.async_mode)
|
|
34
|
-
self.model_name: str = ml_config.llm_model_name
|
|
35
|
-
self.temperature: float =
|
|
44
|
+
self.model_name: str = model_name or ml_config.llm_model_name
|
|
45
|
+
self.temperature: float = (
|
|
46
|
+
temperature if temperature is not None else ml_config.llm_temperature
|
|
47
|
+
)
|
|
36
48
|
self._api_key: Optional[str] = None
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
@property
|
|
51
|
+
def supported_formats(self) -> set[ResponseFormat]:
|
|
52
|
+
"""OpenAI supports PLAIN_TEXT, JSON_OBJECT and JSON_SCHEMA formats."""
|
|
53
|
+
return {
|
|
54
|
+
ResponseFormat.PLAIN_TEXT,
|
|
55
|
+
ResponseFormat.JSON_OBJECT,
|
|
56
|
+
ResponseFormat.JSON_SCHEMA,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def input_role(self) -> str:
|
|
61
|
+
"""Return 'user' for OpenAI."""
|
|
62
|
+
return "user"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def output_role(self) -> str:
|
|
66
|
+
"""Return 'assistant' for OpenAI."""
|
|
67
|
+
return "assistant"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def tool_role(self) -> str:
|
|
71
|
+
"""Return 'tool' for OpenAI."""
|
|
72
|
+
return "tool"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def system_role(self) -> str:
|
|
76
|
+
"""Return 'system' for OpenAI."""
|
|
77
|
+
return "system"
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def content_field(self) -> str:
|
|
81
|
+
"""Return 'content' for OpenAI."""
|
|
82
|
+
return "content"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def role_field(self) -> str:
|
|
86
|
+
"""Return 'role' for OpenAI."""
|
|
87
|
+
return "role"
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def tool_call_id_field(self) -> str:
|
|
91
|
+
"""Return 'tool_call_id' for OpenAI."""
|
|
92
|
+
return "tool_call_id"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def tool_name_field(self) -> str:
|
|
96
|
+
"""Return 'name' for OpenAI."""
|
|
97
|
+
return "name"
|
|
98
|
+
|
|
39
99
|
def invoke(
|
|
40
100
|
self,
|
|
41
|
-
|
|
101
|
+
input: LLModelInput, # noqa: A002
|
|
42
102
|
*,
|
|
43
103
|
attachments: list[FileAttachment] | None = None,
|
|
104
|
+
response_format: ResponseFormat | None = None,
|
|
105
|
+
schema: dict[str, Any] | None = None,
|
|
106
|
+
tools: list[dict[str, Any]] | None = None,
|
|
107
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
44
108
|
) -> str:
|
|
45
109
|
if self.async_mode:
|
|
46
110
|
msg = "Async mode is enabled. Use 'ainvoke' instead."
|
|
@@ -50,18 +114,44 @@ class OpenAIModel(MLModel):
|
|
|
50
114
|
raise RuntimeError(msg)
|
|
51
115
|
|
|
52
116
|
atts = self._validate_attachments(attachments)
|
|
117
|
+
api_response_format = self._map_response_format(response_format, schema)
|
|
118
|
+
|
|
53
119
|
if self._has_file_ids(atts):
|
|
54
|
-
input_content = self._build_input_content(
|
|
55
|
-
return self._call_responses(
|
|
120
|
+
input_content = self._build_input_content(input, atts)
|
|
121
|
+
return self._call_responses(
|
|
122
|
+
input_content, response_format=api_response_format
|
|
123
|
+
)
|
|
56
124
|
|
|
57
|
-
|
|
58
|
-
|
|
125
|
+
if isinstance(input, str):
|
|
126
|
+
final_prompt = self._merge_plain_text(input, atts)
|
|
127
|
+
return self._call_chat(
|
|
128
|
+
[{"role": "user", "content": final_prompt}],
|
|
129
|
+
response_format=api_response_format,
|
|
130
|
+
tools=tools,
|
|
131
|
+
tool_choice=tool_choice,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
messages = list(input)
|
|
135
|
+
attachments_text = self._merge_plain_text("", atts)
|
|
136
|
+
if attachments_text:
|
|
137
|
+
messages.append({"role": "user", "content": attachments_text})
|
|
138
|
+
|
|
139
|
+
return self._call_chat(
|
|
140
|
+
messages,
|
|
141
|
+
response_format=api_response_format,
|
|
142
|
+
tools=tools,
|
|
143
|
+
tool_choice=tool_choice,
|
|
144
|
+
)
|
|
59
145
|
|
|
60
146
|
def stream(
|
|
61
147
|
self,
|
|
62
|
-
|
|
148
|
+
input: LLModelInput, # noqa: A002
|
|
63
149
|
*,
|
|
64
150
|
attachments: list[FileAttachment] | None = None,
|
|
151
|
+
response_format: ResponseFormat | None = None,
|
|
152
|
+
schema: dict[str, Any] | None = None,
|
|
153
|
+
tools: list[dict[str, Any]] | None = None,
|
|
154
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
65
155
|
) -> Iterator[str]:
|
|
66
156
|
if self.async_mode:
|
|
67
157
|
msg = "Async mode is enabled. Use 'astream' instead."
|
|
@@ -71,22 +161,50 @@ class OpenAIModel(MLModel):
|
|
|
71
161
|
raise RuntimeError(msg)
|
|
72
162
|
|
|
73
163
|
atts = self._validate_attachments(attachments)
|
|
164
|
+
api_response_format = self._map_response_format(response_format, schema)
|
|
165
|
+
|
|
74
166
|
if self._has_file_ids(atts):
|
|
75
|
-
input_content = self._build_input_content(
|
|
76
|
-
for chunk in self._call_responses_stream(
|
|
167
|
+
input_content = self._build_input_content(input, atts)
|
|
168
|
+
for chunk in self._call_responses_stream(
|
|
169
|
+
input_content, response_format=api_response_format
|
|
170
|
+
):
|
|
77
171
|
yield chunk
|
|
78
172
|
return
|
|
79
173
|
|
|
80
|
-
|
|
81
|
-
|
|
174
|
+
if isinstance(input, str):
|
|
175
|
+
final_prompt = self._merge_plain_text(input, atts)
|
|
176
|
+
for chunk in self._call_chat_stream(
|
|
177
|
+
[{"role": "user", "content": final_prompt}],
|
|
178
|
+
response_format=api_response_format,
|
|
179
|
+
tools=tools,
|
|
180
|
+
tool_choice=tool_choice,
|
|
181
|
+
):
|
|
182
|
+
yield chunk
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
messages = list(input)
|
|
186
|
+
attachments_text = self._merge_plain_text("", atts)
|
|
187
|
+
if attachments_text:
|
|
188
|
+
messages.append({"role": "user", "content": attachments_text})
|
|
189
|
+
|
|
190
|
+
for chunk in self._call_chat_stream(
|
|
191
|
+
messages,
|
|
192
|
+
response_format=api_response_format,
|
|
193
|
+
tools=tools,
|
|
194
|
+
tool_choice=tool_choice,
|
|
195
|
+
):
|
|
82
196
|
yield chunk
|
|
83
197
|
|
|
84
198
|
# ---------- Public async API ----------
|
|
85
199
|
async def ainvoke(
|
|
86
200
|
self,
|
|
87
|
-
|
|
201
|
+
input: LLModelInput, # noqa: A002
|
|
88
202
|
*,
|
|
89
203
|
attachments: list[FileAttachment] | None = None,
|
|
204
|
+
response_format: ResponseFormat | None = None,
|
|
205
|
+
schema: dict[str, Any] | None = None,
|
|
206
|
+
tools: list[dict[str, Any]] | None = None,
|
|
207
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
90
208
|
) -> str:
|
|
91
209
|
if not self.async_mode:
|
|
92
210
|
msg = "Async mode is disabled. Use 'invoke' instead."
|
|
@@ -97,18 +215,44 @@ class OpenAIModel(MLModel):
|
|
|
97
215
|
raise RuntimeError(msg)
|
|
98
216
|
|
|
99
217
|
atts = self._validate_attachments(attachments)
|
|
218
|
+
api_response_format = self._map_response_format(response_format, schema)
|
|
219
|
+
|
|
100
220
|
if self._has_file_ids(atts):
|
|
101
|
-
input_content = self._build_input_content(
|
|
102
|
-
return await self._acall_responses(
|
|
221
|
+
input_content = self._build_input_content(input, atts)
|
|
222
|
+
return await self._acall_responses(
|
|
223
|
+
input_content, response_format=api_response_format
|
|
224
|
+
)
|
|
103
225
|
|
|
104
|
-
|
|
105
|
-
|
|
226
|
+
if isinstance(input, str):
|
|
227
|
+
final_prompt = self._merge_plain_text(input, atts)
|
|
228
|
+
return await self._acall_chat(
|
|
229
|
+
[{"role": "user", "content": final_prompt}],
|
|
230
|
+
response_format=api_response_format,
|
|
231
|
+
tools=tools,
|
|
232
|
+
tool_choice=tool_choice,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
messages = list(input)
|
|
236
|
+
attachments_text = self._merge_plain_text("", atts)
|
|
237
|
+
if attachments_text:
|
|
238
|
+
messages.append({"role": "user", "content": attachments_text})
|
|
239
|
+
|
|
240
|
+
return await self._acall_chat(
|
|
241
|
+
messages,
|
|
242
|
+
response_format=api_response_format,
|
|
243
|
+
tools=tools,
|
|
244
|
+
tool_choice=tool_choice,
|
|
245
|
+
)
|
|
106
246
|
|
|
107
247
|
async def astream(
|
|
108
248
|
self,
|
|
109
|
-
|
|
249
|
+
input: LLModelInput, # noqa: A002
|
|
110
250
|
*,
|
|
111
251
|
attachments: list[FileAttachment] | None = None,
|
|
252
|
+
response_format: ResponseFormat | None = None,
|
|
253
|
+
schema: dict[str, Any] | None = None,
|
|
254
|
+
tools: list[dict[str, Any]] | None = None,
|
|
255
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
112
256
|
) -> AsyncIterator[str]:
|
|
113
257
|
if not self.async_mode:
|
|
114
258
|
msg = "Async mode is disabled. Use 'stream' instead."
|
|
@@ -119,30 +263,50 @@ class OpenAIModel(MLModel):
|
|
|
119
263
|
raise RuntimeError(msg)
|
|
120
264
|
|
|
121
265
|
atts = self._validate_attachments(attachments)
|
|
266
|
+
api_response_format = self._map_response_format(response_format, schema)
|
|
267
|
+
|
|
122
268
|
if self._has_file_ids(atts):
|
|
123
|
-
input_content = self._build_input_content(
|
|
124
|
-
async for chunk in self._acall_responses_stream(
|
|
269
|
+
input_content = self._build_input_content(input, atts)
|
|
270
|
+
async for chunk in self._acall_responses_stream(
|
|
271
|
+
input_content, response_format=api_response_format
|
|
272
|
+
):
|
|
125
273
|
yield chunk
|
|
126
274
|
return
|
|
127
275
|
|
|
128
|
-
|
|
129
|
-
|
|
276
|
+
if isinstance(input, str):
|
|
277
|
+
final_prompt = self._merge_plain_text(input, atts)
|
|
278
|
+
async for chunk in self._acall_chat_stream(
|
|
279
|
+
[{"role": "user", "content": final_prompt}],
|
|
280
|
+
response_format=api_response_format,
|
|
281
|
+
tools=tools,
|
|
282
|
+
tool_choice=tool_choice,
|
|
283
|
+
):
|
|
284
|
+
yield chunk
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
messages = list(input)
|
|
288
|
+
attachments_text = self._merge_plain_text("", atts)
|
|
289
|
+
if attachments_text:
|
|
290
|
+
messages.append({"role": "user", "content": attachments_text})
|
|
291
|
+
|
|
292
|
+
async for chunk in self._acall_chat_stream(
|
|
293
|
+
messages,
|
|
294
|
+
response_format=api_response_format,
|
|
295
|
+
tools=tools,
|
|
296
|
+
tool_choice=tool_choice,
|
|
297
|
+
):
|
|
130
298
|
yield chunk
|
|
131
299
|
|
|
132
300
|
# ---------- lifecycle ----------
|
|
133
301
|
def setup(self) -> None:
|
|
134
302
|
api_key = os.getenv("OPENAI_API_KEY") or ml_config.resolved_openai_key
|
|
135
303
|
if not api_key:
|
|
136
|
-
msg =
|
|
137
|
-
"OPENAI_API_KEY is required. "
|
|
138
|
-
"Set it via env or ml_config.api_keys.openai."
|
|
139
|
-
)
|
|
304
|
+
msg = "OPENAI_API_KEY is required. Set it via env or ml_config.api_keys.openai."
|
|
140
305
|
raise RuntimeError(msg)
|
|
141
306
|
self._api_key = api_key
|
|
142
307
|
|
|
143
308
|
try:
|
|
144
309
|
if self.async_mode:
|
|
145
|
-
# Only create async client if loop is running; otherwise defer.
|
|
146
310
|
try:
|
|
147
311
|
asyncio.get_running_loop()
|
|
148
312
|
self._ensure_async_client()
|
|
@@ -153,6 +317,40 @@ class OpenAIModel(MLModel):
|
|
|
153
317
|
except Exception as e: # pragma: no cover
|
|
154
318
|
raise self._map_openai_error(e) from e
|
|
155
319
|
|
|
320
|
+
def _map_response_format(
|
|
321
|
+
self, response_format: ResponseFormat | None, schema: dict[str, Any] | None
|
|
322
|
+
) -> dict[str, Any] | None:
|
|
323
|
+
if response_format is None or response_format == ResponseFormat.PLAIN_TEXT:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
if response_format == ResponseFormat.JSON_OBJECT:
|
|
327
|
+
return {"type": "json_object"}
|
|
328
|
+
|
|
329
|
+
if response_format == ResponseFormat.JSON_SCHEMA:
|
|
330
|
+
if self.model_name and self.model_name in [
|
|
331
|
+
'gpt-4', 'gpt-4-0613', 'gpt-4-0314', 'gpt-3.5-turbo',
|
|
332
|
+
'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-1106', 'gpt-3.5-turbo-instruct',
|
|
333
|
+
'gpt-4-0125-preview', 'gpt-4-1106-vision-preview', 'chatgpt-4o-latest',
|
|
334
|
+
'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview',
|
|
335
|
+
'gpt-4-0125-preview', 'gpt-4-1106-vision-preview'
|
|
336
|
+
]:
|
|
337
|
+
warnings.warn(
|
|
338
|
+
f"Model '{self.model_name}' may not support the JSON Schema format. "
|
|
339
|
+
"Consider using a newer model like 'gpt-4o' for guaranteed compatibility.",
|
|
340
|
+
UserWarning,
|
|
341
|
+
stacklevel=2
|
|
342
|
+
)
|
|
343
|
+
return None
|
|
344
|
+
if not schema:
|
|
345
|
+
msg = "`schema` is required for `JSON_SCHEMA` format."
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
return {
|
|
348
|
+
"type": "json_schema",
|
|
349
|
+
"json_schema": schema
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return None
|
|
353
|
+
|
|
156
354
|
def _ensure_async_client(self) -> None:
|
|
157
355
|
if self.client is None:
|
|
158
356
|
try:
|
|
@@ -187,20 +385,19 @@ class OpenAIModel(MLModel):
|
|
|
187
385
|
kinds = {a.type for a in atts}
|
|
188
386
|
unsupported = kinds - self.supported_attachments()
|
|
189
387
|
if unsupported:
|
|
190
|
-
msg = (
|
|
191
|
-
f"{self.__class__.__name__} does not support attachments: "
|
|
192
|
-
f"{', '.join(sorted(unsupported))}"
|
|
193
|
-
)
|
|
388
|
+
msg = f'{self.__class__.__name__} does not support attachments: {", ".join(sorted(unsupported))}'
|
|
194
389
|
raise ModelAPIError(msg)
|
|
195
390
|
|
|
196
391
|
foreign = [
|
|
197
|
-
a
|
|
392
|
+
a
|
|
393
|
+
for a in atts
|
|
394
|
+
if a.type == FILE_ID and (a.metadata or {}).get("provider") != "openai"
|
|
198
395
|
]
|
|
199
396
|
if foreign:
|
|
200
397
|
provs = {(a.metadata or {}).get("provider", "unknown") for a in foreign}
|
|
201
398
|
msg = (
|
|
202
399
|
f"{self.__class__.__name__} only supports FILE_ID with provider='openai'. "
|
|
203
|
-
f
|
|
400
|
+
f'Got providers: {", ".join(sorted(provs))}'
|
|
204
401
|
)
|
|
205
402
|
raise ModelAPIError(msg)
|
|
206
403
|
|
|
@@ -210,14 +407,30 @@ class OpenAIModel(MLModel):
|
|
|
210
407
|
def _has_file_ids(atts: list[FileAttachment]) -> bool:
|
|
211
408
|
return any(a.type == FILE_ID for a in atts)
|
|
212
409
|
|
|
213
|
-
def _build_input_content(
|
|
214
|
-
|
|
410
|
+
def _build_input_content(
|
|
411
|
+
self, input: LLModelInput, atts: list[FileAttachment], # noqa: A002
|
|
412
|
+
) -> list[StructuredMessage]:
|
|
413
|
+
if isinstance(input, str):
|
|
414
|
+
parts: list[dict[str, Any]] = [{"type": "input_text", "text": input}]
|
|
415
|
+
for a in atts:
|
|
416
|
+
if a.type == PLAIN_TEXT:
|
|
417
|
+
parts.append({"type": "input_text", "text": str(a.content)})
|
|
418
|
+
elif a.type == FILE_ID:
|
|
419
|
+
parts.append({"type": "input_file", "file_id": str(a.content)})
|
|
420
|
+
return [{"role": "user", "content": parts}]
|
|
421
|
+
|
|
422
|
+
messages = cast(list[StructuredMessage], [dict(msg) for msg in input])
|
|
423
|
+
parts = []
|
|
215
424
|
for a in atts:
|
|
216
425
|
if a.type == PLAIN_TEXT:
|
|
217
426
|
parts.append({"type": "input_text", "text": str(a.content)})
|
|
218
427
|
elif a.type == FILE_ID:
|
|
219
428
|
parts.append({"type": "input_file", "file_id": str(a.content)})
|
|
220
|
-
|
|
429
|
+
|
|
430
|
+
if parts:
|
|
431
|
+
messages.append({"role": "user", "content": parts})
|
|
432
|
+
|
|
433
|
+
return messages
|
|
221
434
|
|
|
222
435
|
def _merge_plain_text(self, prompt: str, atts: list[FileAttachment]) -> str:
|
|
223
436
|
extras = [str(a.content) for a in atts if a.type == PLAIN_TEXT]
|
|
@@ -240,132 +453,227 @@ class OpenAIModel(MLModel):
|
|
|
240
453
|
payload_repr = resp.json() if resp is not None else None
|
|
241
454
|
except Exception:
|
|
242
455
|
payload_repr = None
|
|
243
|
-
return ModelAPIError(
|
|
456
|
+
return ModelAPIError(
|
|
457
|
+
f"OpenAI API status error ({status}). payload={payload_repr!r}"
|
|
458
|
+
)
|
|
244
459
|
if isinstance(err, openai.APIError):
|
|
245
460
|
return ModelAPIError(str(err))
|
|
246
461
|
return ModelAPIError(str(err))
|
|
247
462
|
|
|
248
463
|
# ---------- Sync core callers ----------
|
|
249
|
-
def _call_chat(
|
|
464
|
+
def _call_chat(
|
|
465
|
+
self,
|
|
466
|
+
messages: Sequence[StructuredMessage],
|
|
467
|
+
response_format: dict[str, Any] | None = None,
|
|
468
|
+
tools: list[dict[str, Any]] | None = None,
|
|
469
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
470
|
+
) -> str:
|
|
250
471
|
client = self._require_sync_client()
|
|
472
|
+
kwargs: dict[str, Any] = {
|
|
473
|
+
"model": self.model_name,
|
|
474
|
+
"messages": messages,
|
|
475
|
+
"temperature": self.temperature,
|
|
476
|
+
}
|
|
477
|
+
if response_format:
|
|
478
|
+
kwargs["response_format"] = response_format
|
|
479
|
+
if tools:
|
|
480
|
+
kwargs["tools"] = tools
|
|
481
|
+
if tool_choice:
|
|
482
|
+
kwargs["tool_choice"] = tool_choice
|
|
483
|
+
|
|
251
484
|
try:
|
|
252
|
-
resp = client.chat.completions.create(
|
|
253
|
-
model=self.model_name,
|
|
254
|
-
messages=[{"role": "user", "content": prompt}],
|
|
255
|
-
temperature=self.temperature,
|
|
256
|
-
)
|
|
257
|
-
return resp.choices[0].message.content or ""
|
|
485
|
+
resp = client.chat.completions.create(**kwargs)
|
|
258
486
|
except Exception as e:
|
|
259
487
|
raise self._map_openai_error(e) from e
|
|
260
488
|
|
|
261
|
-
|
|
489
|
+
if tools:
|
|
490
|
+
return resp.choices[0].message.model_dump_json()
|
|
491
|
+
|
|
492
|
+
return resp.choices[0].message.content or ""
|
|
493
|
+
|
|
494
|
+
def _call_chat_stream(
|
|
495
|
+
self,
|
|
496
|
+
messages: Sequence[StructuredMessage],
|
|
497
|
+
response_format: dict[str, Any] | None = None,
|
|
498
|
+
tools: list[dict[str, Any]] | None = None,
|
|
499
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
500
|
+
) -> Iterator[str]:
|
|
262
501
|
client = self._require_sync_client()
|
|
502
|
+
kwargs: dict[str, Any] = {
|
|
503
|
+
"model": self.model_name,
|
|
504
|
+
"messages": messages,
|
|
505
|
+
"temperature": self.temperature,
|
|
506
|
+
"stream": True,
|
|
507
|
+
}
|
|
508
|
+
if response_format:
|
|
509
|
+
kwargs["response_format"] = response_format
|
|
510
|
+
if tools:
|
|
511
|
+
kwargs["tools"] = tools
|
|
512
|
+
if tool_choice:
|
|
513
|
+
kwargs["tool_choice"] = tool_choice
|
|
514
|
+
|
|
263
515
|
try:
|
|
264
|
-
stream = client.chat.completions.create(
|
|
265
|
-
model=self.model_name,
|
|
266
|
-
messages=[{"role": "user", "content": prompt}],
|
|
267
|
-
temperature=self.temperature,
|
|
268
|
-
stream=True,
|
|
269
|
-
)
|
|
270
|
-
for chunk in stream:
|
|
271
|
-
delta = chunk.choices[0].delta
|
|
272
|
-
if delta and delta.content:
|
|
273
|
-
yield delta.content
|
|
516
|
+
stream = client.chat.completions.create(**kwargs)
|
|
274
517
|
except Exception as e:
|
|
275
518
|
raise self._map_openai_error(e) from e
|
|
276
519
|
|
|
277
|
-
|
|
520
|
+
for chunk in stream:
|
|
521
|
+
delta = chunk.choices[0].delta
|
|
522
|
+
if delta and delta.content:
|
|
523
|
+
yield delta.content
|
|
524
|
+
|
|
525
|
+
def _call_responses(
|
|
526
|
+
self, input_content: Sequence[StructuredMessage], response_format: dict[str, Any] | None = None
|
|
527
|
+
) -> str:
|
|
278
528
|
client = self._require_sync_client()
|
|
529
|
+
kwargs: dict[str, Any] = {
|
|
530
|
+
"model": self.model_name,
|
|
531
|
+
"input": cast(Any, input_content),
|
|
532
|
+
"temperature": self.temperature,
|
|
533
|
+
}
|
|
534
|
+
if response_format:
|
|
535
|
+
kwargs["response_format"] = response_format
|
|
536
|
+
|
|
279
537
|
try:
|
|
280
|
-
resp: Any = client.responses.create(
|
|
281
|
-
model=self.model_name,
|
|
282
|
-
input=cast(Any, input_content),
|
|
283
|
-
temperature=self.temperature,
|
|
284
|
-
)
|
|
285
|
-
return (getattr(resp, "output_text", None) or "").strip()
|
|
538
|
+
resp: Any = client.responses.create(**kwargs)
|
|
286
539
|
except Exception as e:
|
|
287
540
|
raise self._map_openai_error(e) from e
|
|
288
541
|
|
|
289
|
-
|
|
542
|
+
return (getattr(resp, "output_text", None) or "").strip()
|
|
543
|
+
|
|
544
|
+
def _call_responses_stream(
|
|
545
|
+
self, input_content: Sequence[StructuredMessage], response_format: dict[str, Any] | None = None
|
|
546
|
+
) -> Iterator[str]:
|
|
290
547
|
client = self._require_sync_client()
|
|
548
|
+
kwargs: dict[str, Any] = {
|
|
549
|
+
"model": self.model_name,
|
|
550
|
+
"input": cast(Any, input_content),
|
|
551
|
+
"temperature": self.temperature,
|
|
552
|
+
"stream": True,
|
|
553
|
+
}
|
|
554
|
+
if response_format:
|
|
555
|
+
kwargs["response_format"] = response_format
|
|
556
|
+
|
|
291
557
|
try:
|
|
292
|
-
stream_or_resp = client.responses.create(
|
|
293
|
-
model=self.model_name,
|
|
294
|
-
input=cast(Any, input_content),
|
|
295
|
-
temperature=self.temperature,
|
|
296
|
-
stream=True,
|
|
297
|
-
)
|
|
298
|
-
if isinstance(stream_or_resp, Stream):
|
|
299
|
-
for ev in stream_or_resp:
|
|
300
|
-
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
301
|
-
if delta:
|
|
302
|
-
yield delta
|
|
303
|
-
else:
|
|
304
|
-
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
305
|
-
if text:
|
|
306
|
-
yield text
|
|
558
|
+
stream_or_resp = client.responses.create(**kwargs)
|
|
307
559
|
except Exception as e:
|
|
308
560
|
raise self._map_openai_error(e) from e
|
|
309
561
|
|
|
562
|
+
if isinstance(stream_or_resp, Stream):
|
|
563
|
+
for ev in stream_or_resp:
|
|
564
|
+
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
565
|
+
if delta:
|
|
566
|
+
yield delta
|
|
567
|
+
else:
|
|
568
|
+
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
569
|
+
if text:
|
|
570
|
+
yield text
|
|
571
|
+
|
|
310
572
|
# ---------- Async core callers ----------
|
|
311
|
-
async def _acall_chat(
|
|
573
|
+
async def _acall_chat(
|
|
574
|
+
self,
|
|
575
|
+
messages: Sequence[StructuredMessage],
|
|
576
|
+
response_format: dict[str, Any] | None = None,
|
|
577
|
+
tools: list[dict[str, Any]] | None = None,
|
|
578
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
579
|
+
) -> str:
|
|
312
580
|
client = self._require_async_client()
|
|
313
|
-
|
|
581
|
+
kwargs: dict[str, Any] = {
|
|
582
|
+
"model": self.model_name,
|
|
583
|
+
"messages": messages,
|
|
584
|
+
"temperature": self.temperature,
|
|
585
|
+
}
|
|
586
|
+
if response_format:
|
|
587
|
+
kwargs["response_format"] = response_format
|
|
588
|
+
if tools:
|
|
589
|
+
kwargs["tools"] = tools
|
|
590
|
+
if tool_choice:
|
|
591
|
+
kwargs["tool_choice"] = tool_choice
|
|
592
|
+
|
|
314
593
|
try:
|
|
315
|
-
resp = await client.chat.completions.create(
|
|
316
|
-
model=self.model_name,
|
|
317
|
-
messages=[{"role": "user", "content": prompt}],
|
|
318
|
-
temperature=self.temperature,
|
|
319
|
-
)
|
|
320
|
-
return resp.choices[0].message.content or ""
|
|
594
|
+
resp = await client.chat.completions.create(**kwargs)
|
|
321
595
|
except Exception as e:
|
|
322
596
|
raise self._map_openai_error(e) from e
|
|
323
597
|
|
|
324
|
-
|
|
598
|
+
if tools:
|
|
599
|
+
return resp.choices[0].message.model_dump_json()
|
|
600
|
+
|
|
601
|
+
return resp.choices[0].message.content or ""
|
|
602
|
+
|
|
603
|
+
async def _acall_chat_stream(
|
|
604
|
+
self,
|
|
605
|
+
messages: Sequence[StructuredMessage],
|
|
606
|
+
response_format: dict[str, Any] | None = None,
|
|
607
|
+
tools: list[dict[str, Any]] | None = None,
|
|
608
|
+
tool_choice: str | dict[str, Any] | None = None,
|
|
609
|
+
) -> AsyncIterator[str]:
|
|
325
610
|
client = self._require_async_client()
|
|
611
|
+
kwargs: dict[str, Any] = {
|
|
612
|
+
"model": self.model_name,
|
|
613
|
+
"messages": messages,
|
|
614
|
+
"temperature": self.temperature,
|
|
615
|
+
"stream": True,
|
|
616
|
+
}
|
|
617
|
+
if response_format:
|
|
618
|
+
kwargs["response_format"] = response_format
|
|
619
|
+
if tools:
|
|
620
|
+
kwargs["tools"] = tools
|
|
621
|
+
if tool_choice:
|
|
622
|
+
kwargs["tool_choice"] = tool_choice
|
|
623
|
+
|
|
326
624
|
try:
|
|
327
|
-
stream = await client.chat.completions.create(
|
|
328
|
-
model=self.model_name,
|
|
329
|
-
messages=[{"role": "user", "content": prompt}],
|
|
330
|
-
temperature=self.temperature,
|
|
331
|
-
stream=True,
|
|
332
|
-
)
|
|
333
|
-
async for chunk in stream:
|
|
334
|
-
delta = chunk.choices[0].delta
|
|
335
|
-
if delta and delta.content:
|
|
336
|
-
yield delta.content
|
|
625
|
+
stream = await client.chat.completions.create(**kwargs)
|
|
337
626
|
except Exception as e:
|
|
338
627
|
raise self._map_openai_error(e) from e
|
|
339
628
|
|
|
340
|
-
|
|
629
|
+
async for chunk in stream:
|
|
630
|
+
delta = chunk.choices[0].delta
|
|
631
|
+
if delta and delta.content:
|
|
632
|
+
yield delta.content
|
|
633
|
+
|
|
634
|
+
async def _acall_responses(
|
|
635
|
+
self, input_content: Sequence[StructuredMessage], response_format: dict[str, Any] | None = None
|
|
636
|
+
) -> str:
|
|
341
637
|
client = self._require_async_client()
|
|
638
|
+
kwargs: dict[str, Any] = {
|
|
639
|
+
"model": self.model_name,
|
|
640
|
+
"input": cast(Any, input_content),
|
|
641
|
+
"temperature": self.temperature,
|
|
642
|
+
}
|
|
643
|
+
if response_format:
|
|
644
|
+
kwargs["response_format"] = response_format
|
|
645
|
+
|
|
342
646
|
try:
|
|
343
|
-
resp: Any = await client.responses.create(
|
|
344
|
-
model=self.model_name,
|
|
345
|
-
input=cast(Any, input_content),
|
|
346
|
-
temperature=self.temperature,
|
|
347
|
-
)
|
|
348
|
-
return (getattr(resp, "output_text", None) or "").strip()
|
|
647
|
+
resp: Any = await client.responses.create(**kwargs)
|
|
349
648
|
except Exception as e:
|
|
350
649
|
raise self._map_openai_error(e) from e
|
|
351
650
|
|
|
352
|
-
|
|
651
|
+
return (getattr(resp, "output_text", None) or "").strip()
|
|
652
|
+
|
|
653
|
+
async def _acall_responses_stream(
|
|
654
|
+
self, input_content: Sequence[StructuredMessage], response_format: dict[str, Any] | None = None
|
|
655
|
+
) -> AsyncIterator[str]:
|
|
353
656
|
client = self._require_async_client()
|
|
657
|
+
kwargs: dict[str, Any] = {
|
|
658
|
+
"model": self.model_name,
|
|
659
|
+
"input": cast(Any, input_content),
|
|
660
|
+
"temperature": self.temperature,
|
|
661
|
+
"stream": True,
|
|
662
|
+
}
|
|
663
|
+
if response_format:
|
|
664
|
+
kwargs["response_format"] = response_format
|
|
665
|
+
|
|
354
666
|
try:
|
|
355
|
-
stream_or_resp = await client.responses.create(
|
|
356
|
-
model=self.model_name,
|
|
357
|
-
input=cast(Any, input_content),
|
|
358
|
-
temperature=self.temperature,
|
|
359
|
-
stream=True,
|
|
360
|
-
)
|
|
361
|
-
if isinstance(stream_or_resp, AsyncStream):
|
|
362
|
-
async for ev in stream_or_resp:
|
|
363
|
-
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
364
|
-
if delta:
|
|
365
|
-
yield delta
|
|
366
|
-
else:
|
|
367
|
-
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
368
|
-
if text:
|
|
369
|
-
yield text
|
|
667
|
+
stream_or_resp = await client.responses.create(**kwargs)
|
|
370
668
|
except Exception as e:
|
|
371
669
|
raise self._map_openai_error(e) from e
|
|
670
|
+
|
|
671
|
+
if isinstance(stream_or_resp, AsyncStream):
|
|
672
|
+
async for ev in stream_or_resp:
|
|
673
|
+
delta = getattr(getattr(ev, "delta", None), "content", None)
|
|
674
|
+
if delta:
|
|
675
|
+
yield delta
|
|
676
|
+
else:
|
|
677
|
+
text = (getattr(stream_or_resp, "output_text", None) or "").strip()
|
|
678
|
+
if text:
|
|
679
|
+
yield text
|