arize-phoenix 3.16.0__py3-none-any.whl → 7.7.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.
- arize_phoenix-7.7.0.dist-info/METADATA +261 -0
- arize_phoenix-7.7.0.dist-info/RECORD +345 -0
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/WHEEL +1 -1
- arize_phoenix-7.7.0.dist-info/entry_points.txt +3 -0
- phoenix/__init__.py +86 -14
- phoenix/auth.py +309 -0
- phoenix/config.py +675 -45
- phoenix/core/model.py +32 -30
- phoenix/core/model_schema.py +102 -109
- phoenix/core/model_schema_adapter.py +48 -45
- phoenix/datetime_utils.py +24 -3
- phoenix/db/README.md +54 -0
- phoenix/db/__init__.py +4 -0
- phoenix/db/alembic.ini +85 -0
- phoenix/db/bulk_inserter.py +294 -0
- phoenix/db/engines.py +208 -0
- phoenix/db/enums.py +20 -0
- phoenix/db/facilitator.py +113 -0
- phoenix/db/helpers.py +159 -0
- phoenix/db/insertion/constants.py +2 -0
- phoenix/db/insertion/dataset.py +227 -0
- phoenix/db/insertion/document_annotation.py +171 -0
- phoenix/db/insertion/evaluation.py +191 -0
- phoenix/db/insertion/helpers.py +98 -0
- phoenix/db/insertion/span.py +193 -0
- phoenix/db/insertion/span_annotation.py +158 -0
- phoenix/db/insertion/trace_annotation.py +158 -0
- phoenix/db/insertion/types.py +256 -0
- phoenix/db/migrate.py +86 -0
- phoenix/db/migrations/data_migration_scripts/populate_project_sessions.py +199 -0
- phoenix/db/migrations/env.py +114 -0
- phoenix/db/migrations/script.py.mako +26 -0
- phoenix/db/migrations/versions/10460e46d750_datasets.py +317 -0
- phoenix/db/migrations/versions/3be8647b87d8_add_token_columns_to_spans_table.py +126 -0
- phoenix/db/migrations/versions/4ded9e43755f_create_project_sessions_table.py +66 -0
- phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
- phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
- phoenix/db/models.py +807 -0
- phoenix/exceptions.py +5 -1
- phoenix/experiments/__init__.py +6 -0
- phoenix/experiments/evaluators/__init__.py +29 -0
- phoenix/experiments/evaluators/base.py +158 -0
- phoenix/experiments/evaluators/code_evaluators.py +184 -0
- phoenix/experiments/evaluators/llm_evaluators.py +473 -0
- phoenix/experiments/evaluators/utils.py +236 -0
- phoenix/experiments/functions.py +772 -0
- phoenix/experiments/tracing.py +86 -0
- phoenix/experiments/types.py +726 -0
- phoenix/experiments/utils.py +25 -0
- phoenix/inferences/__init__.py +0 -0
- phoenix/{datasets → inferences}/errors.py +6 -5
- phoenix/{datasets → inferences}/fixtures.py +49 -42
- phoenix/{datasets/dataset.py → inferences/inferences.py} +121 -105
- phoenix/{datasets → inferences}/schema.py +11 -11
- phoenix/{datasets → inferences}/validation.py +13 -14
- phoenix/logging/__init__.py +3 -0
- phoenix/logging/_config.py +90 -0
- phoenix/logging/_filter.py +6 -0
- phoenix/logging/_formatter.py +69 -0
- phoenix/metrics/__init__.py +5 -4
- phoenix/metrics/binning.py +4 -3
- phoenix/metrics/metrics.py +2 -1
- phoenix/metrics/mixins.py +7 -6
- phoenix/metrics/retrieval_metrics.py +2 -1
- phoenix/metrics/timeseries.py +5 -4
- phoenix/metrics/wrappers.py +9 -3
- phoenix/pointcloud/clustering.py +5 -5
- phoenix/pointcloud/pointcloud.py +7 -5
- phoenix/pointcloud/projectors.py +5 -6
- phoenix/pointcloud/umap_parameters.py +53 -52
- phoenix/server/api/README.md +28 -0
- phoenix/server/api/auth.py +44 -0
- phoenix/server/api/context.py +152 -9
- phoenix/server/api/dataloaders/__init__.py +91 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +139 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +54 -0
- phoenix/server/api/dataloaders/cache/__init__.py +3 -0
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +68 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +131 -0
- phoenix/server/api/dataloaders/dataset_example_spans.py +38 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +144 -0
- phoenix/server/api/dataloaders/document_evaluations.py +31 -0
- phoenix/server/api/dataloaders/document_retrieval_metrics.py +89 -0
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +79 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +58 -0
- phoenix/server/api/dataloaders/experiment_run_annotations.py +36 -0
- phoenix/server/api/dataloaders/experiment_run_counts.py +49 -0
- phoenix/server/api/dataloaders/experiment_sequence_number.py +44 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +188 -0
- phoenix/server/api/dataloaders/min_start_or_max_end_times.py +85 -0
- phoenix/server/api/dataloaders/project_by_name.py +31 -0
- phoenix/server/api/dataloaders/record_counts.py +116 -0
- phoenix/server/api/dataloaders/session_io.py +79 -0
- phoenix/server/api/dataloaders/session_num_traces.py +30 -0
- phoenix/server/api/dataloaders/session_num_traces_with_error.py +32 -0
- phoenix/server/api/dataloaders/session_token_usages.py +41 -0
- phoenix/server/api/dataloaders/session_trace_latency_ms_quantile.py +55 -0
- phoenix/server/api/dataloaders/span_annotations.py +26 -0
- phoenix/server/api/dataloaders/span_dataset_examples.py +31 -0
- phoenix/server/api/dataloaders/span_descendants.py +57 -0
- phoenix/server/api/dataloaders/span_projects.py +33 -0
- phoenix/server/api/dataloaders/token_counts.py +124 -0
- phoenix/server/api/dataloaders/trace_by_trace_ids.py +25 -0
- phoenix/server/api/dataloaders/trace_root_spans.py +32 -0
- phoenix/server/api/dataloaders/user_roles.py +30 -0
- phoenix/server/api/dataloaders/users.py +33 -0
- phoenix/server/api/exceptions.py +48 -0
- phoenix/server/api/helpers/__init__.py +12 -0
- phoenix/server/api/helpers/dataset_helpers.py +217 -0
- phoenix/server/api/helpers/experiment_run_filters.py +763 -0
- phoenix/server/api/helpers/playground_clients.py +948 -0
- phoenix/server/api/helpers/playground_registry.py +70 -0
- phoenix/server/api/helpers/playground_spans.py +455 -0
- phoenix/server/api/input_types/AddExamplesToDatasetInput.py +16 -0
- phoenix/server/api/input_types/AddSpansToDatasetInput.py +14 -0
- phoenix/server/api/input_types/ChatCompletionInput.py +38 -0
- phoenix/server/api/input_types/ChatCompletionMessageInput.py +24 -0
- phoenix/server/api/input_types/ClearProjectInput.py +15 -0
- phoenix/server/api/input_types/ClusterInput.py +2 -2
- phoenix/server/api/input_types/CreateDatasetInput.py +12 -0
- phoenix/server/api/input_types/CreateSpanAnnotationInput.py +18 -0
- phoenix/server/api/input_types/CreateTraceAnnotationInput.py +18 -0
- phoenix/server/api/input_types/DataQualityMetricInput.py +5 -2
- phoenix/server/api/input_types/DatasetExampleInput.py +14 -0
- phoenix/server/api/input_types/DatasetSort.py +17 -0
- phoenix/server/api/input_types/DatasetVersionSort.py +16 -0
- phoenix/server/api/input_types/DeleteAnnotationsInput.py +7 -0
- phoenix/server/api/input_types/DeleteDatasetExamplesInput.py +13 -0
- phoenix/server/api/input_types/DeleteDatasetInput.py +7 -0
- phoenix/server/api/input_types/DeleteExperimentsInput.py +7 -0
- phoenix/server/api/input_types/DimensionFilter.py +4 -4
- phoenix/server/api/input_types/GenerativeModelInput.py +17 -0
- phoenix/server/api/input_types/Granularity.py +1 -1
- phoenix/server/api/input_types/InvocationParameters.py +162 -0
- phoenix/server/api/input_types/PatchAnnotationInput.py +19 -0
- phoenix/server/api/input_types/PatchDatasetExamplesInput.py +35 -0
- phoenix/server/api/input_types/PatchDatasetInput.py +14 -0
- phoenix/server/api/input_types/PerformanceMetricInput.py +5 -2
- phoenix/server/api/input_types/ProjectSessionSort.py +29 -0
- phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
- phoenix/server/api/input_types/SpanSort.py +134 -69
- phoenix/server/api/input_types/TemplateOptions.py +10 -0
- phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
- phoenix/server/api/input_types/UserRoleInput.py +9 -0
- phoenix/server/api/mutations/__init__.py +28 -0
- phoenix/server/api/mutations/api_key_mutations.py +167 -0
- phoenix/server/api/mutations/chat_mutations.py +593 -0
- phoenix/server/api/mutations/dataset_mutations.py +591 -0
- phoenix/server/api/mutations/experiment_mutations.py +75 -0
- phoenix/server/api/{types/ExportEventsMutation.py → mutations/export_events_mutations.py} +21 -18
- phoenix/server/api/mutations/project_mutations.py +57 -0
- phoenix/server/api/mutations/span_annotations_mutations.py +128 -0
- phoenix/server/api/mutations/trace_annotations_mutations.py +127 -0
- phoenix/server/api/mutations/user_mutations.py +329 -0
- phoenix/server/api/openapi/__init__.py +0 -0
- phoenix/server/api/openapi/main.py +17 -0
- phoenix/server/api/openapi/schema.py +16 -0
- phoenix/server/api/queries.py +738 -0
- phoenix/server/api/routers/__init__.py +11 -0
- phoenix/server/api/routers/auth.py +284 -0
- phoenix/server/api/routers/embeddings.py +26 -0
- phoenix/server/api/routers/oauth2.py +488 -0
- phoenix/server/api/routers/v1/__init__.py +64 -0
- phoenix/server/api/routers/v1/datasets.py +1017 -0
- phoenix/server/api/routers/v1/evaluations.py +362 -0
- phoenix/server/api/routers/v1/experiment_evaluations.py +115 -0
- phoenix/server/api/routers/v1/experiment_runs.py +167 -0
- phoenix/server/api/routers/v1/experiments.py +308 -0
- phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
- phoenix/server/api/routers/v1/spans.py +267 -0
- phoenix/server/api/routers/v1/traces.py +208 -0
- phoenix/server/api/routers/v1/utils.py +95 -0
- phoenix/server/api/schema.py +44 -247
- phoenix/server/api/subscriptions.py +597 -0
- phoenix/server/api/types/Annotation.py +21 -0
- phoenix/server/api/types/AnnotationSummary.py +55 -0
- phoenix/server/api/types/AnnotatorKind.py +16 -0
- phoenix/server/api/types/ApiKey.py +27 -0
- phoenix/server/api/types/AuthMethod.py +9 -0
- phoenix/server/api/types/ChatCompletionMessageRole.py +11 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +46 -0
- phoenix/server/api/types/Cluster.py +25 -24
- phoenix/server/api/types/CreateDatasetPayload.py +8 -0
- phoenix/server/api/types/DataQualityMetric.py +31 -13
- phoenix/server/api/types/Dataset.py +288 -63
- phoenix/server/api/types/DatasetExample.py +85 -0
- phoenix/server/api/types/DatasetExampleRevision.py +34 -0
- phoenix/server/api/types/DatasetVersion.py +14 -0
- phoenix/server/api/types/Dimension.py +32 -31
- phoenix/server/api/types/DocumentEvaluationSummary.py +9 -8
- phoenix/server/api/types/EmbeddingDimension.py +56 -49
- phoenix/server/api/types/Evaluation.py +25 -31
- phoenix/server/api/types/EvaluationSummary.py +30 -50
- phoenix/server/api/types/Event.py +20 -20
- phoenix/server/api/types/ExampleRevisionInterface.py +14 -0
- phoenix/server/api/types/Experiment.py +152 -0
- phoenix/server/api/types/ExperimentAnnotationSummary.py +13 -0
- phoenix/server/api/types/ExperimentComparison.py +17 -0
- phoenix/server/api/types/ExperimentRun.py +119 -0
- phoenix/server/api/types/ExperimentRunAnnotation.py +56 -0
- phoenix/server/api/types/GenerativeModel.py +9 -0
- phoenix/server/api/types/GenerativeProvider.py +85 -0
- phoenix/server/api/types/Inferences.py +80 -0
- phoenix/server/api/types/InferencesRole.py +23 -0
- phoenix/server/api/types/LabelFraction.py +7 -0
- phoenix/server/api/types/MimeType.py +2 -2
- phoenix/server/api/types/Model.py +54 -54
- phoenix/server/api/types/PerformanceMetric.py +8 -5
- phoenix/server/api/types/Project.py +407 -142
- phoenix/server/api/types/ProjectSession.py +139 -0
- phoenix/server/api/types/Segments.py +4 -4
- phoenix/server/api/types/Span.py +221 -176
- phoenix/server/api/types/SpanAnnotation.py +43 -0
- phoenix/server/api/types/SpanIOValue.py +15 -0
- phoenix/server/api/types/SystemApiKey.py +9 -0
- phoenix/server/api/types/TemplateLanguage.py +10 -0
- phoenix/server/api/types/TimeSeries.py +19 -15
- phoenix/server/api/types/TokenUsage.py +11 -0
- phoenix/server/api/types/Trace.py +154 -0
- phoenix/server/api/types/TraceAnnotation.py +45 -0
- phoenix/server/api/types/UMAPPoints.py +7 -7
- phoenix/server/api/types/User.py +60 -0
- phoenix/server/api/types/UserApiKey.py +45 -0
- phoenix/server/api/types/UserRole.py +15 -0
- phoenix/server/api/types/node.py +13 -107
- phoenix/server/api/types/pagination.py +156 -57
- phoenix/server/api/utils.py +34 -0
- phoenix/server/app.py +864 -115
- phoenix/server/bearer_auth.py +163 -0
- phoenix/server/dml_event.py +136 -0
- phoenix/server/dml_event_handler.py +256 -0
- phoenix/server/email/__init__.py +0 -0
- phoenix/server/email/sender.py +97 -0
- phoenix/server/email/templates/__init__.py +0 -0
- phoenix/server/email/templates/password_reset.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/grpc_server.py +102 -0
- phoenix/server/jwt_store.py +505 -0
- phoenix/server/main.py +305 -116
- phoenix/server/oauth2.py +52 -0
- phoenix/server/openapi/__init__.py +0 -0
- phoenix/server/prometheus.py +111 -0
- phoenix/server/rate_limiters.py +188 -0
- phoenix/server/static/.vite/manifest.json +87 -0
- phoenix/server/static/assets/components-Cy9nwIvF.js +2125 -0
- phoenix/server/static/assets/index-BKvHIxkk.js +113 -0
- phoenix/server/static/assets/pages-CUi2xCVQ.js +4449 -0
- phoenix/server/static/assets/vendor-DvC8cT4X.js +894 -0
- phoenix/server/static/assets/vendor-DxkFTwjz.css +1 -0
- phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +662 -0
- phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +24 -0
- phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +59 -0
- phoenix/server/static/assets/vendor-shiki-Cl9QBraO.js +5 -0
- phoenix/server/static/assets/vendor-three-DwGkEfCM.js +2998 -0
- phoenix/server/telemetry.py +68 -0
- phoenix/server/templates/index.html +82 -23
- phoenix/server/thread_server.py +3 -3
- phoenix/server/types.py +275 -0
- phoenix/services.py +27 -18
- phoenix/session/client.py +743 -68
- phoenix/session/data_extractor.py +31 -7
- phoenix/session/evaluation.py +3 -9
- phoenix/session/session.py +263 -219
- phoenix/settings.py +22 -0
- phoenix/trace/__init__.py +2 -22
- phoenix/trace/attributes.py +338 -0
- phoenix/trace/dsl/README.md +116 -0
- phoenix/trace/dsl/filter.py +663 -213
- phoenix/trace/dsl/helpers.py +73 -21
- phoenix/trace/dsl/query.py +574 -201
- phoenix/trace/exporter.py +24 -19
- phoenix/trace/fixtures.py +368 -32
- phoenix/trace/otel.py +71 -219
- phoenix/trace/projects.py +3 -2
- phoenix/trace/schemas.py +33 -11
- phoenix/trace/span_evaluations.py +21 -16
- phoenix/trace/span_json_decoder.py +6 -4
- phoenix/trace/span_json_encoder.py +2 -2
- phoenix/trace/trace_dataset.py +47 -32
- phoenix/trace/utils.py +21 -4
- phoenix/utilities/__init__.py +0 -26
- phoenix/utilities/client.py +132 -0
- phoenix/utilities/deprecation.py +31 -0
- phoenix/utilities/error_handling.py +3 -2
- phoenix/utilities/json.py +109 -0
- phoenix/utilities/logging.py +8 -0
- phoenix/utilities/project.py +2 -2
- phoenix/utilities/re.py +49 -0
- phoenix/utilities/span_store.py +0 -23
- phoenix/utilities/template_formatters.py +99 -0
- phoenix/version.py +1 -1
- arize_phoenix-3.16.0.dist-info/METADATA +0 -495
- arize_phoenix-3.16.0.dist-info/RECORD +0 -178
- phoenix/core/project.py +0 -617
- phoenix/core/traces.py +0 -100
- phoenix/experimental/evals/__init__.py +0 -73
- phoenix/experimental/evals/evaluators.py +0 -413
- phoenix/experimental/evals/functions/__init__.py +0 -4
- phoenix/experimental/evals/functions/classify.py +0 -453
- phoenix/experimental/evals/functions/executor.py +0 -353
- phoenix/experimental/evals/functions/generate.py +0 -138
- phoenix/experimental/evals/functions/processing.py +0 -76
- phoenix/experimental/evals/models/__init__.py +0 -14
- phoenix/experimental/evals/models/anthropic.py +0 -175
- phoenix/experimental/evals/models/base.py +0 -170
- phoenix/experimental/evals/models/bedrock.py +0 -221
- phoenix/experimental/evals/models/litellm.py +0 -134
- phoenix/experimental/evals/models/openai.py +0 -448
- phoenix/experimental/evals/models/rate_limiters.py +0 -246
- phoenix/experimental/evals/models/vertex.py +0 -173
- phoenix/experimental/evals/models/vertexai.py +0 -186
- phoenix/experimental/evals/retrievals.py +0 -96
- phoenix/experimental/evals/templates/__init__.py +0 -50
- phoenix/experimental/evals/templates/default_templates.py +0 -472
- phoenix/experimental/evals/templates/template.py +0 -195
- phoenix/experimental/evals/utils/__init__.py +0 -172
- phoenix/experimental/evals/utils/threads.py +0 -27
- phoenix/server/api/helpers.py +0 -11
- phoenix/server/api/routers/evaluation_handler.py +0 -109
- phoenix/server/api/routers/span_handler.py +0 -70
- phoenix/server/api/routers/trace_handler.py +0 -60
- phoenix/server/api/types/DatasetRole.py +0 -23
- phoenix/server/static/index.css +0 -6
- phoenix/server/static/index.js +0 -7447
- phoenix/storage/span_store/__init__.py +0 -23
- phoenix/storage/span_store/text_file.py +0 -85
- phoenix/trace/dsl/missing.py +0 -60
- phoenix/trace/langchain/__init__.py +0 -3
- phoenix/trace/langchain/instrumentor.py +0 -35
- phoenix/trace/llama_index/__init__.py +0 -3
- phoenix/trace/llama_index/callback.py +0 -102
- phoenix/trace/openai/__init__.py +0 -3
- phoenix/trace/openai/instrumentor.py +0 -30
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-3.16.0.dist-info → arize_phoenix-7.7.0.dist-info}/licenses/LICENSE +0 -0
- /phoenix/{datasets → db/insertion}/__init__.py +0 -0
- /phoenix/{experimental → db/migrations}/__init__.py +0 -0
- /phoenix/{storage → db/migrations/data_migration_scripts}/__init__.py +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from random import getrandbits
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException, Path
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from sqlalchemy import select
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
from starlette.status import HTTP_404_NOT_FOUND
|
|
10
|
+
from strawberry.relay import GlobalID
|
|
11
|
+
|
|
12
|
+
from phoenix.db import models
|
|
13
|
+
from phoenix.db.helpers import SupportedSQLDialect
|
|
14
|
+
from phoenix.db.insertion.helpers import insert_on_conflict
|
|
15
|
+
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
16
|
+
from phoenix.server.dml_event import ExperimentInsertEvent
|
|
17
|
+
|
|
18
|
+
from .pydantic_compat import V1RoutesBaseModel
|
|
19
|
+
from .utils import ResponseBody, add_errors_to_responses
|
|
20
|
+
|
|
21
|
+
router = APIRouter(tags=["experiments"], include_in_schema=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _short_uuid() -> str:
|
|
25
|
+
return str(getrandbits(32).to_bytes(4, "big").hex())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _generate_experiment_name(dataset_name: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Generate a semi-unique name for the experiment.
|
|
31
|
+
"""
|
|
32
|
+
short_ds_name = dataset_name[:8].replace(" ", "-")
|
|
33
|
+
return f"{short_ds_name}-{_short_uuid()}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Experiment(V1RoutesBaseModel):
|
|
37
|
+
id: str = Field(description="The ID of the experiment")
|
|
38
|
+
dataset_id: str = Field(description="The ID of the dataset associated with the experiment")
|
|
39
|
+
dataset_version_id: str = Field(
|
|
40
|
+
description="The ID of the dataset version associated with the experiment"
|
|
41
|
+
)
|
|
42
|
+
repetitions: int = Field(description="Number of times the experiment is repeated")
|
|
43
|
+
metadata: dict[str, Any] = Field(description="Metadata of the experiment")
|
|
44
|
+
project_name: Optional[str] = Field(
|
|
45
|
+
description="The name of the project associated with the experiment"
|
|
46
|
+
)
|
|
47
|
+
created_at: datetime = Field(description="The creation timestamp of the experiment")
|
|
48
|
+
updated_at: datetime = Field(description="The last update timestamp of the experiment")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CreateExperimentRequestBody(V1RoutesBaseModel):
|
|
52
|
+
"""
|
|
53
|
+
Details of the experiment to be created
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: Optional[str] = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description=("Name of the experiment (if omitted, a random name will be generated)"),
|
|
59
|
+
)
|
|
60
|
+
description: Optional[str] = Field(
|
|
61
|
+
default=None, description="An optional description of the experiment"
|
|
62
|
+
)
|
|
63
|
+
metadata: Optional[dict[str, Any]] = Field(
|
|
64
|
+
default=None, description="Metadata for the experiment"
|
|
65
|
+
)
|
|
66
|
+
version_id: Optional[str] = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description=(
|
|
69
|
+
"ID of the dataset version over which the experiment will be run "
|
|
70
|
+
"(if omitted, the latest version will be used)"
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
repetitions: int = Field(
|
|
74
|
+
default=1, description="Number of times the experiment should be repeated for each example"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CreateExperimentResponseBody(ResponseBody[Experiment]):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.post(
|
|
83
|
+
"/datasets/{dataset_id}/experiments",
|
|
84
|
+
operation_id="createExperiment",
|
|
85
|
+
summary="Create experiment on a dataset",
|
|
86
|
+
responses=add_errors_to_responses(
|
|
87
|
+
[{"status_code": HTTP_404_NOT_FOUND, "description": "Dataset or DatasetVersion not found"}]
|
|
88
|
+
),
|
|
89
|
+
response_description="Experiment retrieved successfully",
|
|
90
|
+
)
|
|
91
|
+
async def create_experiment(
|
|
92
|
+
request: Request,
|
|
93
|
+
request_body: CreateExperimentRequestBody,
|
|
94
|
+
dataset_id: str = Path(..., title="Dataset ID"),
|
|
95
|
+
) -> CreateExperimentResponseBody:
|
|
96
|
+
dataset_globalid = GlobalID.from_id(dataset_id)
|
|
97
|
+
try:
|
|
98
|
+
dataset_rowid = from_global_id_with_expected_type(dataset_globalid, "Dataset")
|
|
99
|
+
except ValueError:
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
detail="Dataset with ID {dataset_globalid} does not exist",
|
|
102
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
dataset_version_globalid_str = request_body.version_id
|
|
106
|
+
if dataset_version_globalid_str is not None:
|
|
107
|
+
try:
|
|
108
|
+
dataset_version_globalid = GlobalID.from_id(dataset_version_globalid_str)
|
|
109
|
+
dataset_version_id = from_global_id_with_expected_type(
|
|
110
|
+
dataset_version_globalid, "DatasetVersion"
|
|
111
|
+
)
|
|
112
|
+
except ValueError:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
detail=f"DatasetVersion with ID {dataset_version_globalid_str} does not exist",
|
|
115
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async with request.app.state.db() as session:
|
|
119
|
+
result = (
|
|
120
|
+
await session.execute(select(models.Dataset).where(models.Dataset.id == dataset_rowid))
|
|
121
|
+
).scalar()
|
|
122
|
+
if result is None:
|
|
123
|
+
raise HTTPException(
|
|
124
|
+
detail=f"Dataset with ID {dataset_globalid} does not exist",
|
|
125
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
126
|
+
)
|
|
127
|
+
dataset_name = result.name
|
|
128
|
+
if dataset_version_globalid_str is None:
|
|
129
|
+
dataset_version_result = await session.execute(
|
|
130
|
+
select(models.DatasetVersion)
|
|
131
|
+
.where(models.DatasetVersion.dataset_id == dataset_rowid)
|
|
132
|
+
.order_by(models.DatasetVersion.id.desc())
|
|
133
|
+
)
|
|
134
|
+
dataset_version = dataset_version_result.scalar()
|
|
135
|
+
if not dataset_version:
|
|
136
|
+
raise HTTPException(
|
|
137
|
+
detail=f"Dataset {dataset_globalid} does not have any versions",
|
|
138
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
139
|
+
)
|
|
140
|
+
dataset_version_id = dataset_version.id
|
|
141
|
+
dataset_version_globalid = GlobalID("DatasetVersion", str(dataset_version_id))
|
|
142
|
+
else:
|
|
143
|
+
dataset_version = await session.execute(
|
|
144
|
+
select(models.DatasetVersion).where(models.DatasetVersion.id == dataset_version_id)
|
|
145
|
+
)
|
|
146
|
+
dataset_version = dataset_version.scalar()
|
|
147
|
+
if not dataset_version:
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
detail=f"DatasetVersion with ID {dataset_version_globalid} does not exist",
|
|
150
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# generate a semi-unique name for the experiment
|
|
154
|
+
experiment_name = request_body.name or _generate_experiment_name(dataset_name)
|
|
155
|
+
project_name = f"Experiment-{getrandbits(96).to_bytes(12, 'big').hex()}"
|
|
156
|
+
project_description = (
|
|
157
|
+
f"dataset_id: {dataset_globalid}\ndataset_version_id: {dataset_version_globalid}"
|
|
158
|
+
)
|
|
159
|
+
experiment = models.Experiment(
|
|
160
|
+
dataset_id=int(dataset_rowid),
|
|
161
|
+
dataset_version_id=int(dataset_version_id),
|
|
162
|
+
name=experiment_name,
|
|
163
|
+
description=request_body.description,
|
|
164
|
+
repetitions=request_body.repetitions,
|
|
165
|
+
metadata_=request_body.metadata or {},
|
|
166
|
+
project_name=project_name,
|
|
167
|
+
)
|
|
168
|
+
session.add(experiment)
|
|
169
|
+
await session.flush()
|
|
170
|
+
|
|
171
|
+
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
172
|
+
project_rowid = await session.scalar(
|
|
173
|
+
insert_on_conflict(
|
|
174
|
+
dict(
|
|
175
|
+
name=project_name,
|
|
176
|
+
description=project_description,
|
|
177
|
+
created_at=experiment.created_at,
|
|
178
|
+
updated_at=experiment.updated_at,
|
|
179
|
+
),
|
|
180
|
+
dialect=dialect,
|
|
181
|
+
table=models.Project,
|
|
182
|
+
unique_by=("name",),
|
|
183
|
+
).returning(models.Project.id)
|
|
184
|
+
)
|
|
185
|
+
assert project_rowid is not None
|
|
186
|
+
|
|
187
|
+
experiment_globalid = GlobalID("Experiment", str(experiment.id))
|
|
188
|
+
if dataset_version_globalid_str is None:
|
|
189
|
+
dataset_version_globalid = GlobalID(
|
|
190
|
+
"DatasetVersion", str(experiment.dataset_version_id)
|
|
191
|
+
)
|
|
192
|
+
request.state.event_queue.put(ExperimentInsertEvent((experiment.id,)))
|
|
193
|
+
return CreateExperimentResponseBody(
|
|
194
|
+
data=Experiment(
|
|
195
|
+
id=str(experiment_globalid),
|
|
196
|
+
dataset_id=str(dataset_globalid),
|
|
197
|
+
dataset_version_id=str(dataset_version_globalid),
|
|
198
|
+
repetitions=experiment.repetitions,
|
|
199
|
+
metadata=experiment.metadata_,
|
|
200
|
+
project_name=experiment.project_name,
|
|
201
|
+
created_at=experiment.created_at,
|
|
202
|
+
updated_at=experiment.updated_at,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class GetExperimentResponseBody(ResponseBody[Experiment]):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.get(
|
|
212
|
+
"/experiments/{experiment_id}",
|
|
213
|
+
operation_id="getExperiment",
|
|
214
|
+
summary="Get experiment by ID",
|
|
215
|
+
responses=add_errors_to_responses(
|
|
216
|
+
[{"status_code": HTTP_404_NOT_FOUND, "description": "Experiment not found"}]
|
|
217
|
+
),
|
|
218
|
+
response_description="Experiment retrieved successfully",
|
|
219
|
+
)
|
|
220
|
+
async def get_experiment(request: Request, experiment_id: str) -> GetExperimentResponseBody:
|
|
221
|
+
experiment_globalid = GlobalID.from_id(experiment_id)
|
|
222
|
+
try:
|
|
223
|
+
experiment_rowid = from_global_id_with_expected_type(experiment_globalid, "Experiment")
|
|
224
|
+
except ValueError:
|
|
225
|
+
raise HTTPException(
|
|
226
|
+
detail="Experiment with ID {experiment_globalid} does not exist",
|
|
227
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
async with request.app.state.db() as session:
|
|
231
|
+
experiment = await session.execute(
|
|
232
|
+
select(models.Experiment).where(models.Experiment.id == experiment_rowid)
|
|
233
|
+
)
|
|
234
|
+
experiment = experiment.scalar()
|
|
235
|
+
if not experiment:
|
|
236
|
+
raise HTTPException(
|
|
237
|
+
detail=f"Experiment with ID {experiment_globalid} does not exist",
|
|
238
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
dataset_globalid = GlobalID("Dataset", str(experiment.dataset_id))
|
|
242
|
+
dataset_version_globalid = GlobalID("DatasetVersion", str(experiment.dataset_version_id))
|
|
243
|
+
return GetExperimentResponseBody(
|
|
244
|
+
data=Experiment(
|
|
245
|
+
id=str(experiment_globalid),
|
|
246
|
+
dataset_id=str(dataset_globalid),
|
|
247
|
+
dataset_version_id=str(dataset_version_globalid),
|
|
248
|
+
repetitions=experiment.repetitions,
|
|
249
|
+
metadata=experiment.metadata_,
|
|
250
|
+
project_name=experiment.project_name,
|
|
251
|
+
created_at=experiment.created_at,
|
|
252
|
+
updated_at=experiment.updated_at,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class ListExperimentsResponseBody(ResponseBody[list[Experiment]]):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@router.get(
|
|
262
|
+
"/datasets/{dataset_id}/experiments",
|
|
263
|
+
operation_id="listExperiments",
|
|
264
|
+
summary="List experiments by dataset",
|
|
265
|
+
response_description="Experiments retrieved successfully",
|
|
266
|
+
)
|
|
267
|
+
async def list_experiments(
|
|
268
|
+
request: Request,
|
|
269
|
+
dataset_id: str = Path(..., title="Dataset ID"),
|
|
270
|
+
) -> ListExperimentsResponseBody:
|
|
271
|
+
dataset_gid = GlobalID.from_id(dataset_id)
|
|
272
|
+
try:
|
|
273
|
+
dataset_rowid = from_global_id_with_expected_type(dataset_gid, "Dataset")
|
|
274
|
+
except ValueError:
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
detail=f"Dataset with ID {dataset_gid} does not exist",
|
|
277
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
278
|
+
)
|
|
279
|
+
async with request.app.state.db() as session:
|
|
280
|
+
query = (
|
|
281
|
+
select(models.Experiment)
|
|
282
|
+
.where(models.Experiment.dataset_id == dataset_rowid)
|
|
283
|
+
.order_by(models.Experiment.id.desc())
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
result = await session.execute(query)
|
|
287
|
+
experiments = result.scalars().all()
|
|
288
|
+
|
|
289
|
+
if not experiments:
|
|
290
|
+
return ListExperimentsResponseBody(data=[])
|
|
291
|
+
|
|
292
|
+
data = [
|
|
293
|
+
Experiment(
|
|
294
|
+
id=str(GlobalID("Experiment", str(experiment.id))),
|
|
295
|
+
dataset_id=str(GlobalID("Dataset", str(experiment.dataset_id))),
|
|
296
|
+
dataset_version_id=str(
|
|
297
|
+
GlobalID("DatasetVersion", str(experiment.dataset_version_id))
|
|
298
|
+
),
|
|
299
|
+
repetitions=experiment.repetitions,
|
|
300
|
+
metadata=experiment.metadata_,
|
|
301
|
+
project_name=None,
|
|
302
|
+
created_at=experiment.created_at,
|
|
303
|
+
updated_at=experiment.updated_at,
|
|
304
|
+
)
|
|
305
|
+
for experiment in experiments
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
return ListExperimentsResponseBody(data=data)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from typing_extensions import assert_never
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def datetime_encoder(dt: datetime) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Encodes a `datetime` object to an ISO-formatted timestamp string.
|
|
12
|
+
|
|
13
|
+
By default, Pydantic v2 serializes `datetime` objects in a format that
|
|
14
|
+
cannot be parsed by `datetime.fromisoformat`. Adding this encoder to the
|
|
15
|
+
`json_encoders` config for a Pydantic model ensures that the serialized
|
|
16
|
+
`datetime` objects are parseable.
|
|
17
|
+
"""
|
|
18
|
+
return dt.isoformat()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PydanticMajorVersion(Enum):
|
|
22
|
+
"""
|
|
23
|
+
The major version of `pydantic`.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
V1 = "v1"
|
|
27
|
+
V2 = "v2"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_pydantic_major_version() -> PydanticMajorVersion:
|
|
31
|
+
"""
|
|
32
|
+
Returns the major version of `pydantic` or raises an error if `pydantic` is
|
|
33
|
+
not installed.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
pydantic_version = version("pydantic")
|
|
37
|
+
except PackageNotFoundError:
|
|
38
|
+
raise RuntimeError("Please install pydantic with `pip install pydantic`.")
|
|
39
|
+
if pydantic_version.startswith("1"):
|
|
40
|
+
return PydanticMajorVersion.V1
|
|
41
|
+
elif pydantic_version.startswith("2"):
|
|
42
|
+
return PydanticMajorVersion.V2
|
|
43
|
+
raise ValueError(f"Unsupported Pydantic version: {pydantic_version}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if (pydantic_major_version := get_pydantic_major_version()) is PydanticMajorVersion.V1:
|
|
47
|
+
|
|
48
|
+
class V1RoutesBaseModel(BaseModel):
|
|
49
|
+
class Config:
|
|
50
|
+
json_encoders = {datetime: datetime_encoder}
|
|
51
|
+
|
|
52
|
+
elif pydantic_major_version is PydanticMajorVersion.V2:
|
|
53
|
+
from pydantic import ConfigDict
|
|
54
|
+
|
|
55
|
+
# `json_encoders` is a configuration setting from Pydantic v1 that was
|
|
56
|
+
# removed in Pydantic v2.0.* but restored in Pydantic v2.1.0 with a
|
|
57
|
+
# deprecation warning. At this time, it remains the simplest way to
|
|
58
|
+
# configure custom JSON serialization for specific data types in a manner
|
|
59
|
+
# that is consistent between Pydantic v1 and v2.
|
|
60
|
+
#
|
|
61
|
+
# For details, see:
|
|
62
|
+
# - https://github.com/pydantic/pydantic/pull/6811
|
|
63
|
+
# - https://github.com/pydantic/pydantic/releases/tag/v2.1.0
|
|
64
|
+
#
|
|
65
|
+
# The assertion below is added in case a future release of Pydantic v2 fully
|
|
66
|
+
# removes the `json_encoders` parameter.
|
|
67
|
+
assert "json_encoders" in ConfigDict.__annotations__, (
|
|
68
|
+
"If you encounter this error with `pydantic==2.0.*`, "
|
|
69
|
+
"please upgrade `pydantic` with `pip install -U pydantic>=2.1.0`. "
|
|
70
|
+
"If you encounter this error with `pydantic>=2.1.0`, "
|
|
71
|
+
"please upgrade `arize-phoenix` with `pip install -U arize-phoenix`, "
|
|
72
|
+
"or downgrade `pydantic` to a version that supports the `json_encoders` config setting."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
class V1RoutesBaseModel(BaseModel): # type: ignore[no-redef]
|
|
76
|
+
model_config = ConfigDict({"json_encoders": {datetime: datetime_encoder}})
|
|
77
|
+
else:
|
|
78
|
+
assert_never(pydantic_major_version)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from asyncio import get_running_loop
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from secrets import token_urlsafe
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from fastapi import APIRouter, Header, HTTPException, Query
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from sqlalchemy import select
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import Response, StreamingResponse
|
|
13
|
+
from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
|
|
14
|
+
from strawberry.relay import GlobalID
|
|
15
|
+
|
|
16
|
+
from phoenix.config import DEFAULT_PROJECT_NAME
|
|
17
|
+
from phoenix.datetime_utils import normalize_datetime
|
|
18
|
+
from phoenix.db import models
|
|
19
|
+
from phoenix.db.helpers import SupportedSQLDialect
|
|
20
|
+
from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
|
|
21
|
+
from phoenix.db.insertion.types import Precursors
|
|
22
|
+
from phoenix.server.api.routers.utils import df_to_bytes
|
|
23
|
+
from phoenix.server.dml_event import SpanAnnotationInsertEvent
|
|
24
|
+
from phoenix.trace.dsl import SpanQuery as SpanQuery_
|
|
25
|
+
from phoenix.utilities.json import encode_df_as_json_string
|
|
26
|
+
|
|
27
|
+
from .pydantic_compat import V1RoutesBaseModel
|
|
28
|
+
from .utils import RequestBody, ResponseBody, add_errors_to_responses
|
|
29
|
+
|
|
30
|
+
DEFAULT_SPAN_LIMIT = 1000
|
|
31
|
+
|
|
32
|
+
router = APIRouter(tags=["spans"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SpanQuery(V1RoutesBaseModel):
|
|
36
|
+
select: Optional[dict[str, Any]] = None
|
|
37
|
+
filter: Optional[dict[str, Any]] = None
|
|
38
|
+
explode: Optional[dict[str, Any]] = None
|
|
39
|
+
concat: Optional[dict[str, Any]] = None
|
|
40
|
+
rename: Optional[dict[str, Any]] = None
|
|
41
|
+
index: Optional[dict[str, Any]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class QuerySpansRequestBody(V1RoutesBaseModel):
|
|
45
|
+
queries: list[SpanQuery]
|
|
46
|
+
start_time: Optional[datetime] = None
|
|
47
|
+
end_time: Optional[datetime] = None
|
|
48
|
+
limit: int = DEFAULT_SPAN_LIMIT
|
|
49
|
+
root_spans_only: Optional[bool] = None
|
|
50
|
+
project_name: Optional[str] = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description=(
|
|
53
|
+
"The name of the project to query. "
|
|
54
|
+
"This parameter has been deprecated, use the project_name query parameter instead."
|
|
55
|
+
),
|
|
56
|
+
deprecated=True,
|
|
57
|
+
)
|
|
58
|
+
stop_time: Optional[datetime] = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description=(
|
|
61
|
+
"An upper bound on the time to query for. "
|
|
62
|
+
"This parameter has been deprecated, use the end_time parameter instead."
|
|
63
|
+
),
|
|
64
|
+
deprecated=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# TODO: Add property details to SpanQuery schema
|
|
69
|
+
@router.post(
|
|
70
|
+
"/spans",
|
|
71
|
+
operation_id="querySpans",
|
|
72
|
+
summary="Query spans with query DSL",
|
|
73
|
+
responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
|
|
74
|
+
include_in_schema=False,
|
|
75
|
+
)
|
|
76
|
+
async def query_spans_handler(
|
|
77
|
+
request: Request,
|
|
78
|
+
request_body: QuerySpansRequestBody,
|
|
79
|
+
accept: Optional[str] = Header(None),
|
|
80
|
+
project_name: Optional[str] = Query(
|
|
81
|
+
default=None, description="The project name to get evaluations from"
|
|
82
|
+
),
|
|
83
|
+
) -> Response:
|
|
84
|
+
queries = request_body.queries
|
|
85
|
+
project_name = (
|
|
86
|
+
project_name
|
|
87
|
+
or request.query_params.get("project-name") # for backward compatibility
|
|
88
|
+
or request.headers.get(
|
|
89
|
+
"project-name"
|
|
90
|
+
) # read from headers/payload for backward-compatibility
|
|
91
|
+
or request_body.project_name
|
|
92
|
+
or DEFAULT_PROJECT_NAME
|
|
93
|
+
)
|
|
94
|
+
end_time = request_body.end_time or request_body.stop_time
|
|
95
|
+
try:
|
|
96
|
+
span_queries = [SpanQuery_.from_dict(query.dict()) for query in queries]
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise HTTPException(
|
|
99
|
+
detail=f"Invalid query: {e}",
|
|
100
|
+
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
|
101
|
+
)
|
|
102
|
+
async with request.app.state.db() as session:
|
|
103
|
+
results = []
|
|
104
|
+
for query in span_queries:
|
|
105
|
+
results.append(
|
|
106
|
+
await session.run_sync(
|
|
107
|
+
query,
|
|
108
|
+
project_name=project_name,
|
|
109
|
+
start_time=normalize_datetime(
|
|
110
|
+
request_body.start_time,
|
|
111
|
+
timezone.utc,
|
|
112
|
+
),
|
|
113
|
+
end_time=normalize_datetime(
|
|
114
|
+
end_time,
|
|
115
|
+
timezone.utc,
|
|
116
|
+
),
|
|
117
|
+
limit=request_body.limit,
|
|
118
|
+
root_spans_only=request_body.root_spans_only,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
if not results:
|
|
122
|
+
raise HTTPException(status_code=HTTP_404_NOT_FOUND)
|
|
123
|
+
|
|
124
|
+
if accept == "application/json":
|
|
125
|
+
boundary_token = token_urlsafe(64)
|
|
126
|
+
return StreamingResponse(
|
|
127
|
+
content=_json_multipart(results, boundary_token),
|
|
128
|
+
media_type=f"multipart/mixed; boundary={boundary_token}",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def content() -> AsyncIterator[bytes]:
|
|
132
|
+
for result in results:
|
|
133
|
+
yield df_to_bytes(result)
|
|
134
|
+
|
|
135
|
+
return StreamingResponse(
|
|
136
|
+
content=content(),
|
|
137
|
+
media_type="application/x-pandas-arrow",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _json_multipart(
|
|
142
|
+
results: list[pd.DataFrame],
|
|
143
|
+
boundary_token: str,
|
|
144
|
+
) -> AsyncIterator[str]:
|
|
145
|
+
for df in results:
|
|
146
|
+
yield f"--{boundary_token}\r\n"
|
|
147
|
+
yield "Content-Type: application/json\r\n\r\n"
|
|
148
|
+
yield await get_running_loop().run_in_executor(None, encode_df_as_json_string, df)
|
|
149
|
+
yield "\r\n"
|
|
150
|
+
yield f"--{boundary_token}--\r\n"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@router.get("/spans", include_in_schema=False, deprecated=True)
|
|
154
|
+
async def get_spans_handler(
|
|
155
|
+
request: Request,
|
|
156
|
+
request_body: QuerySpansRequestBody,
|
|
157
|
+
project_name: Optional[str] = Query(
|
|
158
|
+
default=None, description="The project name to get evaluations from"
|
|
159
|
+
),
|
|
160
|
+
) -> Response:
|
|
161
|
+
return await query_spans_handler(request, request_body, project_name)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SpanAnnotationResult(V1RoutesBaseModel):
|
|
165
|
+
label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
|
|
166
|
+
score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
|
|
167
|
+
explanation: Optional[str] = Field(
|
|
168
|
+
default=None, description="Explanation of the annotation result"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class SpanAnnotation(V1RoutesBaseModel):
|
|
173
|
+
span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
|
|
174
|
+
name: str = Field(description="The name of the annotation")
|
|
175
|
+
annotator_kind: Literal["LLM", "HUMAN"] = Field(
|
|
176
|
+
description="The kind of annotator used for the annotation"
|
|
177
|
+
)
|
|
178
|
+
result: Optional[SpanAnnotationResult] = Field(
|
|
179
|
+
default=None, description="The result of the annotation"
|
|
180
|
+
)
|
|
181
|
+
metadata: Optional[dict[str, Any]] = Field(
|
|
182
|
+
default=None, description="Metadata for the annotation"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def as_precursor(self) -> Precursors.SpanAnnotation:
|
|
186
|
+
return Precursors.SpanAnnotation(
|
|
187
|
+
self.span_id,
|
|
188
|
+
models.SpanAnnotation(
|
|
189
|
+
name=self.name,
|
|
190
|
+
annotator_kind=self.annotator_kind,
|
|
191
|
+
score=self.result.score if self.result else None,
|
|
192
|
+
label=self.result.label if self.result else None,
|
|
193
|
+
explanation=self.result.explanation if self.result else None,
|
|
194
|
+
metadata_=self.metadata or {},
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class AnnotateSpansRequestBody(RequestBody[list[SpanAnnotation]]):
|
|
200
|
+
data: list[SpanAnnotation]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class InsertedSpanAnnotation(V1RoutesBaseModel):
|
|
204
|
+
id: str = Field(description="The ID of the inserted span annotation")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class AnnotateSpansResponseBody(ResponseBody[list[InsertedSpanAnnotation]]):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.post(
|
|
212
|
+
"/span_annotations",
|
|
213
|
+
operation_id="annotateSpans",
|
|
214
|
+
summary="Create or update span annotations",
|
|
215
|
+
responses=add_errors_to_responses(
|
|
216
|
+
[{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
|
|
217
|
+
),
|
|
218
|
+
response_description="Span annotations inserted successfully",
|
|
219
|
+
include_in_schema=True,
|
|
220
|
+
)
|
|
221
|
+
async def annotate_spans(
|
|
222
|
+
request: Request,
|
|
223
|
+
request_body: AnnotateSpansRequestBody,
|
|
224
|
+
sync: bool = Query(default=False, description="If true, fulfill request synchronously."),
|
|
225
|
+
) -> AnnotateSpansResponseBody:
|
|
226
|
+
if not request_body.data:
|
|
227
|
+
return AnnotateSpansResponseBody(data=[])
|
|
228
|
+
precursors = [d.as_precursor() for d in request_body.data]
|
|
229
|
+
if not sync:
|
|
230
|
+
await request.state.enqueue(*precursors)
|
|
231
|
+
return AnnotateSpansResponseBody(data=[])
|
|
232
|
+
|
|
233
|
+
span_ids = {p.span_id for p in precursors}
|
|
234
|
+
async with request.app.state.db() as session:
|
|
235
|
+
existing_spans = {
|
|
236
|
+
span.span_id: span.id
|
|
237
|
+
async for span in await session.stream_scalars(
|
|
238
|
+
select(models.Span).filter(models.Span.span_id.in_(span_ids))
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
missing_span_ids = span_ids - set(existing_spans.keys())
|
|
243
|
+
if missing_span_ids:
|
|
244
|
+
raise HTTPException(
|
|
245
|
+
detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
|
|
246
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
247
|
+
)
|
|
248
|
+
inserted_ids = []
|
|
249
|
+
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
250
|
+
for p in precursors:
|
|
251
|
+
values = dict(as_kv(p.as_insertable(existing_spans[p.span_id]).row))
|
|
252
|
+
span_annotation_id = await session.scalar(
|
|
253
|
+
insert_on_conflict(
|
|
254
|
+
values,
|
|
255
|
+
dialect=dialect,
|
|
256
|
+
table=models.SpanAnnotation,
|
|
257
|
+
unique_by=("name", "span_rowid"),
|
|
258
|
+
).returning(models.SpanAnnotation.id)
|
|
259
|
+
)
|
|
260
|
+
inserted_ids.append(span_annotation_id)
|
|
261
|
+
request.state.event_queue.put(SpanAnnotationInsertEvent(tuple(inserted_ids)))
|
|
262
|
+
return AnnotateSpansResponseBody(
|
|
263
|
+
data=[
|
|
264
|
+
InsertedSpanAnnotation(id=str(GlobalID("SpanAnnotation", str(id_))))
|
|
265
|
+
for id_ in inserted_ids
|
|
266
|
+
]
|
|
267
|
+
)
|