arize-phoenix 10.0.4__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-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +124 -72
- arize_phoenix-12.28.1.dist-info/RECORD +499 -0
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-10.0.4.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 +5 -4
- phoenix/auth.py +39 -2
- phoenix/config.py +1763 -91
- phoenix/datetime_utils.py +120 -2
- phoenix/db/README.md +595 -25
- phoenix/db/bulk_inserter.py +145 -103
- phoenix/db/engines.py +140 -33
- phoenix/db/enums.py +3 -12
- phoenix/db/facilitator.py +302 -35
- phoenix/db/helpers.py +1000 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +135 -2
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +17 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span.py +15 -11
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +50 -20
- 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/a20694b15f82_cost.py +196 -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 +669 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/model_provider.py +4 -0
- phoenix/db/types/token_price_customization.py +29 -0
- phoenix/db/types/trace_retention.py +23 -15
- phoenix/experiments/evaluators/utils.py +3 -3
- phoenix/experiments/functions.py +160 -52
- phoenix/experiments/tracing.py +2 -2
- phoenix/experiments/types.py +1 -1
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +38 -7
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +100 -4
- phoenix/server/api/dataloaders/__init__.py +79 -5
- phoenix/server/api/dataloaders/annotation_configs_by_project.py +31 -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/last_used_times_by_generative_model_id.py +35 -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_by_span.py +24 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
- phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +57 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +152 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
- phoenix/server/api/dataloaders/span_costs.py +29 -0
- 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/dataloaders/types.py +29 -0
- phoenix/server/api/exceptions.py +11 -1
- phoenix/server/api/helpers/dataset_helpers.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +1243 -292
- phoenix/server/api/helpers/playground_registry.py +2 -2
- phoenix/server/api/helpers/playground_spans.py +8 -4
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +205 -22
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +6 -2
- phoenix/server/api/input_types/CreateProjectInput.py +27 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +17 -0
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeCredentialInput.py +9 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +5 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +161 -1
- phoenix/server/api/input_types/PromptFilter.py +14 -0
- phoenix/server/api/input_types/PromptVersionInput.py +52 -1
- phoenix/server/api/input_types/SpanSort.py +44 -7
- phoenix/server/api/input_types/TimeBinConfig.py +23 -0
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +10 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +19 -23
- phoenix/server/api/mutations/chat_mutations.py +154 -47
- 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 +210 -0
- phoenix/server/api/mutations/project_mutations.py +49 -10
- 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 +14 -10
- phoenix/server/api/mutations/trace_mutations.py +47 -3
- phoenix/server/api/mutations/user_mutations.py +66 -41
- phoenix/server/api/queries.py +768 -293
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +154 -88
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +369 -106
- phoenix/server/api/routers/v1/__init__.py +24 -4
- phoenix/server/api/routers/v1/annotation_configs.py +23 -31
- phoenix/server/api/routers/v1/annotations.py +481 -17
- phoenix/server/api/routers/v1/datasets.py +395 -81
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +24 -31
- phoenix/server/api/routers/v1/experiment_evaluations.py +19 -8
- phoenix/server/api/routers/v1/experiment_runs.py +337 -59
- phoenix/server/api/routers/v1/experiments.py +479 -48
- phoenix/server/api/routers/v1/models.py +7 -0
- phoenix/server/api/routers/v1/projects.py +18 -49
- phoenix/server/api/routers/v1/prompts.py +54 -40
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +1091 -81
- phoenix/server/api/routers/v1/traces.py +132 -78
- phoenix/server/api/routers/v1/users.py +389 -0
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +305 -88
- 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/CostBreakdown.py +12 -0
- phoenix/server/api/types/Dataset.py +226 -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 +264 -59
- phoenix/server/api/types/ExperimentComparison.py +5 -10
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +169 -65
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +245 -3
- phoenix/server/api/types/GenerativeProvider.py +70 -11
- phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
- phoenix/server/api/types/ModelInterface.py +16 -0
- phoenix/server/api/types/PlaygroundModel.py +20 -0
- phoenix/server/api/types/Project.py +1278 -216
- phoenix/server/api/types/ProjectSession.py +188 -28
- 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/ServerStatus.py +6 -0
- phoenix/server/api/types/Span.py +167 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
- phoenix/server/api/types/SpanCostSummary.py +10 -0
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/TokenPrice.py +16 -0
- phoenix/server/api/types/TokenUsage.py +3 -3
- phoenix/server/api/types/Trace.py +223 -51
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +137 -32
- 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 +290 -45
- phoenix/server/authorization.py +38 -3
- phoenix/server/bearer_auth.py +34 -24
- phoenix/server/cost_tracking/cost_details_calculator.py +196 -0
- phoenix/server/cost_tracking/cost_model_lookup.py +179 -0
- phoenix/server/cost_tracking/helpers.py +68 -0
- phoenix/server/cost_tracking/model_cost_manifest.json +3657 -830
- phoenix/server/cost_tracking/regex_specificity.py +397 -0
- phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
- phoenix/server/daemons/__init__.py +0 -0
- phoenix/server/daemons/db_disk_usage_monitor.py +214 -0
- phoenix/server/daemons/generative_model_store.py +103 -0
- phoenix/server/daemons/span_cost_calculator.py +99 -0
- phoenix/server/dml_event.py +17 -0
- phoenix/server/dml_event_handler.py +5 -0
- phoenix/server/email/sender.py +56 -3
- phoenix/server/email/templates/db_disk_usage_notification.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/experiments/__init__.py +0 -0
- phoenix/server/experiments/utils.py +14 -0
- phoenix/server/grpc_server.py +11 -11
- phoenix/server/jwt_store.py +17 -15
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +26 -10
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +66 -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 +55 -51
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-E0M82BdE.js → index-CTQoemZv.js} +140 -56
- 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-V9cwpXsm.js +37 -0
- phoenix/server/static/assets/vendor-shiki-Do--csgv.js +5 -0
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +40 -6
- phoenix/server/thread_server.py +1 -2
- phoenix/server/types.py +14 -4
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +56 -3
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +14 -5
- phoenix/session/session.py +45 -9
- phoenix/settings.py +5 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/helpers.py +90 -1
- phoenix/trace/dsl/query.py +8 -6
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- arize_phoenix-10.0.4.dist-info/RECORD +0 -405
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/cost_tracking/cost_lookup.py +0 -255
- phoenix/server/static/assets/components-DULKeDfL.js +0 -4365
- phoenix/server/static/assets/pages-Cl0A-0U2.js +0 -7430
- phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Dy-0mSNw.js +0 -649
- phoenix/server/static/assets/vendor-codemirror-DBtifKNr.js +0 -33
- phoenix/server/static/assets/vendor-oB4u9zuV.js +0 -905
- phoenix/server/static/assets/vendor-recharts-D-T4KPz2.js +0 -59
- phoenix/server/static/assets/vendor-shiki-BMn4O_9F.js +0 -5
- phoenix/server/static/assets/vendor-three-C5WAXd5r.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-10.0.4.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,11 +1,12 @@
|
|
|
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
|
|
|
6
7
|
import pandas as pd
|
|
7
8
|
import pyarrow as pa
|
|
8
|
-
from fastapi import APIRouter, Header, HTTPException, Query
|
|
9
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Query
|
|
9
10
|
from google.protobuf.message import DecodeError
|
|
10
11
|
from pandas import DataFrame
|
|
11
12
|
from sqlalchemy import select
|
|
@@ -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
|
|
@@ -28,6 +23,7 @@ from phoenix.db import models
|
|
|
28
23
|
from phoenix.db.insertion.types import Precursors
|
|
29
24
|
from phoenix.exceptions import PhoenixEvaluationNameIsMissing
|
|
30
25
|
from phoenix.server.api.routers.utils import table_to_bytes
|
|
26
|
+
from phoenix.server.authorization import is_not_locked
|
|
31
27
|
from phoenix.server.types import DbSessionFactory
|
|
32
28
|
from phoenix.trace.span_evaluations import (
|
|
33
29
|
DocumentEvaluations,
|
|
@@ -45,19 +41,19 @@ router = APIRouter(tags=["traces"], include_in_schema=True)
|
|
|
45
41
|
|
|
46
42
|
@router.post(
|
|
47
43
|
"/evaluations",
|
|
44
|
+
dependencies=[Depends(is_not_locked)],
|
|
48
45
|
operation_id="addEvaluations",
|
|
49
46
|
summary="Add span, trace, or document evaluations",
|
|
50
|
-
status_code=
|
|
47
|
+
status_code=204,
|
|
51
48
|
responses=add_errors_to_responses(
|
|
52
49
|
[
|
|
53
50
|
{
|
|
54
|
-
"status_code":
|
|
51
|
+
"status_code": 415,
|
|
55
52
|
"description": (
|
|
56
|
-
"Unsupported content type, "
|
|
57
|
-
"only gzipped protobuf and pandas-arrow are supported"
|
|
53
|
+
"Unsupported content type, only gzipped protobuf and pandas-arrow are supported"
|
|
58
54
|
),
|
|
59
55
|
},
|
|
60
|
-
|
|
56
|
+
422,
|
|
61
57
|
]
|
|
62
58
|
),
|
|
63
59
|
openapi_extra={
|
|
@@ -78,29 +74,23 @@ async def post_evaluations(
|
|
|
78
74
|
if content_type == "application/x-pandas-arrow":
|
|
79
75
|
return await _process_pyarrow(request)
|
|
80
76
|
if content_type != "application/x-protobuf":
|
|
81
|
-
raise HTTPException(
|
|
82
|
-
detail="Unsupported content type", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
83
|
-
)
|
|
77
|
+
raise HTTPException(detail="Unsupported content type", status_code=415)
|
|
84
78
|
body = await request.body()
|
|
85
79
|
if content_encoding == "gzip":
|
|
86
80
|
body = gzip.decompress(body)
|
|
87
81
|
elif content_encoding:
|
|
88
|
-
raise HTTPException(
|
|
89
|
-
detail="Unsupported content encoding", status_code=HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
90
|
-
)
|
|
82
|
+
raise HTTPException(detail="Unsupported content encoding", status_code=415)
|
|
91
83
|
evaluation = pb.Evaluation()
|
|
92
84
|
try:
|
|
93
85
|
evaluation.ParseFromString(body)
|
|
94
86
|
except DecodeError:
|
|
95
|
-
raise HTTPException(
|
|
96
|
-
detail="Request body is invalid", status_code=HTTP_422_UNPROCESSABLE_ENTITY
|
|
97
|
-
)
|
|
87
|
+
raise HTTPException(detail="Request body is invalid", status_code=422)
|
|
98
88
|
if not evaluation.name.strip():
|
|
99
89
|
raise HTTPException(
|
|
100
90
|
detail="Evaluation name must not be blank/empty",
|
|
101
|
-
status_code=
|
|
91
|
+
status_code=422,
|
|
102
92
|
)
|
|
103
|
-
await request.state.
|
|
93
|
+
await request.state.enqueue_evaluation(evaluation)
|
|
104
94
|
return Response()
|
|
105
95
|
|
|
106
96
|
|
|
@@ -108,7 +98,7 @@ async def post_evaluations(
|
|
|
108
98
|
"/evaluations",
|
|
109
99
|
operation_id="getEvaluations",
|
|
110
100
|
summary="Get span, trace, or document evaluations from a project",
|
|
111
|
-
responses=add_errors_to_responses([
|
|
101
|
+
responses=add_errors_to_responses([404]),
|
|
112
102
|
)
|
|
113
103
|
async def get_evaluations(
|
|
114
104
|
request: Request,
|
|
@@ -147,7 +137,7 @@ async def get_evaluations(
|
|
|
147
137
|
and span_evals_dataframe.empty
|
|
148
138
|
and document_evals_dataframe.empty
|
|
149
139
|
):
|
|
150
|
-
return Response(status_code=
|
|
140
|
+
return Response(status_code=404)
|
|
151
141
|
|
|
152
142
|
evals = chain(
|
|
153
143
|
map(
|
|
@@ -177,7 +167,7 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
177
167
|
except pa.ArrowInvalid:
|
|
178
168
|
raise HTTPException(
|
|
179
169
|
detail="Request body is not valid pyarrow",
|
|
180
|
-
status_code=
|
|
170
|
+
status_code=422,
|
|
181
171
|
)
|
|
182
172
|
try:
|
|
183
173
|
evaluations = Evaluations.from_pyarrow_reader(reader)
|
|
@@ -185,11 +175,11 @@ async def _process_pyarrow(request: Request) -> Response:
|
|
|
185
175
|
if isinstance(e, PhoenixEvaluationNameIsMissing):
|
|
186
176
|
raise HTTPException(
|
|
187
177
|
detail="Evaluation name must not be blank/empty",
|
|
188
|
-
status_code=
|
|
178
|
+
status_code=422,
|
|
189
179
|
)
|
|
190
180
|
raise HTTPException(
|
|
191
181
|
detail="Invalid data in request body",
|
|
192
|
-
status_code=
|
|
182
|
+
status_code=422,
|
|
193
183
|
)
|
|
194
184
|
return Response(background=BackgroundTask(_add_evaluations, request.state, evaluations))
|
|
195
185
|
|
|
@@ -219,7 +209,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
219
209
|
explanation=explanation,
|
|
220
210
|
metadata_={},
|
|
221
211
|
)
|
|
222
|
-
await state.
|
|
212
|
+
await state.enqueue_annotations(document_annotation)
|
|
223
213
|
elif len(names) == 1 and names[0] in ("context.span_id", "span_id"):
|
|
224
214
|
for index, row in dataframe.iterrows():
|
|
225
215
|
score, label, explanation = _get_annotation_result(row)
|
|
@@ -233,7 +223,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
233
223
|
explanation=explanation,
|
|
234
224
|
metadata_={},
|
|
235
225
|
)
|
|
236
|
-
await state.
|
|
226
|
+
await state.enqueue_annotations(span_annotation)
|
|
237
227
|
elif len(names) == 1 and names[0] in ("context.trace_id", "trace_id"):
|
|
238
228
|
for index, row in dataframe.iterrows():
|
|
239
229
|
score, label, explanation = _get_annotation_result(row)
|
|
@@ -247,7 +237,7 @@ async def _add_evaluations(state: State, evaluations: Evaluations) -> None:
|
|
|
247
237
|
explanation=explanation,
|
|
248
238
|
metadata_={},
|
|
249
239
|
)
|
|
250
|
-
await state.
|
|
240
|
+
await state.enqueue_annotations(trace_annotation)
|
|
251
241
|
|
|
252
242
|
|
|
253
243
|
def _get_annotation_result(
|
|
@@ -268,6 +258,7 @@ def _document_annotation_factory(
|
|
|
268
258
|
Callable[..., Precursors.DocumentAnnotation],
|
|
269
259
|
]:
|
|
270
260
|
return lambda index: lambda **kwargs: Precursors.DocumentAnnotation(
|
|
261
|
+
datetime.now(timezone.utc),
|
|
271
262
|
span_id=str(index[span_id_idx]),
|
|
272
263
|
document_position=int(index[document_position_idx]),
|
|
273
264
|
obj=models.DocumentAnnotation(
|
|
@@ -279,6 +270,7 @@ def _document_annotation_factory(
|
|
|
279
270
|
|
|
280
271
|
def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnotation]:
|
|
281
272
|
return lambda **kwargs: Precursors.SpanAnnotation(
|
|
273
|
+
datetime.now(timezone.utc),
|
|
282
274
|
span_id=str(span_id),
|
|
283
275
|
obj=models.SpanAnnotation(**kwargs),
|
|
284
276
|
)
|
|
@@ -286,6 +278,7 @@ def _span_annotation_factory(span_id: str) -> Callable[..., Precursors.SpanAnnot
|
|
|
286
278
|
|
|
287
279
|
def _trace_annotation_factory(trace_id: str) -> Callable[..., Precursors.TraceAnnotation]:
|
|
288
280
|
return lambda **kwargs: Precursors.TraceAnnotation(
|
|
281
|
+
datetime.now(timezone.utc),
|
|
289
282
|
trace_id=str(trace_id),
|
|
290
283
|
obj=models.TraceAnnotation(**kwargs),
|
|
291
284
|
)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import Any, Literal, Optional
|
|
3
3
|
|
|
4
|
+
from dateutil.parser import isoparse
|
|
4
5
|
from fastapi import APIRouter, HTTPException
|
|
5
|
-
from pydantic import Field
|
|
6
|
+
from pydantic import Field, model_validator
|
|
6
7
|
from starlette.requests import Request
|
|
7
|
-
from starlette.status import HTTP_404_NOT_FOUND
|
|
8
8
|
from strawberry.relay import GlobalID
|
|
9
|
+
from typing_extensions import Self
|
|
9
10
|
|
|
10
11
|
from phoenix.db import models
|
|
11
12
|
from phoenix.db.helpers import SupportedSQLDialect
|
|
@@ -35,15 +36,25 @@ class UpsertExperimentEvaluationRequestBody(V1RoutesBaseModel):
|
|
|
35
36
|
)
|
|
36
37
|
start_time: datetime = Field(description="The start time of the evaluation in ISO format")
|
|
37
38
|
end_time: datetime = Field(description="The end time of the evaluation in ISO format")
|
|
38
|
-
result: ExperimentEvaluationResult = Field(
|
|
39
|
+
result: Optional[ExperimentEvaluationResult] = Field(
|
|
40
|
+
None, description="The result of the evaluation. Either result or error must be provided."
|
|
41
|
+
)
|
|
39
42
|
error: Optional[str] = Field(
|
|
40
|
-
None,
|
|
43
|
+
None,
|
|
44
|
+
description="Error message if the evaluation encountered an error. "
|
|
45
|
+
"Either result or error must be provided.",
|
|
41
46
|
)
|
|
42
47
|
metadata: Optional[dict[str, Any]] = Field(
|
|
43
48
|
default=None, description="Metadata for the evaluation"
|
|
44
49
|
)
|
|
45
50
|
trace_id: Optional[str] = Field(default=None, description="Optional trace ID for tracking")
|
|
46
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
|
+
|
|
47
58
|
|
|
48
59
|
class UpsertExperimentEvaluationResponseBodyData(V1RoutesBaseModel):
|
|
49
60
|
id: str = Field(description="The ID of the upserted experiment evaluation")
|
|
@@ -60,7 +71,7 @@ class UpsertExperimentEvaluationResponseBody(
|
|
|
60
71
|
operation_id="upsertExperimentEvaluation",
|
|
61
72
|
summary="Create or update evaluation for an experiment run",
|
|
62
73
|
responses=add_errors_to_responses(
|
|
63
|
-
[{"status_code":
|
|
74
|
+
[{"status_code": 404, "description": "Experiment run not found"}]
|
|
64
75
|
),
|
|
65
76
|
)
|
|
66
77
|
async def upsert_experiment_evaluation(
|
|
@@ -73,7 +84,7 @@ async def upsert_experiment_evaluation(
|
|
|
73
84
|
except ValueError:
|
|
74
85
|
raise HTTPException(
|
|
75
86
|
detail=f"ExperimentRun with ID {experiment_run_gid} does not exist",
|
|
76
|
-
status_code=
|
|
87
|
+
status_code=404,
|
|
77
88
|
)
|
|
78
89
|
name = request_body.name
|
|
79
90
|
annotator_kind = request_body.annotator_kind
|
|
@@ -95,8 +106,8 @@ async def upsert_experiment_evaluation(
|
|
|
95
106
|
explanation=explanation,
|
|
96
107
|
error=error,
|
|
97
108
|
metadata_=metadata, # `metadata_` must match database
|
|
98
|
-
start_time=
|
|
99
|
-
end_time=
|
|
109
|
+
start_time=isoparse(start_time),
|
|
110
|
+
end_time=isoparse(end_time),
|
|
100
111
|
trace_id=payload.get("trace_id"),
|
|
101
112
|
)
|
|
102
113
|
dialect = SupportedSQLDialect(session.bind.dialect.name)
|