arize-phoenix 3.16.1__py3-none-any.whl → 3.16.2__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.3
2
2
  Name: arize-phoenix
3
- Version: 3.16.1
3
+ Version: 3.16.2
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -17,7 +17,6 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Requires-Python: <3.13,>=3.8
20
- Requires-Dist: ddsketch
21
20
  Requires-Dist: hdbscan>=0.8.33
22
21
  Requires-Dist: jinja2
23
22
  Requires-Dist: numpy
@@ -44,6 +43,8 @@ Requires-Dist: typing-extensions>=4.6; python_version >= '3.12'
44
43
  Requires-Dist: umap-learn
45
44
  Requires-Dist: uvicorn
46
45
  Requires-Dist: wrapt
46
+ Provides-Extra: container
47
+ Requires-Dist: prometheus-client; extra == 'container'
47
48
  Provides-Extra: dev
48
49
  Requires-Dist: anthropic; extra == 'dev'
49
50
  Requires-Dist: arize[autoembeddings,llm-evaluation]; extra == 'dev'
@@ -57,6 +58,7 @@ Requires-Dist: llama-index>=0.10.3; extra == 'dev'
57
58
  Requires-Dist: nbqa; extra == 'dev'
58
59
  Requires-Dist: pandas-stubs<=2.0.2.230605; extra == 'dev'
59
60
  Requires-Dist: pre-commit; extra == 'dev'
61
+ Requires-Dist: prometheus-client; extra == 'dev'
60
62
  Requires-Dist: pytest-asyncio; extra == 'dev'
61
63
  Requires-Dist: pytest-cov; extra == 'dev'
62
64
  Requires-Dist: pytest-lazy-fixture; extra == 'dev'
@@ -4,13 +4,13 @@ phoenix/datetime_utils.py,sha256=D955QLrkgrrSdUM6NyqbCeAu2SMsjhR5rHVQEsVUdng,277
4
4
  phoenix/exceptions.py,sha256=X5k9ipUDfwSCwZB-H5zFJLas86Gf9tAx0W4l5TZxp5k,108
5
5
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
6
  phoenix/services.py,sha256=f6AeyKTuOpy9RCcTCjVH3gx5nYZhbTMFOuv1WSUOB5o,4992
7
- phoenix/version.py,sha256=RQZ-prBEeYUaDo2wVPl3j8O3CLeHe2r0kT17Sc411m0,23
7
+ phoenix/version.py,sha256=6An59m8khxMeeR51SwvjJubBK8eqW_S7vJrujMumRKc,23
8
8
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
10
10
  phoenix/core/model.py,sha256=C-kDATyJEgP-oqYVKOiQM76Ljs66F6VZdT93_b8kTGk,4725
11
11
  phoenix/core/model_schema.py,sha256=lQaTvKS34yurHOJ53YD020uURLfgG3dqKC1NLQftOjA,50222
12
12
  phoenix/core/model_schema_adapter.py,sha256=3GkyzqUST4fYi-Bgs8qAam5hwMCdQRZTDLjZ9Bnzdm4,8268
13
- phoenix/core/project.py,sha256=VjfiWH-51FFzgL4EBGEXduCnoAHICw4I-Frr5b5Swxo,24905
13
+ phoenix/core/project.py,sha256=wtpfifivrQUOb3Tj91wBzk6q2_aKFA1FGjz9tRIVptY,25805
14
14
  phoenix/core/traces.py,sha256=qAlsDmQJhS9Pkl1IFm-jne-8xS7MEqtw5Q1Ohv9TT2w,3432
15
15
  phoenix/datasets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  phoenix/datasets/dataset.py,sha256=scKVZ7zc6Dpc_ntt-pWhzY-KWqOJEwKePuyNnKSVTGE,30515
@@ -55,8 +55,9 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
55
55
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
56
56
  phoenix/pointcloud/umap_parameters.py,sha256=lJsEOrbSuSiqI7g4Yt6xj7kgYxEqoep4ZHWLr6VWBqw,1760
57
57
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- phoenix/server/app.py,sha256=7yGzmItFLoTFa5CBZwz4Qb2VZbVbczh_3qRqTTtkaKw,6888
59
- phoenix/server/main.py,sha256=wrKegfYm-5APU0CqPqpEE_3MUlSgU2LPfpEkYSd870E,9457
58
+ phoenix/server/app.py,sha256=kika5W-3Uy17iqNdRVB-6KdtBEiIrCB_pQRC4LobroI,7170
59
+ phoenix/server/main.py,sha256=zhSu11gEAADD86T2QimmRrIyXlMKrOR2UAkVT_0BlUU,9718
60
+ phoenix/server/prometheus.py,sha256=YQXwXZt3kxXN5JqGKDMH_jI9r4IL23xn6c8Bdrvg15A,2368
60
61
  phoenix/server/thread_server.py,sha256=dP6cm6Cf08jNhDA1TRlVZpziu1YgtPDmaeIJMm725eI,2154
61
62
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
63
  phoenix/server/api/context.py,sha256=wjCzq4QlszKG1iN-xgu5rRLYPqdvTFqX02aFYPipNoQ,512
@@ -127,7 +128,7 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
127
128
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
128
129
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
129
130
  phoenix/server/static/index.css,sha256=KKGpx4iwF91VGRm0YN-4cn8oC-oIqC6HecoPf0x3ZM8,1885
130
- phoenix/server/static/index.js,sha256=ama86RKEPj-mNPhSYzsYXDngLW73aExeCgI3I5ByjO0,3182471
131
+ phoenix/server/static/index.js,sha256=Bcnr7c5XdUC2kyU57-MIsPpNcDfMCNQP1DIYBROQAa0,3182685
131
132
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
132
133
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
134
  phoenix/server/templates/index.html,sha256=lO2wGA5XsftPg03rw_VcyaYf_4vegtlWbIT5ms4fA_c,1982
@@ -154,7 +155,7 @@ phoenix/trace/trace_dataset.py,sha256=RpHIfZLbMmULOIb-fKXJkQLhIdC0sJlAOTjlyJppMY
154
155
  phoenix/trace/utils.py,sha256=7LurVGXn245cjj4MJsc7v6jq4DSJkpK6YGBfIaSywuw,1307
155
156
  phoenix/trace/dsl/__init__.py,sha256=WIQIjJg362XD3s50OsPJJ0xbDsGp41bSv7vDllLrPuA,144
156
157
  phoenix/trace/dsl/filter.py,sha256=paLpcSMnHdgCfcvcroaqOoCe2retAZ5ocp_5cNTnv9s,14167
157
- phoenix/trace/dsl/helpers.py,sha256=TG8EFZAjvRwjXpxitEGAc4QpF3vn4jpqhI_Tcwp5mE4,2134
158
+ phoenix/trace/dsl/helpers.py,sha256=urPNmk_D2ZpoKxYWS5DQsQt0nb38O0CoG3wNjOit-yM,2610
158
159
  phoenix/trace/dsl/missing.py,sha256=BWPOHr2_tBkPDgVeq8GVXXVbNbJiBelu4NtwHBg6mTE,1435
159
160
  phoenix/trace/dsl/query.py,sha256=k0guhWBEo6L7ZJH5FJs2-iGSnWXdUUqu09gd-8M4CGg,14783
160
161
  phoenix/trace/langchain/__init__.py,sha256=F37GfD1pd5Kuw7R7iRUM1zXXpO8xEcycNZh5dwqBXNk,109
@@ -171,8 +172,8 @@ phoenix/utilities/error_handling.py,sha256=7b5rpGFj9EWZ8yrZK1IHvxB89suWk3lggDayU
171
172
  phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,222
172
173
  phoenix/utilities/project.py,sha256=qWsvKnG1oKhOFUowXf9qiOL2ia7jaFe_ijFFHEt8GJo,431
173
174
  phoenix/utilities/span_store.py,sha256=13UK0rE4wQd70yl___WsDRnH0ru-xErng9_Ml7zfEwE,978
174
- arize_phoenix-3.16.1.dist-info/METADATA,sha256=k1zzlIBTgts8dU1rEDWSEDBP8_kondb0f81P0ipb9R4,29181
175
- arize_phoenix-3.16.1.dist-info/WHEEL,sha256=bq9SyP5NxIRA9EpQgMCd-9RmPHWvbH-4lTDGwxgIR64,87
176
- arize_phoenix-3.16.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
177
- arize_phoenix-3.16.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
178
- arize_phoenix-3.16.1.dist-info/RECORD,,
175
+ arize_phoenix-3.16.2.dist-info/METADATA,sha256=woqWRuz22Oi3v7sZupUCnEuztlm4ahUYgjEwJADYOwM,29287
176
+ arize_phoenix-3.16.2.dist-info/WHEEL,sha256=bq9SyP5NxIRA9EpQgMCd-9RmPHWvbH-4lTDGwxgIR64,87
177
+ arize_phoenix-3.16.2.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
178
+ arize_phoenix-3.16.2.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
179
+ arize_phoenix-3.16.2.dist-info/RECORD,,
phoenix/core/project.py CHANGED
@@ -20,7 +20,6 @@ from typing import (
20
20
  )
21
21
 
22
22
  import numpy as np
23
- from ddsketch import DDSketch
24
23
  from google.protobuf.json_format import MessageToDict
25
24
  from openinference.semconv.trace import SpanAttributes
26
25
  from pandas import DataFrame, Index, MultiIndex
@@ -216,10 +215,15 @@ class _Spans:
216
215
  self._start_time_sorted_root_spans: SortedKeyList[WrappedSpan] = SortedKeyList(
217
216
  key=lambda span: span.start_time,
218
217
  )
218
+ """
219
+ A root span is defined to be a span whose parent span is not in our collection.
220
+ This includes spans whose parent is None and spans whose parent has not arrived
221
+ (or will not arrive). For spans whose parent is not None, the root span status
222
+ is temporary and will be revoked when its parent span arrives.
223
+ """
219
224
  self._latency_sorted_root_spans: SortedKeyList[WrappedSpan] = SortedKeyList(
220
225
  key=lambda span: span[ComputedAttributes.LATENCY_MS],
221
226
  )
222
- self._root_span_latency_ms_sketch = DDSketch()
223
227
  self._token_count_total: int = 0
224
228
  self._last_updated_at: Optional[datetime] = None
225
229
 
@@ -285,7 +289,15 @@ class _Spans:
285
289
  def root_span_latency_ms_quantiles(self, probability: float) -> Optional[float]:
286
290
  """Root span latency quantiles in milliseconds"""
287
291
  with self._lock:
288
- return self._root_span_latency_ms_sketch.get_quantile_value(probability)
292
+ spans = self._latency_sorted_root_spans
293
+ if not (n := len(spans)):
294
+ return None
295
+ if probability >= 1:
296
+ return cast(float, spans[-1][ComputedAttributes.LATENCY_MS])
297
+ if probability <= 0:
298
+ return cast(float, spans[0][ComputedAttributes.LATENCY_MS])
299
+ k = max(0, round(n * probability) - 1)
300
+ return cast(float, spans[k][ComputedAttributes.LATENCY_MS])
289
301
 
290
302
  def get_descendant_spans(self, span_id: SpanID) -> Iterator[WrappedSpan]:
291
303
  for span in self._get_descendant_spans(span_id):
@@ -353,26 +365,27 @@ class _Spans:
353
365
  return
354
366
 
355
367
  parent_span_id = span.parent_id
356
- is_root_span = parent_span_id is None
357
- if not is_root_span:
368
+ if parent_span_id is not None:
358
369
  self._child_spans[parent_span_id].add(span)
359
370
  self._parent_span_ids[span_id] = parent_span_id
360
371
 
372
+ for child_span in self._child_spans.get(span_id, ()):
373
+ # A root span is a span whose parent span is not in our collection.
374
+ # Now that their parent span has arrived, they are no longer root spans.
375
+ self._start_time_sorted_root_spans.remove(child_span)
376
+ self._latency_sorted_root_spans.remove(child_span)
377
+
361
378
  # Add computed attributes to span
362
379
  start_time = span.start_time
363
380
  end_time = span.end_time
364
- span[ComputedAttributes.LATENCY_MS] = latency = (
365
- end_time - start_time
366
- ).total_seconds() * 1000
367
- if is_root_span:
368
- self._root_span_latency_ms_sketch.add(latency)
381
+ span[ComputedAttributes.LATENCY_MS] = (end_time - start_time).total_seconds() * 1000
369
382
  span[ComputedAttributes.ERROR_COUNT] = int(span.status_code is SpanStatusCode.ERROR)
370
383
 
371
384
  # Store the new span (after adding computed attributes)
372
385
  self._spans[span_id] = span
373
386
  self._traces[span.context.trace_id].add(span)
374
387
  self._start_time_sorted_spans.add(span)
375
- if is_root_span:
388
+ if parent_span_id is None or parent_span_id not in self._spans:
376
389
  self._start_time_sorted_root_spans.add(span)
377
390
  self._latency_sorted_root_spans.add(span)
378
391
  self._propagate_cumulative_values(span)
phoenix/server/app.py CHANGED
@@ -151,6 +151,7 @@ def create_app(
151
151
  span_store: Optional[SpanStore] = None,
152
152
  debug: bool = False,
153
153
  read_only: bool = False,
154
+ enable_prometheus: bool = False,
154
155
  ) -> Starlette:
155
156
  graphql = GraphQLWithContext(
156
157
  schema=schema,
@@ -160,9 +161,16 @@ def create_app(
160
161
  export_path=export_path,
161
162
  graphiql=True,
162
163
  )
164
+ if enable_prometheus:
165
+ from phoenix.server.prometheus import PrometheusMiddleware
166
+
167
+ prometheus_middlewares = [Middleware(PrometheusMiddleware)]
168
+ else:
169
+ prometheus_middlewares = []
163
170
  return Starlette(
164
171
  middleware=[
165
172
  Middleware(HeadersMiddleware),
173
+ *prometheus_middlewares,
166
174
  ],
167
175
  debug=debug,
168
176
  routes=(
phoenix/server/main.py CHANGED
@@ -129,6 +129,7 @@ if __name__ == "__main__":
129
129
  parser.add_argument("--no-internet", action="store_true")
130
130
  parser.add_argument("--umap_params", type=str, required=False, default=DEFAULT_UMAP_PARAMS_STR)
131
131
  parser.add_argument("--debug", action="store_false")
132
+ parser.add_argument("--enable-prometheus", type=bool, default=False)
132
133
  subparsers = parser.add_subparsers(dest="command", required=True)
133
134
  serve_parser = subparsers.add_parser("serve")
134
135
  datasets_parser = subparsers.add_parser("datasets")
@@ -223,6 +224,10 @@ if __name__ == "__main__":
223
224
  )
224
225
  read_only = args.read_only
225
226
  logger.info(f"Server umap params: {umap_params}")
227
+ if enable_prometheus := args.enable_prometheus:
228
+ from phoenix.server.prometheus import start_prometheus
229
+
230
+ start_prometheus()
226
231
  app = create_app(
227
232
  export_path=export_path,
228
233
  model=model,
@@ -232,6 +237,7 @@ if __name__ == "__main__":
232
237
  debug=args.debug,
233
238
  read_only=read_only,
234
239
  span_store=span_store,
240
+ enable_prometheus=enable_prometheus,
235
241
  )
236
242
  host = args.host or get_env_host()
237
243
  port = args.port or get_env_port()
@@ -0,0 +1,75 @@
1
+ import time
2
+ from threading import Thread
3
+
4
+ import psutil
5
+ from prometheus_client import (
6
+ Counter,
7
+ Gauge,
8
+ Histogram,
9
+ start_http_server,
10
+ )
11
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
12
+ from starlette.requests import Request
13
+ from starlette.responses import Response
14
+ from starlette.routing import Match
15
+
16
+ REQUESTS_PROCESSING_TIME = Histogram(
17
+ name="starlette_requests_processing_time_seconds",
18
+ documentation="Histogram of requests processing time by method and path (in seconds)",
19
+ labelnames=["method", "path"],
20
+ )
21
+ EXCEPTIONS = Counter(
22
+ name="starlette_exceptions_total",
23
+ documentation="Total count of exceptions raised by method, path and exception type",
24
+ labelnames=["method", "path", "exception_type"],
25
+ )
26
+ RAM_METRIC = Gauge(
27
+ name="memory_usage_bytes",
28
+ documentation="Memory usage in bytes",
29
+ labelnames=["type"],
30
+ )
31
+ CPU_METRIC = Gauge(
32
+ name="cpu_usage_percent",
33
+ documentation="CPU usage percent",
34
+ labelnames=["core"],
35
+ )
36
+
37
+
38
+ class PrometheusMiddleware(BaseHTTPMiddleware):
39
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
40
+ for route in request.app.routes:
41
+ match, _ = route.matches(request.scope)
42
+ if match is Match.FULL:
43
+ path = route.path
44
+ break
45
+ else:
46
+ return await call_next(request)
47
+ method = request.method
48
+ start_time = time.perf_counter()
49
+ try:
50
+ response = await call_next(request)
51
+ except BaseException as e:
52
+ EXCEPTIONS.labels(method=method, path=path, exception_type=type(e).__name__).inc()
53
+ raise
54
+ stop_time = time.perf_counter()
55
+ REQUESTS_PROCESSING_TIME.labels(method=method, path=path).observe(stop_time - start_time)
56
+ return response
57
+
58
+
59
+ def start_prometheus() -> None:
60
+ Thread(target=gather_system_data, daemon=True).start()
61
+ start_http_server(9090)
62
+
63
+
64
+ def gather_system_data() -> None:
65
+ while True:
66
+ time.sleep(1)
67
+
68
+ ram = psutil.virtual_memory()
69
+ swap = psutil.swap_memory()
70
+
71
+ RAM_METRIC.labels(type="virtual").set(ram.used)
72
+ RAM_METRIC.labels(type="swap").set(swap.used)
73
+
74
+ for core, percent in enumerate(psutil.cpu_percent(interval=1, percpu=True)):
75
+ CPU_METRIC.labels(core=core).set(percent)