arize-phoenix 8.32.1__py3-none-any.whl → 9.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arize-phoenix might be problematic. Click here for more details.
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/METADATA +2 -2
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/RECORD +76 -56
- phoenix/db/constants.py +1 -0
- phoenix/db/facilitator.py +55 -0
- phoenix/db/insertion/document_annotation.py +31 -13
- phoenix/db/insertion/evaluation.py +15 -3
- phoenix/db/insertion/helpers.py +2 -1
- phoenix/db/insertion/span_annotation.py +26 -9
- phoenix/db/insertion/trace_annotation.py +25 -9
- phoenix/db/insertion/types.py +7 -0
- phoenix/db/migrations/versions/2f9d1a65945f_annotation_config_migration.py +322 -0
- phoenix/db/migrations/versions/8a3764fe7f1a_change_jsonb_to_json_for_prompts.py +76 -0
- phoenix/db/migrations/versions/bb8139330879_create_project_trace_retention_policies_table.py +77 -0
- phoenix/db/models.py +151 -10
- phoenix/db/types/annotation_configs.py +97 -0
- phoenix/db/types/db_models.py +41 -0
- phoenix/db/types/trace_retention.py +267 -0
- phoenix/experiments/functions.py +5 -1
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/context.py +5 -0
- phoenix/server/api/dataloaders/__init__.py +4 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +203 -24
- phoenix/server/api/dataloaders/project_ids_by_trace_retention_policy_id.py +42 -0
- phoenix/server/api/dataloaders/trace_retention_policy_id_by_project_id.py +34 -0
- phoenix/server/api/helpers/annotations.py +9 -0
- phoenix/server/api/helpers/prompts/models.py +34 -67
- phoenix/server/api/input_types/CreateSpanAnnotationInput.py +9 -0
- phoenix/server/api/input_types/CreateTraceAnnotationInput.py +3 -0
- phoenix/server/api/input_types/PatchAnnotationInput.py +3 -0
- phoenix/server/api/input_types/SpanAnnotationFilter.py +67 -0
- phoenix/server/api/mutations/__init__.py +6 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +413 -0
- phoenix/server/api/mutations/dataset_mutations.py +62 -39
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +245 -0
- phoenix/server/api/mutations/span_annotations_mutations.py +272 -70
- phoenix/server/api/mutations/trace_annotations_mutations.py +203 -74
- phoenix/server/api/queries.py +86 -0
- phoenix/server/api/routers/v1/__init__.py +4 -0
- phoenix/server/api/routers/v1/annotation_configs.py +449 -0
- phoenix/server/api/routers/v1/annotations.py +161 -0
- phoenix/server/api/routers/v1/evaluations.py +6 -0
- phoenix/server/api/routers/v1/projects.py +1 -50
- phoenix/server/api/routers/v1/spans.py +35 -8
- phoenix/server/api/routers/v1/traces.py +22 -13
- phoenix/server/api/routers/v1/utils.py +60 -0
- phoenix/server/api/types/Annotation.py +7 -0
- phoenix/server/api/types/AnnotationConfig.py +124 -0
- phoenix/server/api/types/AnnotationSource.py +9 -0
- phoenix/server/api/types/AnnotationSummary.py +28 -14
- phoenix/server/api/types/AnnotatorKind.py +1 -0
- phoenix/server/api/types/CronExpression.py +15 -0
- phoenix/server/api/types/Evaluation.py +4 -30
- phoenix/server/api/types/Project.py +50 -2
- phoenix/server/api/types/ProjectTraceRetentionPolicy.py +110 -0
- phoenix/server/api/types/Span.py +78 -0
- phoenix/server/api/types/SpanAnnotation.py +24 -0
- phoenix/server/api/types/Trace.py +2 -2
- phoenix/server/api/types/TraceAnnotation.py +23 -0
- phoenix/server/app.py +20 -0
- phoenix/server/retention.py +76 -0
- phoenix/server/static/.vite/manifest.json +36 -36
- phoenix/server/static/assets/components-B2MWTXnm.js +4326 -0
- phoenix/server/static/assets/{index-B0CbpsxD.js → index-Bfvpea_-.js} +10 -10
- phoenix/server/static/assets/pages-CZ2vKu8H.js +7268 -0
- phoenix/server/static/assets/vendor-BRDkBC5J.js +903 -0
- phoenix/server/static/assets/{vendor-arizeai-CxXYQNUl.js → vendor-arizeai-BvTqp_W8.js} +3 -3
- phoenix/server/static/assets/{vendor-codemirror-B0NIFPOL.js → vendor-codemirror-COt9UfW7.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-CrrDFWK1.js → vendor-recharts-BoHX9Hvs.js} +2 -2
- phoenix/server/static/assets/{vendor-shiki-C5bJ-RPf.js → vendor-shiki-Cw1dsDAz.js} +1 -1
- phoenix/trace/dsl/filter.py +25 -5
- phoenix/utilities/__init__.py +18 -0
- phoenix/version.py +1 -1
- phoenix/server/static/assets/components-x-gKFJ8C.js +0 -3414
- phoenix/server/static/assets/pages-BU4VdyeH.js +0 -5867
- phoenix/server/static/assets/vendor-BfhM_F1u.js +0 -902
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-8.32.1.dist-info → arize_phoenix-9.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Optional, Union
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
from strawberry import UNSET, Private
|
|
7
|
+
from strawberry.relay import Connection, Node, NodeID
|
|
8
|
+
from strawberry.types import Info
|
|
9
|
+
from typing_extensions import TypeAlias, assert_never
|
|
10
|
+
|
|
11
|
+
from phoenix.db import models
|
|
12
|
+
from phoenix.db.types.trace_retention import MaxCountRule, MaxDaysOrCountRule, MaxDaysRule
|
|
13
|
+
from phoenix.server.api.context import Context
|
|
14
|
+
from phoenix.server.api.types.CronExpression import CronExpression
|
|
15
|
+
from phoenix.server.api.types.pagination import ConnectionArgs, CursorString, connection_from_list
|
|
16
|
+
from phoenix.server.api.types.Project import Project
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@strawberry.type
|
|
20
|
+
class TraceRetentionRuleMaxDays:
|
|
21
|
+
max_days: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@strawberry.type
|
|
25
|
+
class TraceRetentionRuleMaxCount:
|
|
26
|
+
max_count: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@strawberry.type
|
|
30
|
+
class TraceRetentionRuleMaxDaysOrCount(TraceRetentionRuleMaxDays, TraceRetentionRuleMaxCount): ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TraceRetentionRule: TypeAlias = Annotated[
|
|
34
|
+
Union[TraceRetentionRuleMaxDays, TraceRetentionRuleMaxCount, TraceRetentionRuleMaxDaysOrCount],
|
|
35
|
+
strawberry.union("TraceRetentionRule"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@strawberry.type
|
|
40
|
+
class ProjectTraceRetentionPolicy(Node):
|
|
41
|
+
id: NodeID[int]
|
|
42
|
+
db_policy: Private[Optional[models.ProjectTraceRetentionPolicy]] = None
|
|
43
|
+
|
|
44
|
+
@strawberry.field
|
|
45
|
+
async def name(
|
|
46
|
+
self,
|
|
47
|
+
info: Info[Context, None],
|
|
48
|
+
) -> str:
|
|
49
|
+
if self.db_policy:
|
|
50
|
+
value = self.db_policy.name
|
|
51
|
+
else:
|
|
52
|
+
value = await info.context.data_loaders.project_trace_retention_policy_fields.load(
|
|
53
|
+
(self.id, models.ProjectTraceRetentionPolicy.name),
|
|
54
|
+
)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
@strawberry.field
|
|
58
|
+
async def cron_expression(
|
|
59
|
+
self,
|
|
60
|
+
info: Info[Context, None],
|
|
61
|
+
) -> CronExpression:
|
|
62
|
+
if self.db_policy:
|
|
63
|
+
value = self.db_policy.cron_expression
|
|
64
|
+
else:
|
|
65
|
+
value = await info.context.data_loaders.project_trace_retention_policy_fields.load(
|
|
66
|
+
(self.id, models.ProjectTraceRetentionPolicy.cron_expression),
|
|
67
|
+
)
|
|
68
|
+
return CronExpression(value.root)
|
|
69
|
+
|
|
70
|
+
@strawberry.field
|
|
71
|
+
async def rule(
|
|
72
|
+
self,
|
|
73
|
+
info: Info[Context, None],
|
|
74
|
+
) -> TraceRetentionRule:
|
|
75
|
+
if self.db_policy:
|
|
76
|
+
value = self.db_policy.rule
|
|
77
|
+
else:
|
|
78
|
+
value = await info.context.data_loaders.project_trace_retention_policy_fields.load(
|
|
79
|
+
(self.id, models.ProjectTraceRetentionPolicy.rule),
|
|
80
|
+
)
|
|
81
|
+
if isinstance(value.root, MaxDaysRule):
|
|
82
|
+
return TraceRetentionRuleMaxDays(max_days=value.root.max_days)
|
|
83
|
+
if isinstance(value.root, MaxCountRule):
|
|
84
|
+
return TraceRetentionRuleMaxCount(max_count=value.root.max_count)
|
|
85
|
+
if isinstance(value.root, MaxDaysOrCountRule):
|
|
86
|
+
return TraceRetentionRuleMaxDaysOrCount(
|
|
87
|
+
max_days=value.root.max_days, max_count=value.root.max_count
|
|
88
|
+
)
|
|
89
|
+
assert_never(value.root)
|
|
90
|
+
|
|
91
|
+
@strawberry.field
|
|
92
|
+
async def projects(
|
|
93
|
+
self,
|
|
94
|
+
info: Info[Context, None],
|
|
95
|
+
first: Optional[int] = 100,
|
|
96
|
+
last: Optional[int] = UNSET,
|
|
97
|
+
after: Optional[CursorString] = UNSET,
|
|
98
|
+
before: Optional[CursorString] = UNSET,
|
|
99
|
+
) -> Connection[Project]:
|
|
100
|
+
args = ConnectionArgs(
|
|
101
|
+
first=first,
|
|
102
|
+
after=after if isinstance(after, CursorString) else None,
|
|
103
|
+
last=last,
|
|
104
|
+
before=before if isinstance(before, CursorString) else None,
|
|
105
|
+
)
|
|
106
|
+
project_rowids = await info.context.data_loaders.projects_by_trace_retention_policy_id.load(
|
|
107
|
+
self.id
|
|
108
|
+
)
|
|
109
|
+
data = [Project(project_rowid=project_rowid) for project_rowid in project_rowids]
|
|
110
|
+
return connection_from_list(data=data, args=args)
|
phoenix/server/api/types/Span.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from asyncio import gather
|
|
3
|
+
from collections import defaultdict
|
|
3
4
|
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
4
6
|
from datetime import datetime
|
|
5
7
|
from enum import Enum
|
|
6
8
|
from typing import TYPE_CHECKING, Any, Iterable, Optional, cast
|
|
7
9
|
|
|
8
10
|
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
9
12
|
import strawberry
|
|
10
13
|
from openinference.semconv.trace import SpanAttributes
|
|
11
14
|
from strawberry import ID, UNSET
|
|
@@ -21,10 +24,15 @@ from phoenix.server.api.helpers.dataset_helpers import (
|
|
|
21
24
|
get_dataset_example_output,
|
|
22
25
|
)
|
|
23
26
|
from phoenix.server.api.input_types.InvocationParameters import InvocationParameter
|
|
27
|
+
from phoenix.server.api.input_types.SpanAnnotationFilter import (
|
|
28
|
+
SpanAnnotationFilter,
|
|
29
|
+
satisfies_filter,
|
|
30
|
+
)
|
|
24
31
|
from phoenix.server.api.input_types.SpanAnnotationSort import (
|
|
25
32
|
SpanAnnotationColumn,
|
|
26
33
|
SpanAnnotationSort,
|
|
27
34
|
)
|
|
35
|
+
from phoenix.server.api.types.AnnotationSummary import AnnotationSummary
|
|
28
36
|
from phoenix.server.api.types.DocumentRetrievalMetrics import DocumentRetrievalMetrics
|
|
29
37
|
from phoenix.server.api.types.Evaluation import DocumentEvaluation
|
|
30
38
|
from phoenix.server.api.types.ExampleRevisionInterface import ExampleRevision
|
|
@@ -490,11 +498,16 @@ class Span(Node):
|
|
|
490
498
|
self,
|
|
491
499
|
info: Info[Context, None],
|
|
492
500
|
sort: Optional[SpanAnnotationSort] = UNSET,
|
|
501
|
+
filter: Optional[SpanAnnotationFilter] = None,
|
|
493
502
|
) -> list[SpanAnnotation]:
|
|
494
503
|
span_id = self.span_rowid
|
|
495
504
|
annotations = await info.context.data_loaders.span_annotations.load(span_id)
|
|
496
505
|
sort_key = SpanAnnotationColumn.name.value
|
|
497
506
|
sort_descending = False
|
|
507
|
+
if filter:
|
|
508
|
+
annotations = [
|
|
509
|
+
annotation for annotation in annotations if satisfies_filter(annotation, filter)
|
|
510
|
+
]
|
|
498
511
|
if sort:
|
|
499
512
|
sort_key = sort.col.value
|
|
500
513
|
sort_descending = sort.dir is SortDir.desc
|
|
@@ -503,6 +516,71 @@ class Span(Node):
|
|
|
503
516
|
)
|
|
504
517
|
return [to_gql_span_annotation(annotation) for annotation in annotations]
|
|
505
518
|
|
|
519
|
+
@strawberry.field(description=("Notes associated with the span.")) # type: ignore
|
|
520
|
+
async def span_notes(
|
|
521
|
+
self,
|
|
522
|
+
info: Info[Context, None],
|
|
523
|
+
) -> list[SpanAnnotation]:
|
|
524
|
+
span_id = self.span_rowid
|
|
525
|
+
annotations = await info.context.data_loaders.span_annotations.load(span_id)
|
|
526
|
+
annotations = [annotation for annotation in annotations if annotation.name == "note"]
|
|
527
|
+
annotations.sort(key=lambda annotation: getattr(annotation, "created_at"), reverse=False)
|
|
528
|
+
return [to_gql_span_annotation(annotation) for annotation in annotations]
|
|
529
|
+
|
|
530
|
+
@strawberry.field(description="Summarizes each annotation (by name) associated with the span") # type: ignore
|
|
531
|
+
async def span_annotation_summaries(
|
|
532
|
+
self,
|
|
533
|
+
info: Info[Context, None],
|
|
534
|
+
filter: Optional[SpanAnnotationFilter] = None,
|
|
535
|
+
) -> list[AnnotationSummary]:
|
|
536
|
+
"""
|
|
537
|
+
Retrieves and summarizes annotations associated with this span.
|
|
538
|
+
|
|
539
|
+
This method aggregates annotation data by name and label, calculating metrics
|
|
540
|
+
such as count of occurrences and sum of scores. The results are organized
|
|
541
|
+
into a structured format that can be easily converted to a DataFrame.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
info: GraphQL context information
|
|
545
|
+
filter: Optional filter to apply to annotations before processing
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
A list of AnnotationSummary objects, each containing:
|
|
549
|
+
- name: The name of the annotation
|
|
550
|
+
- data: A list of dictionaries with label statistics
|
|
551
|
+
"""
|
|
552
|
+
# Load all annotations for this span from the data loader
|
|
553
|
+
annotations = await info.context.data_loaders.span_annotations.load(self.span_rowid)
|
|
554
|
+
|
|
555
|
+
# Apply filter if provided to narrow down the annotations
|
|
556
|
+
if filter:
|
|
557
|
+
annotations = [
|
|
558
|
+
annotation for annotation in annotations if satisfies_filter(annotation, filter)
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
@dataclass
|
|
562
|
+
class Metrics:
|
|
563
|
+
record_count: int = 0
|
|
564
|
+
label_count: int = 0
|
|
565
|
+
score_sum: float = 0
|
|
566
|
+
score_count: int = 0
|
|
567
|
+
|
|
568
|
+
summaries: defaultdict[str, defaultdict[Optional[str], Metrics]] = defaultdict(
|
|
569
|
+
lambda: defaultdict(Metrics)
|
|
570
|
+
)
|
|
571
|
+
for annotation in annotations:
|
|
572
|
+
metrics = summaries[annotation.name][annotation.label]
|
|
573
|
+
metrics.record_count += 1
|
|
574
|
+
metrics.label_count += int(annotation.label is not None)
|
|
575
|
+
metrics.score_sum += annotation.score or 0
|
|
576
|
+
metrics.score_count += int(annotation.score is not None)
|
|
577
|
+
|
|
578
|
+
result: list[AnnotationSummary] = []
|
|
579
|
+
for name, label_metrics in summaries.items():
|
|
580
|
+
rows = [{"label": label, **asdict(metrics)} for label, metrics in label_metrics.items()]
|
|
581
|
+
result.append(AnnotationSummary(name=name, df=pd.DataFrame(rows), simple_avg=True))
|
|
582
|
+
return result
|
|
583
|
+
|
|
506
584
|
@strawberry.field(
|
|
507
585
|
description="Evaluations of the documents associated with the span, e.g. "
|
|
508
586
|
"if the span is a RETRIEVER with a list of documents in its RETRIEVAL_DOCUMENTS "
|
|
@@ -4,19 +4,26 @@ import strawberry
|
|
|
4
4
|
from strawberry import Private
|
|
5
5
|
from strawberry.relay import GlobalID, Node, NodeID
|
|
6
6
|
from strawberry.scalars import JSON
|
|
7
|
+
from strawberry.types import Info
|
|
7
8
|
|
|
8
9
|
from phoenix.db import models
|
|
10
|
+
from phoenix.server.api.context import Context
|
|
9
11
|
|
|
10
12
|
from .Annotation import Annotation
|
|
13
|
+
from .AnnotationSource import AnnotationSource
|
|
11
14
|
from .AnnotatorKind import AnnotatorKind
|
|
15
|
+
from .User import User, to_gql_user
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
@strawberry.type
|
|
15
19
|
class SpanAnnotation(Node, Annotation):
|
|
16
20
|
id_attr: NodeID[int]
|
|
21
|
+
user_id: Private[Optional[int]]
|
|
17
22
|
annotator_kind: AnnotatorKind
|
|
18
23
|
metadata: JSON
|
|
19
24
|
span_rowid: Private[Optional[int]]
|
|
25
|
+
source: AnnotationSource
|
|
26
|
+
identifier: str
|
|
20
27
|
|
|
21
28
|
@strawberry.field
|
|
22
29
|
async def span_id(self) -> GlobalID:
|
|
@@ -24,6 +31,18 @@ class SpanAnnotation(Node, Annotation):
|
|
|
24
31
|
|
|
25
32
|
return GlobalID(type_name=Span.__name__, node_id=str(self.span_rowid))
|
|
26
33
|
|
|
34
|
+
@strawberry.field
|
|
35
|
+
async def user(
|
|
36
|
+
self,
|
|
37
|
+
info: Info[Context, None],
|
|
38
|
+
) -> Optional[User]:
|
|
39
|
+
if self.user_id is None:
|
|
40
|
+
return None
|
|
41
|
+
user = await info.context.data_loaders.users.load(self.user_id)
|
|
42
|
+
if user is None:
|
|
43
|
+
return None
|
|
44
|
+
return to_gql_user(user)
|
|
45
|
+
|
|
27
46
|
|
|
28
47
|
def to_gql_span_annotation(
|
|
29
48
|
annotation: models.SpanAnnotation,
|
|
@@ -33,6 +52,7 @@ def to_gql_span_annotation(
|
|
|
33
52
|
"""
|
|
34
53
|
return SpanAnnotation(
|
|
35
54
|
id_attr=annotation.id,
|
|
55
|
+
user_id=annotation.user_id,
|
|
36
56
|
span_rowid=annotation.span_rowid,
|
|
37
57
|
name=annotation.name,
|
|
38
58
|
annotator_kind=AnnotatorKind(annotation.annotator_kind),
|
|
@@ -40,4 +60,8 @@ def to_gql_span_annotation(
|
|
|
40
60
|
score=annotation.score,
|
|
41
61
|
explanation=annotation.explanation,
|
|
42
62
|
metadata=annotation.metadata_,
|
|
63
|
+
source=AnnotationSource(annotation.source),
|
|
64
|
+
identifier=annotation.identifier,
|
|
65
|
+
created_at=annotation.created_at,
|
|
66
|
+
updated_at=annotation.updated_at,
|
|
43
67
|
)
|
|
@@ -208,13 +208,13 @@ class Trace(Node):
|
|
|
208
208
|
return connection_from_list(data=data, args=args)
|
|
209
209
|
|
|
210
210
|
@strawberry.field(description="Annotations associated with the trace.") # type: ignore
|
|
211
|
-
async def
|
|
211
|
+
async def trace_annotations(
|
|
212
212
|
self,
|
|
213
213
|
info: Info[Context, None],
|
|
214
214
|
sort: Optional[TraceAnnotationSort] = None,
|
|
215
215
|
) -> list[TraceAnnotation]:
|
|
216
216
|
async with info.context.db() as session:
|
|
217
|
-
stmt = select(models.TraceAnnotation).filter_by(
|
|
217
|
+
stmt = select(models.TraceAnnotation).filter_by(trace_rowid=self.trace_rowid)
|
|
218
218
|
if sort:
|
|
219
219
|
sort_col = getattr(models.TraceAnnotation, sort.col.value)
|
|
220
220
|
if sort.dir is SortDir.desc:
|
|
@@ -4,14 +4,20 @@ import strawberry
|
|
|
4
4
|
from strawberry import Private
|
|
5
5
|
from strawberry.relay import GlobalID, Node, NodeID
|
|
6
6
|
from strawberry.scalars import JSON
|
|
7
|
+
from strawberry.types import Info
|
|
7
8
|
|
|
8
9
|
from phoenix.db import models
|
|
10
|
+
from phoenix.server.api.context import Context
|
|
9
11
|
from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
|
|
10
12
|
|
|
13
|
+
from .AnnotationSource import AnnotationSource
|
|
14
|
+
from .User import User, to_gql_user
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
@strawberry.type
|
|
13
18
|
class TraceAnnotation(Node):
|
|
14
19
|
id_attr: NodeID[int]
|
|
20
|
+
user_id: Private[Optional[int]]
|
|
15
21
|
name: str
|
|
16
22
|
annotator_kind: AnnotatorKind
|
|
17
23
|
label: Optional[str]
|
|
@@ -19,6 +25,8 @@ class TraceAnnotation(Node):
|
|
|
19
25
|
explanation: Optional[str]
|
|
20
26
|
metadata: JSON
|
|
21
27
|
trace_rowid: Private[Optional[int]]
|
|
28
|
+
identifier: str
|
|
29
|
+
source: AnnotationSource
|
|
22
30
|
|
|
23
31
|
@strawberry.field
|
|
24
32
|
async def trace_id(self) -> GlobalID:
|
|
@@ -26,6 +34,18 @@ class TraceAnnotation(Node):
|
|
|
26
34
|
|
|
27
35
|
return GlobalID(type_name=Trace.__name__, node_id=str(self.trace_rowid))
|
|
28
36
|
|
|
37
|
+
@strawberry.field
|
|
38
|
+
async def user(
|
|
39
|
+
self,
|
|
40
|
+
info: Info[Context, None],
|
|
41
|
+
) -> Optional[User]:
|
|
42
|
+
if self.user_id is None:
|
|
43
|
+
return None
|
|
44
|
+
user = await info.context.data_loaders.users.load(self.user_id)
|
|
45
|
+
if user is None:
|
|
46
|
+
return None
|
|
47
|
+
return to_gql_user(user)
|
|
48
|
+
|
|
29
49
|
|
|
30
50
|
def to_gql_trace_annotation(
|
|
31
51
|
annotation: models.TraceAnnotation,
|
|
@@ -35,6 +55,7 @@ def to_gql_trace_annotation(
|
|
|
35
55
|
"""
|
|
36
56
|
return TraceAnnotation(
|
|
37
57
|
id_attr=annotation.id,
|
|
58
|
+
user_id=annotation.user_id,
|
|
38
59
|
trace_rowid=annotation.trace_rowid,
|
|
39
60
|
name=annotation.name,
|
|
40
61
|
annotator_kind=AnnotatorKind(annotation.annotator_kind),
|
|
@@ -42,4 +63,6 @@ def to_gql_trace_annotation(
|
|
|
42
63
|
score=annotation.score,
|
|
43
64
|
explanation=annotation.explanation,
|
|
44
65
|
metadata=annotation.metadata_,
|
|
66
|
+
identifier=annotation.identifier,
|
|
67
|
+
source=AnnotationSource(annotation.source),
|
|
45
68
|
)
|
phoenix/server/app.py
CHANGED
|
@@ -88,6 +88,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
88
88
|
NumChildSpansDataLoader,
|
|
89
89
|
NumSpansPerTraceDataLoader,
|
|
90
90
|
ProjectByNameDataLoader,
|
|
91
|
+
ProjectIdsByTraceRetentionPolicyIdDataLoader,
|
|
91
92
|
PromptVersionSequenceNumberDataLoader,
|
|
92
93
|
RecordCountDataLoader,
|
|
93
94
|
SessionIODataLoader,
|
|
@@ -103,6 +104,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
103
104
|
TableFieldsDataLoader,
|
|
104
105
|
TokenCountDataLoader,
|
|
105
106
|
TraceByTraceIdsDataLoader,
|
|
107
|
+
TraceRetentionPolicyIdByProjectIdDataLoader,
|
|
106
108
|
TraceRootSpansDataLoader,
|
|
107
109
|
UserRolesDataLoader,
|
|
108
110
|
UsersDataLoader,
|
|
@@ -123,6 +125,7 @@ from phoenix.server.grpc_server import GrpcServer
|
|
|
123
125
|
from phoenix.server.jwt_store import JwtStore
|
|
124
126
|
from phoenix.server.middleware.gzip import GZipMiddleware
|
|
125
127
|
from phoenix.server.oauth2 import OAuth2Clients
|
|
128
|
+
from phoenix.server.retention import TraceDataSweeper
|
|
126
129
|
from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
|
|
127
130
|
from phoenix.server.types import (
|
|
128
131
|
CanGetLastUpdatedAt,
|
|
@@ -475,6 +478,7 @@ def _lifespan(
|
|
|
475
478
|
db: DbSessionFactory,
|
|
476
479
|
bulk_inserter: BulkInserter,
|
|
477
480
|
dml_event_handler: DmlEventHandler,
|
|
481
|
+
trace_data_sweeper: Optional[TraceDataSweeper],
|
|
478
482
|
token_store: Optional[TokenStore] = None,
|
|
479
483
|
tracer_provider: Optional["TracerProvider"] = None,
|
|
480
484
|
enable_prometheus: bool = False,
|
|
@@ -507,6 +511,8 @@ def _lifespan(
|
|
|
507
511
|
)
|
|
508
512
|
await stack.enter_async_context(grpc_server)
|
|
509
513
|
await stack.enter_async_context(dml_event_handler)
|
|
514
|
+
if trace_data_sweeper:
|
|
515
|
+
await stack.enter_async_context(trace_data_sweeper)
|
|
510
516
|
if scaffolder_config:
|
|
511
517
|
scaffolder = Scaffolder(
|
|
512
518
|
config=scaffolder_config,
|
|
@@ -633,6 +639,9 @@ def create_graphql_router(
|
|
|
633
639
|
num_child_spans=NumChildSpansDataLoader(db),
|
|
634
640
|
num_spans_per_trace=NumSpansPerTraceDataLoader(db),
|
|
635
641
|
project_fields=TableFieldsDataLoader(db, models.Project),
|
|
642
|
+
projects_by_trace_retention_policy_id=ProjectIdsByTraceRetentionPolicyIdDataLoader(
|
|
643
|
+
db
|
|
644
|
+
),
|
|
636
645
|
prompt_version_sequence_number=PromptVersionSequenceNumberDataLoader(db),
|
|
637
646
|
record_counts=RecordCountDataLoader(
|
|
638
647
|
db,
|
|
@@ -656,6 +665,12 @@ def create_graphql_router(
|
|
|
656
665
|
),
|
|
657
666
|
trace_by_trace_ids=TraceByTraceIdsDataLoader(db),
|
|
658
667
|
trace_fields=TableFieldsDataLoader(db, models.Trace),
|
|
668
|
+
trace_retention_policy_id_by_project_id=TraceRetentionPolicyIdByProjectIdDataLoader(
|
|
669
|
+
db
|
|
670
|
+
),
|
|
671
|
+
project_trace_retention_policy_fields=TableFieldsDataLoader(
|
|
672
|
+
db, models.ProjectTraceRetentionPolicy
|
|
673
|
+
),
|
|
659
674
|
trace_root_spans=TraceRootSpansDataLoader(db),
|
|
660
675
|
project_by_name=ProjectByNameDataLoader(db),
|
|
661
676
|
users=UsersDataLoader(db),
|
|
@@ -817,6 +832,10 @@ def create_app(
|
|
|
817
832
|
cache_for_dataloaders=cache_for_dataloaders,
|
|
818
833
|
last_updated_at=last_updated_at,
|
|
819
834
|
)
|
|
835
|
+
trace_data_sweeper = TraceDataSweeper(
|
|
836
|
+
db=db,
|
|
837
|
+
dml_event_handler=dml_event_handler,
|
|
838
|
+
)
|
|
820
839
|
bulk_inserter = bulk_inserter_factory(
|
|
821
840
|
db,
|
|
822
841
|
enable_prometheus=enable_prometheus,
|
|
@@ -874,6 +893,7 @@ def create_app(
|
|
|
874
893
|
read_only=read_only,
|
|
875
894
|
bulk_inserter=bulk_inserter,
|
|
876
895
|
dml_event_handler=dml_event_handler,
|
|
896
|
+
trace_data_sweeper=trace_data_sweeper,
|
|
877
897
|
token_store=token_store,
|
|
878
898
|
tracer_provider=tracer_provider,
|
|
879
899
|
enable_prometheus=enable_prometheus,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from asyncio import create_task, gather, sleep
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
|
|
6
|
+
import sqlalchemy as sa
|
|
7
|
+
from sqlalchemy.orm import selectinload
|
|
8
|
+
|
|
9
|
+
from phoenix.db.constants import DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
|
|
10
|
+
from phoenix.db.models import Project, ProjectTraceRetentionPolicy
|
|
11
|
+
from phoenix.server.dml_event import SpanDeleteEvent
|
|
12
|
+
from phoenix.server.dml_event_handler import DmlEventHandler
|
|
13
|
+
from phoenix.server.types import DaemonTask, DbSessionFactory
|
|
14
|
+
from phoenix.utilities import hour_of_week
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TraceDataSweeper(DaemonTask):
|
|
18
|
+
def __init__(self, db: DbSessionFactory, dml_event_handler: DmlEventHandler):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._db = db
|
|
21
|
+
self._dml_event_handler = dml_event_handler
|
|
22
|
+
|
|
23
|
+
async def _run(self) -> None:
|
|
24
|
+
"""Check hourly and apply policies."""
|
|
25
|
+
while self._running:
|
|
26
|
+
await self._sleep_until_next_hour()
|
|
27
|
+
if not (policies := await self._get_policies()):
|
|
28
|
+
continue
|
|
29
|
+
current_hour = self._current_hour()
|
|
30
|
+
if tasks := [
|
|
31
|
+
create_task(self._apply(policy))
|
|
32
|
+
for policy in policies
|
|
33
|
+
if self._should_apply(policy, current_hour)
|
|
34
|
+
]:
|
|
35
|
+
await gather(*tasks, return_exceptions=True)
|
|
36
|
+
|
|
37
|
+
async def _get_policies(self) -> list[ProjectTraceRetentionPolicy]:
|
|
38
|
+
stmt = sa.select(ProjectTraceRetentionPolicy).options(
|
|
39
|
+
selectinload(ProjectTraceRetentionPolicy.projects).load_only(Project.id)
|
|
40
|
+
)
|
|
41
|
+
async with self._db() as session:
|
|
42
|
+
result = await session.scalars(stmt)
|
|
43
|
+
# filter out no-op policies, e.g. max_days == 0
|
|
44
|
+
return [policy for policy in result if bool(policy.rule)]
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _now() -> datetime:
|
|
48
|
+
return datetime.now(timezone.utc)
|
|
49
|
+
|
|
50
|
+
def _current_hour(self) -> int:
|
|
51
|
+
return hour_of_week(self._now())
|
|
52
|
+
|
|
53
|
+
def _should_apply(self, policy: ProjectTraceRetentionPolicy, current_hour: int) -> bool:
|
|
54
|
+
if current_hour != policy.cron_expression.get_hour_of_prev_run():
|
|
55
|
+
return False
|
|
56
|
+
if policy.id != DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID and not policy.projects:
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
async def _apply(self, policy: ProjectTraceRetentionPolicy) -> None:
|
|
61
|
+
project_rowids = (
|
|
62
|
+
(
|
|
63
|
+
sa.select(Project.id)
|
|
64
|
+
.where(Project.trace_retention_policy_id.is_(None))
|
|
65
|
+
.scalar_subquery()
|
|
66
|
+
)
|
|
67
|
+
if policy.id == DEFAULT_PROJECT_TRACE_RETENTION_POLICY_ID
|
|
68
|
+
else [p.id for p in policy.projects]
|
|
69
|
+
)
|
|
70
|
+
async with self._db() as session:
|
|
71
|
+
result = await policy.rule.delete_traces(session, project_rowids)
|
|
72
|
+
self._dml_event_handler.put(SpanDeleteEvent(tuple(result)))
|
|
73
|
+
|
|
74
|
+
async def _sleep_until_next_hour(self) -> None:
|
|
75
|
+
next_hour = self._now().replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
|
|
76
|
+
await sleep((next_hour - self._now()).total_seconds())
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_components-
|
|
3
|
-
"file": "assets/components-
|
|
2
|
+
"_components-B2MWTXnm.js": {
|
|
3
|
+
"file": "assets/components-B2MWTXnm.js",
|
|
4
4
|
"name": "components",
|
|
5
5
|
"imports": [
|
|
6
|
-
"_vendor-
|
|
7
|
-
"_pages-
|
|
8
|
-
"_vendor-arizeai-
|
|
9
|
-
"_vendor-codemirror-
|
|
6
|
+
"_vendor-BRDkBC5J.js",
|
|
7
|
+
"_pages-CZ2vKu8H.js",
|
|
8
|
+
"_vendor-arizeai-BvTqp_W8.js",
|
|
9
|
+
"_vendor-codemirror-COt9UfW7.js",
|
|
10
10
|
"_vendor-three-C5WAXd5r.js"
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
|
-
"_pages-
|
|
14
|
-
"file": "assets/pages-
|
|
13
|
+
"_pages-CZ2vKu8H.js": {
|
|
14
|
+
"file": "assets/pages-CZ2vKu8H.js",
|
|
15
15
|
"name": "pages",
|
|
16
16
|
"imports": [
|
|
17
|
-
"_vendor-
|
|
18
|
-
"_vendor-arizeai-
|
|
19
|
-
"_components-
|
|
20
|
-
"_vendor-codemirror-
|
|
21
|
-
"_vendor-recharts-
|
|
17
|
+
"_vendor-BRDkBC5J.js",
|
|
18
|
+
"_vendor-arizeai-BvTqp_W8.js",
|
|
19
|
+
"_components-B2MWTXnm.js",
|
|
20
|
+
"_vendor-codemirror-COt9UfW7.js",
|
|
21
|
+
"_vendor-recharts-BoHX9Hvs.js"
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
|
-
"_vendor-
|
|
25
|
-
"file": "assets/vendor-
|
|
24
|
+
"_vendor-BRDkBC5J.js": {
|
|
25
|
+
"file": "assets/vendor-BRDkBC5J.js",
|
|
26
26
|
"name": "vendor",
|
|
27
27
|
"imports": [
|
|
28
28
|
"_vendor-three-C5WAXd5r.js"
|
|
@@ -35,33 +35,33 @@
|
|
|
35
35
|
"file": "assets/vendor-Cg6lcjUC.css",
|
|
36
36
|
"src": "_vendor-Cg6lcjUC.css"
|
|
37
37
|
},
|
|
38
|
-
"_vendor-arizeai-
|
|
39
|
-
"file": "assets/vendor-arizeai-
|
|
38
|
+
"_vendor-arizeai-BvTqp_W8.js": {
|
|
39
|
+
"file": "assets/vendor-arizeai-BvTqp_W8.js",
|
|
40
40
|
"name": "vendor-arizeai",
|
|
41
41
|
"imports": [
|
|
42
|
-
"_vendor-
|
|
42
|
+
"_vendor-BRDkBC5J.js"
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
|
-
"_vendor-codemirror-
|
|
46
|
-
"file": "assets/vendor-codemirror-
|
|
45
|
+
"_vendor-codemirror-COt9UfW7.js": {
|
|
46
|
+
"file": "assets/vendor-codemirror-COt9UfW7.js",
|
|
47
47
|
"name": "vendor-codemirror",
|
|
48
48
|
"imports": [
|
|
49
|
-
"_vendor-
|
|
50
|
-
"_vendor-shiki-
|
|
49
|
+
"_vendor-BRDkBC5J.js",
|
|
50
|
+
"_vendor-shiki-Cw1dsDAz.js"
|
|
51
51
|
]
|
|
52
52
|
},
|
|
53
|
-
"_vendor-recharts-
|
|
54
|
-
"file": "assets/vendor-recharts-
|
|
53
|
+
"_vendor-recharts-BoHX9Hvs.js": {
|
|
54
|
+
"file": "assets/vendor-recharts-BoHX9Hvs.js",
|
|
55
55
|
"name": "vendor-recharts",
|
|
56
56
|
"imports": [
|
|
57
|
-
"_vendor-
|
|
57
|
+
"_vendor-BRDkBC5J.js"
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
|
-
"_vendor-shiki-
|
|
61
|
-
"file": "assets/vendor-shiki-
|
|
60
|
+
"_vendor-shiki-Cw1dsDAz.js": {
|
|
61
|
+
"file": "assets/vendor-shiki-Cw1dsDAz.js",
|
|
62
62
|
"name": "vendor-shiki",
|
|
63
63
|
"imports": [
|
|
64
|
-
"_vendor-
|
|
64
|
+
"_vendor-BRDkBC5J.js"
|
|
65
65
|
]
|
|
66
66
|
},
|
|
67
67
|
"_vendor-three-C5WAXd5r.js": {
|
|
@@ -69,19 +69,19 @@
|
|
|
69
69
|
"name": "vendor-three"
|
|
70
70
|
},
|
|
71
71
|
"index.tsx": {
|
|
72
|
-
"file": "assets/index-
|
|
72
|
+
"file": "assets/index-Bfvpea_-.js",
|
|
73
73
|
"name": "index",
|
|
74
74
|
"src": "index.tsx",
|
|
75
75
|
"isEntry": true,
|
|
76
76
|
"imports": [
|
|
77
|
-
"_vendor-
|
|
78
|
-
"_vendor-arizeai-
|
|
79
|
-
"_pages-
|
|
80
|
-
"_components-
|
|
77
|
+
"_vendor-BRDkBC5J.js",
|
|
78
|
+
"_vendor-arizeai-BvTqp_W8.js",
|
|
79
|
+
"_pages-CZ2vKu8H.js",
|
|
80
|
+
"_components-B2MWTXnm.js",
|
|
81
81
|
"_vendor-three-C5WAXd5r.js",
|
|
82
|
-
"_vendor-codemirror-
|
|
83
|
-
"_vendor-shiki-
|
|
84
|
-
"_vendor-recharts-
|
|
82
|
+
"_vendor-codemirror-COt9UfW7.js",
|
|
83
|
+
"_vendor-shiki-Cw1dsDAz.js",
|
|
84
|
+
"_vendor-recharts-BoHX9Hvs.js"
|
|
85
85
|
]
|
|
86
86
|
}
|
|
87
87
|
}
|