arize-phoenix 11.23.1__py3-none-any.whl → 12.28.1__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.
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
- phoenix/__generated__/__init__.py +0 -0
- phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
- phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
- phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
- phoenix/__init__.py +2 -1
- phoenix/auth.py +27 -2
- phoenix/config.py +1594 -81
- phoenix/db/README.md +546 -28
- phoenix/db/bulk_inserter.py +119 -116
- phoenix/db/engines.py +140 -33
- phoenix/db/facilitator.py +22 -1
- phoenix/db/helpers.py +818 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +133 -1
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +2 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +41 -18
- phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
- phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
- phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
- phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
- phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
- phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
- phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
- phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
- phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
- phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
- phoenix/db/models.py +364 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/trace_retention.py +7 -6
- phoenix/experiments/functions.py +69 -19
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +60 -0
- phoenix/server/api/dataloaders/__init__.py +36 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
- phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
- phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
- phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
- phoenix/server/api/dataloaders/dataset_labels.py +36 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
- phoenix/server/api/dataloaders/document_evaluations.py +6 -9
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
- phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
- phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
- phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
- phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
- phoenix/server/api/dataloaders/record_counts.py +37 -10
- phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
- phoenix/server/api/dataloaders/span_costs.py +3 -9
- phoenix/server/api/dataloaders/table_fields.py +2 -2
- phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
- phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
- phoenix/server/api/exceptions.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +263 -83
- phoenix/server/api/helpers/playground_spans.py +2 -1
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +61 -19
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +5 -2
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/PromptVersionInput.py +47 -1
- phoenix/server/api/input_types/SpanSort.py +3 -2
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +8 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +15 -20
- phoenix/server/api/mutations/chat_mutations.py +106 -37
- phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
- phoenix/server/api/mutations/dataset_mutations.py +21 -16
- phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/model_mutations.py +11 -9
- phoenix/server/api/mutations/project_mutations.py +4 -4
- phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
- phoenix/server/api/mutations/prompt_mutations.py +65 -129
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
- phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
- phoenix/server/api/mutations/trace_annotations_mutations.py +13 -8
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +55 -26
- phoenix/server/api/queries.py +501 -617
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +141 -87
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +349 -101
- phoenix/server/api/routers/v1/__init__.py +22 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +455 -13
- phoenix/server/api/routers/v1/datasets.py +355 -68
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +20 -28
- phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
- phoenix/server/api/routers/v1/experiment_runs.py +335 -59
- phoenix/server/api/routers/v1/experiments.py +475 -47
- phoenix/server/api/routers/v1/projects.py +16 -50
- phoenix/server/api/routers/v1/prompts.py +50 -39
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +156 -96
- phoenix/server/api/routers/v1/traces.py +51 -77
- phoenix/server/api/routers/v1/users.py +64 -24
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +257 -93
- phoenix/server/api/types/Annotation.py +90 -23
- phoenix/server/api/types/ApiKey.py +13 -17
- phoenix/server/api/types/AuthMethod.py +1 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
- phoenix/server/api/types/Dataset.py +199 -72
- phoenix/server/api/types/DatasetExample.py +88 -18
- phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
- phoenix/server/api/types/DatasetLabel.py +57 -0
- phoenix/server/api/types/DatasetSplit.py +98 -0
- phoenix/server/api/types/DatasetVersion.py +49 -4
- phoenix/server/api/types/DocumentAnnotation.py +212 -0
- phoenix/server/api/types/Experiment.py +215 -68
- phoenix/server/api/types/ExperimentComparison.py +3 -9
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +120 -70
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +95 -42
- phoenix/server/api/types/GenerativeProvider.py +1 -1
- phoenix/server/api/types/ModelInterface.py +7 -2
- phoenix/server/api/types/PlaygroundModel.py +12 -2
- phoenix/server/api/types/Project.py +218 -185
- phoenix/server/api/types/ProjectSession.py +146 -29
- phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
- phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
- phoenix/server/api/types/Prompt.py +119 -39
- phoenix/server/api/types/PromptLabel.py +42 -25
- phoenix/server/api/types/PromptVersion.py +11 -8
- phoenix/server/api/types/PromptVersionTag.py +65 -25
- phoenix/server/api/types/Span.py +130 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/Trace.py +184 -53
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +128 -33
- phoenix/server/api/types/UserApiKey.py +73 -26
- phoenix/server/api/types/node.py +10 -0
- phoenix/server/api/types/pagination.py +11 -2
- phoenix/server/app.py +154 -36
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
- phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
- phoenix/server/daemons/generative_model_store.py +61 -9
- phoenix/server/daemons/span_cost_calculator.py +10 -8
- phoenix/server/dml_event.py +13 -0
- phoenix/server/email/sender.py +29 -2
- phoenix/server/grpc_server.py +9 -9
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +9 -3
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +43 -6
- phoenix/server/rate_limiters.py +4 -9
- phoenix/server/retention.py +33 -20
- phoenix/server/session_filters.py +49 -0
- phoenix/server/static/.vite/manifest.json +51 -53
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
- phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
- phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
- phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
- phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
- phoenix/server/static/assets/{vendor-recharts-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
- phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +7 -1
- phoenix/server/thread_server.py +1 -2
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +55 -1
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +8 -4
- phoenix/session/session.py +44 -8
- phoenix/settings.py +2 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/query.py +2 -0
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
- phoenix/server/static/assets/pages-Creyamao.js +0 -8612
- phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
- phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
- phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
- phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from strawberry.relay import GlobalID
|
|
8
|
+
|
|
9
|
+
from phoenix.db import models
|
|
10
|
+
from phoenix.db.helpers import SupportedSQLDialect
|
|
11
|
+
from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
|
|
12
|
+
from phoenix.server.api.routers.v1.annotations import SpanDocumentAnnotationData
|
|
13
|
+
from phoenix.server.api.types.DocumentAnnotation import DocumentAnnotation
|
|
14
|
+
from phoenix.server.authorization import is_not_locked
|
|
15
|
+
from phoenix.server.bearer_auth import PhoenixUser
|
|
16
|
+
from phoenix.server.dml_event import DocumentAnnotationInsertEvent
|
|
17
|
+
|
|
18
|
+
from .models import V1RoutesBaseModel
|
|
19
|
+
from .utils import RequestBody, ResponseBody, add_errors_to_responses
|
|
20
|
+
|
|
21
|
+
# Since the document annotations are spans related, we place it under spans
|
|
22
|
+
router = APIRouter(tags=["spans"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnnotateSpanDocumentsRequestBody(RequestBody[list[SpanDocumentAnnotationData]]):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InsertedSpanDocumentAnnotation(V1RoutesBaseModel):
|
|
30
|
+
id: str = Field(description="The ID of the inserted span document annotation")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AnnotateSpanDocumentsResponseBody(ResponseBody[list[InsertedSpanDocumentAnnotation]]):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.post(
|
|
38
|
+
"/document_annotations",
|
|
39
|
+
dependencies=[Depends(is_not_locked)],
|
|
40
|
+
operation_id="annotateSpanDocuments",
|
|
41
|
+
responses=add_errors_to_responses(
|
|
42
|
+
[
|
|
43
|
+
{
|
|
44
|
+
"status_code": 404,
|
|
45
|
+
"description": "Span not found",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"status_code": 422,
|
|
49
|
+
"description": "Invalid request - non-empty identifier not supported",
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
),
|
|
53
|
+
response_description="Span document annotation inserted successfully",
|
|
54
|
+
include_in_schema=True,
|
|
55
|
+
)
|
|
56
|
+
async def annotate_span_documents(
|
|
57
|
+
request: Request,
|
|
58
|
+
request_body: AnnotateSpanDocumentsRequestBody,
|
|
59
|
+
sync: bool = Query(
|
|
60
|
+
default=False, description="If set to true, the annotations are inserted synchronously."
|
|
61
|
+
),
|
|
62
|
+
) -> AnnotateSpanDocumentsResponseBody:
|
|
63
|
+
if not request_body.data:
|
|
64
|
+
return AnnotateSpanDocumentsResponseBody(data=[])
|
|
65
|
+
|
|
66
|
+
# Validate that identifiers are empty or only whitespace
|
|
67
|
+
for annotation in request_body.data:
|
|
68
|
+
if annotation.identifier.strip():
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
detail=f"Non-empty identifier '{annotation.identifier}' is not supported",
|
|
71
|
+
status_code=422, # Unprocessable Entity
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
user_id: Optional[int] = None
|
|
75
|
+
if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
|
|
76
|
+
user_id = int(request.user.identity)
|
|
77
|
+
|
|
78
|
+
span_document_annotations = request_body.data
|
|
79
|
+
|
|
80
|
+
precursors = [
|
|
81
|
+
annotation.as_precursor(user_id=user_id) for annotation in span_document_annotations
|
|
82
|
+
]
|
|
83
|
+
if not sync:
|
|
84
|
+
await request.state.enqueue_annotations(*precursors)
|
|
85
|
+
return AnnotateSpanDocumentsResponseBody(data=[])
|
|
86
|
+
|
|
87
|
+
span_ids = {p.span_id for p in precursors}
|
|
88
|
+
# Account for the fact that the spans could arrive after the annotation
|
|
89
|
+
async with request.app.state.db() as session:
|
|
90
|
+
existing_spans = {
|
|
91
|
+
span_id: (id_, num_docs)
|
|
92
|
+
async for span_id, id_, num_docs in await session.stream(
|
|
93
|
+
select(models.Span.span_id, models.Span.id, models.Span.num_documents).filter(
|
|
94
|
+
models.Span.span_id.in_(span_ids)
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
missing_span_ids = span_ids - set(existing_spans.keys())
|
|
100
|
+
# We prefer to fail the entire operation if there are missing spans in sync mode
|
|
101
|
+
if missing_span_ids:
|
|
102
|
+
raise HTTPException(
|
|
103
|
+
detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
|
|
104
|
+
status_code=404,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Validate that document positions are within bounds
|
|
108
|
+
for annotation in span_document_annotations:
|
|
109
|
+
_, num_docs = existing_spans[annotation.span_id]
|
|
110
|
+
if annotation.document_position not in range(num_docs):
|
|
111
|
+
raise HTTPException(
|
|
112
|
+
detail=f"Document position {annotation.document_position} is out of bounds for "
|
|
113
|
+
f"span {annotation.span_id} (max: {num_docs - 1})",
|
|
114
|
+
status_code=422, # Unprocessable Entity
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
inserted_document_annotation_ids = []
|
|
118
|
+
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
119
|
+
for anno in precursors:
|
|
120
|
+
span_rowid, _ = existing_spans[anno.span_id]
|
|
121
|
+
values = dict(as_kv(anno.as_insertable(span_rowid).row))
|
|
122
|
+
span_document_annotation_id = await session.scalar(
|
|
123
|
+
insert_on_conflict(
|
|
124
|
+
values,
|
|
125
|
+
dialect=dialect,
|
|
126
|
+
table=models.DocumentAnnotation,
|
|
127
|
+
unique_by=("name", "span_rowid", "identifier", "document_position"),
|
|
128
|
+
constraint_name="uq_document_annotations_name_span_rowid_document_pos_identifier",
|
|
129
|
+
).returning(models.DocumentAnnotation.id)
|
|
130
|
+
)
|
|
131
|
+
inserted_document_annotation_ids.append(span_document_annotation_id)
|
|
132
|
+
|
|
133
|
+
# We queue an event to let the application know that annotations have changed
|
|
134
|
+
request.state.event_queue.put(
|
|
135
|
+
DocumentAnnotationInsertEvent(tuple(inserted_document_annotation_ids))
|
|
136
|
+
)
|
|
137
|
+
return AnnotateSpanDocumentsResponseBody(
|
|
138
|
+
data=[
|
|
139
|
+
InsertedSpanDocumentAnnotation(id=str(GlobalID(DocumentAnnotation.__name__, str(id_))))
|
|
140
|
+
for id_ in inserted_document_annotation_ids
|
|
141
|
+
]
|
|
142
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import gzip
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
+
from datetime import datetime, timezone
|
|
3
4
|
from itertools import chain
|
|
4
5
|
from typing import Any, Iterator, Optional, Union, cast
|
|
5
6
|
|
|
@@ -14,12 +15,6 @@ from starlette.background import BackgroundTask
|
|
|
14
15
|
from starlette.datastructures import State
|
|
15
16
|
from starlette.requests import Request
|
|
16
17
|
from starlette.responses import Response, StreamingResponse
|
|
17
|
-
from starlette.status import (
|
|
18
|
-
HTTP_204_NO_CONTENT,
|
|
19
|
-
HTTP_404_NOT_FOUND,
|
|
20
|
-
HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
|
21
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
22
|
-
)
|
|
23
18
|
from typing_extensions import TypeAlias
|
|
24
19
|
|
|
25
20
|
import phoenix.trace.v1 as pb
|
|
@@ -49,16 +44,16 @@ router = APIRouter(tags=["traces"], include_in_schema=True)
|
|
|
49
44
|
dependencies=[Depends(is_not_locked)],
|
|
50
45
|
operation_id="addEvaluations",
|
|
51
46
|
summary="Add span, trace, or document evaluations",
|
|
52
|
-
status_code=
|
|
47
|
+
status_code=204,
|
|
53
48
|
responses=add_errors_to_responses(
|
|
54
49
|
[
|
|
55
50
|
{
|
|
56
|
-
"status_code":
|
|
51
|
+
"status_code": 415,
|
|
57
52
|
"description": (
|
|
58
53
|
"Unsupported content type, only gzipped protobuf and pandas-arrow are supported"
|
|
59
54
|
),
|
|
60
55
|
},
|
|
61
|
-
|
|
56
|
+
422,
|
|
62
57
|
]
|
|
63
58
|
),
|
|
64
59
|
openapi_extra={
|
|
@@ -79,29 +74,23 @@ async def post_evaluations(
|
|
|
79
74
|
if content_type == "application/x-pandas-arrow":
|
|
80
75
|
return await _process_pyarrow(request)
|
|
81
76
|
if content_type != "application/x-protobuf":
|
|
82
|
-
raise HTTPException(
|
|
83
|
-
detail="Unsupported content type", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
84
|
-
)
|
|
77
|
+
raise HTTPException(detail="Unsupported content type", status_code=415)
|
|
85
78
|
body = await request.body()
|
|
86
79
|
if content_encoding == "gzip":
|
|
87
80
|
body = gzip.decompress(body)
|
|
88
81
|
elif content_encoding:
|
|
89
|
-
raise HTTPException(
|
|
90
|
-
detail="Unsupported content encoding", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
91
|
-
)
|
|
82
|
+
raise HTTPException(detail="Unsupported content encoding", status_code=415)
|
|
92
83
|
evaluation = pb.Evaluation()
|
|
93
84
|
try:
|
|
94
85
|
evaluation.ParseFromString(body)
|
|
95
86
|
except DecodeError:
|
|
96
|
-
raise HTTPException(
|
|
97
|
-
detail="Request body is invalid", status_code=HTTP_422_UNPROCESSABLE_ENTITY
|
|
98
|
-
)
|
|
87
|
+
raise HTTPException(detail="Request body is invalid", status_code=422)
|
|
99
88
|
if not evaluation.name.strip():
|
|
100
89
|
raise HTTPException(
|
|
101
90
|
detail="Evaluation name must not be blank/empty",
|
|
102
|
-
status_code=
|
|
91
|
+
status_code=422,
|
|
103
92
|
)
|
|
104
|
-
await request.state.
|
|
93
|
+
await request.state.enqueue_evaluation(evaluation)
|
|
105
94
|
return Response()
|
|
106
95
|
|
|
107
96
|
|
|
@@ -109,7 +98,7 @@ async def post_evaluations(
|
|
|
109
98
|
"/evaluations",
|
|
110
99
|
operation_id="getEvaluations",
|
|
111
100
|
summary="Get span, trace, or document evaluations from a project",
|
|
112
|
-
responses=add_errors_to_responses([
|
|
101
|
+
responses=add_errors_to_responses([404]),
|
|
113
102
|
)
|
|
114
103
|
async def get_evaluations(
|
|
115
104
|
request: Request,
|
|
@@ -148,7 +137,7 @@ async def get_evaluations(
|
|
|
148
137
|
and span_evals_dataframe.empty
|
|
149
138
|
and document_evals_dataframe.empty
|
|
150
139
|
):
|
|
151
|
-
return Response(status_code=
|
|
140
|
+
return Response(status_code=404)
|
|
152
141
|
|
|
153
142
|
evals = chain(
|
|
154
143
|
map(
|
|
@@ -178,7 +167,7 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
178
167
|
except pa.ArrowInvalid:
|
|
179
168
|
raise HTTPException(
|
|
180
169
|
detail="Request body is not valid pyarrow",
|
|
181
|
-
status_code=
|
|
170
|
+
status_code=422,
|
|
182
171
|
)
|
|
183
172
|
try:
|
|
184
173
|
evaluations = Evaluations.from_pyarrow_reader(reader)
|
|
@@ -186,11 +175,11 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
186
175
|
if isinstance(e, PhoenixEvaluationNameIsMissing):
|
|
187
176
|
raise HTTPException(
|
|
188
177
|
detail="Evaluation name must not be blank/empty",
|
|
189
|
-
status_code=
|
|
178
|
+
status_code=422,
|
|
190
179
|
)
|
|
191
180
|
raise HTTPException(
|
|
192
181
|
detail="Invalid data in request body",
|
|
193
|
-
status_code=
|
|
182
|
+
status_code=422,
|
|
194
183
|
)
|
|
195
184
|
return Response(background=BackgroundTask(_add_evaluations, request.state, evaluations))
|
|
196
185
|
|
|
@@ -220,7 +209,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
220
209
|
explanation=explanation,
|
|
221
210
|
metadata_={},
|
|
222
211
|
)
|
|
223
|
-
await state.
|
|
212
|
+
await state.enqueue_annotations(document_annotation)
|
|
224
213
|
elif len(names) == 1 and names[0] in ("context.span_id", "span_id"):
|
|
225
214
|
for index, row in dataframe.iterrows():
|
|
226
215
|
score, label, explanation = _get_annotation_result(row)
|
|
@@ -234,7 +223,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
234
223
|
explanation=explanation,
|
|
235
224
|
metadata_={},
|
|
236
225
|
)
|
|
237
|
-
await state.
|
|
226
|
+
await state.enqueue_annotations(span_annotation)
|
|
238
227
|
elif len(names) == 1 and names[0] in ("context.trace_id", "trace_id"):
|
|
239
228
|
for index, row in dataframe.iterrows():
|
|
240
229
|
score, label, explanation = _get_annotation_result(row)
|
|
@@ -248,7 +237,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
248
237
|
explanation=explanation,
|
|
249
238
|
metadata_={},
|
|
250
239
|
)
|
|
251
|
-
await state.
|
|
240
|
+
await state.enqueue_annotations(trace_annotation)
|
|
252
241
|
|
|
253
242
|
|
|
254
243
|
def _get_annotation_result(
|
|
@@ -269,6 +258,7 @@ def _document_annotation_factory(
|
|
|
269
258
|
Callable[..., Precursors.DocumentAnnotation],
|
|
270
259
|
]:
|
|
271
260
|
return lambda index: lambda **kwargs: Precursors.DocumentAnnotation(
|
|
261
|
+
datetime.now(timezone.utc),
|
|
272
262
|
span_id=str(index[span_id_idx]),
|
|
273
263
|
document_position=int(index[document_position_idx]),
|
|
274
264
|
obj=models.DocumentAnnotation(
|
|
@@ -280,6 +270,7 @@ def _document_annotation_factory(
|
|
|
280
270
|
|
|
281
271
|
def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnotation]:
|
|
282
272
|
return lambda **kwargs: Precursors.SpanAnnotation(
|
|
273
|
+
datetime.now(timezone.utc),
|
|
283
274
|
span_id=str(span_id),
|
|
284
275
|
obj=models.SpanAnnotation(**kwargs),
|
|
285
276
|
)
|
|
@@ -287,6 +278,7 @@ def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnot
|
|
|
287
278
|
|
|
288
279
|
def _trace_annotation_factory(trace_id: str) -> Callable[..., Precursors.TraceAnnotation]:
|
|
289
280
|
return lambda **kwargs: Precursors.TraceAnnotation(
|
|
281
|
+
datetime.now(timezone.utc),
|
|
290
282
|
trace_id=str(trace_id),
|
|
291
283
|
obj=models.TraceAnnotation(**kwargs),
|
|
292
284
|
)
|
|
@@ -3,10 +3,10 @@ from typing import Any, Literal, Optional
|
|
|
3
3
|
|
|
4
4
|
from dateutil.parser import isoparse
|
|
5
5
|
from fastapi import APIRouter, HTTPException
|
|
6
|
-
from pydantic import Field
|
|
6
|
+
from pydantic import Field, model_validator
|
|
7
7
|
from starlette.requests import Request
|
|
8
|
-
from starlette.status import HTTP_404_NOT_FOUND
|
|
9
8
|
from strawberry.relay import GlobalID
|
|
9
|
+
from typing_extensions import Self
|
|
10
10
|
|
|
11
11
|
from phoenix.db import models
|
|
12
12
|
from phoenix.db.helpers import SupportedSQLDialect
|
|
@@ -36,15 +36,25 @@ class UpsertExperimentEvaluationRequestBody(V1RoutesBaseModel):
|
|
|
36
36
|
)
|
|
37
37
|
start_time: datetime = Field(description="The start time of the evaluation in ISO format")
|
|
38
38
|
end_time: datetime = Field(description="The end time of the evaluation in ISO format")
|
|
39
|
-
result: ExperimentEvaluationResult = Field(
|
|
39
|
+
result: Optional[ExperimentEvaluationResult] = Field(
|
|
40
|
+
None, description="The result of the evaluation. Either result or error must be provided."
|
|
41
|
+
)
|
|
40
42
|
error: Optional[str] = Field(
|
|
41
|
-
None,
|
|
43
|
+
None,
|
|
44
|
+
description="Error message if the evaluation encountered an error. "
|
|
45
|
+
"Either result or error must be provided.",
|
|
42
46
|
)
|
|
43
47
|
metadata: Optional[dict[str, Any]] = Field(
|
|
44
48
|
default=None, description="Metadata for the evaluation"
|
|
45
49
|
)
|
|
46
50
|
trace_id: Optional[str] = Field(default=None, description="Optional trace ID for tracking")
|
|
47
51
|
|
|
52
|
+
@model_validator(mode="after")
|
|
53
|
+
def validate_result_or_error(self) -> Self:
|
|
54
|
+
if self.result is None and self.error is None:
|
|
55
|
+
raise ValueError("Either 'result' or 'error' must be provided")
|
|
56
|
+
return self
|
|
57
|
+
|
|
48
58
|
|
|
49
59
|
class UpsertExperimentEvaluationResponseBodyData(V1RoutesBaseModel):
|
|
50
60
|
id: str = Field(description="The ID of the upserted experiment evaluation")
|
|
@@ -61,7 +71,7 @@ class UpsertExperimentEvaluationResponseBody(
|
|
|
61
71
|
operation_id="upsertExperimentEvaluation",
|
|
62
72
|
summary="Create or update evaluation for an experiment run",
|
|
63
73
|
responses=add_errors_to_responses(
|
|
64
|
-
[{"status_code":
|
|
74
|
+
[{"status_code": 404, "description": "Experiment run not found"}]
|
|
65
75
|
),
|
|
66
76
|
)
|
|
67
77
|
async def upsert_experiment_evaluation(
|
|
@@ -74,7 +84,7 @@ async def upsert_experiment_evaluation(
|
|
|
74
84
|
except ValueError:
|
|
75
85
|
raise HTTPException(
|
|
76
86
|
detail=f"ExperimentRun with ID {experiment_run_gid} does not exist",
|
|
77
|
-
status_code=
|
|
87
|
+
status_code=404,
|
|
78
88
|
)
|
|
79
89
|
name = request_body.name
|
|
80
90
|
annotator_kind = request_body.annotator_kind
|