arize-phoenix 0.0.32__tar.gz → 0.0.33__tar.gz
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 → arize_phoenix-0.0.33}/.gitignore +3 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/PKG-INFO +11 -5
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/pyproject.toml +47 -15
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/__init__.py +3 -1
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/config.py +23 -1
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/core/model_schema.py +14 -37
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/core/model_schema_adapter.py +0 -1
- arize_phoenix-0.0.33/src/phoenix/core/traces.py +285 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/dataset.py +14 -21
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/errors.py +4 -1
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/schema.py +1 -1
- arize_phoenix-0.0.33/src/phoenix/datetime_utils.py +87 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/callbacks/langchain_tracer.py +228 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/callbacks/llama_index_trace_callback_handler.py +364 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/__init__.py +33 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/functions/__init__.py +4 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/functions/binary.py +156 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/functions/common.py +31 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/functions/generate.py +50 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/models/__init__.py +4 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/models/base.py +130 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/models/openai.py +128 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/experimental/evals/retrievals.py +2 -2
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/templates/__init__.py +24 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/templates/default_templates.py +126 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/templates/template.py +107 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/utils/downloads.py +33 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/utils/threads.py +27 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/utils/types.py +9 -0
- arize_phoenix-0.0.33/src/phoenix/experimental/evals/utils.py +33 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/binning.py +0 -1
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/timeseries.py +2 -3
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/context.py +2 -0
- arize_phoenix-0.0.33/src/phoenix/server/api/input_types/SpanSort.py +60 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/schema.py +85 -4
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DataQualityMetric.py +10 -1
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Dataset.py +2 -4
- arize_phoenix-0.0.33/src/phoenix/server/api/types/DatasetInfo.py +10 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/ExportEventsMutation.py +4 -1
- arize_phoenix-0.0.33/src/phoenix/server/api/types/Functionality.py +15 -0
- arize_phoenix-0.0.33/src/phoenix/server/api/types/MimeType.py +16 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Model.py +3 -5
- arize_phoenix-0.0.33/src/phoenix/server/api/types/SortDir.py +13 -0
- arize_phoenix-0.0.33/src/phoenix/server/api/types/Span.py +229 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/TimeSeries.py +9 -2
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/pagination.py +2 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/app.py +24 -4
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/main.py +60 -24
- arize_phoenix-0.0.33/src/phoenix/server/span_handler.py +39 -0
- arize_phoenix-0.0.33/src/phoenix/server/static/index.js +6712 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/thread_server.py +10 -2
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/services.py +39 -16
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/session/session.py +99 -27
- arize_phoenix-0.0.33/src/phoenix/trace/exporter.py +71 -0
- arize_phoenix-0.0.33/src/phoenix/trace/filter.py +181 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/trace/fixtures.py +23 -8
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/trace/schemas.py +59 -6
- arize_phoenix-0.0.33/src/phoenix/trace/semantic_conventions.py +177 -0
- arize_phoenix-0.0.33/src/phoenix/trace/span_json_decoder.py +108 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/trace/span_json_encoder.py +1 -9
- arize_phoenix-0.0.33/src/phoenix/trace/trace_dataset.py +130 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/trace/tracer.py +26 -3
- arize_phoenix-0.0.33/src/phoenix/trace/v1/__init__.py +522 -0
- arize_phoenix-0.0.33/src/phoenix/trace/v1/trace_pb2.py +52 -0
- arize_phoenix-0.0.33/src/phoenix/trace/v1/trace_pb2.pyi +351 -0
- arize_phoenix-0.0.32/src/phoenix/core/dimension_data_type.py +0 -6
- arize_phoenix-0.0.32/src/phoenix/core/dimension_type.py +0 -9
- arize_phoenix-0.0.32/src/phoenix/server/static/index.js +0 -6235
- arize_phoenix-0.0.32/src/phoenix/trace/semantic_conventions.py +0 -37
- arize_phoenix-0.0.32/src/phoenix/trace/span_json_decoder.py +0 -54
- arize_phoenix-0.0.32/src/phoenix/trace/trace_dataset.py +0 -38
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/IP_NOTICE +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/LICENSE +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/README.md +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/core/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/core/embedding_dimension.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/core/model.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/fixtures.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/datasets/validation.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/experimental/__init__.py +0 -0
- {arize_phoenix-0.0.32/src/phoenix/experimental/evals → arize_phoenix-0.0.33/src/phoenix/experimental/callbacks}/__init__.py +0 -0
- {arize_phoenix-0.0.32/src/phoenix/server → arize_phoenix-0.0.33/src/phoenix/experimental/evals/utils}/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/README.md +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/metrics.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/mixins.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/metrics/wrappers.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/pointcloud/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/pointcloud/clustering.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/pointcloud/pointcloud.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/pointcloud/projectors.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/py.typed +0 -0
- {arize_phoenix-0.0.32/src/phoenix/server/api → arize_phoenix-0.0.33/src/phoenix/server}/__init__.py +0 -0
- {arize_phoenix-0.0.32/src/phoenix/server/api/input_types → arize_phoenix-0.0.33/src/phoenix/server/api}/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/helpers.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/ClusterInput.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/Coordinates.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/DataQualityMetricInput.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/DimensionFilter.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/DimensionInput.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/Granularity.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/PerformanceMetricInput.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/input_types/TimeRange.py +0 -0
- {arize_phoenix-0.0.32/src/phoenix/server/api/types → arize_phoenix-0.0.33/src/phoenix/server/api/input_types}/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/interceptor.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Cluster.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DatasetRole.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DatasetValues.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Dimension.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DimensionDataType.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DimensionShape.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DimensionType.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/DimensionWithValue.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/EmbeddingDimension.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/EmbeddingMetadata.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Event.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/EventMetadata.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/ExportedFile.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/NumericRange.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/PerformanceMetric.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/PromptResponse.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Retrieval.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/ScalarDriftMetricEnum.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/Segments.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/UMAPPoints.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/VectorDriftMetricEnum.py +0 -0
- {arize_phoenix-0.0.32/src/phoenix/session → arize_phoenix-0.0.33/src/phoenix/server/api/types}/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/api/types/node.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-114x114.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-120x120.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-144x144.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-152x152.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-180x180.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-72x72.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon-76x76.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/apple-touch-icon.png +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/favicon.ico +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/index.css +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/index.html +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/server/static/modernizr.js +0 -0
- {arize_phoenix-0.0.32/src/phoenix/trace → arize_phoenix-0.0.33/src/phoenix/session}/__init__.py +0 -0
- /arize_phoenix-0.0.32/src/phoenix/core/traces.py → /arize_phoenix-0.0.33/src/phoenix/trace/__init__.py +0 -0
- {arize_phoenix-0.0.32 → arize_phoenix-0.0.33}/src/phoenix/trace/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: arize-phoenix
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.33
|
|
4
4
|
Summary: ML Observability in your notebook
|
|
5
5
|
Project-URL: Documentation, https://docs.arize.com/phoenix/
|
|
6
6
|
Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
|
|
@@ -19,13 +19,14 @@ Requires-Python: <3.12,>=3.8
|
|
|
19
19
|
Requires-Dist: hdbscan<1.0.0,>=0.8.33
|
|
20
20
|
Requires-Dist: numpy
|
|
21
21
|
Requires-Dist: pandas
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: protobuf<5.0,>=3.20
|
|
23
23
|
Requires-Dist: psutil
|
|
24
24
|
Requires-Dist: pyarrow
|
|
25
25
|
Requires-Dist: scikit-learn<1.3.0
|
|
26
26
|
Requires-Dist: scipy
|
|
27
|
+
Requires-Dist: sortedcontainers
|
|
27
28
|
Requires-Dist: starlette
|
|
28
|
-
Requires-Dist: strawberry-graphql==0.
|
|
29
|
+
Requires-Dist: strawberry-graphql==0.205.0
|
|
29
30
|
Requires-Dist: typing-extensions
|
|
30
31
|
Requires-Dist: umap-learn
|
|
31
32
|
Requires-Dist: uvicorn
|
|
@@ -33,15 +34,20 @@ Requires-Dist: wrapt
|
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: arize[autoembeddings,llm-evaluation]; extra == 'dev'
|
|
35
36
|
Requires-Dist: black[jupyter]; extra == 'dev'
|
|
37
|
+
Requires-Dist: gcsfs; extra == 'dev'
|
|
36
38
|
Requires-Dist: hatch; extra == 'dev'
|
|
37
39
|
Requires-Dist: jupyter; extra == 'dev'
|
|
38
|
-
Requires-Dist:
|
|
40
|
+
Requires-Dist: nbqa; extra == 'dev'
|
|
41
|
+
Requires-Dist: pandas-stubs<=2.0.2.230605; extra == 'dev'
|
|
39
42
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
40
43
|
Requires-Dist: pytest; extra == 'dev'
|
|
41
44
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
42
45
|
Requires-Dist: pytest-lazy-fixture; extra == 'dev'
|
|
43
|
-
Requires-Dist:
|
|
46
|
+
Requires-Dist: ruff==0.0.289; extra == 'dev'
|
|
47
|
+
Requires-Dist: strawberry-graphql[debug-server]==0.205.0; extra == 'dev'
|
|
44
48
|
Provides-Extra: experimental
|
|
49
|
+
Requires-Dist: langchain>=0.0.257; extra == 'experimental'
|
|
50
|
+
Requires-Dist: llama-index>=0.8.25; extra == 'experimental'
|
|
45
51
|
Requires-Dist: openai; extra == 'experimental'
|
|
46
52
|
Requires-Dist: tenacity; extra == 'experimental'
|
|
47
53
|
Description-Content-Type: text/markdown
|
|
@@ -29,29 +29,35 @@ dependencies = [
|
|
|
29
29
|
"starlette",
|
|
30
30
|
"uvicorn",
|
|
31
31
|
"psutil",
|
|
32
|
-
"strawberry-graphql==0.
|
|
32
|
+
"strawberry-graphql==0.205.0",
|
|
33
33
|
"pyarrow",
|
|
34
34
|
"typing_extensions",
|
|
35
35
|
"scipy",
|
|
36
|
-
"portpicker",
|
|
37
36
|
"wrapt",
|
|
37
|
+
"sortedcontainers",
|
|
38
|
+
"protobuf>=3.20, <5.0",
|
|
38
39
|
]
|
|
39
40
|
dynamic = ["version"]
|
|
40
41
|
|
|
41
42
|
[project.optional-dependencies]
|
|
42
43
|
dev = [
|
|
43
44
|
"black[jupyter]",
|
|
45
|
+
"gcsfs",
|
|
44
46
|
"hatch",
|
|
45
47
|
"jupyter",
|
|
46
|
-
"
|
|
48
|
+
"nbqa",
|
|
49
|
+
"ruff==0.0.289",
|
|
50
|
+
"pandas-stubs<=2.0.2.230605", # version 2.0.3.230814 is causing a dependency conflict.
|
|
47
51
|
"pytest",
|
|
48
52
|
"pytest-cov",
|
|
49
53
|
"pytest-lazy-fixture",
|
|
50
|
-
"strawberry-graphql[debug-server]==0.
|
|
54
|
+
"strawberry-graphql[debug-server]==0.205.0",
|
|
51
55
|
"pre-commit",
|
|
52
56
|
"arize[AutoEmbeddings, LLM_Evaluation]",
|
|
53
57
|
]
|
|
54
58
|
experimental = [
|
|
59
|
+
"langchain>=0.0.257",
|
|
60
|
+
"llama-index>=0.8.25",
|
|
55
61
|
"openai",
|
|
56
62
|
"tenacity",
|
|
57
63
|
]
|
|
@@ -90,29 +96,40 @@ artifacts = ["src/phoenix/server/static"]
|
|
|
90
96
|
|
|
91
97
|
[tool.hatch.envs.default]
|
|
92
98
|
dependencies = [
|
|
99
|
+
"pandas==1.4.0",
|
|
93
100
|
"pytest",
|
|
94
101
|
"pytest-cov",
|
|
95
102
|
"pytest-lazy-fixture",
|
|
96
103
|
"arize",
|
|
104
|
+
"langchain>=0.0.257",
|
|
105
|
+
"llama-index>=0.8.25",
|
|
97
106
|
"openai",
|
|
98
107
|
"tenacity",
|
|
108
|
+
"nltk==3.8.1",
|
|
109
|
+
"sentence-transformers==2.2.2",
|
|
110
|
+
"pydantic<2", # for @root_validator in llama-index
|
|
111
|
+
"requests",
|
|
112
|
+
"protobuf==3.20", # version minimum (for tests)
|
|
113
|
+
"responses",
|
|
99
114
|
]
|
|
100
115
|
|
|
101
116
|
[tool.hatch.envs.type]
|
|
102
117
|
dependencies = [
|
|
103
|
-
"mypy==1.
|
|
104
|
-
"
|
|
105
|
-
"
|
|
118
|
+
"mypy==1.5.1",
|
|
119
|
+
"llama-index>=0.8.25",
|
|
120
|
+
"pandas-stubs<=2.0.2.230605", # version 2.0.3.230814 is causing a dependency conflict.
|
|
106
121
|
"types-psutil",
|
|
107
|
-
"
|
|
122
|
+
"types-tqdm",
|
|
123
|
+
"types-requests",
|
|
124
|
+
"types-protobuf",
|
|
108
125
|
]
|
|
109
126
|
|
|
110
127
|
[tool.hatch.envs.style]
|
|
111
128
|
detached = true
|
|
112
129
|
dependencies = [
|
|
113
|
-
"black~=23.
|
|
114
|
-
"black[jupyter]~=23.
|
|
115
|
-
"ruff~=0.0.
|
|
130
|
+
"black~=23.3.0",
|
|
131
|
+
"black[jupyter]~=23.3.0",
|
|
132
|
+
"ruff~=0.0.289",
|
|
116
133
|
]
|
|
117
134
|
|
|
118
135
|
[tool.hatch.envs.notebooks]
|
|
@@ -199,6 +216,7 @@ pypi = [
|
|
|
199
216
|
|
|
200
217
|
[tool.black]
|
|
201
218
|
line-length = 100
|
|
219
|
+
exclude = '_pb2\.pyi?$'
|
|
202
220
|
|
|
203
221
|
[tool.hatch.envs.docs.scripts]
|
|
204
222
|
check = [
|
|
@@ -206,10 +224,23 @@ check = [
|
|
|
206
224
|
]
|
|
207
225
|
|
|
208
226
|
[tool.hatch.envs.gql]
|
|
227
|
+
dependencies = [
|
|
228
|
+
"strawberry-graphql[cli]==0.205.0",
|
|
229
|
+
]
|
|
209
230
|
|
|
210
231
|
[tool.hatch.envs.gql.scripts]
|
|
211
232
|
build = 'strawberry export-schema phoenix.server.api.schema:schema > app/schema.graphql'
|
|
212
233
|
|
|
234
|
+
[tool.hatch.envs.proto]
|
|
235
|
+
detached = true
|
|
236
|
+
dependencies = [
|
|
237
|
+
"grpcio-tools==1.54.3",
|
|
238
|
+
"mypy-protobuf==3.5.0",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
[tool.hatch.envs.proto.scripts]
|
|
242
|
+
recompile = "python -m grpc_tools.protoc -I src/phoenix/proto --python_out=src/phoenix --mypy_out=src/phoenix src/phoenix/proto/trace/v1/trace.proto"
|
|
243
|
+
|
|
213
244
|
[tool.interrogate]
|
|
214
245
|
fail-under = 0
|
|
215
246
|
# generate-badge = "badges/"
|
|
@@ -226,13 +257,14 @@ ignore-nested-classes = false
|
|
|
226
257
|
ignore-setters = false
|
|
227
258
|
|
|
228
259
|
[tool.mypy]
|
|
229
|
-
plugins = ["strawberry.ext.mypy_plugin"]
|
|
260
|
+
plugins = ["strawberry.ext.mypy_plugin", "pydantic.mypy"]
|
|
230
261
|
disallow_untyped_calls = true
|
|
231
262
|
disallow_untyped_defs = true
|
|
232
263
|
disallow_incomplete_defs = true
|
|
233
264
|
strict = true
|
|
234
265
|
exclude = [
|
|
235
266
|
"dist/",
|
|
267
|
+
"scripts/data/",
|
|
236
268
|
"sdist/",
|
|
237
269
|
"tests/",
|
|
238
270
|
"tutorials/",
|
|
@@ -246,14 +278,14 @@ module = [
|
|
|
246
278
|
"scipy.*",
|
|
247
279
|
"sklearn.*",
|
|
248
280
|
"arize.*",
|
|
249
|
-
"portpicker",
|
|
250
281
|
"wrapt",
|
|
251
|
-
"
|
|
282
|
+
"sortedcontainers",
|
|
283
|
+
"langchain.*",
|
|
252
284
|
]
|
|
253
285
|
ignore_missing_imports = true
|
|
254
286
|
|
|
255
287
|
[tool.ruff]
|
|
256
|
-
exclude = [".git", "__pycache__", "docs/source/conf.py"]
|
|
288
|
+
exclude = [".git", "__pycache__", "docs/source/conf.py", "*_pb2.py*"]
|
|
257
289
|
ignore-init-module-imports = true
|
|
258
290
|
line-length = 100
|
|
259
291
|
select = ["E", "F", "W", "I"]
|
|
@@ -3,8 +3,9 @@ from .datasets.fixtures import ExampleDatasets, load_example
|
|
|
3
3
|
from .datasets.schema import EmbeddingColumnNames, RetrievalEmbeddingColumnNames, Schema
|
|
4
4
|
from .session.session import Session, active_session, close_app, launch_app
|
|
5
5
|
from .trace.fixtures import load_example_traces
|
|
6
|
+
from .trace.trace_dataset import TraceDataset
|
|
6
7
|
|
|
7
|
-
__version__ = "0.0.
|
|
8
|
+
__version__ = "0.0.33"
|
|
8
9
|
|
|
9
10
|
# module level doc-string
|
|
10
11
|
__doc__ = """
|
|
@@ -34,4 +35,5 @@ __all__ = [
|
|
|
34
35
|
"launch_app",
|
|
35
36
|
"Session",
|
|
36
37
|
"load_example_traces",
|
|
38
|
+
"TraceDataset",
|
|
37
39
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import tempfile
|
|
2
3
|
from pathlib import Path
|
|
3
|
-
from typing import List
|
|
4
|
+
from typing import List, Optional
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def _get_temp_path() -> Path:
|
|
@@ -18,6 +19,13 @@ def get_pids_path() -> Path:
|
|
|
18
19
|
return path
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def get_running_pid() -> Optional[int]:
|
|
23
|
+
for file in get_pids_path().iterdir():
|
|
24
|
+
if file.name.isnumeric():
|
|
25
|
+
return int(file.name)
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
21
29
|
for path in (
|
|
22
30
|
ROOT_DIR := Path.home().resolve() / ".phoenix",
|
|
23
31
|
EXPORT_DIR := ROOT_DIR / "exports",
|
|
@@ -28,6 +36,8 @@ for path in (
|
|
|
28
36
|
PHOENIX_DIR = Path(__file__).resolve().parent
|
|
29
37
|
# Server config
|
|
30
38
|
SERVER_DIR = PHOENIX_DIR / "server"
|
|
39
|
+
# The host the server will run on after launch_app is called
|
|
40
|
+
HOST = "127.0.0.1"
|
|
31
41
|
# The port the server will run on after launch_app is called
|
|
32
42
|
PORT = 6060
|
|
33
43
|
# The prefix of datasets that are auto-assigned a name
|
|
@@ -49,3 +59,15 @@ def get_exported_files(directory: Path) -> List[Path]:
|
|
|
49
59
|
List of paths of the exported files.
|
|
50
60
|
"""
|
|
51
61
|
return list(directory.glob("*.parquet"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_env_port() -> int:
|
|
65
|
+
return (
|
|
66
|
+
int(port)
|
|
67
|
+
if isinstance(port := os.getenv("PHOENIX_PORT"), str) and port.isnumeric()
|
|
68
|
+
else PORT
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_env_host() -> str:
|
|
73
|
+
return os.getenv("PHOENIX_HOST") or HOST
|
|
@@ -49,6 +49,7 @@ from typing_extensions import TypeAlias, TypeGuard
|
|
|
49
49
|
from wrapt import ObjectProxy
|
|
50
50
|
|
|
51
51
|
from phoenix.config import GENERATED_DATASET_NAME_PREFIX
|
|
52
|
+
from phoenix.datetime_utils import floor_to_minute
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class DimensionRole(IntEnum):
|
|
@@ -328,10 +329,7 @@ class Column:
|
|
|
328
329
|
except KeyError:
|
|
329
330
|
# It's important to glue the index to the default series,
|
|
330
331
|
# so it would look like the series came from the dataframe.
|
|
331
|
-
return self._default(len(data)).set_axis(
|
|
332
|
-
data.index,
|
|
333
|
-
copy=False,
|
|
334
|
-
)
|
|
332
|
+
return self._default(len(data)).set_axis(data.index)
|
|
335
333
|
if isinstance(data, pd.Series):
|
|
336
334
|
try:
|
|
337
335
|
return data.at[self.name]
|
|
@@ -657,8 +655,8 @@ class Events(ModelData):
|
|
|
657
655
|
# open and one minute is the smallest interval allowed.
|
|
658
656
|
stop_time = end_time + timedelta(minutes=1)
|
|
659
657
|
# Round down to the nearest minute.
|
|
660
|
-
start =
|
|
661
|
-
stop =
|
|
658
|
+
start = floor_to_minute(start_time)
|
|
659
|
+
stop = floor_to_minute(stop_time)
|
|
662
660
|
return TimeRange(start, stop)
|
|
663
661
|
|
|
664
662
|
def __iter__(self) -> Iterator[Event]:
|
|
@@ -721,6 +719,10 @@ class Dataset(Events):
|
|
|
721
719
|
def role(self) -> DatasetRole:
|
|
722
720
|
return self._self_role
|
|
723
721
|
|
|
722
|
+
@property
|
|
723
|
+
def is_empty(self) -> bool:
|
|
724
|
+
return len(self) == 0
|
|
725
|
+
|
|
724
726
|
@cached_property
|
|
725
727
|
def primary_key(self) -> pd.Index:
|
|
726
728
|
return pd.Index(self[PREDICTION_ID])
|
|
@@ -736,10 +738,7 @@ class Dataset(Events):
|
|
|
736
738
|
def __getitem__(self, key: Any) -> Any:
|
|
737
739
|
if isinstance(key, list):
|
|
738
740
|
return Events(
|
|
739
|
-
self.iloc[key].set_axis(
|
|
740
|
-
key,
|
|
741
|
-
copy=False,
|
|
742
|
-
),
|
|
741
|
+
self.iloc[key].set_axis(key),
|
|
743
742
|
role=self._self_role,
|
|
744
743
|
_model=self._self_model,
|
|
745
744
|
)
|
|
@@ -913,6 +912,11 @@ class Model:
|
|
|
913
912
|
df, name=dataset.name, role=dataset_role
|
|
914
913
|
)
|
|
915
914
|
|
|
915
|
+
@cached_property
|
|
916
|
+
def is_empty(self) -> bool:
|
|
917
|
+
"""Returns True if the model has no data."""
|
|
918
|
+
return not any(map(len, self._datasets.values()))
|
|
919
|
+
|
|
916
920
|
def export_rows_as_parquet_file(
|
|
917
921
|
self,
|
|
918
922
|
row_numbers: Mapping[DatasetRole, Iterable[int]],
|
|
@@ -1390,7 +1394,6 @@ def _coerce_str_column_names(
|
|
|
1390
1394
|
df.set_axis(
|
|
1391
1395
|
df.columns.astype(str),
|
|
1392
1396
|
axis=1,
|
|
1393
|
-
copy=False,
|
|
1394
1397
|
)
|
|
1395
1398
|
for df in dataframes
|
|
1396
1399
|
)
|
|
@@ -1432,32 +1435,6 @@ def _title_case_no_underscore(name: str) -> str:
|
|
|
1432
1435
|
return _id_pat.sub("ID", name.replace("_", " ").title())
|
|
1433
1436
|
|
|
1434
1437
|
|
|
1435
|
-
MINUTE_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:00%z"
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
def _floor_to_minute(dt: datetime) -> datetime:
|
|
1439
|
-
"""Floor datetime to the minute by taking a round-trip through string
|
|
1440
|
-
format because there isn't always an available function to strip the
|
|
1441
|
-
nanoseconds if present."""
|
|
1442
|
-
try:
|
|
1443
|
-
dt_as_string = dt.astimezone(
|
|
1444
|
-
timezone.utc,
|
|
1445
|
-
).strftime(
|
|
1446
|
-
MINUTE_DATETIME_FORMAT,
|
|
1447
|
-
)
|
|
1448
|
-
except ValueError:
|
|
1449
|
-
# NOTE: as of Python 3.8.16, pandas 1.5.3:
|
|
1450
|
-
# >>> isinstance(pd.NaT, datetime.datetime)
|
|
1451
|
-
# True
|
|
1452
|
-
return cast(datetime, pd.NaT)
|
|
1453
|
-
return datetime.strptime(
|
|
1454
|
-
dt_as_string,
|
|
1455
|
-
MINUTE_DATETIME_FORMAT,
|
|
1456
|
-
).astimezone(
|
|
1457
|
-
timezone.utc,
|
|
1458
|
-
)
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
1438
|
def _jsonify(obj: Any) -> Any:
|
|
1462
1439
|
if getattr(obj, "__dataclass_fields__", None):
|
|
1463
1440
|
return {
|
|
@@ -43,7 +43,6 @@ def create_model_from_datasets(*datasets: Optional[Dataset]) -> Model:
|
|
|
43
43
|
df = df.set_axis(
|
|
44
44
|
map(str, df.columns),
|
|
45
45
|
axis=1,
|
|
46
|
-
copy=False,
|
|
47
46
|
)
|
|
48
47
|
named_dataframes.append((dataset.name, df))
|
|
49
48
|
dataset_schema = dataset.schema if dataset.schema is not None else DatasetSchema()
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import weakref
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from queue import SimpleQueue
|
|
5
|
+
from threading import RLock, Thread
|
|
6
|
+
from types import MethodType
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
DefaultDict,
|
|
10
|
+
Dict,
|
|
11
|
+
Iterable,
|
|
12
|
+
Iterator,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
SupportsFloat,
|
|
16
|
+
Tuple,
|
|
17
|
+
Union,
|
|
18
|
+
cast,
|
|
19
|
+
)
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
from sortedcontainers import SortedKeyList
|
|
23
|
+
from typing_extensions import TypeAlias
|
|
24
|
+
from wrapt import ObjectProxy
|
|
25
|
+
|
|
26
|
+
import phoenix.trace.v1.trace_pb2 as pb
|
|
27
|
+
from phoenix.datetime_utils import right_open_time_range
|
|
28
|
+
from phoenix.trace import semantic_conventions
|
|
29
|
+
from phoenix.trace.schemas import (
|
|
30
|
+
ATTRIBUTE_PREFIX,
|
|
31
|
+
COMPUTED_PREFIX,
|
|
32
|
+
CONTEXT_PREFIX,
|
|
33
|
+
Span,
|
|
34
|
+
SpanAttributes,
|
|
35
|
+
SpanID,
|
|
36
|
+
TraceID,
|
|
37
|
+
)
|
|
38
|
+
from phoenix.trace.v1 import decode, encode
|
|
39
|
+
|
|
40
|
+
NAME = "name"
|
|
41
|
+
STATUS_CODE = "status_code"
|
|
42
|
+
SPAN_KIND = "span_kind"
|
|
43
|
+
TRACE_ID = CONTEXT_PREFIX + "trace_id"
|
|
44
|
+
SPAN_ID = CONTEXT_PREFIX + "span_id"
|
|
45
|
+
PARENT_ID = "parent_id"
|
|
46
|
+
START_TIME = "start_time"
|
|
47
|
+
END_TIME = "end_time"
|
|
48
|
+
LLM_TOKEN_COUNT_TOTAL = ATTRIBUTE_PREFIX + semantic_conventions.LLM_TOKEN_COUNT_TOTAL
|
|
49
|
+
LLM_TOKEN_COUNT_PROMPT = ATTRIBUTE_PREFIX + semantic_conventions.LLM_TOKEN_COUNT_PROMPT
|
|
50
|
+
LLM_TOKEN_COUNT_COMPLETION = ATTRIBUTE_PREFIX + semantic_conventions.LLM_TOKEN_COUNT_COMPLETION
|
|
51
|
+
LATENCY_MS = COMPUTED_PREFIX + "latency_ms" # The latency (or duration) of the span in milliseconds
|
|
52
|
+
CUMULATIVE_LLM_TOKEN_COUNT_TOTAL = COMPUTED_PREFIX + "cumulative_token_count_total"
|
|
53
|
+
CUMULATIVE_LLM_TOKEN_COUNT_PROMPT = COMPUTED_PREFIX + "cumulative_token_count_prompt"
|
|
54
|
+
CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION = COMPUTED_PREFIX + "cumulative_token_count_completion"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ReadableSpan(ObjectProxy): # type: ignore
|
|
58
|
+
"""
|
|
59
|
+
A wrapped a protobuf Span, with access methods and ability to decode to
|
|
60
|
+
a python span. It's meant to be interface layer separating use from
|
|
61
|
+
implementation. It can also provide computed values that are not intrinsic
|
|
62
|
+
to the span, e.g. the latency rank percent which can change as more spans
|
|
63
|
+
are ingested, and would need to be re-computed on the fly.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
__wrapped__: pb.Span
|
|
67
|
+
|
|
68
|
+
def __init__(self, span: pb.Span) -> None:
|
|
69
|
+
super().__init__(span)
|
|
70
|
+
self._self_computed_values: Dict[str, SupportsFloat] = {}
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def span(self) -> Span:
|
|
74
|
+
span = decode(self.__wrapped__)
|
|
75
|
+
span.attributes.update(cast(SpanAttributes, self._self_computed_values))
|
|
76
|
+
# TODO: compute latency rank percent (which can change depending on how
|
|
77
|
+
# many spans already ingested).
|
|
78
|
+
return span
|
|
79
|
+
|
|
80
|
+
def __getitem__(self, key: str) -> Any:
|
|
81
|
+
if key.startswith(COMPUTED_PREFIX):
|
|
82
|
+
return self._self_computed_values.get(key)
|
|
83
|
+
if key.startswith(CONTEXT_PREFIX):
|
|
84
|
+
suffix_key = key[len(CONTEXT_PREFIX) :]
|
|
85
|
+
return getattr(self.__wrapped__.context, suffix_key, None)
|
|
86
|
+
if key.startswith(ATTRIBUTE_PREFIX):
|
|
87
|
+
suffix_key = key[len(ATTRIBUTE_PREFIX) :]
|
|
88
|
+
if suffix_key not in self.__wrapped__.attributes:
|
|
89
|
+
return None
|
|
90
|
+
return self.__wrapped__.attributes[suffix_key]
|
|
91
|
+
return getattr(self.__wrapped__, key, None)
|
|
92
|
+
|
|
93
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
94
|
+
if not key.startswith(COMPUTED_PREFIX):
|
|
95
|
+
raise KeyError(f"{key} is not a computed value")
|
|
96
|
+
self._self_computed_values[key] = value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
ParentSpanID: TypeAlias = SpanID
|
|
100
|
+
ChildSpanID: TypeAlias = SpanID
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Traces:
|
|
104
|
+
def __init__(self, spans: Optional[Iterable[Span]] = None) -> None:
|
|
105
|
+
self._queue: "SimpleQueue[Optional[pb.Span]]" = SimpleQueue()
|
|
106
|
+
# Putting `None` as the sentinel value for queue termination.
|
|
107
|
+
weakref.finalize(self, self._queue.put, None)
|
|
108
|
+
for span in spans or ():
|
|
109
|
+
self.put(span)
|
|
110
|
+
self._lock = RLock()
|
|
111
|
+
self._spans: Dict[SpanID, ReadableSpan] = {}
|
|
112
|
+
self._parent_span_ids: Dict[SpanID, ParentSpanID] = {}
|
|
113
|
+
self._traces: Dict[TraceID, List[SpanID]] = defaultdict(list)
|
|
114
|
+
self._child_span_ids: DefaultDict[SpanID, List[ChildSpanID]] = defaultdict(list)
|
|
115
|
+
self._orphan_spans: DefaultDict[ParentSpanID, List[pb.Span]] = defaultdict(list)
|
|
116
|
+
self._start_time_sorted_span_ids: SortedKeyList[SpanID] = SortedKeyList(
|
|
117
|
+
key=lambda span_id: self._spans[span_id].start_time.ToDatetime(timezone.utc),
|
|
118
|
+
)
|
|
119
|
+
self._start_time_sorted_root_span_ids: SortedKeyList[SpanID] = SortedKeyList(
|
|
120
|
+
key=lambda span_id: self._spans[span_id].start_time.ToDatetime(timezone.utc),
|
|
121
|
+
)
|
|
122
|
+
self._latency_sorted_root_span_ids: SortedKeyList[SpanID] = SortedKeyList(
|
|
123
|
+
key=lambda span_id: self._spans[span_id][LATENCY_MS],
|
|
124
|
+
)
|
|
125
|
+
self._min_start_time: Optional[datetime] = None
|
|
126
|
+
self._max_start_time: Optional[datetime] = None
|
|
127
|
+
self._start_consumer()
|
|
128
|
+
|
|
129
|
+
def put(self, span: Optional[Union[Span, pb.Span]] = None) -> None:
|
|
130
|
+
self._queue.put(encode(span) if isinstance(span, Span) else span)
|
|
131
|
+
|
|
132
|
+
def get_trace(self, trace_id: TraceID) -> Iterator[Span]:
|
|
133
|
+
for span_id in self._traces[trace_id]:
|
|
134
|
+
if span := self[span_id]:
|
|
135
|
+
yield span
|
|
136
|
+
|
|
137
|
+
def get_spans(
|
|
138
|
+
self,
|
|
139
|
+
start_time: Optional[datetime] = None,
|
|
140
|
+
stop_time: Optional[datetime] = None,
|
|
141
|
+
root_spans_only: Optional[bool] = False,
|
|
142
|
+
) -> Iterator[Span]:
|
|
143
|
+
if not self._spans:
|
|
144
|
+
return
|
|
145
|
+
min_start_time, max_stop_time = cast(
|
|
146
|
+
Tuple[datetime, datetime],
|
|
147
|
+
self.right_open_time_range,
|
|
148
|
+
)
|
|
149
|
+
start_time = start_time or min_start_time
|
|
150
|
+
stop_time = stop_time or max_stop_time
|
|
151
|
+
sorted_span_ids = (
|
|
152
|
+
self._start_time_sorted_root_span_ids
|
|
153
|
+
if root_spans_only
|
|
154
|
+
else self._start_time_sorted_span_ids
|
|
155
|
+
)
|
|
156
|
+
for span_id in sorted_span_ids.irange_key(
|
|
157
|
+
start_time.astimezone(timezone.utc),
|
|
158
|
+
stop_time.astimezone(timezone.utc),
|
|
159
|
+
inclusive=(True, False),
|
|
160
|
+
reverse=True, # most recent spans first
|
|
161
|
+
):
|
|
162
|
+
if span := self[span_id]:
|
|
163
|
+
yield span
|
|
164
|
+
|
|
165
|
+
def latency_rank_percent(self, latency_ms: float) -> Optional[float]:
|
|
166
|
+
"""
|
|
167
|
+
Returns a value between 0 and 100 approximating the rank of the
|
|
168
|
+
latency value as percent of the total count of root spans. E.g., for
|
|
169
|
+
a latency value at the 75th percentile, the result is roughly 75.
|
|
170
|
+
"""
|
|
171
|
+
root_span_ids = self._latency_sorted_root_span_ids
|
|
172
|
+
if not (n := len(root_span_ids)):
|
|
173
|
+
return None
|
|
174
|
+
rank = cast(int, root_span_ids.bisect_key_left(latency_ms))
|
|
175
|
+
return rank / n * 100
|
|
176
|
+
|
|
177
|
+
def get_descendant_span_ids(self, span_id: SpanID) -> Iterator[SpanID]:
|
|
178
|
+
for child_span_id in self._child_span_ids.get(span_id) or ():
|
|
179
|
+
yield child_span_id
|
|
180
|
+
yield from self.get_descendant_span_ids(child_span_id)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def span_count(self) -> int:
|
|
184
|
+
"""Total number of spans (excluding orphan spans if any)"""
|
|
185
|
+
return len(self._spans)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def right_open_time_range(self) -> Tuple[Optional[datetime], Optional[datetime]]:
|
|
189
|
+
return right_open_time_range(self._min_start_time, self._max_start_time)
|
|
190
|
+
|
|
191
|
+
def __getitem__(self, span_id: SpanID) -> Optional[Span]:
|
|
192
|
+
with self._lock:
|
|
193
|
+
if span := self._spans.get(span_id):
|
|
194
|
+
return span.span
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def _start_consumer(self) -> None:
|
|
198
|
+
Thread(
|
|
199
|
+
target=MethodType(
|
|
200
|
+
self.__class__._consume_spans,
|
|
201
|
+
weakref.proxy(self),
|
|
202
|
+
),
|
|
203
|
+
daemon=True,
|
|
204
|
+
).start()
|
|
205
|
+
|
|
206
|
+
def _consume_spans(self) -> None:
|
|
207
|
+
while True:
|
|
208
|
+
if not (span := self._queue.get()):
|
|
209
|
+
return
|
|
210
|
+
with self._lock:
|
|
211
|
+
self._process_span(span)
|
|
212
|
+
|
|
213
|
+
def _process_span(self, span: pb.Span) -> None:
|
|
214
|
+
span_id = UUID(bytes=span.context.span_id)
|
|
215
|
+
existing_span = self._spans.get(span_id)
|
|
216
|
+
if existing_span and existing_span.end_time:
|
|
217
|
+
# Reject updates if span has ended.
|
|
218
|
+
return
|
|
219
|
+
is_root_span = not span.HasField("parent_span_id")
|
|
220
|
+
if not is_root_span:
|
|
221
|
+
parent_span_id = UUID(bytes=span.parent_span_id.value)
|
|
222
|
+
if parent_span_id not in self._spans:
|
|
223
|
+
# Span can't be processed before its parent.
|
|
224
|
+
self._orphan_spans[parent_span_id].append(span)
|
|
225
|
+
return
|
|
226
|
+
self._child_span_ids[parent_span_id].append(span_id)
|
|
227
|
+
self._parent_span_ids[span_id] = parent_span_id
|
|
228
|
+
new_span = ReadableSpan(span)
|
|
229
|
+
start_time = span.start_time.ToDatetime(timezone.utc)
|
|
230
|
+
end_time = span.end_time.ToDatetime(timezone.utc) if span.HasField("end_time") else None
|
|
231
|
+
if end_time:
|
|
232
|
+
new_span[LATENCY_MS] = (end_time - start_time).total_seconds() * 1000
|
|
233
|
+
self._spans[span_id] = new_span
|
|
234
|
+
if is_root_span and end_time:
|
|
235
|
+
self._latency_sorted_root_span_ids.add(span_id)
|
|
236
|
+
if not existing_span:
|
|
237
|
+
trace_id = UUID(bytes=span.context.trace_id)
|
|
238
|
+
self._traces[trace_id].append(span_id)
|
|
239
|
+
if is_root_span:
|
|
240
|
+
self._start_time_sorted_root_span_ids.add(span_id)
|
|
241
|
+
self._start_time_sorted_span_ids.add(span_id)
|
|
242
|
+
self._min_start_time = (
|
|
243
|
+
start_time
|
|
244
|
+
if self._min_start_time is None
|
|
245
|
+
else min(self._min_start_time, start_time)
|
|
246
|
+
)
|
|
247
|
+
self._max_start_time = (
|
|
248
|
+
start_time
|
|
249
|
+
if self._max_start_time is None
|
|
250
|
+
else max(self._max_start_time, start_time)
|
|
251
|
+
)
|
|
252
|
+
# Update cumulative values for span's ancestors.
|
|
253
|
+
for attribute_name, cumulative_attribute_name in (
|
|
254
|
+
(LLM_TOKEN_COUNT_TOTAL, CUMULATIVE_LLM_TOKEN_COUNT_TOTAL),
|
|
255
|
+
(LLM_TOKEN_COUNT_PROMPT, CUMULATIVE_LLM_TOKEN_COUNT_PROMPT),
|
|
256
|
+
(LLM_TOKEN_COUNT_COMPLETION, CUMULATIVE_LLM_TOKEN_COUNT_COMPLETION),
|
|
257
|
+
):
|
|
258
|
+
existing_value = (existing_span[attribute_name] or 0) if existing_span else 0
|
|
259
|
+
new_value = new_span[attribute_name] or 0
|
|
260
|
+
if not (difference := new_value - existing_value):
|
|
261
|
+
continue
|
|
262
|
+
existing_cumulative_value = (
|
|
263
|
+
(existing_span[cumulative_attribute_name] or 0) if existing_span else 0
|
|
264
|
+
)
|
|
265
|
+
new_span[cumulative_attribute_name] = difference + existing_cumulative_value
|
|
266
|
+
self._add_value_to_span_ancestors(
|
|
267
|
+
span_id,
|
|
268
|
+
cumulative_attribute_name,
|
|
269
|
+
difference,
|
|
270
|
+
)
|
|
271
|
+
# Process previously orphaned spans, if any.
|
|
272
|
+
for orphan_span in self._orphan_spans[span_id]:
|
|
273
|
+
self._process_span(orphan_span)
|
|
274
|
+
|
|
275
|
+
def _add_value_to_span_ancestors(
|
|
276
|
+
self,
|
|
277
|
+
span_id: SpanID,
|
|
278
|
+
attribute_name: str,
|
|
279
|
+
value: float,
|
|
280
|
+
) -> None:
|
|
281
|
+
while parent_span_id := self._parent_span_ids.get(span_id):
|
|
282
|
+
parent_span = self._spans[parent_span_id]
|
|
283
|
+
cumulative_value = parent_span[attribute_name] or 0
|
|
284
|
+
parent_span[attribute_name] = cumulative_value + value
|
|
285
|
+
span_id = parent_span_id
|