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
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from asyncio import sleep
|
|
5
|
-
from datetime import datetime
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
6
|
from typing import Any, Mapping, Optional
|
|
7
7
|
|
|
8
8
|
import sqlalchemy as sa
|
|
@@ -16,13 +16,39 @@ logger = logging.getLogger(__name__)
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class GenerativeModelStore(DaemonTask):
|
|
19
|
+
"""A daemon that periodically fetches generative models and maintains an in-memory cache.
|
|
20
|
+
|
|
21
|
+
This daemon periodically fetches generative models and their token prices from the
|
|
22
|
+
database and maintains an in-memory cache for fast lookups. It uses an incremental
|
|
23
|
+
fetch strategy to minimize database egress costs. Instead of fetching all models on
|
|
24
|
+
every refresh, we track the last fetch time and only query for models that have
|
|
25
|
+
changed since then (using updated_at/deleted_at).
|
|
26
|
+
|
|
27
|
+
Rationale: Database egress is expensive in cloud environments (especially managed
|
|
28
|
+
databases), and generative models change infrequently (mostly static reference data).
|
|
29
|
+
The cost calculation daemon queries this store frequently (once per span), so trading
|
|
30
|
+
memory for reduced database egress provides significant cost savings.
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
This strategy relies on GenerativeModel.updated_at being properly maintained. Any
|
|
34
|
+
code that modifies GenerativeModel or TokenPrice records MUST ensure updated_at
|
|
35
|
+
is explicitly set (see model_mutations.py). Relying solely on SQLAlchemy's
|
|
36
|
+
onupdate=func.now() is insufficient because SQLAlchemy may skip the UPDATE if it
|
|
37
|
+
detects no "real" changes to scalar fields (even if child records like TokenPrice
|
|
38
|
+
are modified).
|
|
39
|
+
"""
|
|
40
|
+
|
|
19
41
|
def __init__(
|
|
20
42
|
self,
|
|
21
43
|
db: DbSessionFactory,
|
|
44
|
+
refresh_interval_seconds: int = 5,
|
|
22
45
|
) -> None:
|
|
23
46
|
super().__init__()
|
|
24
47
|
self._db = db
|
|
25
48
|
self._lookup = CostModelLookup()
|
|
49
|
+
self._last_fetch_time: Optional[datetime] = None
|
|
50
|
+
self._last_fetch_id: Optional[int] = None
|
|
51
|
+
self._refresh_interval_seconds = refresh_interval_seconds
|
|
26
52
|
|
|
27
53
|
def find_model(
|
|
28
54
|
self,
|
|
@@ -33,19 +59,45 @@ class GenerativeModelStore(DaemonTask):
|
|
|
33
59
|
|
|
34
60
|
async def _run(self) -> None:
|
|
35
61
|
while self._running:
|
|
62
|
+
# Capture time before query with 2-second buffer for clock skew tolerance
|
|
63
|
+
fetch_start_time = datetime.now(timezone.utc) - timedelta(seconds=2)
|
|
36
64
|
try:
|
|
37
65
|
await self._fetch_models()
|
|
38
66
|
except Exception:
|
|
39
67
|
logger.exception("Failed to refresh generative models")
|
|
40
|
-
|
|
68
|
+
else:
|
|
69
|
+
self._last_fetch_time = fetch_start_time
|
|
70
|
+
await sleep(self._refresh_interval_seconds)
|
|
41
71
|
|
|
42
72
|
async def _fetch_models(self) -> None:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
"""
|
|
74
|
+
Fetch generative models from the database using an incremental strategy.
|
|
75
|
+
|
|
76
|
+
On the first run, fetches all models. On subsequent runs, only fetches models
|
|
77
|
+
where updated_at or deleted_at is at or after the last fetch time (with a 2-second
|
|
78
|
+
buffer). Some models may be refetched, but .merge() handles duplicates idempotently.
|
|
79
|
+
"""
|
|
80
|
+
stmt = sa.select(models.GenerativeModel).options(
|
|
81
|
+
joinedload(models.GenerativeModel.token_prices)
|
|
48
82
|
)
|
|
83
|
+
if self._last_fetch_time:
|
|
84
|
+
# Incremental fetch: get models changed since last fetch.
|
|
85
|
+
# Use >= for updated_at/deleted_at to catch models from the buffer window.
|
|
86
|
+
# Include id check as redundant safety check.
|
|
87
|
+
stmt = stmt.where(
|
|
88
|
+
sa.or_(
|
|
89
|
+
models.GenerativeModel.id > self._last_fetch_id,
|
|
90
|
+
models.GenerativeModel.updated_at >= self._last_fetch_time,
|
|
91
|
+
models.GenerativeModel.deleted_at >= self._last_fetch_time,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
49
94
|
async with self._db() as session:
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
generative_models = (await session.scalars(stmt)).unique().all()
|
|
96
|
+
|
|
97
|
+
if not generative_models:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
self._lookup.merge(generative_models)
|
|
101
|
+
|
|
102
|
+
# Track max id for redundant safety check.
|
|
103
|
+
self._last_fetch_id = max(model.id for model in generative_models)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from asyncio import sleep
|
|
5
|
+
from collections import deque
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from typing import Any, Mapping, NamedTuple, Optional
|
|
7
8
|
|
|
@@ -35,21 +36,25 @@ class SpanCostCalculator(DaemonTask):
|
|
|
35
36
|
super().__init__()
|
|
36
37
|
self._db = db
|
|
37
38
|
self._model_store = model_store
|
|
38
|
-
self._queue:
|
|
39
|
+
self._queue: deque[SpanCostCalculatorQueueItem] = deque()
|
|
40
|
+
self._max_items_per_transaction = 1000
|
|
39
41
|
|
|
40
42
|
async def _run(self) -> None:
|
|
41
43
|
while self._running:
|
|
44
|
+
num_items_to_insert = min(self._max_items_per_transaction, len(self._queue))
|
|
42
45
|
try:
|
|
43
|
-
await self._insert_costs()
|
|
46
|
+
await self._insert_costs(num_items_to_insert)
|
|
44
47
|
except Exception as e:
|
|
45
48
|
logger.exception(f"Failed to insert costs: {e}")
|
|
46
49
|
await sleep(self._SLEEP_INTERVAL)
|
|
47
50
|
|
|
48
|
-
async def _insert_costs(self) -> None:
|
|
49
|
-
if not self._queue:
|
|
51
|
+
async def _insert_costs(self, num_items_to_insert: int) -> None:
|
|
52
|
+
if not num_items_to_insert or not self._queue:
|
|
50
53
|
return
|
|
51
54
|
costs: list[models.SpanCost] = []
|
|
52
|
-
|
|
55
|
+
while num_items_to_insert > 0:
|
|
56
|
+
num_items_to_insert -= 1
|
|
57
|
+
item = self._queue.popleft()
|
|
53
58
|
try:
|
|
54
59
|
cost = self.calculate_cost(item.span_start_time, item.attributes)
|
|
55
60
|
except Exception as e:
|
|
@@ -65,9 +70,6 @@ class SpanCostCalculator(DaemonTask):
|
|
|
65
70
|
session.add_all(costs)
|
|
66
71
|
except Exception as e:
|
|
67
72
|
logger.exception(f"Failed to insert costs: {e}")
|
|
68
|
-
finally:
|
|
69
|
-
# Clear the queue after processing
|
|
70
|
-
self._queue.clear()
|
|
71
73
|
|
|
72
74
|
def put_nowait(self, item: SpanCostCalculatorQueueItem) -> None:
|
|
73
75
|
self._queue.append(item)
|
phoenix/server/dml_event.py
CHANGED
|
@@ -127,6 +127,19 @@ class TraceAnnotationInsertEvent(TraceAnnotationDmlEvent): ...
|
|
|
127
127
|
class TraceAnnotationDeleteEvent(TraceAnnotationDmlEvent): ...
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class ProjectSessionAnnotationDmlEvent(DmlEvent):
|
|
132
|
+
table = models.ProjectSessionAnnotation
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class ProjectSessionAnnotationInsertEvent(ProjectSessionAnnotationDmlEvent): ...
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class ProjectSessionAnnotationDeleteEvent(ProjectSessionAnnotationDmlEvent): ...
|
|
141
|
+
|
|
142
|
+
|
|
130
143
|
@dataclass(frozen=True)
|
|
131
144
|
class DocumentAnnotationDmlEvent(DmlEvent):
|
|
132
145
|
table = models.DocumentAnnotation
|
phoenix/server/email/sender.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import smtplib
|
|
2
3
|
import ssl
|
|
3
4
|
from email.message import EmailMessage
|
|
@@ -5,11 +6,14 @@ from pathlib import Path
|
|
|
5
6
|
from typing import Literal
|
|
6
7
|
|
|
7
8
|
from anyio import to_thread
|
|
9
|
+
from email_validator import EmailNotValidError, validate_email
|
|
8
10
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
9
11
|
from typing_extensions import TypeAlias
|
|
10
12
|
|
|
11
13
|
from phoenix.config import get_env_root_url, get_env_support_email
|
|
12
14
|
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
13
17
|
EMAIL_TEMPLATE_FOLDER = Path(__file__).parent / "templates"
|
|
14
18
|
|
|
15
19
|
ConnectionMethod: TypeAlias = Literal["STARTTLS", "SSL", "PLAIN"]
|
|
@@ -44,6 +48,12 @@ class SimpleEmailSender:
|
|
|
44
48
|
email: str,
|
|
45
49
|
name: str,
|
|
46
50
|
) -> None:
|
|
51
|
+
try:
|
|
52
|
+
email = validate_email(email, check_deliverability=False).normalized
|
|
53
|
+
except EmailNotValidError:
|
|
54
|
+
logger.warning("Skipping welcome email for user with invalid email address")
|
|
55
|
+
return
|
|
56
|
+
|
|
47
57
|
subject = "[Phoenix] Welcome to Arize Phoenix"
|
|
48
58
|
template_name = "welcome.html"
|
|
49
59
|
|
|
@@ -67,6 +77,12 @@ class SimpleEmailSender:
|
|
|
67
77
|
email: str,
|
|
68
78
|
reset_url: str,
|
|
69
79
|
) -> None:
|
|
80
|
+
try:
|
|
81
|
+
email = validate_email(email, check_deliverability=False).normalized
|
|
82
|
+
except EmailNotValidError:
|
|
83
|
+
logger.warning("Skipping password reset email for user with invalid email address")
|
|
84
|
+
return
|
|
85
|
+
|
|
70
86
|
subject = "[Phoenix] Password Reset Request"
|
|
71
87
|
template_name = "password_reset.html"
|
|
72
88
|
|
|
@@ -88,21 +104,32 @@ class SimpleEmailSender:
|
|
|
88
104
|
allocated_storage_gibibytes: float,
|
|
89
105
|
notification_threshold_percentage: float,
|
|
90
106
|
) -> None:
|
|
91
|
-
|
|
107
|
+
try:
|
|
108
|
+
email = validate_email(email, check_deliverability=False).normalized
|
|
109
|
+
except EmailNotValidError:
|
|
110
|
+
logger.warning(
|
|
111
|
+
"Skipping database usage warning email for user with invalid email address"
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
subject = "[Phoenix] Database Disk Space Usage Threshold Exceeded"
|
|
92
116
|
template_name = "db_disk_usage_notification.html"
|
|
93
117
|
|
|
118
|
+
support_email = get_env_support_email()
|
|
94
119
|
template = self.env.get_template(template_name)
|
|
95
120
|
html_content = template.render(
|
|
96
121
|
current_usage_gibibytes=current_usage_gibibytes,
|
|
97
122
|
allocated_storage_gibibytes=allocated_storage_gibibytes,
|
|
98
123
|
notification_threshold_percentage=notification_threshold_percentage,
|
|
99
|
-
support_email=
|
|
124
|
+
support_email=support_email,
|
|
100
125
|
)
|
|
101
126
|
|
|
102
127
|
msg = EmailMessage()
|
|
103
128
|
msg["Subject"] = subject
|
|
104
129
|
msg["From"] = self.sender_email
|
|
105
130
|
msg["To"] = email
|
|
131
|
+
if support_email:
|
|
132
|
+
msg["Cc"] = support_email
|
|
106
133
|
msg.set_content(html_content, subtype="html")
|
|
107
134
|
|
|
108
135
|
await to_thread.run_sync(self._send_email, msg)
|
phoenix/server/grpc_server.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import TYPE_CHECKING, Any, Iterable, Optional
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, Optional
|
|
3
2
|
|
|
4
3
|
import grpc
|
|
5
4
|
from grpc.aio import RpcContext, Server, ServerInterceptor
|
|
@@ -11,6 +10,7 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import (
|
|
|
11
10
|
TraceServiceServicer,
|
|
12
11
|
add_TraceServiceServicer_to_server,
|
|
13
12
|
)
|
|
13
|
+
from starlette.concurrency import run_in_threadpool
|
|
14
14
|
from typing_extensions import TypeAlias
|
|
15
15
|
|
|
16
16
|
from phoenix.auth import CanReadToken
|
|
@@ -34,10 +34,10 @@ ProjectName: TypeAlias = str
|
|
|
34
34
|
class Servicer(TraceServiceServicer): # type: ignore[misc,unused-ignore]
|
|
35
35
|
def __init__(
|
|
36
36
|
self,
|
|
37
|
-
|
|
37
|
+
enqueue_span: Callable[[Span, ProjectName], Awaitable[None]],
|
|
38
38
|
) -> None:
|
|
39
39
|
super().__init__()
|
|
40
|
-
self.
|
|
40
|
+
self._enqueue_span = enqueue_span
|
|
41
41
|
|
|
42
42
|
async def Export(
|
|
43
43
|
self,
|
|
@@ -48,22 +48,22 @@ class Servicer(TraceServiceServicer): # type: ignore[misc,unused-ignore]
|
|
|
48
48
|
project_name = get_project_name(resource_spans.resource.attributes)
|
|
49
49
|
for scope_span in resource_spans.scope_spans:
|
|
50
50
|
for otlp_span in scope_span.spans:
|
|
51
|
-
span = decode_otlp_span
|
|
52
|
-
await self.
|
|
51
|
+
span = await run_in_threadpool(decode_otlp_span, otlp_span)
|
|
52
|
+
await self._enqueue_span(span, project_name)
|
|
53
53
|
return ExportTraceServiceResponse()
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class GrpcServer:
|
|
57
57
|
def __init__(
|
|
58
58
|
self,
|
|
59
|
-
|
|
59
|
+
enqueue_span: Callable[[Span, ProjectName], Awaitable[None]],
|
|
60
60
|
tracer_provider: Optional["TracerProvider"] = None,
|
|
61
61
|
enable_prometheus: bool = False,
|
|
62
62
|
disabled: bool = False,
|
|
63
63
|
token_store: Optional[CanReadToken] = None,
|
|
64
64
|
interceptors: Iterable[ServerInterceptor] = (),
|
|
65
65
|
) -> None:
|
|
66
|
-
self.
|
|
66
|
+
self._enqueue_span = enqueue_span
|
|
67
67
|
self._server: Optional[Server] = None
|
|
68
68
|
self._tracer_provider = tracer_provider
|
|
69
69
|
self._enable_prometheus = enable_prometheus
|
|
@@ -106,7 +106,7 @@ class GrpcServer:
|
|
|
106
106
|
server.add_secure_port(f"[::]:{get_env_grpc_port()}", server_credentials)
|
|
107
107
|
else:
|
|
108
108
|
server.add_insecure_port(f"[::]:{get_env_grpc_port()}")
|
|
109
|
-
add_TraceServiceServicer_to_server(Servicer(self.
|
|
109
|
+
add_TraceServiceServicer_to_server(Servicer(self._enqueue_span), server) # type: ignore[no-untyped-call,unused-ignore]
|
|
110
110
|
await server.start()
|
|
111
111
|
self._server = server
|
|
112
112
|
|
phoenix/server/jwt_store.py
CHANGED
|
@@ -164,7 +164,7 @@ class JwtStore:
|
|
|
164
164
|
for token_id in token_ids:
|
|
165
165
|
if isinstance(token_id, PasswordResetTokenId):
|
|
166
166
|
password_reset_token_ids.append(token_id)
|
|
167
|
-
|
|
167
|
+
elif isinstance(token_id, AccessTokenId):
|
|
168
168
|
access_token_ids.append(token_id)
|
|
169
169
|
elif isinstance(token_id, RefreshTokenId):
|
|
170
170
|
refresh_token_ids.append(token_id)
|
|
@@ -182,10 +182,10 @@ class JwtStore:
|
|
|
182
182
|
await gather(*coroutines)
|
|
183
183
|
|
|
184
184
|
async def log_out(self, user_id: UserId) -> None:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
async with self._db() as session:
|
|
186
|
+
for cls in (AccessTokenId, RefreshTokenId):
|
|
187
|
+
table = cls.table
|
|
188
|
+
stmt = delete(table).where(table.user_id == int(user_id)).returning(table.id)
|
|
189
189
|
async for id_ in await session.stream_scalars(stmt):
|
|
190
190
|
await self._evict(cls(id_))
|
|
191
191
|
|
|
@@ -314,7 +314,9 @@ class _Store(DaemonTask, Generic[_ClaimSetT, _TokenT, _TokenIdT, _RecordT], ABC)
|
|
|
314
314
|
|
|
315
315
|
async def _delete_expired_tokens(self, session: Any) -> None:
|
|
316
316
|
now = datetime.now(timezone.utc)
|
|
317
|
-
|
|
317
|
+
# Per JWT RFC 7519 Section 4.1.4, tokens expire "on or after" the expiration time.
|
|
318
|
+
# Use <= to include tokens expiring at exactly this moment.
|
|
319
|
+
await session.execute(delete(self._table).where(self._table.expires_at <= now))
|
|
318
320
|
|
|
319
321
|
async def _run(self) -> None:
|
|
320
322
|
while self._running:
|