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

Files changed (23) hide show
  1. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/RECORD +23 -23
  3. phoenix/config.py +13 -0
  4. phoenix/db/facilitator.py +3 -2
  5. phoenix/server/api/helpers/playground_clients.py +29 -13
  6. phoenix/server/api/helpers/playground_spans.py +6 -0
  7. phoenix/server/api/mutations/chat_mutations.py +75 -34
  8. phoenix/server/api/subscriptions.py +108 -49
  9. phoenix/server/static/.vite/manifest.json +31 -31
  10. phoenix/server/static/assets/{components-BXIz9ZO8.js → components-C_HASv83.js} +90 -90
  11. phoenix/server/static/assets/{index-DTut7g1y.js → index-D7UiCRtr.js} +2 -2
  12. phoenix/server/static/assets/{pages-B8FpJuXu.js → pages-DYHcAdjT.js} +313 -264
  13. phoenix/server/static/assets/{vendor-BX8_Znqy.js → vendor-BCxsh5i3.js} +150 -150
  14. phoenix/server/static/assets/{vendor-arizeai-CtHir-Ua.js → vendor-arizeai-C2CDZgMz.js} +28 -28
  15. phoenix/server/static/assets/{vendor-codemirror-DLlGiguX.js → vendor-codemirror-DYbtnCTn.js} +1 -1
  16. phoenix/server/static/assets/{vendor-recharts-CJRple0d.js → vendor-recharts-P6W8G0Mb.js} +1 -1
  17. phoenix/trace/span_evaluations.py +4 -3
  18. phoenix/utilities/json.py +7 -1
  19. phoenix/version.py +1 -1
  20. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/WHEEL +0 -0
  21. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/entry_points.txt +0 -0
  22. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/licenses/IP_NOTICE +0 -0
  23. {arize_phoenix-5.10.0.dist-info → arize_phoenix-5.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,9 +2,10 @@ import asyncio
2
2
  import logging
3
3
  from asyncio import FIRST_COMPLETED, Queue, QueueEmpty, Task, create_task, wait, wait_for
4
4
  from collections.abc import AsyncIterator, Iterator
5
- from datetime import datetime, timezone
5
+ from datetime import datetime, timedelta, timezone
6
6
  from typing import (
7
7
  Any,
8
+ AsyncGenerator,
8
9
  Iterable,
9
10
  Mapping,
10
11
  Optional,
@@ -24,7 +25,7 @@ from typing_extensions import TypeAlias, assert_never
24
25
  from phoenix.datetime_utils import local_now, normalize_datetime
25
26
  from phoenix.db import models
26
27
  from phoenix.server.api.context import Context
27
- from phoenix.server.api.exceptions import BadRequest, NotFound
28
+ from phoenix.server.api.exceptions import BadRequest, CustomGraphQLError, NotFound
28
29
  from phoenix.server.api.helpers.playground_clients import (
29
30
  PlaygroundStreamingClient,
30
31
  initialize_playground_clients,
@@ -79,6 +80,7 @@ DatasetExampleID: TypeAlias = GlobalID
79
80
  ChatCompletionResult: TypeAlias = tuple[
80
81
  DatasetExampleID, Optional[models.Span], models.ExperimentRun
81
82
  ]
83
+ ChatStream: TypeAlias = AsyncGenerator[ChatCompletionSubscriptionPayload, None]
82
84
  PLAYGROUND_PROJECT_NAME = "playground"
83
85
 
84
86
 
@@ -91,11 +93,19 @@ class Subscription:
91
93
  provider_key = input.model.provider_key
92
94
  llm_client_class = PLAYGROUND_CLIENT_REGISTRY.get_client(provider_key, input.model.name)
93
95
  if llm_client_class is None:
94
- raise BadRequest(f"No LLM client registered for provider '{provider_key}'")
95
- llm_client = llm_client_class(
96
- model=input.model,
97
- api_key=input.api_key,
98
- )
96
+ raise BadRequest(f"Unknown LLM provider: '{provider_key.value}'")
97
+ try:
98
+ llm_client = llm_client_class(
99
+ model=input.model,
100
+ api_key=input.api_key,
101
+ )
102
+ except CustomGraphQLError:
103
+ raise
104
+ except Exception as error:
105
+ raise BadRequest(
106
+ f"Failed to connect to LLM API for {provider_key.value} {input.model.name}: "
107
+ f"{str(error)}"
108
+ )
99
109
 
100
110
  messages = [
101
111
  (
@@ -158,7 +168,19 @@ class Subscription:
158
168
  provider_key = input.model.provider_key
159
169
  llm_client_class = PLAYGROUND_CLIENT_REGISTRY.get_client(provider_key, input.model.name)
160
170
  if llm_client_class is None:
161
- raise BadRequest(f"No LLM client registered for provider '{provider_key}'")
171
+ raise BadRequest(f"Unknown LLM provider: '{provider_key.value}'")
172
+ try:
173
+ llm_client = llm_client_class(
174
+ model=input.model,
175
+ api_key=input.api_key,
176
+ )
177
+ except CustomGraphQLError:
178
+ raise
179
+ except Exception as error:
180
+ raise BadRequest(
181
+ f"Failed to connect to LLM API for {provider_key.value} {input.model.name}: "
182
+ f"{str(error)}"
183
+ )
162
184
 
163
185
  dataset_id = from_global_id_with_expected_type(input.dataset_id, Dataset.__name__)
164
186
  version_id = (
@@ -264,45 +286,76 @@ class Subscription:
264
286
  experiment=to_gql_experiment(experiment)
265
287
  ) # eagerly yields experiment so it can be linked by consumers of the subscription
266
288
 
267
- results_queue: Queue[ChatCompletionResult] = Queue()
268
- chat_completion_streams = [
269
- _stream_chat_completion_over_dataset_example(
270
- input=input,
271
- llm_client_class=llm_client_class,
272
- revision=revision,
273
- results_queue=results_queue,
274
- experiment_id=experiment.id,
275
- project_id=playground_project_id,
289
+ results: Queue[ChatCompletionResult] = Queue()
290
+ not_started: list[tuple[DatasetExampleID, ChatStream]] = [
291
+ (
292
+ GlobalID(DatasetExample.__name__, str(revision.dataset_example_id)),
293
+ _stream_chat_completion_over_dataset_example(
294
+ input=input,
295
+ llm_client=llm_client,
296
+ revision=revision,
297
+ results=results,
298
+ experiment_id=experiment.id,
299
+ project_id=playground_project_id,
300
+ ),
276
301
  )
277
302
  for revision in revisions
278
303
  ]
279
- stream_to_async_tasks: dict[
280
- AsyncIterator[ChatCompletionSubscriptionPayload],
281
- Task[ChatCompletionSubscriptionPayload],
282
- ] = {iterator: _create_task_with_timeout(iterator) for iterator in chat_completion_streams}
283
- batch_size = 10
284
- while stream_to_async_tasks:
285
- async_tasks_to_run = [task for task in stream_to_async_tasks.values()]
304
+ in_progress: list[
305
+ tuple[Optional[DatasetExampleID], ChatStream, Task[ChatCompletionSubscriptionPayload]]
306
+ ] = []
307
+ max_in_progress = 3
308
+ write_batch_size = 10
309
+ write_interval = timedelta(seconds=10)
310
+ last_write_time = datetime.now()
311
+ while not_started or in_progress:
312
+ while not_started and len(in_progress) < max_in_progress:
313
+ ex_id, stream = not_started.pop()
314
+ task = _create_task_with_timeout(stream)
315
+ in_progress.append((ex_id, stream, task))
316
+ async_tasks_to_run = [task for _, _, task in in_progress]
286
317
  completed_tasks, _ = await wait(async_tasks_to_run, return_when=FIRST_COMPLETED)
287
- for task in completed_tasks:
288
- iterator = next(it for it, t in stream_to_async_tasks.items() if t == task)
318
+ for completed_task in completed_tasks:
319
+ idx = [task for _, _, task in in_progress].index(completed_task)
320
+ example_id, stream, _ = in_progress[idx]
289
321
  try:
290
- yield task.result()
291
- except (StopAsyncIteration, asyncio.TimeoutError):
292
- del stream_to_async_tasks[iterator] # removes exhausted iterator
322
+ yield completed_task.result()
323
+ except StopAsyncIteration:
324
+ del in_progress[idx] # removes exhausted stream
325
+ except asyncio.TimeoutError:
326
+ del in_progress[idx] # removes timed-out stream
327
+ if example_id is not None:
328
+ yield ChatCompletionSubscriptionError(
329
+ message="Timed out", dataset_example_id=example_id
330
+ )
293
331
  except Exception as error:
294
- del stream_to_async_tasks[iterator] # removes failed iterator
332
+ del in_progress[idx] # removes failed stream
333
+ if example_id is not None:
334
+ yield ChatCompletionSubscriptionError(
335
+ message="An unexpected error occurred", dataset_example_id=example_id
336
+ )
295
337
  logger.exception(error)
296
338
  else:
297
- stream_to_async_tasks[iterator] = _create_task_with_timeout(iterator)
298
- if results_queue.qsize() >= batch_size:
299
- result_iterator = _chat_completion_result_payloads(
300
- db=info.context.db, results=_drain_no_wait(results_queue)
301
- )
302
- stream_to_async_tasks[result_iterator] = _create_task_with_timeout(
303
- result_iterator
339
+ task = _create_task_with_timeout(stream)
340
+ in_progress[idx] = (example_id, stream, task)
341
+
342
+ exceeded_write_batch_size = results.qsize() >= write_batch_size
343
+ exceeded_write_interval = datetime.now() - last_write_time > write_interval
344
+ write_already_in_progress = any(
345
+ _is_result_payloads_stream(stream) for _, stream, _ in in_progress
346
+ )
347
+ if (
348
+ not results.empty()
349
+ and (exceeded_write_batch_size or exceeded_write_interval)
350
+ and not write_already_in_progress
351
+ ):
352
+ result_payloads_stream = _chat_completion_result_payloads(
353
+ db=info.context.db, results=_drain_no_wait(results)
304
354
  )
305
- if remaining_results := await _drain(results_queue):
355
+ task = _create_task_with_timeout(result_payloads_stream)
356
+ in_progress.append((None, result_payloads_stream, task))
357
+ last_write_time = datetime.now()
358
+ if remaining_results := await _drain(results):
306
359
  async for result_payload in _chat_completion_result_payloads(
307
360
  db=info.context.db, results=remaining_results
308
361
  ):
@@ -312,17 +365,13 @@ class Subscription:
312
365
  async def _stream_chat_completion_over_dataset_example(
313
366
  *,
314
367
  input: ChatCompletionOverDatasetInput,
315
- llm_client_class: type["PlaygroundStreamingClient"],
368
+ llm_client: PlaygroundStreamingClient,
316
369
  revision: models.DatasetExampleRevision,
317
- results_queue: Queue[ChatCompletionResult],
370
+ results: Queue[ChatCompletionResult],
318
371
  experiment_id: int,
319
372
  project_id: int,
320
- ) -> AsyncIterator[ChatCompletionSubscriptionPayload]:
373
+ ) -> ChatStream:
321
374
  example_id = GlobalID(DatasetExample.__name__, str(revision.dataset_example_id))
322
- llm_client = llm_client_class(
323
- model=input.model,
324
- api_key=input.api_key,
325
- )
326
375
  invocation_parameters = llm_client.construct_invocation_parameters(input.invocation_parameters)
327
376
  messages = [
328
377
  (
@@ -345,7 +394,7 @@ async def _stream_chat_completion_over_dataset_example(
345
394
  except TemplateFormatterError as error:
346
395
  format_end_time = cast(datetime, normalize_datetime(dt=local_now(), tz=timezone.utc))
347
396
  yield ChatCompletionSubscriptionError(message=str(error), dataset_example_id=example_id)
348
- await results_queue.put(
397
+ await results.put(
349
398
  (
350
399
  example_id,
351
400
  None,
@@ -380,7 +429,7 @@ async def _stream_chat_completion_over_dataset_example(
380
429
  db_run = get_db_experiment_run(
381
430
  db_span, db_trace, experiment_id=experiment_id, example_id=revision.dataset_example_id
382
431
  )
383
- await results_queue.put((example_id, db_span, db_run))
432
+ await results.put((example_id, db_span, db_run))
384
433
  if span.status_message is not None:
385
434
  yield ChatCompletionSubscriptionError(
386
435
  message=span.status_message, dataset_example_id=example_id
@@ -391,7 +440,7 @@ async def _chat_completion_result_payloads(
391
440
  *,
392
441
  db: DbSessionFactory,
393
442
  results: Sequence[ChatCompletionResult],
394
- ) -> AsyncIterator[ChatCompletionSubscriptionResult]:
443
+ ) -> ChatStream:
395
444
  if not results:
396
445
  return
397
446
  async with db() as session:
@@ -408,8 +457,18 @@ async def _chat_completion_result_payloads(
408
457
  )
409
458
 
410
459
 
460
+ def _is_result_payloads_stream(
461
+ stream: ChatStream,
462
+ ) -> bool:
463
+ """
464
+ Checks if the given generator was instantiated from
465
+ `_chat_completion_result_payloads`
466
+ """
467
+ return stream.ag_code == _chat_completion_result_payloads.__code__
468
+
469
+
411
470
  def _create_task_with_timeout(
412
- iterable: AsyncIterator[GenericType], timeout_in_seconds: int = 60
471
+ iterable: AsyncIterator[GenericType], timeout_in_seconds: int = 90
413
472
  ) -> Task[GenericType]:
414
473
  return create_task(wait_for(_as_coroutine(iterable), timeout=timeout_in_seconds))
415
474
 
@@ -1,32 +1,32 @@
1
1
  {
2
- "_components-BXIz9ZO8.js": {
3
- "file": "assets/components-BXIz9ZO8.js",
2
+ "_components-C_HASv83.js": {
3
+ "file": "assets/components-C_HASv83.js",
4
4
  "name": "components",
5
5
  "imports": [
6
- "_vendor-BX8_Znqy.js",
7
- "_pages-B8FpJuXu.js",
8
- "_vendor-arizeai-CtHir-Ua.js",
9
- "_vendor-codemirror-DLlGiguX.js",
6
+ "_vendor-BCxsh5i3.js",
7
+ "_pages-DYHcAdjT.js",
8
+ "_vendor-arizeai-C2CDZgMz.js",
9
+ "_vendor-codemirror-DYbtnCTn.js",
10
10
  "_vendor-three-DwGkEfCM.js"
11
11
  ]
12
12
  },
13
- "_pages-B8FpJuXu.js": {
14
- "file": "assets/pages-B8FpJuXu.js",
13
+ "_pages-DYHcAdjT.js": {
14
+ "file": "assets/pages-DYHcAdjT.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
- "_vendor-BX8_Znqy.js",
18
- "_vendor-arizeai-CtHir-Ua.js",
19
- "_components-BXIz9ZO8.js",
20
- "_vendor-recharts-CJRple0d.js",
21
- "_vendor-codemirror-DLlGiguX.js"
17
+ "_vendor-BCxsh5i3.js",
18
+ "_vendor-arizeai-C2CDZgMz.js",
19
+ "_components-C_HASv83.js",
20
+ "_vendor-recharts-P6W8G0Mb.js",
21
+ "_vendor-codemirror-DYbtnCTn.js"
22
22
  ]
23
23
  },
24
24
  "_vendor-!~{003}~.js": {
25
25
  "file": "assets/vendor-DxkFTwjz.css",
26
26
  "src": "_vendor-!~{003}~.js"
27
27
  },
28
- "_vendor-BX8_Znqy.js": {
29
- "file": "assets/vendor-BX8_Znqy.js",
28
+ "_vendor-BCxsh5i3.js": {
29
+ "file": "assets/vendor-BCxsh5i3.js",
30
30
  "name": "vendor",
31
31
  "imports": [
32
32
  "_vendor-three-DwGkEfCM.js"
@@ -35,25 +35,25 @@
35
35
  "assets/vendor-DxkFTwjz.css"
36
36
  ]
37
37
  },
38
- "_vendor-arizeai-CtHir-Ua.js": {
39
- "file": "assets/vendor-arizeai-CtHir-Ua.js",
38
+ "_vendor-arizeai-C2CDZgMz.js": {
39
+ "file": "assets/vendor-arizeai-C2CDZgMz.js",
40
40
  "name": "vendor-arizeai",
41
41
  "imports": [
42
- "_vendor-BX8_Znqy.js"
42
+ "_vendor-BCxsh5i3.js"
43
43
  ]
44
44
  },
45
- "_vendor-codemirror-DLlGiguX.js": {
46
- "file": "assets/vendor-codemirror-DLlGiguX.js",
45
+ "_vendor-codemirror-DYbtnCTn.js": {
46
+ "file": "assets/vendor-codemirror-DYbtnCTn.js",
47
47
  "name": "vendor-codemirror",
48
48
  "imports": [
49
- "_vendor-BX8_Znqy.js"
49
+ "_vendor-BCxsh5i3.js"
50
50
  ]
51
51
  },
52
- "_vendor-recharts-CJRple0d.js": {
53
- "file": "assets/vendor-recharts-CJRple0d.js",
52
+ "_vendor-recharts-P6W8G0Mb.js": {
53
+ "file": "assets/vendor-recharts-P6W8G0Mb.js",
54
54
  "name": "vendor-recharts",
55
55
  "imports": [
56
- "_vendor-BX8_Znqy.js"
56
+ "_vendor-BCxsh5i3.js"
57
57
  ]
58
58
  },
59
59
  "_vendor-three-DwGkEfCM.js": {
@@ -61,18 +61,18 @@
61
61
  "name": "vendor-three"
62
62
  },
63
63
  "index.tsx": {
64
- "file": "assets/index-DTut7g1y.js",
64
+ "file": "assets/index-D7UiCRtr.js",
65
65
  "name": "index",
66
66
  "src": "index.tsx",
67
67
  "isEntry": true,
68
68
  "imports": [
69
- "_vendor-BX8_Znqy.js",
70
- "_vendor-arizeai-CtHir-Ua.js",
71
- "_pages-B8FpJuXu.js",
72
- "_components-BXIz9ZO8.js",
69
+ "_vendor-BCxsh5i3.js",
70
+ "_vendor-arizeai-C2CDZgMz.js",
71
+ "_pages-DYHcAdjT.js",
72
+ "_components-C_HASv83.js",
73
73
  "_vendor-three-DwGkEfCM.js",
74
- "_vendor-recharts-CJRple0d.js",
75
- "_vendor-codemirror-DLlGiguX.js"
74
+ "_vendor-recharts-P6W8G0Mb.js",
75
+ "_vendor-codemirror-DYbtnCTn.js"
76
76
  ]
77
77
  }
78
78
  }