arize-phoenix 3.4.0__py3-none-any.whl → 3.5.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 (39) hide show
  1. {arize_phoenix-3.4.0.dist-info → arize_phoenix-3.5.0.dist-info}/METADATA +31 -31
  2. {arize_phoenix-3.4.0.dist-info → arize_phoenix-3.5.0.dist-info}/RECORD +39 -38
  3. phoenix/core/evals.py +9 -9
  4. phoenix/core/model.py +18 -18
  5. phoenix/core/model_schema.py +22 -44
  6. phoenix/experimental/evals/functions/executor.py +1 -2
  7. phoenix/experimental/evals/functions/processing.py +33 -1
  8. phoenix/experimental/evals/models/base.py +4 -8
  9. phoenix/experimental/evals/models/litellm.py +1 -1
  10. phoenix/experimental/evals/models/rate_limiters.py +1 -2
  11. phoenix/metrics/__init__.py +2 -4
  12. phoenix/metrics/binning.py +3 -6
  13. phoenix/metrics/mixins.py +1 -0
  14. phoenix/metrics/wrappers.py +1 -0
  15. phoenix/pointcloud/pointcloud.py +2 -4
  16. phoenix/server/api/input_types/SpanSort.py +1 -2
  17. phoenix/server/api/interceptor.py +1 -2
  18. phoenix/server/api/routers/trace_handler.py +1 -2
  19. phoenix/server/api/schema.py +20 -3
  20. phoenix/server/api/types/Project.py +72 -0
  21. phoenix/server/api/types/Segments.py +2 -4
  22. phoenix/server/api/types/Span.py +18 -0
  23. phoenix/server/app.py +4 -0
  24. phoenix/server/main.py +35 -2
  25. phoenix/server/static/index.js +534 -494
  26. phoenix/server/templates/index.html +2 -1
  27. phoenix/session/data_extractor.py +2 -4
  28. phoenix/session/evaluation.py +1 -0
  29. phoenix/trace/dsl/filter.py +1 -2
  30. phoenix/trace/dsl/helpers.py +3 -2
  31. phoenix/trace/dsl/query.py +3 -7
  32. phoenix/trace/langchain/tracer.py +1 -0
  33. phoenix/trace/span_evaluations.py +1 -2
  34. phoenix/trace/span_json_encoder.py +13 -3
  35. phoenix/trace/tracer.py +2 -2
  36. phoenix/version.py +1 -1
  37. {arize_phoenix-3.4.0.dist-info → arize_phoenix-3.5.0.dist-info}/WHEEL +0 -0
  38. {arize_phoenix-3.4.0.dist-info → arize_phoenix-3.5.0.dist-info}/licenses/IP_NOTICE +0 -0
  39. {arize_phoenix-3.4.0.dist-info → arize_phoenix-3.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -134,8 +134,7 @@ class AdaptiveTokenBucket:
134
134
  continue
135
135
 
136
136
 
137
- class RateLimitError(PhoenixException):
138
- ...
137
+ class RateLimitError(PhoenixException): ...
139
138
 
140
139
 
141
140
  class RateLimiter:
@@ -33,12 +33,10 @@ class Metric(ABC):
33
33
  return self.initial_value
34
34
 
35
35
  @abstractmethod
36
- def calc(self, dataframe: pd.DataFrame) -> Any:
37
- ...
36
+ def calc(self, dataframe: pd.DataFrame) -> Any: ...
38
37
 
39
38
  @abstractmethod
40
- def operands(self) -> List[Column]:
41
- ...
39
+ def operands(self) -> List[Column]: ...
42
40
 
43
41
  def __call__(
44
42
  self,
@@ -25,8 +25,7 @@ class BinningMethod(ABC):
25
25
  missing values will be grouped into a bin of their own)"""
26
26
 
27
27
  @abstractmethod
28
- def histogram(self, data: "pd.Series[Any]") -> Histogram:
29
- ...
28
+ def histogram(self, data: "pd.Series[Any]") -> Histogram: ...
30
29
 
31
30
  @abstractmethod
32
31
  def segmented_summary(
@@ -34,8 +33,7 @@ class BinningMethod(ABC):
34
33
  group_by: Column,
35
34
  dataframe: pd.DataFrame,
36
35
  metrics: Iterable[Metric],
37
- ) -> pd.DataFrame:
38
- ...
36
+ ) -> pd.DataFrame: ...
39
37
 
40
38
 
41
39
  NumericBin: TypeAlias = "pd.Interval[float]"
@@ -296,8 +294,7 @@ class Normalizer(ABC):
296
294
  """A function that normalizes counts/frequencies to probabilities."""
297
295
 
298
296
  @abstractmethod
299
- def __call__(self, counts: Histogram) -> Distribution:
300
- ...
297
+ def __call__(self, counts: Histogram) -> Distribution: ...
301
298
 
302
299
 
303
300
  @dataclass(frozen=True)
phoenix/metrics/mixins.py CHANGED
@@ -3,6 +3,7 @@ Mixins are behavioral building blocks of metrics. All metrics inherit from
3
3
  BaseMetric. Other mixins provide specialized functionalities. Mixins rely
4
4
  on cooperative multiple inheritance and method resolution order in Python.
5
5
  """
6
+
6
7
  import collections
7
8
  import inspect
8
9
  from abc import ABC, abstractmethod
@@ -12,6 +12,7 @@ function, the wrappers need to perform a variety of preprocessing steps, e.g.
12
12
  So, after the missing values are removed, we still need to coerce the dtype
13
13
  to that of the first non-missing value in the series.
14
14
  """
15
+
15
16
  import inspect
16
17
  from abc import ABC
17
18
  from enum import Enum
@@ -14,13 +14,11 @@ RowIndex: TypeAlias = int
14
14
 
15
15
 
16
16
  class DimensionalityReducer(Protocol):
17
- def project(self, mat: Matrix, n_components: int) -> Matrix:
18
- ...
17
+ def project(self, mat: Matrix, n_components: int) -> Matrix: ...
19
18
 
20
19
 
21
20
  class ClustersFinder(Protocol):
22
- def find_clusters(self, mat: Matrix) -> List[RawCluster]:
23
- ...
21
+ def find_clusters(self, mat: Matrix) -> List[RawCluster]: ...
24
22
 
25
23
 
26
24
  @dataclass(frozen=True)
@@ -43,8 +43,7 @@ class EvalResultKey:
43
43
 
44
44
 
45
45
  class SupportsGetSpanEvaluation(Protocol):
46
- def get_span_evaluation(self, span_id: SpanID, name: str) -> Optional[pb.Evaluation]:
47
- ...
46
+ def get_span_evaluation(self, span_id: SpanID, name: str) -> Optional[pb.Evaluation]: ...
48
47
 
49
48
 
50
49
  @strawberry.input(
@@ -18,8 +18,7 @@ class Interceptor(ABC):
18
18
  return self if obj is None else getattr(obj, self._name)
19
19
 
20
20
  @abstractmethod
21
- def __set__(self, obj: Any, value: Any) -> None:
22
- ...
21
+ def __set__(self, obj: Any, value: Any) -> None: ...
23
22
 
24
23
 
25
24
  class GqlValueMediator(Interceptor):
@@ -15,8 +15,7 @@ from starlette.status import HTTP_415_UNSUPPORTED_MEDIA_TYPE, HTTP_422_UNPROCESS
15
15
 
16
16
 
17
17
  class SupportsPutSpan(Protocol):
18
- def put(self, span: Span) -> None:
19
- ...
18
+ def put(self, span: Span) -> None: ...
20
19
 
21
20
 
22
21
  class TraceHandler(HTTPEndpoint):
@@ -20,6 +20,7 @@ from phoenix.server.api.input_types.Coordinates import (
20
20
  )
21
21
  from phoenix.server.api.input_types.SpanSort import SpanSort
22
22
  from phoenix.server.api.types.Cluster import Cluster, to_gql_clusters
23
+ from phoenix.server.api.types.Project import Project
23
24
  from phoenix.trace.dsl import SpanFilter
24
25
  from phoenix.trace.schemas import SpanID, TraceID
25
26
 
@@ -48,6 +49,22 @@ from .types.ValidationResult import ValidationResult
48
49
 
49
50
  @strawberry.type
50
51
  class Query:
52
+ @strawberry.field
53
+ def projects(
54
+ self,
55
+ first: Optional[int] = 50,
56
+ last: Optional[int] = UNSET,
57
+ after: Optional[Cursor] = UNSET,
58
+ before: Optional[Cursor] = UNSET,
59
+ ) -> Connection[Project]:
60
+ args = ConnectionArgs(
61
+ first=first,
62
+ after=after if isinstance(after, Cursor) else None,
63
+ last=last,
64
+ before=before if isinstance(before, Cursor) else None,
65
+ )
66
+ return connection_from_list(data=[Project(id_attr=0, name="default")], args=args)
67
+
51
68
  @strawberry.field
52
69
  def functionality(self, info: Info[Context, None]) -> "Functionality":
53
70
  has_model_inferences = not info.context.model.is_empty
@@ -70,7 +87,8 @@ class Query:
70
87
  elif type_name == "EmbeddingDimension":
71
88
  embedding_dimension = info.context.model.embedding_dimensions[node_id]
72
89
  return to_gql_embedding_dimension(node_id, embedding_dimension)
73
-
90
+ elif type_name == "Project":
91
+ return Project(id_attr=0, name="default") # TODO: implement ID
74
92
  raise Exception(f"Unknown node type: {type}")
75
93
 
76
94
  @strawberry.field
@@ -431,8 +449,7 @@ class Query:
431
449
 
432
450
 
433
451
  @strawberry.type
434
- class Mutation(ExportEventsMutation):
435
- ...
452
+ class Mutation(ExportEventsMutation): ...
436
453
 
437
454
 
438
455
  schema = strawberry.Schema(query=Query, mutation=Mutation)
@@ -0,0 +1,72 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import strawberry
5
+ from strawberry.types import Info
6
+
7
+ from phoenix.server.api.context import Context
8
+
9
+ from .node import Node
10
+
11
+
12
+ @strawberry.type
13
+ class Project(Node):
14
+ name: str
15
+
16
+ @strawberry.field
17
+ def start_time(
18
+ self,
19
+ info: Info[Context, None],
20
+ ) -> Optional[datetime]:
21
+ if (traces := info.context.traces) is None:
22
+ return None
23
+ start_time, _ = traces.right_open_time_range
24
+ return start_time
25
+
26
+ @strawberry.field
27
+ def end_time(
28
+ self,
29
+ info: Info[Context, None],
30
+ ) -> Optional[datetime]:
31
+ if (traces := info.context.traces) is None:
32
+ return None
33
+ _, end_time = traces.right_open_time_range
34
+ return end_time
35
+
36
+ @strawberry.field
37
+ def record_count(
38
+ self,
39
+ info: Info[Context, None],
40
+ ) -> int:
41
+ if (traces := info.context.traces) is None:
42
+ return 0
43
+ return traces.span_count
44
+
45
+ @strawberry.field
46
+ def token_count_total(
47
+ self,
48
+ info: Info[Context, None],
49
+ ) -> int:
50
+ if (traces := info.context.traces) is None:
51
+ return 0
52
+ return traces.token_count_total
53
+
54
+ @strawberry.field
55
+ def latency_ms_p50(
56
+ self,
57
+ info: Info[Context, None],
58
+ ) -> Optional[float]:
59
+ if (traces := info.context.traces) is None:
60
+ return None
61
+ (latency,) = traces.root_span_latency_ms_quantiles(0.50)
62
+ return latency
63
+
64
+ @strawberry.field
65
+ def latency_ms_p99(
66
+ self,
67
+ info: Info[Context, None],
68
+ ) -> Optional[float]:
69
+ if (traces := info.context.traces) is None:
70
+ return None
71
+ (latency,) = traces.root_span_latency_ms_quantiles(0.99)
72
+ return latency
@@ -38,12 +38,10 @@ class GqlBinFactory:
38
38
  numeric_ubound: float = np.inf
39
39
 
40
40
  @overload
41
- def __call__(self, bin: "pd.Interval[float]") -> IntervalBin:
42
- ...
41
+ def __call__(self, bin: "pd.Interval[float]") -> IntervalBin: ...
43
42
 
44
43
  @overload
45
- def __call__(self, bin: Union[str, int, float]) -> Union[NominalBin, MissingValueBin]:
46
- ...
44
+ def __call__(self, bin: Union[str, int, float]) -> Union[NominalBin, MissingValueBin]: ...
47
45
 
48
46
  def __call__(self, bin: Any) -> Union[NominalBin, IntervalBin, MissingValueBin]:
49
47
  if isinstance(bin, pd.Interval):
@@ -24,6 +24,7 @@ INPUT_VALUE = SpanAttributes.INPUT_VALUE
24
24
  LLM_TOKEN_COUNT_COMPLETION = SpanAttributes.LLM_TOKEN_COUNT_COMPLETION
25
25
  LLM_TOKEN_COUNT_PROMPT = SpanAttributes.LLM_TOKEN_COUNT_PROMPT
26
26
  LLM_TOKEN_COUNT_TOTAL = SpanAttributes.LLM_TOKEN_COUNT_TOTAL
27
+ METADATA = SpanAttributes.METADATA
27
28
  OUTPUT_MIME_TYPE = SpanAttributes.OUTPUT_MIME_TYPE
28
29
  OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE
29
30
  RETRIEVAL_DOCUMENTS = SpanAttributes.RETRIEVAL_DOCUMENTS
@@ -107,6 +108,9 @@ class Span:
107
108
  attributes: str = strawberry.field(
108
109
  description="Span attributes as a JSON string",
109
110
  )
111
+ metadata: Optional[str] = strawberry.field(
112
+ description="Metadata as a JSON string",
113
+ )
110
114
  num_documents: Optional[int]
111
115
  token_count_total: Optional[int]
112
116
  token_count_prompt: Optional[int]
@@ -240,6 +244,7 @@ def to_gql_span(span: trace_schema.Span) -> "Span":
240
244
  _nested_attributes(_hide_embedding_vectors(span.attributes)),
241
245
  default=_json_encode,
242
246
  ),
247
+ metadata=_convert_metadata_to_string(span.attributes.get(METADATA)),
243
248
  num_documents=num_documents,
244
249
  token_count_total=cast(
245
250
  Optional[int],
@@ -327,3 +332,16 @@ def _hide_embedding_vectors(
327
332
  _embeddings.append(_embedding)
328
333
  _attributes[EMBEDDING_EMBEDDINGS] = _embeddings
329
334
  return _attributes
335
+
336
+
337
+ def _convert_metadata_to_string(metadata: Any) -> Optional[str]:
338
+ """
339
+ Converts metadata to a string representation.
340
+ """
341
+
342
+ if metadata is None or isinstance(metadata, str):
343
+ return metadata
344
+ try:
345
+ return json.dumps(metadata)
346
+ except Exception:
347
+ return str(metadata)
phoenix/server/app.py CHANGED
@@ -36,6 +36,8 @@ templates = Jinja2Templates(directory=SERVER_DIR / "templates")
36
36
 
37
37
 
38
38
  class AppConfig(NamedTuple):
39
+ has_inferences: bool
40
+ """ Whether the model has inferences (e.g. a primary dataset) """
39
41
  has_corpus: bool
40
42
  min_dist: float
41
43
  n_neighbors: int
@@ -64,6 +66,7 @@ class Static(StaticFiles):
64
66
  response = templates.TemplateResponse(
65
67
  "index.html",
66
68
  context={
69
+ "has_inferences": self._app_config.has_inferences,
67
70
  "has_corpus": self._app_config.has_corpus,
68
71
  "min_dist": self._app_config.min_dist,
69
72
  "n_neighbors": self._app_config.n_neighbors,
@@ -209,6 +212,7 @@ def create_app(
209
212
  app=Static(
210
213
  directory=SERVER_DIR / "static",
211
214
  app_config=AppConfig(
215
+ has_inferences=model.is_empty is not True,
212
216
  has_corpus=corpus is not None,
213
217
  min_dist=umap_params.min_dist,
214
218
  n_neighbors=umap_params.n_neighbors,
phoenix/server/main.py CHANGED
@@ -8,6 +8,7 @@ from threading import Thread
8
8
  from time import sleep, time
9
9
  from typing import Iterable, Optional, Protocol, TypeVar
10
10
 
11
+ import pkg_resources
11
12
  from uvicorn import Config, Server
12
13
 
13
14
  from phoenix.config import EXPORT_DIR, get_env_host, get_env_port, get_pids_path
@@ -34,6 +35,31 @@ from phoenix.trace.span_json_decoder import json_string_to_span
34
35
 
35
36
  logger = logging.getLogger(__name__)
36
37
 
38
+ _WELCOME_MESSAGE = """
39
+
40
+ ██████╗ ██╗ ██╗ ██████╗ ███████╗███╗ ██╗██╗██╗ ██╗
41
+ ██╔══██╗██║ ██║██╔═══██╗██╔════╝████╗ ██║██║╚██╗██╔╝
42
+ ██████╔╝███████║██║ ██║█████╗ ██╔██╗ ██║██║ ╚███╔╝
43
+ ██╔═══╝ ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔██╗
44
+ ██║ ██║ ██║╚██████╔╝███████╗██║ ╚████║██║██╔╝ ██╗
45
+ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝ v{0}
46
+
47
+ |
48
+ | 🌎 Join our Community 🌎
49
+ | https://join.slack.com/t/arize-ai/shared_invite/zt-1px8dcmlf-fmThhDFD_V_48oU7ALan4Q
50
+ |
51
+ | ⭐️ Leave us a Star ⭐️
52
+ | https://github.com/Arize-ai/phoenix
53
+ |
54
+ | 📚 Documentation 📚
55
+ | https://docs.arize.com/phoenix
56
+ |
57
+ | 🚀 Phoenix Server 🚀
58
+ | Phoenix UI: http://{1}:{2}
59
+ | Log traces: /v1/traces over HTTP
60
+ |
61
+ """
62
+
37
63
 
38
64
  def _write_pid_file_when_ready(
39
65
  server: Server,
@@ -60,8 +86,7 @@ _Item = TypeVar("_Item", contravariant=True)
60
86
 
61
87
 
62
88
  class _SupportsPut(Protocol[_Item]):
63
- def put(self, item: _Item) -> None:
64
- ...
89
+ def put(self, item: _Item) -> None: ...
65
90
 
66
91
 
67
92
  def _load_items(
@@ -202,4 +227,12 @@ if __name__ == "__main__":
202
227
  port = args.port or get_env_port()
203
228
  server = Server(config=Config(app, host=host, port=port))
204
229
  Thread(target=_write_pid_file_when_ready, args=(server,), daemon=True).start()
230
+
231
+ # Print information about the server
232
+ phoenix_version = pkg_resources.get_distribution("arize-phoenix").version
233
+ print(
234
+ _WELCOME_MESSAGE.format(phoenix_version, host if host != "0.0.0.0" else "localhost", port)
235
+ )
236
+
237
+ # Start the server
205
238
  server.run()