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,210 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
import strawberry
|
|
7
|
+
from sqlalchemy import delete
|
|
8
|
+
from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
|
|
9
|
+
from sqlalchemy.orm import joinedload
|
|
10
|
+
from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
|
|
11
|
+
from strawberry.relay import GlobalID
|
|
12
|
+
from strawberry.types import Info
|
|
13
|
+
|
|
14
|
+
from phoenix.db import models
|
|
15
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
16
|
+
from phoenix.server.api.context import Context
|
|
17
|
+
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
18
|
+
from phoenix.server.api.queries import Query
|
|
19
|
+
from phoenix.server.api.types.GenerativeModel import GenerativeModel
|
|
20
|
+
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
21
|
+
from phoenix.server.api.types.TokenPrice import TokenKind
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@strawberry.input
|
|
25
|
+
class TokenPriceInput:
|
|
26
|
+
token_type: str
|
|
27
|
+
cost_per_million_tokens: float
|
|
28
|
+
kind: TokenKind
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def token_prices(self) -> models.TokenPrice:
|
|
32
|
+
"""Generate TokenPrice instances based on the input."""
|
|
33
|
+
return models.TokenPrice(
|
|
34
|
+
token_type=self.token_type,
|
|
35
|
+
is_prompt=self.kind == TokenKind.PROMPT,
|
|
36
|
+
base_rate=self.cost_per_million_tokens / 1_000_000,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@strawberry.input
|
|
41
|
+
class CreateModelMutationInput:
|
|
42
|
+
name: str
|
|
43
|
+
provider: Optional[str] = None
|
|
44
|
+
name_pattern: str
|
|
45
|
+
costs: list[TokenPriceInput]
|
|
46
|
+
start_time: Optional[datetime] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@strawberry.type
|
|
50
|
+
class CreateModelMutationPayload:
|
|
51
|
+
model: GenerativeModel
|
|
52
|
+
query: Query
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@strawberry.input
|
|
56
|
+
class UpdateModelMutationInput:
|
|
57
|
+
id: GlobalID
|
|
58
|
+
name: str
|
|
59
|
+
provider: Optional[str]
|
|
60
|
+
name_pattern: str
|
|
61
|
+
costs: list[TokenPriceInput]
|
|
62
|
+
start_time: Optional[datetime] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@strawberry.type
|
|
66
|
+
class UpdateModelMutationPayload:
|
|
67
|
+
model: GenerativeModel
|
|
68
|
+
query: Query
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@strawberry.input
|
|
72
|
+
class DeleteModelMutationInput:
|
|
73
|
+
id: GlobalID
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@strawberry.type
|
|
77
|
+
class DeleteModelMutationPayload:
|
|
78
|
+
model: GenerativeModel
|
|
79
|
+
query: Query
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@strawberry.type
|
|
83
|
+
class ModelMutationMixin:
|
|
84
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
85
|
+
async def create_model(
|
|
86
|
+
self,
|
|
87
|
+
info: Info[Context, None],
|
|
88
|
+
input: CreateModelMutationInput,
|
|
89
|
+
) -> CreateModelMutationPayload:
|
|
90
|
+
cost_types = set(cost.token_type for cost in input.costs)
|
|
91
|
+
if "input" not in cost_types:
|
|
92
|
+
raise BadRequest("input cost is required")
|
|
93
|
+
if "output" not in cost_types:
|
|
94
|
+
raise BadRequest("output cost is required")
|
|
95
|
+
name_pattern = _compile_regular_expression(input.name_pattern)
|
|
96
|
+
token_prices = [cost.token_prices for cost in input.costs]
|
|
97
|
+
model = models.GenerativeModel(
|
|
98
|
+
name=input.name,
|
|
99
|
+
provider=input.provider,
|
|
100
|
+
name_pattern=name_pattern,
|
|
101
|
+
is_built_in=False,
|
|
102
|
+
token_prices=token_prices,
|
|
103
|
+
start_time=input.start_time,
|
|
104
|
+
)
|
|
105
|
+
async with info.context.db() as session:
|
|
106
|
+
session.add(model)
|
|
107
|
+
try:
|
|
108
|
+
await session.flush()
|
|
109
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
110
|
+
raise Conflict(f"Model with name '{input.name}' already exists")
|
|
111
|
+
|
|
112
|
+
return CreateModelMutationPayload(
|
|
113
|
+
model=GenerativeModel(id=model.id, db_record=model),
|
|
114
|
+
query=Query(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
118
|
+
async def update_model(
|
|
119
|
+
self,
|
|
120
|
+
info: Info[Context, None],
|
|
121
|
+
input: UpdateModelMutationInput,
|
|
122
|
+
) -> UpdateModelMutationPayload:
|
|
123
|
+
try:
|
|
124
|
+
model_id = from_global_id_with_expected_type(input.id, GenerativeModel.__name__)
|
|
125
|
+
except ValueError:
|
|
126
|
+
raise BadRequest(f'Invalid model id: "{input.id}"')
|
|
127
|
+
|
|
128
|
+
cost_types = set(cost.token_type for cost in input.costs)
|
|
129
|
+
if "input" not in cost_types:
|
|
130
|
+
raise BadRequest("input cost is required")
|
|
131
|
+
if "output" not in cost_types:
|
|
132
|
+
raise BadRequest("output cost is required")
|
|
133
|
+
name_pattern = _compile_regular_expression(input.name_pattern)
|
|
134
|
+
token_prices = [cost.token_prices for cost in input.costs]
|
|
135
|
+
async with info.context.db() as session:
|
|
136
|
+
model = await session.scalar(
|
|
137
|
+
sa.select(models.GenerativeModel)
|
|
138
|
+
.where(models.GenerativeModel.deleted_at.is_(None))
|
|
139
|
+
.where(models.GenerativeModel.id == model_id)
|
|
140
|
+
.options(joinedload(models.GenerativeModel.token_prices))
|
|
141
|
+
)
|
|
142
|
+
if model is None:
|
|
143
|
+
raise NotFound(f'Model "{input.id}" not found')
|
|
144
|
+
if model.is_built_in:
|
|
145
|
+
raise BadRequest("Cannot update built-in model")
|
|
146
|
+
|
|
147
|
+
await session.execute(
|
|
148
|
+
delete(models.TokenPrice).where(models.TokenPrice.model_id == model.id)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
await session.refresh(model)
|
|
152
|
+
|
|
153
|
+
model.name = input.name
|
|
154
|
+
model.provider = input.provider or ""
|
|
155
|
+
model.name_pattern = name_pattern
|
|
156
|
+
model.token_prices = token_prices
|
|
157
|
+
model.start_time = input.start_time
|
|
158
|
+
# Explicitly set updated_at so the GenerativeModelStore daemon picks up this
|
|
159
|
+
# change (SQLAlchemy's onupdate may not trigger for relationship-only changes).
|
|
160
|
+
model.updated_at = datetime.now(timezone.utc)
|
|
161
|
+
session.add(model)
|
|
162
|
+
try:
|
|
163
|
+
await session.flush()
|
|
164
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
165
|
+
raise Conflict(f"Model with name '{input.name}' already exists")
|
|
166
|
+
|
|
167
|
+
return UpdateModelMutationPayload(
|
|
168
|
+
model=GenerativeModel(id=model.id, db_record=model),
|
|
169
|
+
query=Query(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
173
|
+
async def delete_model(
|
|
174
|
+
self,
|
|
175
|
+
info: Info[Context, None],
|
|
176
|
+
input: DeleteModelMutationInput,
|
|
177
|
+
) -> DeleteModelMutationPayload:
|
|
178
|
+
try:
|
|
179
|
+
model_id = from_global_id_with_expected_type(input.id, GenerativeModel.__name__)
|
|
180
|
+
except ValueError:
|
|
181
|
+
raise BadRequest(f'Invalid model id: "{input.id}"')
|
|
182
|
+
|
|
183
|
+
async with info.context.db() as session:
|
|
184
|
+
model = await session.scalar(
|
|
185
|
+
sa.update(models.GenerativeModel)
|
|
186
|
+
.values(deleted_at=datetime.now(timezone.utc))
|
|
187
|
+
.where(models.GenerativeModel.deleted_at.is_(None))
|
|
188
|
+
.where(models.GenerativeModel.id == model_id)
|
|
189
|
+
.returning(models.GenerativeModel)
|
|
190
|
+
)
|
|
191
|
+
if model is None:
|
|
192
|
+
raise NotFound(f'Model "{input.id}" not found')
|
|
193
|
+
if model.is_built_in:
|
|
194
|
+
await session.rollback()
|
|
195
|
+
raise BadRequest("Cannot delete built-in model")
|
|
196
|
+
return DeleteModelMutationPayload(
|
|
197
|
+
model=GenerativeModel(id=model.id, db_record=model),
|
|
198
|
+
query=Query(),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _compile_regular_expression(maybe_regex: str) -> re.Pattern[str]:
|
|
203
|
+
"""
|
|
204
|
+
Compile the given string as a regular expression.
|
|
205
|
+
Raises a BadRequest error if the given string is not a valid regex.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
return re.compile(maybe_regex)
|
|
209
|
+
except re.error as error:
|
|
210
|
+
raise BadRequest(f"Invalid regex: {str(error)}")
|
|
@@ -1,22 +1,58 @@
|
|
|
1
1
|
import strawberry
|
|
2
2
|
from sqlalchemy import delete, select
|
|
3
|
+
from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
|
|
3
4
|
from sqlalchemy.orm import load_only
|
|
5
|
+
from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
|
|
4
6
|
from strawberry.relay import GlobalID
|
|
5
7
|
from strawberry.types import Info
|
|
6
8
|
|
|
7
9
|
from phoenix.config import DEFAULT_PROJECT_NAME
|
|
8
10
|
from phoenix.db import models
|
|
9
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
11
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
10
12
|
from phoenix.server.api.context import Context
|
|
13
|
+
from phoenix.server.api.exceptions import BadRequest, Conflict
|
|
11
14
|
from phoenix.server.api.input_types.ClearProjectInput import ClearProjectInput
|
|
15
|
+
from phoenix.server.api.input_types.CreateProjectInput import CreateProjectInput
|
|
12
16
|
from phoenix.server.api.queries import Query
|
|
13
17
|
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
14
|
-
from phoenix.server.
|
|
18
|
+
from phoenix.server.api.types.Project import Project, to_gql_project
|
|
19
|
+
from phoenix.server.dml_event import ProjectDeleteEvent, ProjectInsertEvent, SpanDeleteEvent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@strawberry.type
|
|
23
|
+
class ProjectMutationPayload:
|
|
24
|
+
project: Project
|
|
25
|
+
query: Query
|
|
15
26
|
|
|
16
27
|
|
|
17
28
|
@strawberry.type
|
|
18
29
|
class ProjectMutationMixin:
|
|
19
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
30
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
31
|
+
async def create_project(
|
|
32
|
+
self,
|
|
33
|
+
info: Info[Context, None],
|
|
34
|
+
input: CreateProjectInput,
|
|
35
|
+
) -> ProjectMutationPayload:
|
|
36
|
+
if not (name := input.name.strip()):
|
|
37
|
+
raise BadRequest("Name cannot be empty")
|
|
38
|
+
description = (input.description or "").strip() or None
|
|
39
|
+
gradient_start_color = (input.gradient_start_color or "").strip() or None
|
|
40
|
+
gradient_end_color = (input.gradient_end_color or "").strip() or None
|
|
41
|
+
project = models.Project(
|
|
42
|
+
name=name,
|
|
43
|
+
description=description,
|
|
44
|
+
gradient_start_color=gradient_start_color,
|
|
45
|
+
gradient_end_color=gradient_end_color,
|
|
46
|
+
)
|
|
47
|
+
try:
|
|
48
|
+
async with info.context.db() as session:
|
|
49
|
+
session.add(project)
|
|
50
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError):
|
|
51
|
+
raise Conflict(f"Project with name '{name}' already exists")
|
|
52
|
+
info.context.event_queue.put(ProjectInsertEvent((project.id,)))
|
|
53
|
+
return ProjectMutationPayload(project=to_gql_project(project), query=Query())
|
|
54
|
+
|
|
55
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
20
56
|
async def delete_project(self, info: Info[Context, None], id: GlobalID) -> Query:
|
|
21
57
|
project_id = from_global_id_with_expected_type(global_id=id, expected_type_name="Project")
|
|
22
58
|
async with info.context.db() as session:
|
|
@@ -33,7 +69,7 @@ class ProjectMutationMixin:
|
|
|
33
69
|
info.context.event_queue.put(ProjectDeleteEvent((project_id,)))
|
|
34
70
|
return Query()
|
|
35
71
|
|
|
36
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
72
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
37
73
|
async def clear_project(self, info: Info[Context, None], input: ClearProjectInput) -> Query:
|
|
38
74
|
project_id = from_global_id_with_expected_type(
|
|
39
75
|
global_id=input.id, expected_type_name="Project"
|
|
@@ -47,11 +83,14 @@ class ProjectMutationMixin:
|
|
|
47
83
|
delete_statement = delete_statement.where(models.Trace.start_time < input.end_time)
|
|
48
84
|
async with info.context.db() as session:
|
|
49
85
|
deleted_trace_project_session_ids = await session.scalars(delete_statement)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
86
|
+
session_ids_to_delete = [
|
|
87
|
+
id_ for id_ in set(deleted_trace_project_session_ids) if id_ is not None
|
|
88
|
+
]
|
|
89
|
+
# Process deletions in chunks of 10000 to avoid PostgreSQL argument limit
|
|
90
|
+
chunk_size = 10000
|
|
91
|
+
stmt = delete(models.ProjectSession)
|
|
92
|
+
for i in range(0, len(session_ids_to_delete), chunk_size):
|
|
93
|
+
chunk = session_ids_to_delete[i : i + chunk_size]
|
|
94
|
+
await session.execute(stmt.where(models.ProjectSession.id.in_(chunk)))
|
|
56
95
|
info.context.event_queue.put(SpanDeleteEvent((project_id,)))
|
|
57
96
|
return Query()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import strawberry
|
|
4
|
+
from sqlalchemy.exc import IntegrityError as PostgreSQLIntegrityError
|
|
5
|
+
from sqlean.dbapi2 import IntegrityError as SQLiteIntegrityError # type: ignore[import-untyped]
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from strawberry import Info
|
|
8
|
+
from strawberry.relay import GlobalID
|
|
9
|
+
|
|
10
|
+
from phoenix.db import models
|
|
11
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
12
|
+
from phoenix.server.api.context import Context
|
|
13
|
+
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
|
|
14
|
+
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
15
|
+
from phoenix.server.api.input_types.CreateProjectSessionAnnotationInput import (
|
|
16
|
+
CreateProjectSessionAnnotationInput,
|
|
17
|
+
)
|
|
18
|
+
from phoenix.server.api.input_types.UpdateAnnotationInput import UpdateAnnotationInput
|
|
19
|
+
from phoenix.server.api.queries import Query
|
|
20
|
+
from phoenix.server.api.types.AnnotationSource import AnnotationSource
|
|
21
|
+
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
22
|
+
from phoenix.server.api.types.ProjectSessionAnnotation import ProjectSessionAnnotation
|
|
23
|
+
from phoenix.server.bearer_auth import PhoenixUser
|
|
24
|
+
from phoenix.server.dml_event import (
|
|
25
|
+
ProjectSessionAnnotationDeleteEvent,
|
|
26
|
+
ProjectSessionAnnotationInsertEvent,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@strawberry.type
|
|
31
|
+
class ProjectSessionAnnotationMutationPayload:
|
|
32
|
+
project_session_annotation: ProjectSessionAnnotation
|
|
33
|
+
query: Query
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@strawberry.type
|
|
37
|
+
class ProjectSessionAnnotationMutationMixin:
|
|
38
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
39
|
+
async def create_project_session_annotations(
|
|
40
|
+
self, info: Info[Context, None], input: CreateProjectSessionAnnotationInput
|
|
41
|
+
) -> ProjectSessionAnnotationMutationPayload:
|
|
42
|
+
assert isinstance(request := info.context.request, Request)
|
|
43
|
+
user_id: Optional[int] = None
|
|
44
|
+
if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
|
|
45
|
+
user_id = int(user.identity)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
project_session_id = from_global_id_with_expected_type(
|
|
49
|
+
input.project_session_id, "ProjectSession"
|
|
50
|
+
)
|
|
51
|
+
except ValueError:
|
|
52
|
+
raise BadRequest(f"Invalid session ID: {input.project_session_id}")
|
|
53
|
+
|
|
54
|
+
identifier = ""
|
|
55
|
+
if isinstance(input.identifier, str):
|
|
56
|
+
identifier = input.identifier # Already trimmed in __post_init__
|
|
57
|
+
elif input.source == AnnotationSource.APP and user_id is not None:
|
|
58
|
+
identifier = get_user_identifier(user_id)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
async with info.context.db() as session:
|
|
62
|
+
anno = models.ProjectSessionAnnotation(
|
|
63
|
+
project_session_id=project_session_id,
|
|
64
|
+
name=input.name,
|
|
65
|
+
label=input.label,
|
|
66
|
+
score=input.score,
|
|
67
|
+
explanation=input.explanation,
|
|
68
|
+
annotator_kind=input.annotator_kind.value,
|
|
69
|
+
metadata_=input.metadata,
|
|
70
|
+
identifier=identifier,
|
|
71
|
+
source=input.source.value,
|
|
72
|
+
user_id=user_id,
|
|
73
|
+
)
|
|
74
|
+
session.add(anno)
|
|
75
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
|
|
76
|
+
raise Conflict(f"Error creating annotation: {e}")
|
|
77
|
+
|
|
78
|
+
info.context.event_queue.put(ProjectSessionAnnotationInsertEvent((anno.id,)))
|
|
79
|
+
|
|
80
|
+
return ProjectSessionAnnotationMutationPayload(
|
|
81
|
+
project_session_annotation=ProjectSessionAnnotation(id=anno.id, db_record=anno),
|
|
82
|
+
query=Query(),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
86
|
+
async def update_project_session_annotations(
|
|
87
|
+
self, info: Info[Context, None], input: UpdateAnnotationInput
|
|
88
|
+
) -> ProjectSessionAnnotationMutationPayload:
|
|
89
|
+
assert isinstance(request := info.context.request, Request)
|
|
90
|
+
user_id: Optional[int] = None
|
|
91
|
+
if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
|
|
92
|
+
user_id = int(user.identity)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
id_ = from_global_id_with_expected_type(input.id, "ProjectSessionAnnotation")
|
|
96
|
+
except ValueError:
|
|
97
|
+
raise BadRequest(f"Invalid session annotation ID: {input.id}")
|
|
98
|
+
|
|
99
|
+
async with info.context.db() as session:
|
|
100
|
+
if not (anno := await session.get(models.ProjectSessionAnnotation, id_)):
|
|
101
|
+
raise NotFound(f"Could not find session annotation with ID: {input.id}")
|
|
102
|
+
if anno.user_id != user_id:
|
|
103
|
+
raise Unauthorized("Session annotation is not associated with the current user.")
|
|
104
|
+
|
|
105
|
+
# Update the annotation fields
|
|
106
|
+
anno.name = input.name
|
|
107
|
+
anno.label = input.label
|
|
108
|
+
anno.score = input.score
|
|
109
|
+
anno.explanation = input.explanation
|
|
110
|
+
anno.annotator_kind = input.annotator_kind.value
|
|
111
|
+
anno.metadata_ = input.metadata
|
|
112
|
+
anno.source = input.source.value
|
|
113
|
+
|
|
114
|
+
session.add(anno)
|
|
115
|
+
try:
|
|
116
|
+
await session.flush()
|
|
117
|
+
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as e:
|
|
118
|
+
raise Conflict(f"Error updating annotation: {e}")
|
|
119
|
+
|
|
120
|
+
info.context.event_queue.put(ProjectSessionAnnotationInsertEvent((anno.id,)))
|
|
121
|
+
return ProjectSessionAnnotationMutationPayload(
|
|
122
|
+
project_session_annotation=ProjectSessionAnnotation(id=anno.id, db_record=anno),
|
|
123
|
+
query=Query(),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
127
|
+
async def delete_project_session_annotation(
|
|
128
|
+
self, info: Info[Context, None], id: GlobalID
|
|
129
|
+
) -> ProjectSessionAnnotationMutationPayload:
|
|
130
|
+
try:
|
|
131
|
+
id_ = from_global_id_with_expected_type(id, "ProjectSessionAnnotation")
|
|
132
|
+
except ValueError:
|
|
133
|
+
raise BadRequest(f"Invalid session annotation ID: {id}")
|
|
134
|
+
|
|
135
|
+
assert isinstance(request := info.context.request, Request)
|
|
136
|
+
user_id: Optional[int] = None
|
|
137
|
+
user_is_admin = False
|
|
138
|
+
if "user" in request.scope and isinstance((user := info.context.user), PhoenixUser):
|
|
139
|
+
user_id = int(user.identity)
|
|
140
|
+
user_is_admin = user.is_admin
|
|
141
|
+
|
|
142
|
+
async with info.context.db() as session:
|
|
143
|
+
if not (anno := await session.get(models.ProjectSessionAnnotation, id_)):
|
|
144
|
+
raise NotFound(f"Could not find session annotation with ID: {id}")
|
|
145
|
+
|
|
146
|
+
if not user_is_admin and anno.user_id != user_id:
|
|
147
|
+
raise Unauthorized(
|
|
148
|
+
"Session annotation is not associated with the current user and "
|
|
149
|
+
"the current user is not an admin."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
await session.delete(anno)
|
|
153
|
+
|
|
154
|
+
deleted_gql_annotation = ProjectSessionAnnotation(id=anno.id, db_record=anno)
|
|
155
|
+
info.context.event_queue.put(ProjectSessionAnnotationDeleteEvent((id_,)))
|
|
156
|
+
return ProjectSessionAnnotationMutationPayload(
|
|
157
|
+
project_session_annotation=deleted_gql_annotation, query=Query()
|
|
158
|
+
)
|
|
@@ -16,7 +16,7 @@ from phoenix.db.types.trace_retention import (
|
|
|
16
16
|
TraceRetentionCronExpression,
|
|
17
17
|
TraceRetentionRule,
|
|
18
18
|
)
|
|
19
|
-
from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly
|
|
19
|
+
from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly, IsNotViewer
|
|
20
20
|
from phoenix.server.api.context import Context
|
|
21
21
|
from phoenix.server.api.exceptions import BadRequest, NotFound
|
|
22
22
|
from phoenix.server.api.queries import Query
|
|
@@ -113,7 +113,9 @@ class ProjectTraceRetentionPolicyMutationPayload:
|
|
|
113
113
|
|
|
114
114
|
@strawberry.type
|
|
115
115
|
class ProjectTraceRetentionPolicyMutationMixin:
|
|
116
|
-
@strawberry.mutation(
|
|
116
|
+
@strawberry.mutation(
|
|
117
|
+
permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
|
|
118
|
+
) # type: ignore
|
|
117
119
|
async def create_project_trace_retention_policy(
|
|
118
120
|
self,
|
|
119
121
|
info: Info[Context, None],
|
|
@@ -146,7 +148,9 @@ class ProjectTraceRetentionPolicyMutationMixin:
|
|
|
146
148
|
node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
|
|
147
149
|
)
|
|
148
150
|
|
|
149
|
-
@strawberry.mutation(
|
|
151
|
+
@strawberry.mutation(
|
|
152
|
+
permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
|
|
153
|
+
) # type: ignore
|
|
150
154
|
async def patch_project_trace_retention_policy(
|
|
151
155
|
self,
|
|
152
156
|
info: Info[Context, None],
|
|
@@ -204,7 +208,7 @@ class ProjectTraceRetentionPolicyMutationMixin:
|
|
|
204
208
|
node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
|
|
205
209
|
)
|
|
206
210
|
|
|
207
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdminIfAuthEnabled]) # type: ignore
|
|
211
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled]) # type: ignore
|
|
208
212
|
async def delete_project_trace_retention_policy(
|
|
209
213
|
self,
|
|
210
214
|
info: Info[Context, None],
|