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.
- {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/METADATA +11 -5
- {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/RECORD +69 -40
- phoenix/__init__.py +3 -1
- phoenix/config.py +23 -1
- phoenix/core/model_schema.py +14 -37
- phoenix/core/model_schema_adapter.py +0 -1
- phoenix/core/traces.py +285 -0
- phoenix/datasets/dataset.py +14 -21
- phoenix/datasets/errors.py +4 -1
- phoenix/datasets/schema.py +1 -1
- phoenix/datetime_utils.py +87 -0
- phoenix/experimental/callbacks/__init__.py +0 -0
- phoenix/experimental/callbacks/langchain_tracer.py +228 -0
- phoenix/experimental/callbacks/llama_index_trace_callback_handler.py +364 -0
- phoenix/experimental/evals/__init__.py +33 -0
- phoenix/experimental/evals/functions/__init__.py +4 -0
- phoenix/experimental/evals/functions/binary.py +156 -0
- phoenix/experimental/evals/functions/common.py +31 -0
- phoenix/experimental/evals/functions/generate.py +50 -0
- phoenix/experimental/evals/models/__init__.py +4 -0
- phoenix/experimental/evals/models/base.py +130 -0
- phoenix/experimental/evals/models/openai.py +128 -0
- phoenix/experimental/evals/retrievals.py +2 -2
- phoenix/experimental/evals/templates/__init__.py +24 -0
- phoenix/experimental/evals/templates/default_templates.py +126 -0
- phoenix/experimental/evals/templates/template.py +107 -0
- phoenix/experimental/evals/utils/__init__.py +0 -0
- phoenix/experimental/evals/utils/downloads.py +33 -0
- phoenix/experimental/evals/utils/threads.py +27 -0
- phoenix/experimental/evals/utils/types.py +9 -0
- phoenix/experimental/evals/utils.py +33 -0
- phoenix/metrics/binning.py +0 -1
- phoenix/metrics/timeseries.py +2 -3
- phoenix/server/api/context.py +2 -0
- phoenix/server/api/input_types/SpanSort.py +60 -0
- phoenix/server/api/schema.py +85 -4
- phoenix/server/api/types/DataQualityMetric.py +10 -1
- phoenix/server/api/types/Dataset.py +2 -4
- phoenix/server/api/types/DatasetInfo.py +10 -0
- phoenix/server/api/types/ExportEventsMutation.py +4 -1
- phoenix/server/api/types/Functionality.py +15 -0
- phoenix/server/api/types/MimeType.py +16 -0
- phoenix/server/api/types/Model.py +3 -5
- phoenix/server/api/types/SortDir.py +13 -0
- phoenix/server/api/types/Span.py +229 -0
- phoenix/server/api/types/TimeSeries.py +9 -2
- phoenix/server/api/types/pagination.py +2 -0
- phoenix/server/app.py +24 -4
- phoenix/server/main.py +60 -24
- phoenix/server/span_handler.py +39 -0
- phoenix/server/static/index.js +956 -479
- phoenix/server/thread_server.py +10 -2
- phoenix/services.py +39 -16
- phoenix/session/session.py +99 -27
- phoenix/trace/exporter.py +71 -0
- phoenix/trace/filter.py +181 -0
- phoenix/trace/fixtures.py +23 -8
- phoenix/trace/schemas.py +59 -6
- phoenix/trace/semantic_conventions.py +141 -1
- phoenix/trace/span_json_decoder.py +60 -6
- phoenix/trace/span_json_encoder.py +1 -9
- phoenix/trace/trace_dataset.py +100 -8
- phoenix/trace/tracer.py +26 -3
- phoenix/trace/v1/__init__.py +522 -0
- phoenix/trace/v1/trace_pb2.py +52 -0
- phoenix/trace/v1/trace_pb2.pyi +351 -0
- phoenix/core/dimension_data_type.py +0 -6
- phoenix/core/dimension_type.py +0 -9
- {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/WHEEL +0 -0
- {arize_phoenix-0.0.32.dist-info → arize_phoenix-0.0.33.dist-info}/licenses/IP_NOTICE +0 -0
- {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,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.')
|
phoenix/metrics/binning.py
CHANGED
phoenix/metrics/timeseries.py
CHANGED
|
@@ -38,7 +38,7 @@ StopIndex: TypeAlias = int
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def row_interval_from_sorted_time_index(
|
|
41
|
-
time_index: pd.
|
|
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, :]
|
phoenix/server/api/context.py
CHANGED
|
@@ -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)
|
phoenix/server/api/schema.py
CHANGED
|
@@ -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
|
|
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[
|
|
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[
|
|
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
|
|
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
|
|
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,
|