arize-phoenix 0.0.32__py3-none-any.whl → 0.0.33__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 (71) hide show
  1. {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/METADATA +11 -5
  2. {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/RECORD +69 -40
  3. phoenix/__init__.py +3 -1
  4. phoenix/config.py +23 -1
  5. phoenix/core/model_schema.py +14 -37
  6. phoenix/core/model_schema_adapter.py +0 -1
  7. phoenix/core/traces.py +285 -0
  8. phoenix/datasets/dataset.py +14 -21
  9. phoenix/datasets/errors.py +4 -1
  10. phoenix/datasets/schema.py +1 -1
  11. phoenix/datetime_utils.py +87 -0
  12. phoenix/experimental/callbacks/__init__.py +0 -0
  13. phoenix/experimental/callbacks/langchain_tracer.py +228 -0
  14. phoenix/experimental/callbacks/llama_index_trace_callback_handler.py +364 -0
  15. phoenix/experimental/evals/__init__.py +33 -0
  16. phoenix/experimental/evals/functions/__init__.py +4 -0
  17. phoenix/experimental/evals/functions/binary.py +156 -0
  18. phoenix/experimental/evals/functions/common.py +31 -0
  19. phoenix/experimental/evals/functions/generate.py +50 -0
  20. phoenix/experimental/evals/models/__init__.py +4 -0
  21. phoenix/experimental/evals/models/base.py +130 -0
  22. phoenix/experimental/evals/models/openai.py +128 -0
  23. phoenix/experimental/evals/retrievals.py +2 -2
  24. phoenix/experimental/evals/templates/__init__.py +24 -0
  25. phoenix/experimental/evals/templates/default_templates.py +126 -0
  26. phoenix/experimental/evals/templates/template.py +107 -0
  27. phoenix/experimental/evals/utils/__init__.py +0 -0
  28. phoenix/experimental/evals/utils/downloads.py +33 -0
  29. phoenix/experimental/evals/utils/threads.py +27 -0
  30. phoenix/experimental/evals/utils/types.py +9 -0
  31. phoenix/experimental/evals/utils.py +33 -0
  32. phoenix/metrics/binning.py +0 -1
  33. phoenix/metrics/timeseries.py +2 -3
  34. phoenix/server/api/context.py +2 -0
  35. phoenix/server/api/input_types/SpanSort.py +60 -0
  36. phoenix/server/api/schema.py +85 -4
  37. phoenix/server/api/types/DataQualityMetric.py +10 -1
  38. phoenix/server/api/types/Dataset.py +2 -4
  39. phoenix/server/api/types/DatasetInfo.py +10 -0
  40. phoenix/server/api/types/ExportEventsMutation.py +4 -1
  41. phoenix/server/api/types/Functionality.py +15 -0
  42. phoenix/server/api/types/MimeType.py +16 -0
  43. phoenix/server/api/types/Model.py +3 -5
  44. phoenix/server/api/types/SortDir.py +13 -0
  45. phoenix/server/api/types/Span.py +229 -0
  46. phoenix/server/api/types/TimeSeries.py +9 -2
  47. phoenix/server/api/types/pagination.py +2 -0
  48. phoenix/server/app.py +24 -4
  49. phoenix/server/main.py +60 -24
  50. phoenix/server/span_handler.py +39 -0
  51. phoenix/server/static/index.js +956 -479
  52. phoenix/server/thread_server.py +10 -2
  53. phoenix/services.py +39 -16
  54. phoenix/session/session.py +99 -27
  55. phoenix/trace/exporter.py +71 -0
  56. phoenix/trace/filter.py +181 -0
  57. phoenix/trace/fixtures.py +23 -8
  58. phoenix/trace/schemas.py +59 -6
  59. phoenix/trace/semantic_conventions.py +141 -1
  60. phoenix/trace/span_json_decoder.py +60 -6
  61. phoenix/trace/span_json_encoder.py +1 -9
  62. phoenix/trace/trace_dataset.py +100 -8
  63. phoenix/trace/tracer.py +26 -3
  64. phoenix/trace/v1/__init__.py +522 -0
  65. phoenix/trace/v1/trace_pb2.py +52 -0
  66. phoenix/trace/v1/trace_pb2.pyi +351 -0
  67. phoenix/core/dimension_data_type.py +0 -6
  68. phoenix/core/dimension_type.py +0 -9
  69. {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/WHEEL +0 -0
  70. {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/licenses/IP_NOTICE +0 -0
  71. {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,107 @@
1
+ from dataclasses import dataclass
2
+ from string import Formatter
3
+ from typing import Dict, List, Tuple, Union
4
+
5
+ from ..utils.types import is_list_of
6
+
7
+ DEFAULT_START_DELIM = "{"
8
+ DEFAULT_END_DELIM = "}"
9
+
10
+
11
+ @dataclass
12
+ class PromptTemplate:
13
+ text: str
14
+ variables: List[str]
15
+
16
+ def __init__(self, text: str, delimiters: List[str] = [DEFAULT_START_DELIM, DEFAULT_END_DELIM]):
17
+ self.text = text
18
+ self._start_delim, self._end_delim = self._get_delimiters(delimiters)
19
+ self._validate()
20
+ self.variables = self._parse_variables()
21
+
22
+ def format(self, variable_values: Dict[str, Union[bool, int, float, str]]) -> str:
23
+ prompt = self.text
24
+ for variable_name in self.variables:
25
+ prompt = prompt.replace(
26
+ self._start_delim + variable_name + self._end_delim,
27
+ str(variable_values[variable_name]),
28
+ )
29
+ return prompt
30
+
31
+ def _get_delimiters(self, delimiters: List[str]) -> Tuple[str, str]:
32
+ if not is_list_of(delimiters, str):
33
+ raise TypeError("delimiters must be a list of strings")
34
+ if len(delimiters) == 1:
35
+ return delimiters[0], delimiters[0]
36
+ elif len(delimiters) == 2:
37
+ return delimiters[0], delimiters[1]
38
+ else:
39
+ raise ValueError("delimiters must only contain 2 items in the list")
40
+
41
+ def _validate(self) -> None:
42
+ # Validate that for every open delimiter, we have the corresponding closing one
43
+ start_count = self.text.count(self._start_delim)
44
+ end_count = self.text.count(self._end_delim)
45
+ if start_count != end_count:
46
+ raise ValueError(
47
+ f"text poorly formatted. Found {start_count} instances of delimiter "
48
+ f"{self._start_delim} and {end_count} instances of {self._end_delim}. "
49
+ "They must be equal to be correctly paired."
50
+ )
51
+
52
+ def _parse_variables(self) -> List[str]:
53
+ variables = []
54
+ formatter = Formatter()
55
+
56
+ text = self.text
57
+
58
+ # Example of this could be a template like: My name is ::name::
59
+ if self._start_delim == self._end_delim:
60
+ delim_length = len(self._start_delim)
61
+ delim_count = text.count(self._start_delim)
62
+ while delim_count > 0:
63
+ left_index = text.find(self._start_delim)
64
+ right_index = text[left_index + delim_length :].find(self._start_delim)
65
+ text = (
66
+ text[0:left_index]
67
+ + DEFAULT_START_DELIM
68
+ + text[left_index + delim_length : left_index + delim_length + right_index]
69
+ + DEFAULT_END_DELIM
70
+ + text[left_index + 2 * delim_length + right_index :]
71
+ )
72
+ delim_count = text.count(self._start_delim)
73
+ else:
74
+ if self._start_delim != "{":
75
+ text = text.replace(self._start_delim, DEFAULT_START_DELIM)
76
+ if self._end_delim != "{":
77
+ text = text.replace(self._end_delim, DEFAULT_END_DELIM)
78
+
79
+ for _, variable_name, _, _ in formatter.parse(text):
80
+ if variable_name:
81
+ variables.append(variable_name)
82
+
83
+ return variables
84
+
85
+
86
+ def normalize_template(template: Union[PromptTemplate, str]) -> "PromptTemplate":
87
+ """
88
+ Normalizes a template to a PromptTemplate object.
89
+ Args:
90
+ template (Union[PromptTemplate, str]): The template to be normalized.
91
+ Returns:
92
+ PromptTemplate: The normalized template.
93
+ """
94
+ normalized_template = template
95
+ if not (isinstance(template, PromptTemplate) or isinstance(template, str)):
96
+ raise TypeError(
97
+ "Invalid type for argument `template`. Expected a string or PromptTemplate "
98
+ f"but found {type(template)}."
99
+ )
100
+ if isinstance(template, str):
101
+ try:
102
+ normalized_template = PromptTemplate(text=template)
103
+ except Exception as e:
104
+ raise RuntimeError(f"Error while initializing the PromptTemplate: {e}")
105
+ else:
106
+ normalized_template = template
107
+ return normalized_template
File without changes
@@ -0,0 +1,33 @@
1
+ """
2
+ Utility functions for evaluations.
3
+ """
4
+
5
+ import json
6
+ from io import BytesIO
7
+ from urllib.error import HTTPError
8
+ from urllib.request import urlopen
9
+ from zipfile import ZipFile
10
+
11
+ import pandas as pd
12
+
13
+
14
+ def download_benchmark_dataset(task: str, dataset_name: str) -> pd.DataFrame:
15
+ """Downloads an Arize evals benchmark dataset as a pandas dataframe.
16
+
17
+ Args:
18
+ task (str): Task to be performed.
19
+ dataset_name (str): Name of the dataset.
20
+
21
+ Returns:
22
+ pandas.DataFrame: A pandas dataframe containing the data.
23
+ """
24
+ jsonl_file_name = f"{dataset_name}.jsonl"
25
+ url = f"http://storage.googleapis.com/arize-assets/phoenix/evals/{task}/{jsonl_file_name}.zip"
26
+ try:
27
+ with urlopen(url) as response:
28
+ zip_byte_stream = BytesIO(response.read())
29
+ with ZipFile(zip_byte_stream) as zip_file:
30
+ with zip_file.open(jsonl_file_name) as jsonl_file:
31
+ return pd.DataFrame(map(json.loads, jsonl_file.readlines()))
32
+ except HTTPError:
33
+ raise ValueError(f'Dataset "{dataset_name}" for "{task}" task does not exist.')
@@ -0,0 +1,27 @@
1
+ """High-level support for working with threads in asyncio
2
+ Directly copied from: https://github.com/python/cpython/blob/main/Lib/asyncio/threads.py#L12
3
+ since this helper function 'to_thread' is not available in python<3.9
4
+ """
5
+
6
+ import contextvars
7
+ import functools
8
+ from asyncio import events
9
+ from typing import Any
10
+
11
+ __all__ = ("to_thread",)
12
+
13
+
14
+ async def to_thread(func, /, *args, **kwargs) -> Any: # type:ignore
15
+ """Asynchronously run function *func* in a separate thread.
16
+
17
+ Any *args and **kwargs supplied for this function are directly passed
18
+ to *func*. Also, the current :class:`contextvars.Context` is propagated,
19
+ allowing context variables from the main thread to be accessed in the
20
+ separate thread.
21
+
22
+ Return a coroutine that can be awaited to get the eventual result of *func*.
23
+ """
24
+ loop = events.get_running_loop()
25
+ ctx = contextvars.copy_context()
26
+ func_call = functools.partial(ctx.run, func, *args, **kwargs)
27
+ return await loop.run_in_executor(None, func_call)
@@ -0,0 +1,9 @@
1
+ from typing import Sequence
2
+
3
+ from typing_extensions import TypeVar
4
+
5
+ T = TypeVar("T", bound=type)
6
+
7
+
8
+ def is_list_of(lst: Sequence[object], tp: T) -> bool:
9
+ return isinstance(lst, list) and all(isinstance(x, tp) for x in lst)
@@ -0,0 +1,33 @@
1
+ """
2
+ Utility functions for evaluations.
3
+ """
4
+
5
+ import json
6
+ from io import BytesIO
7
+ from urllib.error import HTTPError
8
+ from urllib.request import urlopen
9
+ from zipfile import ZipFile
10
+
11
+ import pandas as pd
12
+
13
+
14
+ def download_benchmark_dataset(task: str, dataset_name: str) -> pd.DataFrame:
15
+ """Downloads an Arize evals benchmark dataset as a pandas dataframe.
16
+
17
+ Args:
18
+ task (str): Task to be performed.
19
+ dataset_name (str): Name of the dataset.
20
+
21
+ Returns:
22
+ pandas.DataFrame: A pandas dataframe containing the data.
23
+ """
24
+ jsonl_file_name = f"{dataset_name}.jsonl"
25
+ url = f"http://storage.googleapis.com/arize-assets/phoenix/evals/{task}/{jsonl_file_name}.zip"
26
+ try:
27
+ with urlopen(url) as response:
28
+ zip_byte_stream = BytesIO(response.read())
29
+ with ZipFile(zip_byte_stream) as zip_file:
30
+ with zip_file.open(jsonl_file_name) as jsonl_file:
31
+ return pd.DataFrame(map(json.loads, jsonl_file.readlines()))
32
+ except HTTPError:
33
+ raise ValueError(f'Dataset "{dataset_name}" for "{task}" task does not exist.')
@@ -154,7 +154,6 @@ class IntervalBinning(BinningMethod):
154
154
  return summary.set_axis(
155
155
  categories,
156
156
  axis=0,
157
- copy=False,
158
157
  )
159
158
 
160
159
 
@@ -38,7 +38,7 @@ StopIndex: TypeAlias = int
38
38
 
39
39
 
40
40
  def row_interval_from_sorted_time_index(
41
- time_index: pd.Index,
41
+ time_index: pd.DatetimeIndex,
42
42
  time_start: datetime,
43
43
  time_stop: datetime,
44
44
  ) -> Tuple[StartIndex, StopIndex]:
@@ -170,7 +170,7 @@ def _results(
170
170
  sampling_interval=sampling_interval,
171
171
  ):
172
172
  row_start, row_stop = row_interval_from_sorted_time_index(
173
- time_index=dataframe.index,
173
+ time_index=cast(pd.DatetimeIndex, dataframe.index),
174
174
  time_start=time_start, # inclusive
175
175
  time_stop=time_stop, # exclusive
176
176
  )
@@ -191,7 +191,6 @@ def _results(
191
191
  timezone.utc,
192
192
  ),
193
193
  axis=0,
194
- copy=False,
195
194
  )
196
195
 
197
196
  yield res.loc[result_slice, :]
@@ -7,6 +7,7 @@ from starlette.responses import Response
7
7
  from starlette.websockets import WebSocket
8
8
 
9
9
  from phoenix.core.model_schema import Model
10
+ from phoenix.core.traces import Traces
10
11
 
11
12
 
12
13
  @dataclass
@@ -16,3 +17,4 @@ class Context:
16
17
  model: Model
17
18
  export_path: Path
18
19
  corpus: Optional[Model] = None
20
+ traces: Optional[Traces] = None
@@ -0,0 +1,60 @@
1
+ from enum import Enum
2
+ from functools import partial
3
+ from typing import Any, Iterable, Iterator
4
+
5
+ import pandas as pd
6
+ import strawberry
7
+
8
+ from phoenix.core.traces import (
9
+ CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION,
10
+ CUMULATIVE_LLM_TOKEN_COUNT_PROMPT,
11
+ CUMULATIVE_LLM_TOKEN_COUNT_TOTAL,
12
+ END_TIME,
13
+ LATENCY_MS,
14
+ LLM_TOKEN_COUNT_COMPLETION,
15
+ LLM_TOKEN_COUNT_PROMPT,
16
+ LLM_TOKEN_COUNT_TOTAL,
17
+ START_TIME,
18
+ )
19
+ from phoenix.server.api.types.SortDir import SortDir
20
+ from phoenix.trace.schemas import Span
21
+
22
+
23
+ @strawberry.enum
24
+ class SpanColumn(Enum):
25
+ startTime = START_TIME
26
+ endTime = END_TIME
27
+ latencyMs = LATENCY_MS
28
+ tokenCountTotal = LLM_TOKEN_COUNT_TOTAL
29
+ tokenCountPrompt = LLM_TOKEN_COUNT_PROMPT
30
+ tokenCountCompletion = LLM_TOKEN_COUNT_COMPLETION
31
+ cumulativeTokenCountTotal = CUMULATIVE_LLM_TOKEN_COUNT_TOTAL
32
+ cumulativeTokenCountPrompt = CUMULATIVE_LLM_TOKEN_COUNT_PROMPT
33
+ cumulativeTokenCountCompletion = CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION
34
+
35
+
36
+ @strawberry.input
37
+ class SpanSort:
38
+ """
39
+ The sort column and direction for span connections
40
+ """
41
+
42
+ col: SpanColumn
43
+ dir: SortDir
44
+
45
+ def __call__(self, spans: Iterable[Span]) -> Iterator[Span]:
46
+ """
47
+ Sorts the spans by the given column and direction
48
+ """
49
+ yield from pd.Series(spans, dtype=object).sort_values(
50
+ key=lambda series: series.apply(partial(_get_column, span_column=self.col)),
51
+ ascending=self.dir.value == SortDir.asc.value,
52
+ )
53
+
54
+
55
+ def _get_column(span: Span, span_column: SpanColumn) -> Any:
56
+ if span_column is SpanColumn.startTime:
57
+ return span.start_time
58
+ if span_column is SpanColumn.endTime:
59
+ return span.end_time
60
+ return span.attributes.get(span_column.value)
@@ -1,6 +1,8 @@
1
1
  from collections import defaultdict
2
+ from datetime import datetime
2
3
  from itertools import chain
3
- from typing import Dict, List, Optional, Set, Union
4
+ from typing import Dict, List, Optional, Set, Tuple, Union, cast
5
+ from uuid import UUID
4
6
 
5
7
  import numpy as np
6
8
  import numpy.typing as npt
@@ -12,10 +14,17 @@ from typing_extensions import Annotated
12
14
  from phoenix.pointcloud.clustering import Hdbscan
13
15
  from phoenix.server.api.helpers import ensure_list
14
16
  from phoenix.server.api.input_types.ClusterInput import ClusterInput
17
+ from phoenix.server.api.input_types.Coordinates import (
18
+ InputCoordinate2D,
19
+ InputCoordinate3D,
20
+ )
21
+ from phoenix.server.api.input_types.SpanSort import SpanSort
15
22
  from phoenix.server.api.types.Cluster import Cluster, to_gql_clusters
16
23
 
24
+ from ...trace.filter import SpanFilter
17
25
  from .context import Context
18
- from .input_types import Coordinates
26
+ from .input_types.TimeRange import TimeRange
27
+ from .types.DatasetInfo import DatasetInfo
19
28
  from .types.DatasetRole import AncillaryDatasetRole, DatasetRole
20
29
  from .types.Dimension import to_gql_dimension
21
30
  from .types.EmbeddingDimension import (
@@ -26,12 +35,24 @@ from .types.EmbeddingDimension import (
26
35
  )
27
36
  from .types.Event import create_event_id, unpack_event_id
28
37
  from .types.ExportEventsMutation import ExportEventsMutation
38
+ from .types.Functionality import Functionality
29
39
  from .types.Model import Model
30
40
  from .types.node import GlobalID, Node, from_global_id
41
+ from .types.pagination import Connection, ConnectionArgs, Cursor, connection_from_list
42
+ from .types.Span import Span, to_gql_span
31
43
 
32
44
 
33
45
  @strawberry.type
34
46
  class Query:
47
+ @strawberry.field
48
+ def functionality(self, info: Info[Context, None]) -> "Functionality":
49
+ has_model_inferences = not info.context.model.is_empty
50
+ has_traces = info.context.traces is not None
51
+ return Functionality(
52
+ model_inferences=has_model_inferences,
53
+ tracing=has_traces,
54
+ )
55
+
35
56
  @strawberry.field
36
57
  def model(self) -> Model:
37
58
  return Model()
@@ -71,13 +92,13 @@ class Query:
71
92
  ),
72
93
  ],
73
94
  coordinates_2d: Annotated[
74
- Optional[List[Coordinates.InputCoordinate2D]],
95
+ Optional[List[InputCoordinate2D]],
75
96
  strawberry.argument(
76
97
  description="Point coordinates. Must be either 2D or 3D.",
77
98
  ),
78
99
  ] = UNSET,
79
100
  coordinates_3d: Annotated[
80
- Optional[List[Coordinates.InputCoordinate3D]],
101
+ Optional[List[InputCoordinate3D]],
81
102
  strawberry.argument(
82
103
  description="Point coordinates. Must be either 2D or 3D.",
83
104
  ),
@@ -178,6 +199,66 @@ class Query:
178
199
  clustered_events=clustered_events,
179
200
  )
180
201
 
202
+ @strawberry.field
203
+ def spans(
204
+ self,
205
+ info: Info[Context, None],
206
+ time_range: Optional[TimeRange] = UNSET,
207
+ trace_ids: Optional[List[ID]] = UNSET,
208
+ first: Optional[int] = 50,
209
+ last: Optional[int] = UNSET,
210
+ after: Optional[Cursor] = UNSET,
211
+ before: Optional[Cursor] = UNSET,
212
+ sort: Optional[SpanSort] = UNSET,
213
+ root_spans_only: Optional[bool] = False,
214
+ filter_condition: Optional[str] = None,
215
+ ) -> Connection[Span]:
216
+ args = ConnectionArgs(
217
+ first=first,
218
+ after=after if isinstance(after, Cursor) else None,
219
+ last=last,
220
+ before=before if isinstance(before, Cursor) else None,
221
+ )
222
+ if (traces := info.context.traces) is None:
223
+ return connection_from_list(data=[], args=args)
224
+ try:
225
+ predicate = SpanFilter(filter_condition) if filter_condition else None
226
+ except SyntaxError as e:
227
+ raise Exception(f"invalid filter condition: {e.msg}") from e # TODO: add details
228
+ if not trace_ids:
229
+ spans = traces.get_spans(
230
+ start_time=time_range.start if time_range else None,
231
+ stop_time=time_range.end if time_range else None,
232
+ root_spans_only=root_spans_only,
233
+ )
234
+ else:
235
+ spans = chain.from_iterable(map(traces.get_trace, map(UUID, trace_ids)))
236
+ if predicate:
237
+ spans = filter(predicate, spans)
238
+ if sort:
239
+ spans = sort(spans)
240
+ data = list(map(to_gql_span, spans))
241
+ return connection_from_list(data=data, args=args)
242
+
243
+ @strawberry.field
244
+ def trace_dataset_info(
245
+ self,
246
+ info: Info[Context, None],
247
+ ) -> Optional[DatasetInfo]:
248
+ if (traces := info.context.traces) is None:
249
+ return None
250
+ if not (span_count := traces.span_count):
251
+ return None
252
+ start_time, stop_time = cast(
253
+ Tuple[datetime, datetime],
254
+ traces.right_open_time_range,
255
+ )
256
+ return DatasetInfo(
257
+ start_time=start_time,
258
+ end_time=stop_time,
259
+ record_count=span_count,
260
+ )
261
+
181
262
 
182
263
  @strawberry.type
183
264
  class Mutation(ExportEventsMutation):
@@ -3,7 +3,16 @@ from functools import partial
3
3
 
4
4
  import strawberry
5
5
 
6
- from phoenix.metrics.metrics import Cardinality, Count, Max, Mean, Min, PercentEmpty, Quantile, Sum
6
+ from phoenix.metrics.metrics import (
7
+ Cardinality,
8
+ Count,
9
+ Max,
10
+ Mean,
11
+ Min,
12
+ PercentEmpty,
13
+ Quantile,
14
+ Sum,
15
+ )
7
16
 
8
17
 
9
18
  @strawberry.enum
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from typing import Iterable, List, Optional, Set, Union
3
2
 
4
3
  import strawberry
@@ -9,15 +8,14 @@ import phoenix.core.model_schema as ms
9
8
  from phoenix.core.model_schema import FEATURE, TAG, ScalarDimension
10
9
 
11
10
  from ..input_types.DimensionInput import DimensionInput
11
+ from .DatasetInfo import DatasetInfo
12
12
  from .DatasetRole import AncillaryDatasetRole, DatasetRole
13
13
  from .Dimension import Dimension, to_gql_dimension
14
14
  from .Event import Event, create_event, create_event_id, parse_event_ids_by_dataset_role
15
15
 
16
16
 
17
17
  @strawberry.type
18
- class Dataset:
19
- start_time: datetime = strawberry.field(description="The start bookend of the data")
20
- end_time: datetime = strawberry.field(description="The end bookend of the data")
18
+ class Dataset(DatasetInfo):
21
19
  dataset: strawberry.Private[ms.Dataset]
22
20
  dataset_role: strawberry.Private[Union[DatasetRole, AncillaryDatasetRole]]
23
21
  model: strawberry.Private[ms.Model]
@@ -0,0 +1,10 @@
1
+ from datetime import datetime
2
+
3
+ import strawberry
4
+
5
+
6
+ @strawberry.type
7
+ class DatasetInfo:
8
+ start_time: datetime = strawberry.field(description="The start bookend of the data")
9
+ end_time: datetime = strawberry.field(description="The end bookend of the data")
10
+ record_count: int = strawberry.field(description="The record count of the data")
@@ -11,7 +11,10 @@ import phoenix.core.model_schema as ms
11
11
  from phoenix.server.api.context import Context
12
12
  from phoenix.server.api.input_types.ClusterInput import ClusterInput
13
13
  from phoenix.server.api.types.DatasetRole import AncillaryDatasetRole, DatasetRole
14
- from phoenix.server.api.types.Event import parse_event_ids_by_dataset_role, unpack_event_id
14
+ from phoenix.server.api.types.Event import (
15
+ parse_event_ids_by_dataset_role,
16
+ unpack_event_id,
17
+ )
15
18
  from phoenix.server.api.types.ExportedFile import ExportedFile
16
19
 
17
20
 
@@ -0,0 +1,15 @@
1
+ import strawberry
2
+
3
+
4
+ @strawberry.type
5
+ class Functionality:
6
+ """
7
+ Describes the the functionality of the platform that is enabled
8
+ """
9
+
10
+ model_inferences: bool = strawberry.field(
11
+ description="Model inferences are available for analysis"
12
+ )
13
+ tracing: bool = strawberry.field(
14
+ description="Generative tracing records are available for analysis"
15
+ )
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+ from typing import Any, Optional
3
+
4
+ import strawberry
5
+
6
+ from phoenix.trace import semantic_conventions
7
+
8
+
9
+ @strawberry.enum
10
+ class MimeType(Enum):
11
+ text = semantic_conventions.MimeType.TEXT
12
+ json = semantic_conventions.MimeType.JSON
13
+
14
+ @classmethod
15
+ def _missing_(cls, v: Any) -> Optional["MimeType"]:
16
+ return None if v else cls.text
@@ -40,11 +40,6 @@ class Model:
40
40
  include: Optional[DimensionFilter] = UNSET,
41
41
  exclude: Optional[DimensionFilter] = UNSET,
42
42
  ) -> Connection[Dimension]:
43
- """
44
- A non-trivial implementation should efficiently fetch only
45
- the necessary books after the offset.
46
- For simplicity, here we build the list and then slice it accordingly
47
- """
48
43
  model = info.context.model
49
44
  return connection_from_list(
50
45
  [
@@ -68,6 +63,7 @@ class Model:
68
63
  return Dataset(
69
64
  start_time=start,
70
65
  end_time=stop,
66
+ record_count=len(dataset),
71
67
  dataset=dataset,
72
68
  dataset_role=DatasetRole.primary,
73
69
  model=info.context.model,
@@ -81,6 +77,7 @@ class Model:
81
77
  return Dataset(
82
78
  start_time=start,
83
79
  end_time=stop,
80
+ record_count=len(dataset),
84
81
  dataset=dataset,
85
82
  dataset_role=DatasetRole.reference,
86
83
  model=info.context.model,
@@ -96,6 +93,7 @@ class Model:
96
93
  return Dataset(
97
94
  start_time=start,
98
95
  end_time=stop,
96
+ record_count=len(dataset),
99
97
  dataset=dataset,
100
98
  dataset_role=AncillaryDatasetRole.corpus,
101
99
  model=info.context.corpus,
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+ import strawberry
4
+
5
+
6
+ @strawberry.enum
7
+ class SortDir(Enum):
8
+ """
9
+ Sort directions
10
+ """
11
+
12
+ asc = "asc"
13
+ desc = "desc"