llama-index-llms-openai 0.3.28__py3-none-any.whl → 0.3.29__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.
- llama_index/llms/openai/__init__.py +2 -1
- llama_index/llms/openai/responses.py +952 -0
- llama_index/llms/openai/utils.py +130 -8
- {llama_index_llms_openai-0.3.28.dist-info → llama_index_llms_openai-0.3.29.dist-info}/METADATA +2 -2
- llama_index_llms_openai-0.3.29.dist-info/RECORD +9 -0
- llama_index_llms_openai-0.3.28.dist-info/RECORD +0 -8
- {llama_index_llms_openai-0.3.28.dist-info → llama_index_llms_openai-0.3.29.dist-info}/LICENSE +0 -0
- {llama_index_llms_openai-0.3.28.dist-info → llama_index_llms_openai-0.3.29.dist-info}/WHEEL +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
from llama_index.llms.openai.base import AsyncOpenAI, OpenAI, SyncOpenAI, Tokenizer
|
|
2
|
+
from llama_index.llms.openai.responses import OpenAIResponses
|
|
2
3
|
|
|
3
|
-
__all__ = ["OpenAI", "Tokenizer", "SyncOpenAI", "AsyncOpenAI"]
|
|
4
|
+
__all__ = ["OpenAI", "OpenAIResponses", "Tokenizer", "SyncOpenAI", "AsyncOpenAI"]
|
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import httpx
|
|
3
|
+
import tiktoken
|
|
4
|
+
from openai import AsyncOpenAI, AzureOpenAI
|
|
5
|
+
from openai import OpenAI as SyncOpenAI
|
|
6
|
+
from openai.types.responses import (
|
|
7
|
+
Response,
|
|
8
|
+
ResponseStreamEvent,
|
|
9
|
+
ResponseCompletedEvent,
|
|
10
|
+
ResponseCreatedEvent,
|
|
11
|
+
ResponseFileSearchCallCompletedEvent,
|
|
12
|
+
ResponseFunctionCallArgumentsDeltaEvent,
|
|
13
|
+
ResponseFunctionCallArgumentsDoneEvent,
|
|
14
|
+
ResponseInProgressEvent,
|
|
15
|
+
ResponseOutputItemAddedEvent,
|
|
16
|
+
ResponseTextAnnotationDeltaEvent,
|
|
17
|
+
ResponseTextDeltaEvent,
|
|
18
|
+
ResponseWebSearchCallCompletedEvent,
|
|
19
|
+
ResponseOutputItem,
|
|
20
|
+
ResponseOutputMessage,
|
|
21
|
+
ResponseFileSearchToolCall,
|
|
22
|
+
ResponseFunctionToolCall,
|
|
23
|
+
ResponseFunctionWebSearch,
|
|
24
|
+
ResponseComputerToolCall,
|
|
25
|
+
ResponseReasoningItem,
|
|
26
|
+
)
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
AsyncGenerator,
|
|
31
|
+
Callable,
|
|
32
|
+
Dict,
|
|
33
|
+
Generator,
|
|
34
|
+
List,
|
|
35
|
+
Literal,
|
|
36
|
+
Optional,
|
|
37
|
+
Protocol,
|
|
38
|
+
Sequence,
|
|
39
|
+
Tuple,
|
|
40
|
+
Type,
|
|
41
|
+
Union,
|
|
42
|
+
runtime_checkable,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
import llama_index.core.instrumentation as instrument
|
|
46
|
+
from llama_index.core.base.llms.generic_utils import (
|
|
47
|
+
achat_to_completion_decorator,
|
|
48
|
+
astream_chat_to_completion_decorator,
|
|
49
|
+
chat_to_completion_decorator,
|
|
50
|
+
stream_chat_to_completion_decorator,
|
|
51
|
+
)
|
|
52
|
+
from llama_index.core.base.llms.types import (
|
|
53
|
+
ChatMessage,
|
|
54
|
+
ChatResponse,
|
|
55
|
+
ChatResponseAsyncGen,
|
|
56
|
+
ChatResponseGen,
|
|
57
|
+
CompletionResponse,
|
|
58
|
+
CompletionResponseAsyncGen,
|
|
59
|
+
CompletionResponseGen,
|
|
60
|
+
LLMMetadata,
|
|
61
|
+
MessageRole,
|
|
62
|
+
TextBlock,
|
|
63
|
+
)
|
|
64
|
+
from llama_index.core.bridge.pydantic import (
|
|
65
|
+
Field,
|
|
66
|
+
PrivateAttr,
|
|
67
|
+
)
|
|
68
|
+
from llama_index.core.constants import (
|
|
69
|
+
DEFAULT_TEMPERATURE,
|
|
70
|
+
)
|
|
71
|
+
from llama_index.core.llms.callbacks import (
|
|
72
|
+
llm_chat_callback,
|
|
73
|
+
llm_completion_callback,
|
|
74
|
+
)
|
|
75
|
+
from llama_index.core.llms.function_calling import FunctionCallingLLM
|
|
76
|
+
from llama_index.core.llms.llm import ToolSelection, Model
|
|
77
|
+
from llama_index.core.llms.utils import parse_partial_json
|
|
78
|
+
from llama_index.core.prompts import PromptTemplate
|
|
79
|
+
from llama_index.core.program.utils import FlexibleModel
|
|
80
|
+
from llama_index.llms.openai.utils import (
|
|
81
|
+
O1_MODELS,
|
|
82
|
+
create_retry_decorator,
|
|
83
|
+
is_function_calling_model,
|
|
84
|
+
openai_modelname_to_contextsize,
|
|
85
|
+
resolve_openai_credentials,
|
|
86
|
+
resolve_tool_choice,
|
|
87
|
+
to_openai_message_dicts,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
dispatcher = instrument.get_dispatcher(__name__)
|
|
92
|
+
|
|
93
|
+
if TYPE_CHECKING:
|
|
94
|
+
from llama_index.core.tools.types import BaseTool
|
|
95
|
+
|
|
96
|
+
DEFAULT_OPENAI_MODEL = "gpt-4o-mini"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def llm_retry_decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
100
|
+
@functools.wraps(f)
|
|
101
|
+
def wrapper(self, *args: Any, **kwargs: Any) -> Any:
|
|
102
|
+
max_retries = getattr(self, "max_retries", 0)
|
|
103
|
+
if max_retries <= 0:
|
|
104
|
+
return f(self, *args, **kwargs)
|
|
105
|
+
|
|
106
|
+
retry = create_retry_decorator(
|
|
107
|
+
max_retries=max_retries,
|
|
108
|
+
random_exponential=True,
|
|
109
|
+
stop_after_delay_seconds=60,
|
|
110
|
+
min_seconds=1,
|
|
111
|
+
max_seconds=20,
|
|
112
|
+
)
|
|
113
|
+
return retry(f)(self, *args, **kwargs)
|
|
114
|
+
|
|
115
|
+
return wrapper
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@runtime_checkable
|
|
119
|
+
class Tokenizer(Protocol):
|
|
120
|
+
"""Tokenizers support an encode function that returns a list of ints."""
|
|
121
|
+
|
|
122
|
+
def encode(self, text: str) -> List[int]: # fmt: skip
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def force_single_tool_call(response: ChatResponse) -> None:
|
|
127
|
+
tool_calls = response.message.additional_kwargs.get("tool_calls", [])
|
|
128
|
+
if len(tool_calls) > 1:
|
|
129
|
+
response.message.additional_kwargs["tool_calls"] = [tool_calls[0]]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class OpenAIResponses(FunctionCallingLLM):
|
|
133
|
+
"""
|
|
134
|
+
OpenAI Responses LLM.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
model: name of the OpenAI model to use.
|
|
138
|
+
temperature: a float from 0 to 1 controlling randomness in generation; higher will lead to more creative, less deterministic responses.
|
|
139
|
+
max_output_tokens: the maximum number of tokens to generate.
|
|
140
|
+
include: Additional output data to include in the model response.
|
|
141
|
+
instructions: Instructions for the model to follow.
|
|
142
|
+
track_previous_responses: Whether to track previous responses. If true, the LLM class will statefully track previous responses.
|
|
143
|
+
store: Whether to store previous responses in OpenAI's storage.
|
|
144
|
+
built_in_tools: The built-in tools to use for the model to augment responses.
|
|
145
|
+
truncation: Whether to auto-truncate the input if it exceeds the model's context window.
|
|
146
|
+
user: An optional identifier to help track the user's requests for abuse.
|
|
147
|
+
strict: Whether to enforce strict validation of the structured output.
|
|
148
|
+
additional_kwargs: Add additional parameters to OpenAI request body.
|
|
149
|
+
max_retries: How many times to retry the API call if it fails.
|
|
150
|
+
timeout: How long to wait, in seconds, for an API call before failing.
|
|
151
|
+
api_key: Your OpenAI api key
|
|
152
|
+
api_base: The base URL of the API to call
|
|
153
|
+
api_version: the version of the API to call
|
|
154
|
+
default_headers: override the default headers for API requests.
|
|
155
|
+
http_client: pass in your own httpx.Client instance.
|
|
156
|
+
async_http_client: pass in your own httpx.AsyncClient instance.
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
`pip install llama-index-llms-openai`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from llama_index.llms.openai import OpenAIResponses
|
|
163
|
+
|
|
164
|
+
llm = OpenAIResponses(model="gpt-4o-mini", api_key="sk-...")
|
|
165
|
+
|
|
166
|
+
response = llm.complete("Hi, write a short story")
|
|
167
|
+
print(response.text)
|
|
168
|
+
```
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
model: str = Field(
|
|
172
|
+
default=DEFAULT_OPENAI_MODEL, description="The OpenAI model to use."
|
|
173
|
+
)
|
|
174
|
+
temperature: float = Field(
|
|
175
|
+
default=DEFAULT_TEMPERATURE,
|
|
176
|
+
description="The temperature to use during generation.",
|
|
177
|
+
ge=0.0,
|
|
178
|
+
le=2.0,
|
|
179
|
+
)
|
|
180
|
+
top_p: float = Field(
|
|
181
|
+
default=1.0,
|
|
182
|
+
description="The top-p value to use during generation.",
|
|
183
|
+
ge=0.0,
|
|
184
|
+
le=1.0,
|
|
185
|
+
)
|
|
186
|
+
max_output_tokens: Optional[int] = Field(
|
|
187
|
+
description="The maximum number of tokens to generate.",
|
|
188
|
+
gt=0,
|
|
189
|
+
)
|
|
190
|
+
include: Optional[List[str]] = Field(
|
|
191
|
+
default=None,
|
|
192
|
+
description="Additional output data to include in the model response.",
|
|
193
|
+
)
|
|
194
|
+
instructions: Optional[str] = Field(
|
|
195
|
+
default=None,
|
|
196
|
+
description="Instructions for the model to follow.",
|
|
197
|
+
)
|
|
198
|
+
track_previous_responses: bool = Field(
|
|
199
|
+
default=False,
|
|
200
|
+
description="Whether to track previous responses. If true, the LLM class will statefully track previous responses.",
|
|
201
|
+
)
|
|
202
|
+
store: bool = Field(
|
|
203
|
+
default=False,
|
|
204
|
+
description="Whether to store previous responses in OpenAI's storage.",
|
|
205
|
+
)
|
|
206
|
+
built_in_tools: Optional[List[dict]] = Field(
|
|
207
|
+
default=None,
|
|
208
|
+
description="The built-in tools to use for the model to augment responses.",
|
|
209
|
+
)
|
|
210
|
+
truncation: str = Field(
|
|
211
|
+
default="disabled",
|
|
212
|
+
description="Whether to auto-truncate the input if it exceeds the model's context window.",
|
|
213
|
+
)
|
|
214
|
+
user: Optional[str] = Field(
|
|
215
|
+
default=None,
|
|
216
|
+
description="An optional identifier to help track the user's requests for abuse.",
|
|
217
|
+
)
|
|
218
|
+
call_metadata: Optional[Dict[str, Any]] = Field(
|
|
219
|
+
default=None,
|
|
220
|
+
description="Metadata to include in the API call.",
|
|
221
|
+
)
|
|
222
|
+
additional_kwargs: Dict[str, Any] = Field(
|
|
223
|
+
default_factory=dict,
|
|
224
|
+
description="Additional kwargs for the OpenAI API at inference time.",
|
|
225
|
+
)
|
|
226
|
+
max_retries: int = Field(
|
|
227
|
+
default=3,
|
|
228
|
+
description="The maximum number of API retries.",
|
|
229
|
+
ge=0,
|
|
230
|
+
)
|
|
231
|
+
timeout: float = Field(
|
|
232
|
+
default=60.0,
|
|
233
|
+
description="The timeout, in seconds, for API requests.",
|
|
234
|
+
ge=0,
|
|
235
|
+
)
|
|
236
|
+
strict: bool = Field(
|
|
237
|
+
default=False,
|
|
238
|
+
description="Whether to enforce strict validation of the structured output.",
|
|
239
|
+
)
|
|
240
|
+
default_headers: Optional[Dict[str, str]] = Field(
|
|
241
|
+
default=None, description="The default headers for API requests."
|
|
242
|
+
)
|
|
243
|
+
api_key: str = Field(default=None, description="The OpenAI API key.")
|
|
244
|
+
api_base: str = Field(description="The base URL for OpenAI API.")
|
|
245
|
+
api_version: str = Field(description="The API version for OpenAI API.")
|
|
246
|
+
reasoning_effort: Optional[Literal["low", "medium", "high"]] = Field(
|
|
247
|
+
default=None,
|
|
248
|
+
description="The effort to use for reasoning models.",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
_client: SyncOpenAI = PrivateAttr()
|
|
252
|
+
_aclient: AsyncOpenAI = PrivateAttr()
|
|
253
|
+
_http_client: Optional[httpx.Client] = PrivateAttr()
|
|
254
|
+
_async_http_client: Optional[httpx.AsyncClient] = PrivateAttr()
|
|
255
|
+
_previous_response_id: Optional[str] = PrivateAttr()
|
|
256
|
+
|
|
257
|
+
def __init__(
|
|
258
|
+
self,
|
|
259
|
+
model: str = DEFAULT_OPENAI_MODEL,
|
|
260
|
+
temperature: float = DEFAULT_TEMPERATURE,
|
|
261
|
+
max_output_tokens: Optional[int] = None,
|
|
262
|
+
reasoning_effort: Optional[Literal["low", "medium", "high"]] = None,
|
|
263
|
+
include: Optional[List[str]] = None,
|
|
264
|
+
instructions: Optional[str] = None,
|
|
265
|
+
track_previous_responses: bool = False,
|
|
266
|
+
store: bool = False,
|
|
267
|
+
built_in_tools: Optional[List[dict]] = None,
|
|
268
|
+
truncation: str = "disabled",
|
|
269
|
+
user: Optional[str] = None,
|
|
270
|
+
previous_response_id: Optional[str] = None,
|
|
271
|
+
call_metadata: Optional[Dict[str, Any]] = None,
|
|
272
|
+
strict: bool = False,
|
|
273
|
+
additional_kwargs: Optional[Dict[str, Any]] = None,
|
|
274
|
+
max_retries: int = 3,
|
|
275
|
+
timeout: float = 60.0,
|
|
276
|
+
api_key: Optional[str] = None,
|
|
277
|
+
api_base: Optional[str] = None,
|
|
278
|
+
api_version: Optional[str] = None,
|
|
279
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
280
|
+
http_client: Optional[httpx.Client] = None,
|
|
281
|
+
async_http_client: Optional[httpx.AsyncClient] = None,
|
|
282
|
+
openai_client: Optional[SyncOpenAI] = None,
|
|
283
|
+
async_openai_client: Optional[AsyncOpenAI] = None,
|
|
284
|
+
**kwargs: Any,
|
|
285
|
+
) -> None:
|
|
286
|
+
additional_kwargs = additional_kwargs or {}
|
|
287
|
+
|
|
288
|
+
api_key, api_base, api_version = resolve_openai_credentials(
|
|
289
|
+
api_key=api_key,
|
|
290
|
+
api_base=api_base,
|
|
291
|
+
api_version=api_version,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# TODO: Temp forced to 1.0 for o1
|
|
295
|
+
if model in O1_MODELS:
|
|
296
|
+
temperature = 1.0
|
|
297
|
+
|
|
298
|
+
super().__init__(
|
|
299
|
+
model=model,
|
|
300
|
+
temperature=temperature,
|
|
301
|
+
max_output_tokens=max_output_tokens,
|
|
302
|
+
reasoning_effort=reasoning_effort,
|
|
303
|
+
include=include,
|
|
304
|
+
instructions=instructions,
|
|
305
|
+
track_previous_responses=track_previous_responses,
|
|
306
|
+
store=store,
|
|
307
|
+
built_in_tools=built_in_tools,
|
|
308
|
+
truncation=truncation,
|
|
309
|
+
user=user,
|
|
310
|
+
additional_kwargs=additional_kwargs,
|
|
311
|
+
max_retries=max_retries,
|
|
312
|
+
api_key=api_key,
|
|
313
|
+
api_version=api_version,
|
|
314
|
+
api_base=api_base,
|
|
315
|
+
timeout=timeout,
|
|
316
|
+
default_headers=default_headers,
|
|
317
|
+
call_metadata=call_metadata,
|
|
318
|
+
strict=strict,
|
|
319
|
+
**kwargs,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
self._previous_response_id = previous_response_id
|
|
323
|
+
|
|
324
|
+
# store is set to true if track_previous_responses is true
|
|
325
|
+
if self.track_previous_responses:
|
|
326
|
+
self.store = True
|
|
327
|
+
|
|
328
|
+
self._http_client = http_client
|
|
329
|
+
self._async_http_client = async_http_client
|
|
330
|
+
self._client = openai_client or SyncOpenAI(**self._get_credential_kwargs())
|
|
331
|
+
self._aclient = async_openai_client or AsyncOpenAI(
|
|
332
|
+
**self._get_credential_kwargs(is_async=True)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def class_name(cls) -> str:
|
|
337
|
+
return "openai_responses_llm"
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def metadata(self) -> LLMMetadata:
|
|
341
|
+
return LLMMetadata(
|
|
342
|
+
context_window=openai_modelname_to_contextsize(self._get_model_name()),
|
|
343
|
+
num_output=self.max_output_tokens or -1,
|
|
344
|
+
is_chat_model=True,
|
|
345
|
+
is_function_calling_model=is_function_calling_model(
|
|
346
|
+
model=self._get_model_name()
|
|
347
|
+
),
|
|
348
|
+
model_name=self.model,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def _tokenizer(self) -> Optional[Tokenizer]:
|
|
353
|
+
"""
|
|
354
|
+
Get a tokenizer for this model, or None if a tokenizing method is unknown.
|
|
355
|
+
|
|
356
|
+
OpenAI can do this using the tiktoken package, subclasses may not have
|
|
357
|
+
this convenience.
|
|
358
|
+
"""
|
|
359
|
+
return tiktoken.encoding_for_model(self._get_model_name())
|
|
360
|
+
|
|
361
|
+
def _get_model_name(self) -> str:
|
|
362
|
+
model_name = self.model
|
|
363
|
+
if "ft-" in model_name: # legacy fine-tuning
|
|
364
|
+
model_name = model_name.split(":")[0]
|
|
365
|
+
elif model_name.startswith("ft:"):
|
|
366
|
+
model_name = model_name.split(":")[1]
|
|
367
|
+
return model_name
|
|
368
|
+
|
|
369
|
+
def _is_azure_client(self) -> bool:
|
|
370
|
+
return isinstance(self._get_client(), AzureOpenAI)
|
|
371
|
+
|
|
372
|
+
def _get_credential_kwargs(self, is_async: bool = False) -> Dict[str, Any]:
|
|
373
|
+
return {
|
|
374
|
+
"api_key": self.api_key,
|
|
375
|
+
"base_url": self.api_base,
|
|
376
|
+
"max_retries": self.max_retries,
|
|
377
|
+
"timeout": self.timeout,
|
|
378
|
+
"default_headers": self.default_headers,
|
|
379
|
+
"http_client": self._async_http_client if is_async else self._http_client,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
def _get_model_kwargs(self, **kwargs: Any) -> Dict[str, Any]:
|
|
383
|
+
model_kwargs = {
|
|
384
|
+
"model": self.model,
|
|
385
|
+
"include": self.include,
|
|
386
|
+
"instructions": self.instructions,
|
|
387
|
+
"max_output_tokens": self.max_output_tokens,
|
|
388
|
+
"metadata": self.call_metadata,
|
|
389
|
+
"previous_response_id": self._previous_response_id,
|
|
390
|
+
"store": self.store,
|
|
391
|
+
"temperature": self.temperature,
|
|
392
|
+
"tools": self.built_in_tools,
|
|
393
|
+
"top_p": self.top_p,
|
|
394
|
+
"truncation": self.truncation,
|
|
395
|
+
"user": self.user,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if self.model in O1_MODELS and self.reasoning_effort is not None:
|
|
399
|
+
# O1 models support reasoning_effort of low, medium, high
|
|
400
|
+
model_kwargs["reasoning_effort"] = {"effort": self.reasoning_effort}
|
|
401
|
+
|
|
402
|
+
# add tools or extend openai tools
|
|
403
|
+
if "tools" in kwargs:
|
|
404
|
+
if isinstance(model_kwargs["tools"], list):
|
|
405
|
+
model_kwargs["tools"].extend(kwargs.pop("tools"))
|
|
406
|
+
else:
|
|
407
|
+
model_kwargs["tools"] = kwargs.pop("tools")
|
|
408
|
+
|
|
409
|
+
# priority is class args > additional_kwargs > runtime args
|
|
410
|
+
model_kwargs.update(self.additional_kwargs)
|
|
411
|
+
|
|
412
|
+
kwargs = kwargs or {}
|
|
413
|
+
model_kwargs.update(kwargs)
|
|
414
|
+
|
|
415
|
+
return model_kwargs
|
|
416
|
+
|
|
417
|
+
@llm_chat_callback()
|
|
418
|
+
def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse:
|
|
419
|
+
return self._chat(messages, **kwargs)
|
|
420
|
+
|
|
421
|
+
@llm_chat_callback()
|
|
422
|
+
def stream_chat(
|
|
423
|
+
self, messages: Sequence[ChatMessage], **kwargs: Any
|
|
424
|
+
) -> ChatResponseGen:
|
|
425
|
+
return self._stream_chat(messages, **kwargs)
|
|
426
|
+
|
|
427
|
+
@llm_completion_callback()
|
|
428
|
+
def complete(
|
|
429
|
+
self, prompt: str, formatted: bool = False, **kwargs: Any
|
|
430
|
+
) -> CompletionResponse:
|
|
431
|
+
complete_fn = chat_to_completion_decorator(self._chat)
|
|
432
|
+
|
|
433
|
+
return complete_fn(prompt, **kwargs)
|
|
434
|
+
|
|
435
|
+
@llm_completion_callback()
|
|
436
|
+
def stream_complete(
|
|
437
|
+
self, prompt: str, formatted: bool = False, **kwargs: Any
|
|
438
|
+
) -> CompletionResponseGen:
|
|
439
|
+
stream_complete_fn = stream_chat_to_completion_decorator(self._stream_chat)
|
|
440
|
+
|
|
441
|
+
return stream_complete_fn(prompt, **kwargs)
|
|
442
|
+
|
|
443
|
+
def _parse_response_output(self, output: List[ResponseOutputItem]) -> ChatResponse:
|
|
444
|
+
message = ChatMessage(role=MessageRole.ASSISTANT, blocks=[])
|
|
445
|
+
additional_kwargs = {"built_in_tool_calls": []}
|
|
446
|
+
tool_calls = []
|
|
447
|
+
for item in output:
|
|
448
|
+
if isinstance(item, ResponseOutputMessage):
|
|
449
|
+
blocks = []
|
|
450
|
+
for part in item.content:
|
|
451
|
+
if hasattr(part, "text"):
|
|
452
|
+
blocks.append(TextBlock(text=part.text))
|
|
453
|
+
if hasattr(part, "annotations"):
|
|
454
|
+
additional_kwargs["annotations"] = part.annotations
|
|
455
|
+
if hasattr(part, "refusal"):
|
|
456
|
+
additional_kwargs["refusal"] = part.refusal
|
|
457
|
+
|
|
458
|
+
message.blocks.extend(blocks)
|
|
459
|
+
elif isinstance(item, ResponseFileSearchToolCall):
|
|
460
|
+
additional_kwargs["built_in_tool_calls"].append(item)
|
|
461
|
+
elif isinstance(item, ResponseFunctionToolCall):
|
|
462
|
+
tool_calls.append(item)
|
|
463
|
+
elif isinstance(item, ResponseFunctionWebSearch):
|
|
464
|
+
additional_kwargs["built_in_tool_calls"].append(item)
|
|
465
|
+
elif isinstance(item, ResponseComputerToolCall):
|
|
466
|
+
additional_kwargs["built_in_tool_calls"].append(item)
|
|
467
|
+
elif isinstance(item, ResponseReasoningItem):
|
|
468
|
+
additional_kwargs["reasoning"] = item
|
|
469
|
+
|
|
470
|
+
if tool_calls and message:
|
|
471
|
+
message.additional_kwargs["tool_calls"] = tool_calls
|
|
472
|
+
|
|
473
|
+
return ChatResponse(message=message, additional_kwargs=additional_kwargs)
|
|
474
|
+
|
|
475
|
+
@llm_retry_decorator
|
|
476
|
+
def _chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse:
|
|
477
|
+
message_dicts = to_openai_message_dicts(
|
|
478
|
+
messages,
|
|
479
|
+
model=self.model,
|
|
480
|
+
is_responses_api=True,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
response: Response = self._client.responses.create(
|
|
484
|
+
input=message_dicts,
|
|
485
|
+
stream=False,
|
|
486
|
+
**self._get_model_kwargs(**kwargs),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if self.track_previous_responses:
|
|
490
|
+
self._previous_response_id = response.id
|
|
491
|
+
|
|
492
|
+
chat_response = self._parse_response_output(response.output)
|
|
493
|
+
chat_response.raw = response
|
|
494
|
+
chat_response.additional_kwargs["usage"] = response.usage
|
|
495
|
+
|
|
496
|
+
return chat_response
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def process_response_event(
|
|
500
|
+
event: ResponseStreamEvent,
|
|
501
|
+
content: str,
|
|
502
|
+
tool_calls: List[ResponseFunctionToolCall],
|
|
503
|
+
built_in_tool_calls: List[Any],
|
|
504
|
+
additional_kwargs: Dict[str, Any],
|
|
505
|
+
current_tool_call: Optional[ResponseFunctionToolCall],
|
|
506
|
+
track_previous_responses: bool,
|
|
507
|
+
previous_response_id: Optional[str] = None,
|
|
508
|
+
) -> Tuple[
|
|
509
|
+
str,
|
|
510
|
+
List[ResponseFunctionToolCall],
|
|
511
|
+
List[Any],
|
|
512
|
+
Dict[str, Any],
|
|
513
|
+
Optional[ResponseFunctionToolCall],
|
|
514
|
+
Optional[str],
|
|
515
|
+
str,
|
|
516
|
+
]:
|
|
517
|
+
"""
|
|
518
|
+
Process a ResponseStreamEvent and update the state accordingly.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
event: The response stream event to process
|
|
522
|
+
content: Current accumulated content string
|
|
523
|
+
tool_calls: List of completed tool calls
|
|
524
|
+
built_in_tool_calls: List of built-in tool calls
|
|
525
|
+
additional_kwargs: Additional keyword arguments to include in ChatResponse
|
|
526
|
+
current_tool_call: The currently in-progress tool call, if any
|
|
527
|
+
track_previous_responses: Whether to track previous response IDs
|
|
528
|
+
previous_response_id: Previous response ID if tracking
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
A tuple containing the updated state:
|
|
532
|
+
(content, tool_calls, built_in_tool_calls, additional_kwargs, current_tool_call, updated_previous_response_id, delta)
|
|
533
|
+
"""
|
|
534
|
+
delta = ""
|
|
535
|
+
updated_previous_response_id = previous_response_id
|
|
536
|
+
|
|
537
|
+
if isinstance(event, ResponseCreatedEvent) or isinstance(
|
|
538
|
+
event, ResponseInProgressEvent
|
|
539
|
+
):
|
|
540
|
+
# Initial events, track the response id
|
|
541
|
+
if track_previous_responses:
|
|
542
|
+
updated_previous_response_id = event.response.id
|
|
543
|
+
elif isinstance(event, ResponseOutputItemAddedEvent):
|
|
544
|
+
# New output item (message, tool call, etc.)
|
|
545
|
+
if isinstance(event.item, ResponseFunctionToolCall):
|
|
546
|
+
current_tool_call = event.item
|
|
547
|
+
elif isinstance(event, ResponseTextDeltaEvent):
|
|
548
|
+
# Text content is being added
|
|
549
|
+
delta = event.delta
|
|
550
|
+
content += delta
|
|
551
|
+
elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent):
|
|
552
|
+
# Function call arguments are being streamed
|
|
553
|
+
if current_tool_call is not None:
|
|
554
|
+
current_tool_call.arguments += event.delta
|
|
555
|
+
elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent):
|
|
556
|
+
# Function call arguments are complete
|
|
557
|
+
if current_tool_call is not None:
|
|
558
|
+
current_tool_call.arguments = event.arguments
|
|
559
|
+
current_tool_call.status = "completed"
|
|
560
|
+
|
|
561
|
+
# append a copy of the tool call to the list
|
|
562
|
+
tool_calls.append(
|
|
563
|
+
ResponseFunctionToolCall(**current_tool_call.model_dump())
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# clear the current tool call
|
|
567
|
+
current_tool_call = None
|
|
568
|
+
elif isinstance(event, ResponseTextAnnotationDeltaEvent):
|
|
569
|
+
# Annotations for the text
|
|
570
|
+
annotations = additional_kwargs.get("annotations", [])
|
|
571
|
+
annotations.append(event.annotation)
|
|
572
|
+
additional_kwargs["annotations"] = annotations
|
|
573
|
+
elif isinstance(event, ResponseFileSearchCallCompletedEvent):
|
|
574
|
+
# File search tool call completed
|
|
575
|
+
built_in_tool_calls.append(event)
|
|
576
|
+
elif isinstance(event, ResponseWebSearchCallCompletedEvent):
|
|
577
|
+
# Web search tool call completed
|
|
578
|
+
built_in_tool_calls.append(event)
|
|
579
|
+
elif isinstance(event, ResponseReasoningItem):
|
|
580
|
+
# Reasoning information
|
|
581
|
+
additional_kwargs["reasoning"] = event
|
|
582
|
+
elif isinstance(event, ResponseCompletedEvent):
|
|
583
|
+
# Response is complete
|
|
584
|
+
if hasattr(event, "response") and hasattr(event.response, "usage"):
|
|
585
|
+
additional_kwargs["usage"] = event.response.usage
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
content,
|
|
589
|
+
tool_calls,
|
|
590
|
+
built_in_tool_calls,
|
|
591
|
+
additional_kwargs,
|
|
592
|
+
current_tool_call,
|
|
593
|
+
updated_previous_response_id,
|
|
594
|
+
delta,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
@llm_retry_decorator
|
|
598
|
+
def _stream_chat(
|
|
599
|
+
self, messages: Sequence[ChatMessage], **kwargs: Any
|
|
600
|
+
) -> ChatResponseGen:
|
|
601
|
+
message_dicts = to_openai_message_dicts(
|
|
602
|
+
messages,
|
|
603
|
+
model=self.model,
|
|
604
|
+
is_responses_api=True,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
def gen() -> ChatResponseGen:
|
|
608
|
+
content = ""
|
|
609
|
+
tool_calls = []
|
|
610
|
+
built_in_tool_calls = []
|
|
611
|
+
additional_kwargs = {"built_in_tool_calls": []}
|
|
612
|
+
current_tool_call: Optional[ResponseFunctionToolCall] = None
|
|
613
|
+
local_previous_response_id = self._previous_response_id
|
|
614
|
+
|
|
615
|
+
for event in self._client.responses.create(
|
|
616
|
+
input=message_dicts,
|
|
617
|
+
stream=True,
|
|
618
|
+
**self._get_model_kwargs(**kwargs),
|
|
619
|
+
):
|
|
620
|
+
# Process the event and update state
|
|
621
|
+
(
|
|
622
|
+
content,
|
|
623
|
+
tool_calls,
|
|
624
|
+
built_in_tool_calls,
|
|
625
|
+
additional_kwargs,
|
|
626
|
+
current_tool_call,
|
|
627
|
+
local_previous_response_id,
|
|
628
|
+
delta,
|
|
629
|
+
) = OpenAIResponses.process_response_event(
|
|
630
|
+
event=event,
|
|
631
|
+
content=content,
|
|
632
|
+
tool_calls=tool_calls,
|
|
633
|
+
built_in_tool_calls=built_in_tool_calls,
|
|
634
|
+
additional_kwargs=additional_kwargs,
|
|
635
|
+
current_tool_call=current_tool_call,
|
|
636
|
+
track_previous_responses=self.track_previous_responses,
|
|
637
|
+
previous_response_id=local_previous_response_id,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
if (
|
|
641
|
+
self.track_previous_responses
|
|
642
|
+
and local_previous_response_id != self._previous_response_id
|
|
643
|
+
):
|
|
644
|
+
self._previous_response_id = local_previous_response_id
|
|
645
|
+
|
|
646
|
+
if built_in_tool_calls:
|
|
647
|
+
additional_kwargs["built_in_tool_calls"] = built_in_tool_calls
|
|
648
|
+
|
|
649
|
+
# For any event, yield a ChatResponse with the current state
|
|
650
|
+
yield ChatResponse(
|
|
651
|
+
message=ChatMessage(
|
|
652
|
+
role=MessageRole.ASSISTANT,
|
|
653
|
+
content=content,
|
|
654
|
+
additional_kwargs={"tool_calls": tool_calls}
|
|
655
|
+
if tool_calls
|
|
656
|
+
else {},
|
|
657
|
+
),
|
|
658
|
+
delta=delta,
|
|
659
|
+
raw=event,
|
|
660
|
+
additional_kwargs=additional_kwargs,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
return gen()
|
|
664
|
+
|
|
665
|
+
# ===== Async Endpoints =====
|
|
666
|
+
@llm_chat_callback()
|
|
667
|
+
async def achat(
|
|
668
|
+
self,
|
|
669
|
+
messages: Sequence[ChatMessage],
|
|
670
|
+
**kwargs: Any,
|
|
671
|
+
) -> ChatResponse:
|
|
672
|
+
return await self._achat(messages, **kwargs)
|
|
673
|
+
|
|
674
|
+
@llm_chat_callback()
|
|
675
|
+
async def astream_chat(
|
|
676
|
+
self,
|
|
677
|
+
messages: Sequence[ChatMessage],
|
|
678
|
+
**kwargs: Any,
|
|
679
|
+
) -> ChatResponseAsyncGen:
|
|
680
|
+
return await self._astream_chat(messages, **kwargs)
|
|
681
|
+
|
|
682
|
+
@llm_completion_callback()
|
|
683
|
+
async def acomplete(
|
|
684
|
+
self, prompt: str, formatted: bool = False, **kwargs: Any
|
|
685
|
+
) -> CompletionResponse:
|
|
686
|
+
acomplete_fn = achat_to_completion_decorator(self._achat)
|
|
687
|
+
|
|
688
|
+
return await acomplete_fn(prompt, **kwargs)
|
|
689
|
+
|
|
690
|
+
@llm_completion_callback()
|
|
691
|
+
async def astream_complete(
|
|
692
|
+
self, prompt: str, formatted: bool = False, **kwargs: Any
|
|
693
|
+
) -> CompletionResponseAsyncGen:
|
|
694
|
+
astream_complete_fn = astream_chat_to_completion_decorator(self._astream_chat)
|
|
695
|
+
|
|
696
|
+
return await astream_complete_fn(prompt, **kwargs)
|
|
697
|
+
|
|
698
|
+
@llm_retry_decorator
|
|
699
|
+
async def _achat(
|
|
700
|
+
self, messages: Sequence[ChatMessage], **kwargs: Any
|
|
701
|
+
) -> ChatResponse:
|
|
702
|
+
message_dicts = to_openai_message_dicts(
|
|
703
|
+
messages,
|
|
704
|
+
model=self.model,
|
|
705
|
+
is_responses_api=True,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
response: Response = await self._aclient.responses.create(
|
|
709
|
+
input=message_dicts,
|
|
710
|
+
stream=False,
|
|
711
|
+
**self._get_model_kwargs(**kwargs),
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
if self.track_previous_responses:
|
|
715
|
+
self._previous_response_id = response.id
|
|
716
|
+
|
|
717
|
+
chat_response = self._parse_response_output(response.output)
|
|
718
|
+
chat_response.raw = response
|
|
719
|
+
chat_response.additional_kwargs["usage"] = response.usage
|
|
720
|
+
|
|
721
|
+
return chat_response
|
|
722
|
+
|
|
723
|
+
@llm_retry_decorator
|
|
724
|
+
async def _astream_chat(
|
|
725
|
+
self, messages: Sequence[ChatMessage], **kwargs: Any
|
|
726
|
+
) -> ChatResponseAsyncGen:
|
|
727
|
+
message_dicts = to_openai_message_dicts(
|
|
728
|
+
messages,
|
|
729
|
+
model=self.model,
|
|
730
|
+
is_responses_api=True,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
async def gen() -> ChatResponseAsyncGen:
|
|
734
|
+
content = ""
|
|
735
|
+
tool_calls = []
|
|
736
|
+
built_in_tool_calls = []
|
|
737
|
+
additional_kwargs = {"built_in_tool_calls": []}
|
|
738
|
+
current_tool_call: Optional[ResponseFunctionToolCall] = None
|
|
739
|
+
local_previous_response_id = self._previous_response_id
|
|
740
|
+
|
|
741
|
+
response_stream = await self._aclient.responses.create(
|
|
742
|
+
input=message_dicts,
|
|
743
|
+
stream=True,
|
|
744
|
+
**self._get_model_kwargs(**kwargs),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
async for event in response_stream:
|
|
748
|
+
# Process the event and update state
|
|
749
|
+
(
|
|
750
|
+
content,
|
|
751
|
+
tool_calls,
|
|
752
|
+
built_in_tool_calls,
|
|
753
|
+
additional_kwargs,
|
|
754
|
+
current_tool_call,
|
|
755
|
+
local_previous_response_id,
|
|
756
|
+
delta,
|
|
757
|
+
) = OpenAIResponses.process_response_event(
|
|
758
|
+
event=event,
|
|
759
|
+
content=content,
|
|
760
|
+
tool_calls=tool_calls,
|
|
761
|
+
built_in_tool_calls=built_in_tool_calls,
|
|
762
|
+
additional_kwargs=additional_kwargs,
|
|
763
|
+
current_tool_call=current_tool_call,
|
|
764
|
+
track_previous_responses=self.track_previous_responses,
|
|
765
|
+
previous_response_id=local_previous_response_id,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
if (
|
|
769
|
+
self.track_previous_responses
|
|
770
|
+
and local_previous_response_id != self._previous_response_id
|
|
771
|
+
):
|
|
772
|
+
self._previous_response_id = local_previous_response_id
|
|
773
|
+
|
|
774
|
+
if built_in_tool_calls:
|
|
775
|
+
additional_kwargs["built_in_tool_calls"] = built_in_tool_calls
|
|
776
|
+
|
|
777
|
+
# For any event, yield a ChatResponse with the current state
|
|
778
|
+
yield ChatResponse(
|
|
779
|
+
message=ChatMessage(
|
|
780
|
+
role=MessageRole.ASSISTANT,
|
|
781
|
+
content=content,
|
|
782
|
+
additional_kwargs={"tool_calls": tool_calls}
|
|
783
|
+
if tool_calls
|
|
784
|
+
else {},
|
|
785
|
+
),
|
|
786
|
+
delta=delta,
|
|
787
|
+
raw=event,
|
|
788
|
+
additional_kwargs=additional_kwargs,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
return gen()
|
|
792
|
+
|
|
793
|
+
def _prepare_chat_with_tools(
|
|
794
|
+
self,
|
|
795
|
+
tools: Sequence["BaseTool"],
|
|
796
|
+
user_msg: Optional[Union[str, ChatMessage]] = None,
|
|
797
|
+
chat_history: Optional[List[ChatMessage]] = None,
|
|
798
|
+
allow_parallel_tool_calls: bool = True,
|
|
799
|
+
tool_choice: Union[str, dict] = "auto",
|
|
800
|
+
verbose: bool = False,
|
|
801
|
+
strict: Optional[bool] = None,
|
|
802
|
+
**kwargs: Any,
|
|
803
|
+
) -> Dict[str, Any]:
|
|
804
|
+
"""Predict and call the tool."""
|
|
805
|
+
|
|
806
|
+
# openai responses api has a slightly different tool spec format
|
|
807
|
+
tool_specs = [
|
|
808
|
+
{"type": "function", **tool.metadata.to_openai_tool()["function"]}
|
|
809
|
+
for tool in tools
|
|
810
|
+
]
|
|
811
|
+
|
|
812
|
+
if strict is not None:
|
|
813
|
+
strict = strict
|
|
814
|
+
else:
|
|
815
|
+
strict = self.strict
|
|
816
|
+
|
|
817
|
+
if strict:
|
|
818
|
+
for tool_spec in tool_specs:
|
|
819
|
+
tool_spec["strict"] = True
|
|
820
|
+
tool_spec["parameters"]["additionalProperties"] = False
|
|
821
|
+
|
|
822
|
+
if isinstance(user_msg, str):
|
|
823
|
+
user_msg = ChatMessage(role=MessageRole.USER, content=user_msg)
|
|
824
|
+
|
|
825
|
+
messages = chat_history or []
|
|
826
|
+
if user_msg:
|
|
827
|
+
messages.append(user_msg)
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
"messages": messages,
|
|
831
|
+
"tools": tool_specs or None,
|
|
832
|
+
"tool_choice": resolve_tool_choice(tool_choice) if tool_specs else None,
|
|
833
|
+
"parallel_tool_calls": allow_parallel_tool_calls,
|
|
834
|
+
**kwargs,
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
def get_tool_calls_from_response(
|
|
838
|
+
self,
|
|
839
|
+
response: "ChatResponse",
|
|
840
|
+
error_on_no_tool_call: bool = True,
|
|
841
|
+
**kwargs: Any,
|
|
842
|
+
) -> List[ToolSelection]:
|
|
843
|
+
"""Predict and call the tool."""
|
|
844
|
+
tool_calls: List[
|
|
845
|
+
ResponseFunctionToolCall
|
|
846
|
+
] = response.message.additional_kwargs.get("tool_calls", [])
|
|
847
|
+
|
|
848
|
+
if len(tool_calls) < 1:
|
|
849
|
+
if error_on_no_tool_call:
|
|
850
|
+
raise ValueError(
|
|
851
|
+
f"Expected at least one tool call, but got {len(tool_calls)} tool calls."
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
return []
|
|
855
|
+
|
|
856
|
+
tool_selections = []
|
|
857
|
+
for tool_call in tool_calls:
|
|
858
|
+
# this should handle both complete and partial jsons
|
|
859
|
+
try:
|
|
860
|
+
argument_dict = parse_partial_json(tool_call.arguments)
|
|
861
|
+
except ValueError:
|
|
862
|
+
argument_dict = {}
|
|
863
|
+
|
|
864
|
+
tool_selections.append(
|
|
865
|
+
ToolSelection(
|
|
866
|
+
tool_id=tool_call.call_id,
|
|
867
|
+
tool_name=tool_call.name,
|
|
868
|
+
tool_kwargs=argument_dict,
|
|
869
|
+
)
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
return tool_selections
|
|
873
|
+
|
|
874
|
+
@dispatcher.span
|
|
875
|
+
def structured_predict(
|
|
876
|
+
self,
|
|
877
|
+
output_cls: Type[Model],
|
|
878
|
+
prompt: PromptTemplate,
|
|
879
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
880
|
+
**prompt_args: Any,
|
|
881
|
+
) -> Model:
|
|
882
|
+
"""Structured predict."""
|
|
883
|
+
llm_kwargs = llm_kwargs or {}
|
|
884
|
+
|
|
885
|
+
llm_kwargs["tool_choice"] = (
|
|
886
|
+
"required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
|
|
887
|
+
)
|
|
888
|
+
# by default structured prediction uses function calling to extract structured outputs
|
|
889
|
+
# here we force tool_choice to be required
|
|
890
|
+
return super().structured_predict(
|
|
891
|
+
output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
@dispatcher.span
|
|
895
|
+
async def astructured_predict(
|
|
896
|
+
self,
|
|
897
|
+
output_cls: Type[Model],
|
|
898
|
+
prompt: PromptTemplate,
|
|
899
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
900
|
+
**prompt_args: Any,
|
|
901
|
+
) -> Model:
|
|
902
|
+
"""Structured predict."""
|
|
903
|
+
llm_kwargs = llm_kwargs or {}
|
|
904
|
+
|
|
905
|
+
llm_kwargs["tool_choice"] = (
|
|
906
|
+
"required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
|
|
907
|
+
)
|
|
908
|
+
# by default structured prediction uses function calling to extract structured outputs
|
|
909
|
+
# here we force tool_choice to be required
|
|
910
|
+
return await super().astructured_predict(
|
|
911
|
+
output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
@dispatcher.span
|
|
915
|
+
def stream_structured_predict(
|
|
916
|
+
self,
|
|
917
|
+
output_cls: Type[Model],
|
|
918
|
+
prompt: PromptTemplate,
|
|
919
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
920
|
+
**prompt_args: Any,
|
|
921
|
+
) -> Generator[Union[Model, FlexibleModel], None, None]:
|
|
922
|
+
"""Stream structured predict."""
|
|
923
|
+
llm_kwargs = llm_kwargs or {}
|
|
924
|
+
|
|
925
|
+
llm_kwargs["tool_choice"] = (
|
|
926
|
+
"required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
|
|
927
|
+
)
|
|
928
|
+
# by default structured prediction uses function calling to extract structured outputs
|
|
929
|
+
# here we force tool_choice to be required
|
|
930
|
+
return super().stream_structured_predict(
|
|
931
|
+
output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
@dispatcher.span
|
|
935
|
+
async def astream_structured_predict(
|
|
936
|
+
self,
|
|
937
|
+
output_cls: Type[Model],
|
|
938
|
+
prompt: PromptTemplate,
|
|
939
|
+
llm_kwargs: Optional[Dict[str, Any]] = None,
|
|
940
|
+
**prompt_args: Any,
|
|
941
|
+
) -> AsyncGenerator[Union[Model, FlexibleModel], None]:
|
|
942
|
+
"""Stream structured predict."""
|
|
943
|
+
llm_kwargs = llm_kwargs or {}
|
|
944
|
+
|
|
945
|
+
llm_kwargs["tool_choice"] = (
|
|
946
|
+
"required" if "tool_choice" not in llm_kwargs else llm_kwargs["tool_choice"]
|
|
947
|
+
)
|
|
948
|
+
# by default structured prediction uses function calling to extract structured outputs
|
|
949
|
+
# here we force tool_choice to be required
|
|
950
|
+
return await super().astream_structured_predict(
|
|
951
|
+
output_cls, prompt, llm_kwargs=llm_kwargs, **prompt_args
|
|
952
|
+
)
|
llama_index/llms/openai/utils.py
CHANGED
|
@@ -391,20 +391,142 @@ def to_openai_message_dict(
|
|
|
391
391
|
return message_dict # type: ignore
|
|
392
392
|
|
|
393
393
|
|
|
394
|
+
def to_openai_responses_message_dict(
|
|
395
|
+
message: ChatMessage,
|
|
396
|
+
drop_none: bool = False,
|
|
397
|
+
model: Optional[str] = None,
|
|
398
|
+
) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
|
|
399
|
+
"""Convert a ChatMessage to an OpenAI message dict."""
|
|
400
|
+
content = []
|
|
401
|
+
content_txt = ""
|
|
402
|
+
|
|
403
|
+
for block in message.blocks:
|
|
404
|
+
if isinstance(block, TextBlock):
|
|
405
|
+
content.append({"type": "input_text", "text": block.text})
|
|
406
|
+
content_txt += block.text
|
|
407
|
+
elif isinstance(block, ImageBlock):
|
|
408
|
+
if block.url:
|
|
409
|
+
content.append(
|
|
410
|
+
{
|
|
411
|
+
"type": "input_image",
|
|
412
|
+
"image_url": str(block.url),
|
|
413
|
+
"detail": block.detail or "auto",
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
img_bytes = block.resolve_image(as_base64=True).read()
|
|
418
|
+
img_str = img_bytes.decode("utf-8")
|
|
419
|
+
content.append(
|
|
420
|
+
{
|
|
421
|
+
"type": "input_image",
|
|
422
|
+
"image_url": f"data:{block.image_mimetype};base64,{img_str}",
|
|
423
|
+
"detail": block.detail or "auto",
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
msg = f"Unsupported content block type: {type(block).__name__}"
|
|
428
|
+
raise ValueError(msg)
|
|
429
|
+
|
|
430
|
+
# NOTE: Sending a null value (None) for Tool Message to OpenAI will cause error
|
|
431
|
+
# It's only Allowed to send None if it's an Assistant Message and either a function call or tool calls were performed
|
|
432
|
+
# Reference: https://platform.openai.com/docs/api-reference/chat/create
|
|
433
|
+
content_txt = (
|
|
434
|
+
None
|
|
435
|
+
if content_txt == ""
|
|
436
|
+
and message.role == MessageRole.ASSISTANT
|
|
437
|
+
and (
|
|
438
|
+
"function_call" in message.additional_kwargs
|
|
439
|
+
or "tool_calls" in message.additional_kwargs
|
|
440
|
+
)
|
|
441
|
+
else content_txt
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# NOTE: Despite what the openai docs say, if the role is ASSISTANT, SYSTEM
|
|
445
|
+
# or TOOL, 'content' cannot be a list and must be string instead.
|
|
446
|
+
# Furthermore, if all blocks are text blocks, we can use the content_txt
|
|
447
|
+
# as the content. This will avoid breaking openai-like APIs.
|
|
448
|
+
if message.role.value == "tool":
|
|
449
|
+
call_id = message.additional_kwargs.get(
|
|
450
|
+
"tool_call_id", message.additional_kwargs.get("call_id")
|
|
451
|
+
)
|
|
452
|
+
if call_id is None:
|
|
453
|
+
raise ValueError(
|
|
454
|
+
"tool_call_id or call_id is required in additional_kwargs for tool messages"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
message_dict = {
|
|
458
|
+
"type": "function_call_output",
|
|
459
|
+
"output": content_txt,
|
|
460
|
+
"call_id": call_id,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return message_dict
|
|
464
|
+
elif "tool_calls" in message.additional_kwargs:
|
|
465
|
+
message_dicts = [
|
|
466
|
+
tool_call if isinstance(tool_call, dict) else tool_call.model_dump()
|
|
467
|
+
for tool_call in message.additional_kwargs["tool_calls"]
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
return message_dicts
|
|
471
|
+
else:
|
|
472
|
+
message_dict = {
|
|
473
|
+
"role": message.role.value,
|
|
474
|
+
"content": (
|
|
475
|
+
content_txt
|
|
476
|
+
if message.role.value in ("assistant", "system", "developer")
|
|
477
|
+
or all(isinstance(block, TextBlock) for block in message.blocks)
|
|
478
|
+
else content
|
|
479
|
+
),
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# TODO: O1 models do not support system prompts
|
|
483
|
+
if (
|
|
484
|
+
model is not None
|
|
485
|
+
and model in O1_MODELS
|
|
486
|
+
and model not in O1_MODELS_WITHOUT_FUNCTION_CALLING
|
|
487
|
+
):
|
|
488
|
+
if message_dict["role"] == "system":
|
|
489
|
+
message_dict["role"] = "developer"
|
|
490
|
+
|
|
491
|
+
null_keys = [key for key, value in message_dict.items() if value is None]
|
|
492
|
+
# if drop_none is True, remove keys with None values
|
|
493
|
+
if drop_none:
|
|
494
|
+
for key in null_keys:
|
|
495
|
+
message_dict.pop(key)
|
|
496
|
+
|
|
497
|
+
return message_dict # type: ignore
|
|
498
|
+
|
|
499
|
+
|
|
394
500
|
def to_openai_message_dicts(
|
|
395
501
|
messages: Sequence[ChatMessage],
|
|
396
502
|
drop_none: bool = False,
|
|
397
503
|
model: Optional[str] = None,
|
|
504
|
+
is_responses_api: bool = False,
|
|
398
505
|
) -> List[ChatCompletionMessageParam]:
|
|
399
506
|
"""Convert generic messages to OpenAI message dicts."""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
507
|
+
if is_responses_api:
|
|
508
|
+
final_message_dicts = []
|
|
509
|
+
for message in messages:
|
|
510
|
+
message_dicts = to_openai_responses_message_dict(
|
|
511
|
+
message,
|
|
512
|
+
drop_none=drop_none,
|
|
513
|
+
model=model,
|
|
514
|
+
)
|
|
515
|
+
if isinstance(message_dicts, list):
|
|
516
|
+
final_message_dicts.extend(message_dicts)
|
|
517
|
+
else:
|
|
518
|
+
final_message_dicts.append(message_dicts)
|
|
519
|
+
|
|
520
|
+
return final_message_dicts
|
|
521
|
+
else:
|
|
522
|
+
return [
|
|
523
|
+
to_openai_message_dict(
|
|
524
|
+
message,
|
|
525
|
+
drop_none=drop_none,
|
|
526
|
+
model=model,
|
|
527
|
+
)
|
|
528
|
+
for message in messages
|
|
529
|
+
]
|
|
408
530
|
|
|
409
531
|
|
|
410
532
|
def from_openai_message(
|
{llama_index_llms_openai-0.3.28.dist-info → llama_index_llms_openai-0.3.29.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: llama-index-llms-openai
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.29
|
|
4
4
|
Summary: llama-index llms openai integration
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: llama-index
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: llama-index-core (>=0.12.17,<0.13.0)
|
|
15
|
-
Requires-Dist: openai (>=1.
|
|
15
|
+
Requires-Dist: openai (>=1.66.3,<2.0.0)
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
18
|
# LlamaIndex Llms Integration: Openai
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
llama_index/llms/openai/__init__.py,sha256=8nmgixeXifQ4eVSgtCic54WxXqrrpXQPL4rhACWCSFs,229
|
|
2
|
+
llama_index/llms/openai/base.py,sha256=RjkISrh-RvbrQWOfdNdH4nimDQN0byUFm_n6r703jdM,38609
|
|
3
|
+
llama_index/llms/openai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
llama_index/llms/openai/responses.py,sha256=TikqnpW-UQmgjmMYGznsBx7eEo5prNIaxE81RO1ZZjE,34465
|
|
5
|
+
llama_index/llms/openai/utils.py,sha256=qp9qpXY7HbUnUsVDx6TgK98feibzTRi-bLdq_F3S0fo,26017
|
|
6
|
+
llama_index_llms_openai-0.3.29.dist-info/LICENSE,sha256=JPQLUZD9rKvCTdu192Nk0V5PAwklIg6jANii3UmTyMs,1065
|
|
7
|
+
llama_index_llms_openai-0.3.29.dist-info/METADATA,sha256=g0FtAtfto485JxdoqCDwulLfoKYoPcglr1ETMZ_lFjg,3322
|
|
8
|
+
llama_index_llms_openai-0.3.29.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
9
|
+
llama_index_llms_openai-0.3.29.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
llama_index/llms/openai/__init__.py,sha256=vm3cIBSGkBFlE77GyfyN0EhpJcnJZN95QMhPN53EkbE,148
|
|
2
|
-
llama_index/llms/openai/base.py,sha256=RjkISrh-RvbrQWOfdNdH4nimDQN0byUFm_n6r703jdM,38609
|
|
3
|
-
llama_index/llms/openai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
llama_index/llms/openai/utils.py,sha256=n7GEv864j34idRUR2ouu0McSBqBTVX2Tko9vf1YOl-k,21624
|
|
5
|
-
llama_index_llms_openai-0.3.28.dist-info/LICENSE,sha256=JPQLUZD9rKvCTdu192Nk0V5PAwklIg6jANii3UmTyMs,1065
|
|
6
|
-
llama_index_llms_openai-0.3.28.dist-info/METADATA,sha256=Bmx7FvGOcpdMpwP5gx-Wx2Dlbkp_42N7QC-zVodKXuw,3322
|
|
7
|
-
llama_index_llms_openai-0.3.28.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
8
|
-
llama_index_llms_openai-0.3.28.dist-info/RECORD,,
|
{llama_index_llms_openai-0.3.28.dist-info → llama_index_llms_openai-0.3.29.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|