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.
Files changed (62) hide show
  1. amsdal_ml/Third-Party Materials - AMSDAL Dependencies - License Notices.md +617 -0
  2. amsdal_ml/__about__.py +1 -1
  3. amsdal_ml/agents/__init__.py +13 -0
  4. amsdal_ml/agents/agent.py +5 -7
  5. amsdal_ml/agents/default_qa_agent.py +108 -143
  6. amsdal_ml/agents/functional_calling_agent.py +233 -0
  7. amsdal_ml/agents/mcp_client_tool.py +46 -0
  8. amsdal_ml/agents/python_tool.py +86 -0
  9. amsdal_ml/agents/retriever_tool.py +5 -6
  10. amsdal_ml/agents/tool_adapters.py +98 -0
  11. amsdal_ml/fileio/base_loader.py +7 -5
  12. amsdal_ml/fileio/openai_loader.py +16 -17
  13. amsdal_ml/mcp_client/base.py +2 -0
  14. amsdal_ml/mcp_client/http_client.py +7 -1
  15. amsdal_ml/mcp_client/stdio_client.py +19 -16
  16. amsdal_ml/mcp_server/server_retriever_stdio.py +8 -11
  17. amsdal_ml/ml_ingesting/__init__.py +29 -0
  18. amsdal_ml/ml_ingesting/default_ingesting.py +49 -51
  19. amsdal_ml/ml_ingesting/embedders/__init__.py +4 -0
  20. amsdal_ml/ml_ingesting/embedders/embedder.py +12 -0
  21. amsdal_ml/ml_ingesting/embedders/openai_embedder.py +30 -0
  22. amsdal_ml/ml_ingesting/embedding_data.py +3 -0
  23. amsdal_ml/ml_ingesting/loaders/__init__.py +6 -0
  24. amsdal_ml/ml_ingesting/loaders/folder_loader.py +52 -0
  25. amsdal_ml/ml_ingesting/loaders/loader.py +28 -0
  26. amsdal_ml/ml_ingesting/loaders/pdf_loader.py +136 -0
  27. amsdal_ml/ml_ingesting/loaders/text_loader.py +44 -0
  28. amsdal_ml/ml_ingesting/model_ingester.py +278 -0
  29. amsdal_ml/ml_ingesting/pipeline.py +131 -0
  30. amsdal_ml/ml_ingesting/pipeline_interface.py +31 -0
  31. amsdal_ml/ml_ingesting/processors/__init__.py +4 -0
  32. amsdal_ml/ml_ingesting/processors/cleaner.py +14 -0
  33. amsdal_ml/ml_ingesting/processors/text_cleaner.py +42 -0
  34. amsdal_ml/ml_ingesting/splitters/__init__.py +4 -0
  35. amsdal_ml/ml_ingesting/splitters/splitter.py +15 -0
  36. amsdal_ml/ml_ingesting/splitters/token_splitter.py +85 -0
  37. amsdal_ml/ml_ingesting/stores/__init__.py +4 -0
  38. amsdal_ml/ml_ingesting/stores/embedding_data.py +63 -0
  39. amsdal_ml/ml_ingesting/stores/store.py +22 -0
  40. amsdal_ml/ml_ingesting/types.py +40 -0
  41. amsdal_ml/ml_models/models.py +96 -4
  42. amsdal_ml/ml_models/openai_model.py +430 -122
  43. amsdal_ml/ml_models/utils.py +7 -0
  44. amsdal_ml/ml_retrievers/__init__.py +17 -0
  45. amsdal_ml/ml_retrievers/adapters.py +93 -0
  46. amsdal_ml/ml_retrievers/default_retriever.py +11 -1
  47. amsdal_ml/ml_retrievers/openai_retriever.py +27 -7
  48. amsdal_ml/ml_retrievers/query_retriever.py +487 -0
  49. amsdal_ml/ml_retrievers/retriever.py +12 -0
  50. amsdal_ml/models/embedding_model.py +7 -7
  51. amsdal_ml/prompts/__init__.py +77 -0
  52. amsdal_ml/prompts/database_query_agent.prompt +14 -0
  53. amsdal_ml/prompts/functional_calling_agent_base.prompt +9 -0
  54. amsdal_ml/prompts/nl_query_filter.prompt +318 -0
  55. amsdal_ml/{agents/promts → prompts}/react_chat.prompt +17 -8
  56. amsdal_ml/utils/__init__.py +5 -0
  57. amsdal_ml/utils/query_utils.py +189 -0
  58. {amsdal_ml-0.1.4.dist-info → amsdal_ml-0.2.0.dist-info}/METADATA +59 -1
  59. amsdal_ml-0.2.0.dist-info/RECORD +72 -0
  60. {amsdal_ml-0.1.4.dist-info → amsdal_ml-0.2.0.dist-info}/WHEEL +1 -1
  61. amsdal_ml/agents/promts/__init__.py +0 -58
  62. 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__(self) -> None:
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 = ml_config.llm_temperature
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
- # ---------- Public sync API ----------
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
- prompt: str,
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(prompt, atts)
55
- return self._call_responses(input_content)
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
- final_prompt = self._merge_plain_text(prompt, atts)
58
- return self._call_chat(final_prompt)
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
- prompt: str,
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(prompt, atts)
76
- for chunk in self._call_responses_stream(input_content):
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
- final_prompt = self._merge_plain_text(prompt, atts)
81
- for chunk in self._call_chat_stream(final_prompt):
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
- prompt: str,
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(prompt, atts)
102
- return await self._acall_responses(input_content)
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
- final_prompt = self._merge_plain_text(prompt, atts)
105
- return await self._acall_chat(final_prompt)
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
- prompt: str,
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(prompt, atts)
124
- async for chunk in self._acall_responses_stream(input_content):
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
- final_prompt = self._merge_plain_text(prompt, atts)
129
- async for chunk in self._acall_chat_stream(final_prompt):
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 for a in atts if a.type == FILE_ID and (a.metadata or {}).get("provider") != "openai"
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"Got providers: {', '.join(sorted(provs))}"
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(self, prompt: str, atts: list[FileAttachment]) -> list[dict[str, Any]]:
214
- parts: list[dict[str, Any]] = [{"type": "input_text", "text": prompt}]
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
- return [{"role": "user", "content": parts}]
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(f"OpenAI API status error ({status}). payload={payload_repr!r}")
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(self, prompt: str) -> str:
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
- def _call_chat_stream(self, prompt: str) -> Iterator[str]:
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
- def _call_responses(self, input_content: list[dict[str, Any]]) -> str:
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
- def _call_responses_stream(self, input_content: list[dict[str, Any]]) -> Iterator[str]:
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(self, prompt: str) -> str:
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
- print("acall_chat:", prompt) # noqa: T201
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
- async def _acall_chat_stream(self, prompt: str) -> AsyncIterator[str]:
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
- async def _acall_responses(self, input_content: list[dict[str, Any]]) -> str:
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
- async def _acall_responses_stream(self, input_content: list[dict[str, Any]]) -> AsyncIterator[str]:
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