llama-index-llms-openai 0.3.2__tar.gz → 0.3.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: llama-index-llms-openai
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: llama-index llms openai integration
5
5
  License: MIT
6
6
  Author: llama-index
@@ -11,8 +11,8 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: llama-index-core (>=0.12.0,<0.13.0)
15
- Requires-Dist: openai (>=1.40.0,<2.0.0)
14
+ Requires-Dist: llama-index-core (>=0.12.4,<0.13.0)
15
+ Requires-Dist: openai (>=1.57.1,<2.0.0)
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # LlamaIndex Llms Integration: Openai
@@ -2,10 +2,10 @@ import functools
2
2
  from typing import (
3
3
  TYPE_CHECKING,
4
4
  Any,
5
- Generator,
6
5
  Awaitable,
7
6
  Callable,
8
7
  Dict,
8
+ Generator,
9
9
  List,
10
10
  Optional,
11
11
  Protocol,
@@ -18,6 +18,8 @@ from typing import (
18
18
 
19
19
  import httpx
20
20
  import tiktoken
21
+
22
+ import llama_index.core.instrumentation as instrument
21
23
  from llama_index.core.base.llms.generic_utils import (
22
24
  achat_to_completion_decorator,
23
25
  acompletion_to_chat_decorator,
@@ -39,7 +41,11 @@ from llama_index.core.base.llms.types import (
39
41
  LLMMetadata,
40
42
  MessageRole,
41
43
  )
42
- from llama_index.core.bridge.pydantic import Field, PrivateAttr
44
+ from llama_index.core.bridge.pydantic import (
45
+ BaseModel,
46
+ Field,
47
+ PrivateAttr,
48
+ )
43
49
  from llama_index.core.callbacks import CallbackManager
44
50
  from llama_index.core.constants import (
45
51
  DEFAULT_TEMPERATURE,
@@ -50,7 +56,8 @@ from llama_index.core.llms.callbacks import (
50
56
  )
51
57
  from llama_index.core.llms.function_calling import FunctionCallingLLM
52
58
  from llama_index.core.llms.llm import ToolSelection
53
- from llama_index.core.types import BaseOutputParser, PydanticProgramMode, Model
59
+ from llama_index.core.llms.utils import parse_partial_json
60
+ from llama_index.core.types import BaseOutputParser, Model, PydanticProgramMode
54
61
  from llama_index.llms.openai.utils import (
55
62
  O1_MODELS,
56
63
  OpenAIToolCall,
@@ -62,14 +69,10 @@ from llama_index.llms.openai.utils import (
62
69
  is_function_calling_model,
63
70
  openai_modelname_to_contextsize,
64
71
  resolve_openai_credentials,
65
- to_openai_message_dicts,
66
72
  resolve_tool_choice,
73
+ to_openai_message_dicts,
67
74
  update_tool_calls,
68
75
  )
69
- from llama_index.core.bridge.pydantic import (
70
- BaseModel,
71
- )
72
-
73
76
  from openai import AsyncOpenAI, AzureOpenAI
74
77
  from openai import OpenAI as SyncOpenAI
75
78
  from openai.types.chat.chat_completion_chunk import (
@@ -77,9 +80,6 @@ from openai.types.chat.chat_completion_chunk import (
77
80
  ChoiceDelta,
78
81
  ChoiceDeltaToolCall,
79
82
  )
80
- from llama_index.core.llms.utils import parse_partial_json
81
-
82
- import llama_index.core.instrumentation as instrument
83
83
 
84
84
  dispatcher = instrument.get_dispatcher(__name__)
85
85
 
@@ -89,7 +89,7 @@ if TYPE_CHECKING:
89
89
  DEFAULT_OPENAI_MODEL = "gpt-3.5-turbo"
90
90
 
91
91
 
92
- def llm_retry_decorator(f: Callable[[Any], Any]) -> Callable[[Any], Any]:
92
+ def llm_retry_decorator(f: Callable[..., Any]) -> Callable[..., Any]:
93
93
  @functools.wraps(f)
94
94
  def wrapper(self, *args: Any, **kwargs: Any) -> Any:
95
95
  max_retries = getattr(self, "max_retries", 0)
@@ -112,7 +112,7 @@ def llm_retry_decorator(f: Callable[[Any], Any]) -> Callable[[Any], Any]:
112
112
  class Tokenizer(Protocol):
113
113
  """Tokenizers support an encode function that returns a list of ints."""
114
114
 
115
- def encode(self, text: str) -> List[int]:
115
+ def encode(self, text: str) -> List[int]: # fmt: skip
116
116
  ...
117
117
 
118
118
 
@@ -828,7 +828,7 @@ class OpenAI(FunctionCallingLLM):
828
828
 
829
829
  def _prepare_chat_with_tools(
830
830
  self,
831
- tools: List["BaseTool"],
831
+ tools: Sequence["BaseTool"],
832
832
  user_msg: Optional[Union[str, ChatMessage]] = None,
833
833
  chat_history: Optional[List[ChatMessage]] = None,
834
834
  verbose: bool = False,
@@ -850,9 +850,8 @@ class OpenAI(FunctionCallingLLM):
850
850
  for tool_spec in tool_specs:
851
851
  if tool_spec["type"] == "function":
852
852
  tool_spec["function"]["strict"] = strict
853
- tool_spec["function"]["parameters"][
854
- "additionalProperties"
855
- ] = False # in current openai 1.40.0 it is always false.
853
+ # in current openai 1.40.0 it is always false.
854
+ tool_spec["function"]["parameters"]["additionalProperties"] = False
856
855
 
857
856
  if isinstance(user_msg, str):
858
857
  user_msg = ChatMessage(role=MessageRole.USER, content=user_msg)
@@ -871,7 +870,7 @@ class OpenAI(FunctionCallingLLM):
871
870
  def _validate_chat_with_tools_response(
872
871
  self,
873
872
  response: ChatResponse,
874
- tools: List["BaseTool"],
873
+ tools: Sequence["BaseTool"],
875
874
  allow_parallel_tool_calls: bool = False,
876
875
  **kwargs: Any,
877
876
  ) -> ChatResponse:
@@ -966,7 +965,7 @@ class OpenAI(FunctionCallingLLM):
966
965
  return super().stream_structured_predict(*args, llm_kwargs=llm_kwargs, **kwargs)
967
966
 
968
967
  @dispatcher.span
969
- def stream_structured_predict(
968
+ async def astream_structured_predict(
970
969
  self, *args: Any, llm_kwargs: Optional[Dict[str, Any]] = None, **kwargs: Any
971
970
  ) -> Generator[Union[Model, List[Model]], None, None]:
972
971
  """Stream structured predict."""
@@ -978,4 +977,6 @@ class OpenAI(FunctionCallingLLM):
978
977
  )
979
978
  # by default structured prediction uses function calling to extract structured outputs
980
979
  # here we force tool_choice to be required
981
- return super().stream_structured_predict(*args, llm_kwargs=llm_kwargs, **kwargs)
980
+ return await super().astream_structured_predict(
981
+ *args, llm_kwargs=llm_kwargs, **kwargs
982
+ )
@@ -3,9 +3,6 @@ import os
3
3
  from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
4
4
 
5
5
  from deprecated import deprecated
6
- from llama_index.core.base.llms.types import ChatMessage, LogProb, CompletionResponse
7
- from llama_index.core.bridge.pydantic import BaseModel
8
- from llama_index.core.base.llms.generic_utils import get_from_param_or_env
9
6
  from tenacity import (
10
7
  before_sleep_log,
11
8
  retry,
@@ -18,12 +15,19 @@ from tenacity import (
18
15
  from tenacity.stop import stop_base
19
16
 
20
17
  import openai
18
+ from llama_index.core.base.llms.generic_utils import get_from_param_or_env
19
+ from llama_index.core.base.llms.types import (
20
+ ChatMessage,
21
+ ImageBlock,
22
+ LogProb,
23
+ TextBlock,
24
+ )
25
+ from llama_index.core.bridge.pydantic import BaseModel
21
26
  from openai.types.chat import ChatCompletionMessageParam, ChatCompletionMessageToolCall
22
27
  from openai.types.chat.chat_completion_chunk import ChoiceDeltaToolCall
23
28
  from openai.types.chat.chat_completion_message import ChatCompletionMessage
24
29
  from openai.types.chat.chat_completion_token_logprob import ChatCompletionTokenLogprob
25
30
  from openai.types.completion_choice import Logprobs
26
- from openai.types.completion import Completion
27
31
 
28
32
  DEFAULT_OPENAI_API_TYPE = "open_ai"
29
33
  DEFAULT_OPENAI_API_BASE = "https://api.openai.com/v1"
@@ -190,7 +194,8 @@ def create_retry_decorator(
190
194
 
191
195
 
192
196
  def openai_modelname_to_contextsize(modelname: str) -> int:
193
- """Calculate the maximum number of tokens possible to generate for a model.
197
+ """
198
+ Calculate the maximum number of tokens possible to generate for a model.
194
199
 
195
200
  Args:
196
201
  modelname: The modelname we want to know the context size for.
@@ -250,10 +255,43 @@ def is_function_calling_model(model: str) -> bool:
250
255
  def to_openai_message_dict(
251
256
  message: ChatMessage, drop_none: bool = False, model: Optional[str] = None
252
257
  ) -> ChatCompletionMessageParam:
253
- """Convert generic message to OpenAI message dict."""
258
+ """Convert a ChatMessage to an OpenAI message dict."""
259
+ content = []
260
+ content_txt = ""
261
+ for block in message.blocks:
262
+ if isinstance(block, TextBlock):
263
+ if message.role.value in ("assistant", "tool", "system"):
264
+ # Despite the docs say otherwise, when role is ASSISTANT, SYSTEM
265
+ # or TOOL, 'content' cannot be a list and must be string instead.
266
+ content_txt += block.text
267
+ else:
268
+ content.append({"type": "text", "text": block.text})
269
+ elif isinstance(block, ImageBlock):
270
+ if block.url:
271
+ content.append(
272
+ {"type": "image_url", "image_url": {"url": str(block.url)}}
273
+ )
274
+ else:
275
+ img_bytes = block.resolve_image(as_base64=True).read()
276
+ img_str = img_bytes.decode("utf-8")
277
+ content.append(
278
+ {
279
+ "type": "image_url",
280
+ "image_url": {
281
+ "url": f"data:{block.image_mimetype};base64,{img_str}",
282
+ "detail": block.detail or "low",
283
+ },
284
+ }
285
+ )
286
+ else:
287
+ msg = f"Unsupported content block type: {type(block).__name__}"
288
+ raise ValueError(msg)
289
+
254
290
  message_dict = {
255
291
  "role": message.role.value,
256
- "content": message.content,
292
+ "content": content_txt
293
+ if message.role.value in ("assistant", "tool", "system")
294
+ else content,
257
295
  }
258
296
 
259
297
  # TODO: O1 models do not support system prompts
@@ -314,7 +352,7 @@ def from_openai_token_logprob(
314
352
  LogProb(token=el.token, logprob=el.logprob, bytes=el.bytes or [])
315
353
  for el in openai_token_logprob.top_logprobs
316
354
  ]
317
- except Exception as e:
355
+ except Exception:
318
356
  print(openai_token_logprob)
319
357
  raise
320
358
  return result
@@ -332,7 +370,7 @@ def from_openai_token_logprobs(
332
370
 
333
371
 
334
372
  def from_openai_completion_logprob(
335
- openai_completion_logprob: Dict[str, float]
373
+ openai_completion_logprob: Dict[str, float],
336
374
  ) -> List[LogProb]:
337
375
  """Convert openai completion logprobs to generic list of LogProb."""
338
376
  return [
@@ -354,11 +392,6 @@ def from_openai_completion_logprobs(
354
392
  return result
355
393
 
356
394
 
357
- def from_openai_completion(openai_completion: Completion) -> CompletionResponse:
358
- """Convert openai completion to CompletionResponse."""
359
- text = openai_completion.choices[0].text
360
-
361
-
362
395
  def from_openai_messages(
363
396
  openai_messages: Sequence[ChatCompletionMessage],
364
397
  ) -> List[ChatMessage]:
@@ -370,13 +403,32 @@ def from_openai_message_dict(message_dict: dict) -> ChatMessage:
370
403
  """Convert openai message dict to generic message."""
371
404
  role = message_dict["role"]
372
405
  # NOTE: Azure OpenAI returns function calling messages without a content key
373
- content = message_dict.get("content", None)
406
+ content = message_dict.get("content")
407
+ blocks = []
408
+ if isinstance(content, list):
409
+ for elem in content:
410
+ t = elem.get("type")
411
+ if t == "text":
412
+ blocks.append(TextBlock(text=elem.get("text")))
413
+ elif t == "image_url":
414
+ img = elem["image_url"]["url"]
415
+ detail = elem["image_url"]["detail"]
416
+ if img.startswith("data:"):
417
+ blocks.append(ImageBlock(image=img, detail=detail))
418
+ else:
419
+ blocks.append(ImageBlock(url=img, detail=detail))
420
+ else:
421
+ msg = f"Unsupported message type: {t}"
422
+ raise ValueError(msg)
423
+ content = None
374
424
 
375
425
  additional_kwargs = message_dict.copy()
376
426
  additional_kwargs.pop("role")
377
427
  additional_kwargs.pop("content", None)
378
428
 
379
- return ChatMessage(role=role, content=content, additional_kwargs=additional_kwargs)
429
+ return ChatMessage(
430
+ role=role, content=content, additional_kwargs=additional_kwargs, blocks=blocks
431
+ )
380
432
 
381
433
 
382
434
  def from_openai_message_dicts(message_dicts: Sequence[dict]) -> List[ChatMessage]:
@@ -386,7 +438,8 @@ def from_openai_message_dicts(message_dicts: Sequence[dict]) -> List[ChatMessage
386
438
 
387
439
  @deprecated("Deprecated in favor of `to_openai_tool`, which should be used instead.")
388
440
  def to_openai_function(pydantic_class: Type[BaseModel]) -> Dict[str, Any]:
389
- """Deprecated in favor of `to_openai_tool`.
441
+ """
442
+ Deprecated in favor of `to_openai_tool`.
390
443
 
391
444
  Convert pydantic class to OpenAI function.
392
445
  """
@@ -412,7 +465,8 @@ def resolve_openai_credentials(
412
465
  api_base: Optional[str] = None,
413
466
  api_version: Optional[str] = None,
414
467
  ) -> Tuple[Optional[str], str, str]:
415
- """ "Resolve OpenAI credentials.
468
+ """
469
+ "Resolve OpenAI credentials.
416
470
 
417
471
  The order of precedence is:
418
472
  1. param
@@ -443,7 +497,8 @@ def validate_openai_api_key(api_key: Optional[str] = None) -> None:
443
497
 
444
498
 
445
499
  def resolve_tool_choice(tool_choice: Union[str, dict] = "auto") -> Union[str, dict]:
446
- """Resolve tool choice.
500
+ """
501
+ Resolve tool choice.
447
502
 
448
503
  If tool_choice is a function name string, return the appropriate dict.
449
504
  """
@@ -29,12 +29,12 @@ exclude = ["**/BUILD"]
29
29
  license = "MIT"
30
30
  name = "llama-index-llms-openai"
31
31
  readme = "README.md"
32
- version = "0.3.2"
32
+ version = "0.3.4"
33
33
 
34
34
  [tool.poetry.dependencies]
35
35
  python = ">=3.9,<4.0"
36
- openai = "^1.40.0"
37
- llama-index-core = "^0.12.0"
36
+ openai = "^1.57.1"
37
+ llama-index-core = "^0.12.4"
38
38
 
39
39
  [tool.poetry.group.dev.dependencies]
40
40
  ipython = "8.10.0"
@@ -63,3 +63,6 @@ version = ">=v2.2.6"
63
63
 
64
64
  [[tool.poetry.packages]]
65
65
  include = "llama_index/"
66
+
67
+ [tool.ruff.lint.flake8-pytest-style]
68
+ fixture-parentheses = true