arize-phoenix 0.1.1__py3-none-any.whl → 1.0.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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: arize-phoenix
3
- Version: 0.1.1
3
+ Version: 1.0.0
4
4
  Summary: ML Observability in your notebook
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -38,15 +38,15 @@ Requires-Dist: arize[autoembeddings,llm-evaluation]; extra == 'dev'
38
38
  Requires-Dist: gcsfs; extra == 'dev'
39
39
  Requires-Dist: hatch; extra == 'dev'
40
40
  Requires-Dist: jupyter; extra == 'dev'
41
- Requires-Dist: langchain>=0.0.324; extra == 'dev'
42
- Requires-Dist: llama-index>=0.8.29; extra == 'dev'
41
+ Requires-Dist: langchain>=0.0.334; extra == 'dev'
42
+ Requires-Dist: llama-index>=0.8.64; extra == 'dev'
43
43
  Requires-Dist: nbqa; extra == 'dev'
44
44
  Requires-Dist: pandas-stubs<=2.0.2.230605; extra == 'dev'
45
45
  Requires-Dist: pre-commit; extra == 'dev'
46
46
  Requires-Dist: pytest; extra == 'dev'
47
47
  Requires-Dist: pytest-cov; extra == 'dev'
48
48
  Requires-Dist: pytest-lazy-fixture; extra == 'dev'
49
- Requires-Dist: ruff==0.1.3; extra == 'dev'
49
+ Requires-Dist: ruff==0.1.5; extra == 'dev'
50
50
  Requires-Dist: strawberry-graphql[debug-server]==0.208.2; extra == 'dev'
51
51
  Provides-Extra: experimental
52
52
  Requires-Dist: tenacity; extra == 'experimental'
@@ -101,6 +101,7 @@ Phoenix provides MLOps and LLMOps insights at lightning speed with zero-config o
101
101
  - [Exportable Clusters](#exportable-clusters)
102
102
  - [Retrieval-Augmented Generation Analysis](#retrieval-augmented-generation-analysis)
103
103
  - [Structured Data Analysis](#structured-data-analysis)
104
+ - [Breaking Changes](#breaking-changes)
104
105
  - [Community](#community)
105
106
  - [Thanks](#thanks)
106
107
  - [Copyright, Patent, and License](#copyright-patent-and-license)
@@ -418,6 +419,10 @@ train_ds = px.Dataset(dataframe=train_df, schema=schema, name="training")
418
419
  session = px.launch_app(primary=prod_ds, reference=train_ds)
419
420
  ```
420
421
 
422
+ ## Breaking Changes
423
+
424
+ - **v1.0.0** - Phoenix now exclusively supports the `openai>=1.0.0` sdk. If you are using an older version of the OpenAI SDK, you can continue to use `arize-phoenix==0.1.1`. However, we recommend upgrading to the latest version of the OpenAI SDK as it contains many improvements. If you are using Phoenix with LlamaIndex and and LangChain, you will have to upgrade to the versions of these packages that support the OpenAI `1.0.0` SDK as well (`llama-index>=0.8.64`, `langchain>=0.0.334`)
425
+
421
426
  ## Community
422
427
 
423
428
  Join our community to connect with thousands of machine learning practitioners and ML observability enthusiasts.
@@ -1,4 +1,4 @@
1
- phoenix/__init__.py,sha256=IoLOUxDREfuPTH9sfQ6_CvtYjEH1WggcNiMK9O8Ztu8,1254
1
+ phoenix/__init__.py,sha256=k_bhtimjBnEIM2Kj1-rQ8CLF2EP_mvJvctBBI_000AU,1254
2
2
  phoenix/config.py,sha256=TdMKmU7V490I38x_hvB1s14Y8pV3ldLSpJTKq6crzBY,1952
3
3
  phoenix/datetime_utils.py,sha256=D955QLrkgrrSdUM6NyqbCeAu2SMsjhR5rHVQEsVUdng,2773
4
4
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -18,7 +18,7 @@ phoenix/datasets/validation.py,sha256=dZ9lCFUV0EY7HCkQkQBrs-GLAEIZdpOqUxwD5l4dp8
18
18
  phoenix/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  phoenix/experimental/evals/__init__.py,sha256=Ikm-KMcRTpR8Lq-HAIcqKlBgaQ3Gra5YUkRRUuyAL0A,1122
20
20
  phoenix/experimental/evals/evaluators.py,sha256=hIOYfSyLUedVnC8i8aEgVdsHi65FzsiW1Z-bGV81DYA,5797
21
- phoenix/experimental/evals/retrievals.py,sha256=Y3YupYrrzt_orTMEFFW3eDBrHcMnBsqTqEQu7BWAUlk,3828
21
+ phoenix/experimental/evals/retrievals.py,sha256=o3fqrsYbYZjyGj_jWkN_9VQVyXjLkDKDw5Ws7l8bwdI,3828
22
22
  phoenix/experimental/evals/utils.py,sha256=ivrYuX5Xotjh12BWOpYk9O7TgOt8uGDfdnRpYfrybmQ,1102
23
23
  phoenix/experimental/evals/functions/__init__.py,sha256=3FMGrjmgxegXAwgDV_RpaN-73cFVyBiO8YwZvml5P9c,156
24
24
  phoenix/experimental/evals/functions/classify.py,sha256=Yv8Y2keoSr72SfwBfdb6U5gsuXtDnSe1BXdWLPk5T08,13356
@@ -27,7 +27,7 @@ phoenix/experimental/evals/functions/processing.py,sha256=F4xtLsulLV4a8CkuLldRdd
27
27
  phoenix/experimental/evals/models/__init__.py,sha256=HnVMm4o0EVo4ez6AOSyea7sJRI_Ht1xTeFpL2yus2bo,243
28
28
  phoenix/experimental/evals/models/base.py,sha256=GGHkHpTiOTw9lmSIrsW-YlUCMlb939Ls5CdRJBNT-24,7003
29
29
  phoenix/experimental/evals/models/bedrock.py,sha256=xppB9YaehlapGeyQqWAUEMJUWd7Z18g9MxzL7OEAP0M,7322
30
- phoenix/experimental/evals/models/openai.py,sha256=JCFJtLxhr5Paqa46CmuV3sRhjk1HJE4qCtGXo3UB1UQ,12268
30
+ phoenix/experimental/evals/models/openai.py,sha256=pfFb0eS4r0ImrS3Vp_wI74LH9w_17DW62bWJ7FWMwZg,13824
31
31
  phoenix/experimental/evals/models/vertexai.py,sha256=xcUVSkDEnIxOHDX0eCYoNJ_JRsjX-KskiEKd0pUhUxI,5442
32
32
  phoenix/experimental/evals/templates/__init__.py,sha256=Tf1gzN-dkgv-szgU08SIj7oZrX-r7VjQ3dcXqoN0Gec,831
33
33
  phoenix/experimental/evals/templates/default_templates.py,sha256=0X_NoQZC-dqPeDfhoqo_7-stCfnxFmdOizCSGsNlAlA,6160
@@ -138,15 +138,15 @@ phoenix/trace/llama_index/__init__.py,sha256=wCcQgD9CG5TA8i-1XsSed4ZzwHTUmqZwegQ
138
138
  phoenix/trace/llama_index/callback.py,sha256=7-4uYReimSU7DrGgRohiF0wmbc9yGu_PrvBUDmgQz8k,22961
139
139
  phoenix/trace/llama_index/debug_callback.py,sha256=SKToD9q_QADSGTJ5lhilqRVKaUnUSRXUvURCzN4by2U,1367
140
140
  phoenix/trace/openai/__init__.py,sha256=J3G0uqCxGdksUpaQVHds_Egv2drvh8UEqoLjiQAOveg,79
141
- phoenix/trace/openai/instrumentor.py,sha256=kIQYfqwQtHuQALq4XtUFxSTjK_Y8q17pcBUryxsdqvs,9466
141
+ phoenix/trace/openai/instrumentor.py,sha256=tVlqu0pqDqSxi4xQZx6yksSYiTrWg5v86ZghqSaBpl0,9697
142
142
  phoenix/trace/v1/__init__.py,sha256=n0KAffs0WiLywZmwhZ7hAQllPoi__QAGYo8hqokIhzw,17106
143
143
  phoenix/trace/v1/trace_pb2.py,sha256=DPDwoF4HOa3T2R-Qi7c2uXkbypOXcrOkCysMB-yrKXw,5580
144
144
  phoenix/trace/v1/trace_pb2.pyi,sha256=2JpgiYz3s8HrxnVIi5Brk7c3RJB4LqDGzwRYonhliRA,16258
145
145
  phoenix/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  phoenix/utilities/error_handling.py,sha256=7b5rpGFj9EWZ8yrZK1IHvxB89suWk3lggDayUQcvZds,1946
147
147
  phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,222
148
- arize_phoenix-0.1.1.dist-info/METADATA,sha256=9hZSAfnZHCOMHXpqulmpwHjj4tS3wVRfvDlxj-0UhqI,25532
149
- arize_phoenix-0.1.1.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
150
- arize_phoenix-0.1.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
151
- arize_phoenix-0.1.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
152
- arize_phoenix-0.1.1.dist-info/RECORD,,
148
+ arize_phoenix-1.0.0.dist-info/METADATA,sha256=irGlF8ajtVaSTMg4q6JcXE19U7cg4N5YEFuFZbtBSKM,26087
149
+ arize_phoenix-1.0.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
150
+ arize_phoenix-1.0.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
151
+ arize_phoenix-1.0.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
152
+ arize_phoenix-1.0.0.dist-info/RECORD,,
phoenix/__init__.py CHANGED
@@ -5,7 +5,7 @@ from .session.session import Session, active_session, close_app, launch_app
5
5
  from .trace.fixtures import load_example_traces
6
6
  from .trace.trace_dataset import TraceDataset
7
7
 
8
- __version__ = "0.1.1"
8
+ __version__ = "1.0.0"
9
9
 
10
10
  # module level doc-string
11
11
  __doc__ = """
@@ -1,7 +1,18 @@
1
1
  import logging
2
2
  import os
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
3
+ from dataclasses import dataclass, field, fields
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Callable,
8
+ Dict,
9
+ List,
10
+ Optional,
11
+ Tuple,
12
+ Union,
13
+ get_args,
14
+ get_origin,
15
+ )
5
16
 
6
17
  from phoenix.experimental.evals.models.base import BaseEvalModel
7
18
 
@@ -9,7 +20,7 @@ if TYPE_CHECKING:
9
20
  from tiktoken import Encoding
10
21
 
11
22
  OPENAI_API_KEY_ENVVAR_NAME = "OPENAI_API_KEY"
12
- MINIMUM_OPENAI_VERSION = "0.26.4"
23
+ MINIMUM_OPENAI_VERSION = "1.0.0"
13
24
  MODEL_TOKEN_LIMIT_MAPPING = {
14
25
  "gpt-3.5-turbo-instruct": 4096,
15
26
  "gpt-3.5-turbo-0301": 4096,
@@ -24,17 +35,31 @@ LEGACY_COMPLETION_API_MODELS = ("gpt-3.5-turbo-instruct",)
24
35
  logger = logging.getLogger(__name__)
25
36
 
26
37
 
38
+ @dataclass
39
+ class AzureOptions:
40
+ api_version: str
41
+ azure_endpoint: str
42
+ azure_deployment: Optional[str]
43
+ azure_ad_token: Optional[str]
44
+ azure_ad_token_provider: Optional[Callable[[], str]]
45
+
46
+
27
47
  @dataclass
28
48
  class OpenAIModel(BaseEvalModel):
29
- openai_api_type: Optional[str] = field(default=None)
30
- openai_api_version: Optional[str] = field(default=None)
31
- openai_api_key: Optional[str] = field(repr=False, default=None)
32
- openai_api_base: Optional[str] = field(repr=False, default=None)
33
- openai_organization: Optional[str] = field(repr=False, default=None)
34
- engine: str = ""
35
- """Azure engine (the Deployment Name of your model)"""
49
+ api_key: Optional[str] = field(repr=False, default=None)
50
+ """Your OpenAI key. If not provided, will be read from the environment variable"""
51
+ organization: Optional[str] = field(repr=False, default=None)
52
+ """
53
+ The organization to use for the OpenAI API. If not provided, will default
54
+ to what's configured in OpenAI
55
+ """
56
+ base_url: Optional[str] = field(repr=False, default=None)
57
+ """
58
+ An optional base URL to use for the OpenAI API. If not provided, will default
59
+ to what's configured in OpenAI
60
+ """
36
61
  model_name: str = "gpt-4"
37
- """Model name to use."""
62
+ """Model name to use. In of azure, this is the deployment name such as gpt-35-instant"""
38
63
  temperature: float = 0.0
39
64
  """What sampling temperature to use."""
40
65
  max_tokens: int = 256
@@ -63,6 +88,18 @@ class OpenAIModel(BaseEvalModel):
63
88
  retry_max_seconds: int = 60
64
89
  """Maximum number of seconds to wait when retrying."""
65
90
 
91
+ # Azure options
92
+ api_version: Optional[str] = field(default=None)
93
+ """https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning"""
94
+ azure_endpoint: Optional[str] = field(default=None)
95
+ """
96
+ The endpoint to use for azure openai. Available in the azure portal.
97
+ https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
98
+ """
99
+ azure_deployment: Optional[str] = field(default=None)
100
+ azure_ad_token: Optional[str] = field(default=None)
101
+ azure_ad_token_provider: Optional[Callable[[], str]] = field(default=None)
102
+
66
103
  def __post_init__(self) -> None:
67
104
  self._init_environment()
68
105
  self._init_open_ai()
@@ -71,12 +108,10 @@ class OpenAIModel(BaseEvalModel):
71
108
  def _init_environment(self) -> None:
72
109
  try:
73
110
  import openai
74
- import openai.util
75
- from openai import error as openai_error
111
+ import openai._utils as openai_util
76
112
 
77
113
  self._openai = openai
78
- self._openai_error = openai_error
79
- self._openai_util = openai.util
114
+ self._openai_util = openai_util
80
115
  except ImportError:
81
116
  self._raise_import_error(
82
117
  package_display_name="OpenAI",
@@ -93,52 +128,76 @@ class OpenAIModel(BaseEvalModel):
93
128
  )
94
129
 
95
130
  def _init_open_ai(self) -> None:
131
+ # For Azure, you need to provide the endpoint and the endpoint
132
+ self._is_azure = bool(self.azure_endpoint)
133
+
96
134
  self._model_uses_legacy_completion_api = self.model_name.startswith(
97
135
  LEGACY_COMPLETION_API_MODELS
98
136
  )
99
- if self.openai_api_key is None:
137
+ if self.api_key is None:
100
138
  api_key = os.getenv(OPENAI_API_KEY_ENVVAR_NAME)
101
139
  if api_key is None:
102
140
  # TODO: Create custom AuthenticationError
103
141
  raise RuntimeError(
104
- "OpenAI's API key not provided. Pass it as an argument to 'openai_api_key' "
142
+ "OpenAI's API key not provided. Pass it as an argument to 'api_key' "
105
143
  "or set it in your environment: 'export OPENAI_API_KEY=sk-****'"
106
144
  )
107
- self.openai_api_key = api_key
108
- self.openai_api_base = self.openai_api_base or self._openai.api_base
109
- self.openai_api_type = self.openai_api_type or self._openai.api_type
110
- self.openai_api_version = self.openai_api_version or self._openai.api_version
111
- self.openai_organization = self.openai_organization or self._openai.organization
112
- # use enum to validate api type
113
- self._openai_util.ApiType.from_str(self.openai_api_type) # type: ignore
114
- self._is_azure = self.openai_api_type.lower().startswith("azure")
145
+ self.api_key = api_key
115
146
 
147
+ # Set the version, organization, and base_url - default to openAI
148
+ self.api_version = self.api_version or self._openai.api_version
149
+ self.organization = self.organization or self._openai.organization
150
+
151
+ # Initialize specific clients depending on the API backend
152
+ # Set the type first
153
+ self._client: Union[self._openai.OpenAI, self._openai.AzureOpenAI] # type: ignore
116
154
  if self._is_azure:
117
- if not self.engine:
118
- raise ValueError(
119
- "You must provide the deployment name in the 'engine' parameter "
120
- "to access the Azure OpenAI service"
121
- )
122
- self._openai_api_model_name = self.engine
123
- elif self.model_name in MODEL_TOKEN_LIMIT_MAPPING.keys():
124
- self._openai_api_model_name = self.model_name
125
- elif "gpt-3.5-turbo" in self.model_name:
126
- self._openai_api_model_name = "gpt-3.5-turbo-0613"
127
- elif "gpt-4" in self.model_name:
128
- self._openai_api_model_name = "gpt-4-0613"
129
- else:
130
- raise NotImplementedError(
131
- f"openai_api_model_name not available for model {self.model_name}. "
155
+ # Validate the azure options and construct a client
156
+ azure_options = self._get_azure_options()
157
+ self._client = self._openai.AzureOpenAI(
158
+ azure_endpoint=azure_options.azure_endpoint,
159
+ azure_deployment=azure_options.azure_deployment,
160
+ api_version=azure_options.api_version,
161
+ azure_ad_token=azure_options.azure_ad_token,
162
+ azure_ad_token_provider=azure_options.azure_ad_token_provider,
163
+ api_key=self.api_key,
164
+ organization=self.organization,
132
165
  )
166
+ # return early since we don't need to check the model
167
+ return
168
+
169
+ # The client is not azure, so it must be openai
170
+ self._client = self._openai.OpenAI(
171
+ api_key=self.api_key,
172
+ organization=self.organization,
173
+ base_url=(self.base_url or self._openai.base_url),
174
+ )
133
175
 
134
176
  def _init_tiktoken(self) -> None:
135
177
  try:
136
- encoding = self._tiktoken.encoding_for_model(self.openai_api_model_name)
178
+ encoding = self._tiktoken.encoding_for_model(self.model_name)
137
179
  except KeyError:
138
- logger.warning("Warning: model not found. Using cl100k_base encoding.")
139
180
  encoding = self._tiktoken.get_encoding("cl100k_base")
140
181
  self._tiktoken_encoding = encoding
141
182
 
183
+ def _get_azure_options(self) -> AzureOptions:
184
+ options = {}
185
+ for option in fields(AzureOptions):
186
+ if (value := getattr(self, option.name)) is not None:
187
+ options[option.name] = value
188
+ else:
189
+ # raise ValueError if field is not optional
190
+ # See if the field is optional - e.g. get_origin(Optional[T]) = typing.Union
191
+ option_is_optional = get_origin(option.type) is Union and type(None) in get_args(
192
+ option.type
193
+ )
194
+ if not option_is_optional:
195
+ raise ValueError(
196
+ f"Option '{option.name}' must be set when using Azure OpenAI API"
197
+ )
198
+ options[option.name] = None
199
+ return AzureOptions(**options)
200
+
142
201
  @staticmethod
143
202
  def _build_messages(
144
203
  prompt: str, system_instruction: Optional[str] = None
@@ -173,11 +232,11 @@ class OpenAIModel(BaseEvalModel):
173
232
  def _generate_with_retry(self, **kwargs: Any) -> Any:
174
233
  """Use tenacity to retry the completion call."""
175
234
  openai_retry_errors = [
176
- self._openai_error.Timeout,
177
- self._openai_error.APIError,
178
- self._openai_error.APIConnectionError,
179
- self._openai_error.RateLimitError,
180
- self._openai_error.ServiceUnavailableError,
235
+ self._openai.APITimeoutError,
236
+ self._openai.APIError,
237
+ self._openai.APIConnectionError,
238
+ self._openai.RateLimitError,
239
+ self._openai.InternalServerError,
181
240
  ]
182
241
 
183
242
  @self.retry(
@@ -193,17 +252,22 @@ class OpenAIModel(BaseEvalModel):
193
252
  (message.get("content") or "")
194
253
  for message in (kwargs.pop("messages", None) or ())
195
254
  )
196
- return self._openai.Completion.create(**kwargs)
197
- return self._openai.ChatCompletion.create(**kwargs)
255
+ # OpenAI 1.0.0 API responses are pydantic objects, not dicts
256
+ # We must dump the model to get the dict
257
+ return self._client.completions.create(**kwargs).model_dump()
258
+ return self._client.chat.completions.create(**kwargs).model_dump()
198
259
 
199
260
  return _completion_with_retry(**kwargs)
200
261
 
201
262
  @property
202
263
  def max_context_size(self) -> int:
203
- model_name = self.openai_api_model_name
264
+ model_name = self.model_name
204
265
  # handling finetuned models
205
266
  if "ft-" in model_name:
206
267
  model_name = self.model_name.split(":")[0]
268
+ if model_name == "gpt-4":
269
+ # Map gpt-4 to the current default
270
+ model_name = "gpt-4-0613"
207
271
 
208
272
  context_size = MODEL_TOKEN_LIMIT_MAPPING.get(model_name, None)
209
273
 
@@ -219,7 +283,7 @@ class OpenAIModel(BaseEvalModel):
219
283
  @property
220
284
  def public_invocation_params(self) -> Dict[str, Any]:
221
285
  return {
222
- **({"engine": self.engine} if self._is_azure else {"model": self.model_name}),
286
+ **({"model": self.model_name}),
223
287
  **self._default_params,
224
288
  **self.model_kwargs,
225
289
  }
@@ -228,18 +292,6 @@ class OpenAIModel(BaseEvalModel):
228
292
  def invocation_params(self) -> Dict[str, Any]:
229
293
  return {
230
294
  **self.public_invocation_params,
231
- **self._credentials,
232
- }
233
-
234
- @property
235
- def _credentials(self) -> Dict[str, Any]:
236
- """Get the default parameters for calling OpenAI API."""
237
- return {
238
- "api_key": self.openai_api_key,
239
- "api_base": self.openai_api_base,
240
- "api_type": self.openai_api_type,
241
- "api_version": self.openai_api_version,
242
- "organization": self.openai_organization,
243
295
  }
244
296
 
245
297
  @property
@@ -252,13 +304,9 @@ class OpenAIModel(BaseEvalModel):
252
304
  "presence_penalty": self.presence_penalty,
253
305
  "top_p": self.top_p,
254
306
  "n": self.n,
255
- "request_timeout": self.request_timeout,
307
+ "timeout": self.request_timeout,
256
308
  }
257
309
 
258
- @property
259
- def openai_api_model_name(self) -> str:
260
- return self._openai_api_model_name
261
-
262
310
  @property
263
311
  def encoder(self) -> "Encoding":
264
312
  return self._tiktoken_encoding
@@ -268,7 +316,7 @@ class OpenAIModel(BaseEvalModel):
268
316
 
269
317
  Official documentation: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb
270
318
  """ # noqa
271
- model_name = self.openai_api_model_name
319
+ model_name = self.model_name
272
320
  if model_name == "gpt-3.5-turbo-0301":
273
321
  tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
274
322
  tokens_per_name = -1 # if there's a name, the role is omitted
@@ -297,10 +345,10 @@ class OpenAIModel(BaseEvalModel):
297
345
  def supports_function_calling(self) -> bool:
298
346
  if (
299
347
  self._is_azure
300
- and self.openai_api_version
348
+ and self.api_version
301
349
  # The first api version supporting function calling is 2023-07-01-preview.
302
350
  # See https://github.com/Azure/azure-rest-api-specs/blob/58e92dd03733bc175e6a9540f4bc53703b57fcc9/specification/cognitiveservices/data-plane/AzureOpenAI/inference/preview/2023-07-01-preview/inference.json#L895 # noqa E501
303
- and self.openai_api_version[:10] < "2023-07-01"
351
+ and self.api_version[:10] < "2023-07-01"
304
352
  ):
305
353
  return False
306
354
  if self._model_uses_legacy_completion_api:
@@ -75,19 +75,22 @@ def classify_relevance(query: str, document: str, model_name: str) -> Optional[b
75
75
  unparseable output.
76
76
  """
77
77
 
78
- from openai import ChatCompletion
78
+ from openai import OpenAI
79
+
80
+ client = OpenAI()
79
81
 
80
82
  prompt = _QUERY_CONTEXT_PROMPT_TEMPLATE.format(
81
83
  query=query,
82
84
  reference=document,
83
85
  )
84
- response = ChatCompletion.create( # type: ignore
86
+ response = client.chat.completions.create(
85
87
  messages=[
86
88
  {"role": "system", "content": _EVALUATION_SYSTEM_MESSAGE},
87
89
  {"role": "user", "content": prompt},
88
90
  ],
89
91
  model=model_name,
90
92
  )
91
- raw_response_text = str(response["choices"][0]["message"]["content"]).strip()
93
+
94
+ raw_response_text = str(response.choices[0].message.content).strip()
92
95
  relevance_classification = {"relevant": True, "irrelevant": False}.get(raw_response_text)
93
96
  return relevance_classification
@@ -10,9 +10,10 @@ from typing import (
10
10
  List,
11
11
  Mapping,
12
12
  Optional,
13
- cast,
14
13
  )
15
14
 
15
+ from typing_extensions import TypeGuard
16
+
16
17
  from phoenix.trace.schemas import (
17
18
  SpanAttributes,
18
19
  SpanEvent,
@@ -44,7 +45,7 @@ from phoenix.trace.utils import get_stacktrace, import_package
44
45
  from ..tracer import Tracer
45
46
 
46
47
  if TYPE_CHECKING:
47
- from openai.openai_response import OpenAIResponse
48
+ from openai.types.chat import ChatCompletion
48
49
 
49
50
 
50
51
  Parameters = Mapping[str, Any]
@@ -75,21 +76,21 @@ class OpenAIInstrumentor:
75
76
  """
76
77
  openai = import_package("openai")
77
78
  is_instrumented = hasattr(
78
- openai.api_requestor.APIRequestor.request,
79
+ openai.OpenAI,
79
80
  INSTRUMENTED_ATTRIBUTE_NAME,
80
81
  )
81
82
  if not is_instrumented:
82
- openai.api_requestor.APIRequestor.request = _wrap_openai_api_requestor(
83
- openai.api_requestor.APIRequestor.request, self._tracer
83
+ openai.OpenAI.request = _wrapped_openai_client_request_function(
84
+ openai.OpenAI.request, self._tracer
84
85
  )
85
86
  setattr(
86
- openai.api_requestor.APIRequestor.request,
87
+ openai.OpenAI,
87
88
  INSTRUMENTED_ATTRIBUTE_NAME,
88
89
  True,
89
90
  )
90
91
 
91
92
 
92
- def _wrap_openai_api_requestor(
93
+ def _wrapped_openai_client_request_function(
93
94
  request_fn: Callable[..., Any], tracer: Tracer
94
95
  ) -> Callable[..., Any]:
95
96
  """Wraps the OpenAI APIRequestor.request method to create spans for each API call.
@@ -105,9 +106,10 @@ def _wrap_openai_api_requestor(
105
106
  def wrapped(*args: Any, **kwargs: Any) -> Any:
106
107
  call_signature = signature(request_fn)
107
108
  bound_arguments = call_signature.bind(*args, **kwargs)
108
- parameters = bound_arguments.arguments["params"]
109
- is_streaming = parameters.get("stream", False)
110
- url = bound_arguments.arguments["url"]
109
+ is_streaming = bound_arguments.arguments["stream"]
110
+ options = bound_arguments.arguments["options"]
111
+ parameters = options.json_data
112
+ url = options.url
111
113
  current_status_code = SpanStatusCode.UNSET
112
114
  events: List[SpanEvent] = []
113
115
  attributes: SpanAttributes = dict()
@@ -118,13 +120,13 @@ def _wrap_openai_api_requestor(
118
120
  ) in _PARAMETER_ATTRIBUTE_FUNCTIONS.items():
119
121
  if (attribute_value := get_parameter_attribute_fn(parameters)) is not None:
120
122
  attributes[attribute_name] = attribute_value
121
- outputs = None
123
+ response = None
122
124
  try:
123
125
  start_time = datetime.now()
124
- outputs = request_fn(*args, **kwargs)
126
+ response = request_fn(*args, **kwargs)
125
127
  end_time = datetime.now()
126
128
  current_status_code = SpanStatusCode.OK
127
- return outputs
129
+ return response
128
130
  except Exception as error:
129
131
  end_time = datetime.now()
130
132
  current_status_code = SpanStatusCode.ERROR
@@ -138,16 +140,17 @@ def _wrap_openai_api_requestor(
138
140
  )
139
141
  raise
140
142
  finally:
141
- if outputs:
142
- response = outputs[0]
143
+ if _is_chat_completion(response):
143
144
  for (
144
145
  attribute_name,
145
- get_response_attribute_fn,
146
- ) in _RESPONSE_ATTRIBUTE_FUNCTIONS.items():
147
- if (attribute_value := get_response_attribute_fn(response)) is not None:
146
+ get_chat_completion_attribute_fn,
147
+ ) in _CHAT_COMPLETION_ATTRIBUTE_FUNCTIONS.items():
148
+ if (
149
+ attribute_value := get_chat_completion_attribute_fn(response)
150
+ ) is not None:
148
151
  attributes[attribute_name] = attribute_value
149
152
  tracer.create_span(
150
- name="openai.ChatCompletion.create",
153
+ name="OpenAI Chat Completion",
151
154
  span_kind=SpanKind.LLM,
152
155
  start_time=start_time,
153
156
  end_time=end_time,
@@ -182,48 +185,46 @@ def _llm_invocation_parameters(
182
185
  return json.dumps(parameters)
183
186
 
184
187
 
185
- def _output_value(response: "OpenAIResponse") -> str:
186
- return json.dumps(response.data)
188
+ def _output_value(chat_completion: "ChatCompletion") -> str:
189
+ return chat_completion.json()
187
190
 
188
191
 
189
192
  def _output_mime_type(_: Any) -> MimeType:
190
193
  return MimeType.JSON
191
194
 
192
195
 
193
- def _llm_output_messages(response: "OpenAIResponse") -> List[OpenInferenceMessage]:
196
+ def _llm_output_messages(chat_completion: "ChatCompletion") -> List[OpenInferenceMessage]:
194
197
  return [
195
- _to_openinference_message(choice["message"], expects_name=False)
196
- for choice in response.data["choices"]
198
+ _to_openinference_message(choice.message.dict(), expects_name=False)
199
+ for choice in chat_completion.choices
197
200
  ]
198
201
 
199
202
 
200
- def _llm_token_count_prompt(response: "OpenAIResponse") -> Optional[int]:
201
- if token_usage := response.data.get("usage"):
202
- return cast(int, token_usage["prompt_tokens"])
203
+ def _llm_token_count_prompt(chat_completion: "ChatCompletion") -> Optional[int]:
204
+ if completion_usage := chat_completion.usage:
205
+ return completion_usage.prompt_tokens
203
206
  return None
204
207
 
205
208
 
206
- def _llm_token_count_completion(response: "OpenAIResponse") -> Optional[int]:
207
- if token_usage := response.data.get("usage"):
208
- return cast(int, token_usage["completion_tokens"])
209
+ def _llm_token_count_completion(chat_completion: "ChatCompletion") -> Optional[int]:
210
+ if completion_usage := chat_completion.usage:
211
+ return completion_usage.completion_tokens
209
212
  return None
210
213
 
211
214
 
212
- def _llm_token_count_total(response: "OpenAIResponse") -> Optional[int]:
213
- if token_usage := response.data.get("usage"):
214
- return cast(int, token_usage["total_tokens"])
215
+ def _llm_token_count_total(chat_completion: "ChatCompletion") -> Optional[int]:
216
+ if completion_usage := chat_completion.usage:
217
+ return completion_usage.total_tokens
215
218
  return None
216
219
 
217
220
 
218
221
  def _llm_function_call(
219
- response: "OpenAIResponse",
222
+ chat_completion: "ChatCompletion",
220
223
  ) -> Optional[str]:
221
- choices = response.data["choices"]
224
+ choices = chat_completion.choices
222
225
  choice = choices[0]
223
- if choice.get("finish_reason") == "function_call" and (
224
- function_call_data := choice["message"].get("function_call")
225
- ):
226
- return json.dumps(function_call_data)
226
+ if choice.finish_reason == "function_call" and (function_call := choice.message.function_call):
227
+ return function_call.json()
227
228
  return None
228
229
 
229
230
 
@@ -274,7 +275,7 @@ _PARAMETER_ATTRIBUTE_FUNCTIONS: Dict[str, Callable[[Parameters], Any]] = {
274
275
  LLM_INPUT_MESSAGES: _llm_input_messages,
275
276
  LLM_INVOCATION_PARAMETERS: _llm_invocation_parameters,
276
277
  }
277
- _RESPONSE_ATTRIBUTE_FUNCTIONS: Dict[str, Callable[["OpenAIResponse"], Any]] = {
278
+ _CHAT_COMPLETION_ATTRIBUTE_FUNCTIONS: Dict[str, Callable[["ChatCompletion"], Any]] = {
278
279
  OUTPUT_VALUE: _output_value,
279
280
  OUTPUT_MIME_TYPE: _output_mime_type,
280
281
  LLM_OUTPUT_MESSAGES: _llm_output_messages,
@@ -283,3 +284,11 @@ _RESPONSE_ATTRIBUTE_FUNCTIONS: Dict[str, Callable[["OpenAIResponse"], Any]] = {
283
284
  LLM_TOKEN_COUNT_TOTAL: _llm_token_count_total,
284
285
  LLM_FUNCTION_CALL: _llm_function_call,
285
286
  }
287
+
288
+
289
+ def _is_chat_completion(response: Any) -> TypeGuard["ChatCompletion"]:
290
+ """
291
+ Type guard for ChatCompletion.
292
+ """
293
+ openai = import_package("openai")
294
+ return isinstance(response, openai.types.chat.ChatCompletion)