Skip to content

Commit 5222e91

Browse files
committed
feat!: add db table and graphql mutations for session annotations (#8993)
1 parent 5ad7ed9 commit 5222e91

15 files changed

+1945
-417
lines changed

app/schema.graphql

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,18 @@ input CreateProjectInput {
431431
gradientEndColor: String
432432
}
433433

434+
input CreateProjectSessionAnnotationInput {
435+
projectSessionId: ID!
436+
name: String!
437+
annotatorKind: AnnotatorKind! = HUMAN
438+
label: String = null
439+
score: Float = null
440+
explanation: String = null
441+
metadata: JSON! = {}
442+
source: AnnotationSource! = APP
443+
identifier: String
444+
}
445+
434446
input CreateProjectTraceRetentionPolicyInput {
435447
name: String!
436448
cronExpression: CronExpression!
@@ -1579,6 +1591,9 @@ type Mutation {
15791591
createSpanNote(annotationInput: CreateSpanNoteInput!): SpanAnnotationMutationPayload!
15801592
patchSpanAnnotations(input: [PatchAnnotationInput!]!): SpanAnnotationMutationPayload!
15811593
deleteSpanAnnotations(input: DeleteAnnotationsInput!): SpanAnnotationMutationPayload!
1594+
createProjectSessionAnnotations(input: CreateProjectSessionAnnotationInput!): ProjectSessionAnnotationMutationPayload!
1595+
updateProjectSessionAnnotations(input: UpdateAnnotationInput!): ProjectSessionAnnotationMutationPayload!
1596+
deleteProjectSessionAnnotation(id: ID!): ProjectSessionAnnotationMutationPayload!
15821597
createTraceAnnotations(input: [CreateTraceAnnotationInput!]!): TraceAnnotationMutationPayload!
15831598
patchTraceAnnotations(input: [PatchAnnotationInput!]!): TraceAnnotationMutationPayload!
15841599
deleteTraceAnnotations(input: DeleteAnnotationsInput!): TraceAnnotationMutationPayload!
@@ -1822,6 +1837,26 @@ type ProjectSession implements Node {
18221837
costDetailSummaryEntries: [SpanCostDetailSummaryEntry!]!
18231838
}
18241839

1840+
type ProjectSessionAnnotation implements Node {
1841+
"""The Globally Unique ID of this object"""
1842+
id: ID!
1843+
name: String!
1844+
annotatorKind: AnnotatorKind!
1845+
label: String
1846+
score: Float
1847+
explanation: String
1848+
metadata: JSON!
1849+
identifier: String!
1850+
source: AnnotationSource!
1851+
projectSessionId: ID!
1852+
user: User
1853+
}
1854+
1855+
type ProjectSessionAnnotationMutationPayload {
1856+
projectSessionAnnotation: ProjectSessionAnnotation!
1857+
query: Query!
1858+
}
1859+
18251860
enum ProjectSessionColumn {
18261861
startTime
18271862
endTime
@@ -2852,6 +2887,17 @@ type UpdateAnnotationConfigPayload {
28522887
annotationConfig: AnnotationConfig!
28532888
}
28542889

2890+
input UpdateAnnotationInput {
2891+
id: ID!
2892+
name: String!
2893+
annotatorKind: AnnotatorKind! = HUMAN
2894+
label: String = null
2895+
score: Float = null
2896+
explanation: String = null
2897+
metadata: JSON! = {}
2898+
source: AnnotationSource! = APP
2899+
}
2900+
28552901
input UpdateModelMutationInput {
28562902
id: ID!
28572903
name: String!
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""add session annotations table
2+
3+
Revision ID: 0df286449799
4+
Revises: 735d3d93c33e
5+
Create Date: 2025-08-06 11:27:01.479664
6+
7+
"""
8+
9+
from typing import Any, Sequence, Union
10+
11+
import sqlalchemy as sa
12+
from alembic import op
13+
from sqlalchemy import JSON
14+
from sqlalchemy.dialects import postgresql
15+
from sqlalchemy.ext.compiler import compiles
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = "0df286449799"
19+
down_revision: Union[str, None] = "735d3d93c33e"
20+
branch_labels: Union[str, Sequence[str], None] = None
21+
depends_on: Union[str, Sequence[str], None] = None
22+
23+
24+
class JSONB(JSON):
25+
# See https://docs.sqlalchemy.org/en/20/core/custom_types.html
26+
__visit_name__ = "JSONB"
27+
28+
29+
@compiles(JSONB, "sqlite")
30+
def _(*args: Any, **kwargs: Any) -> str:
31+
# See https://docs.sqlalchemy.org/en/20/core/custom_types.html
32+
return "JSONB"
33+
34+
35+
JSON_ = (
36+
JSON()
37+
.with_variant(
38+
postgresql.JSONB(),
39+
"postgresql",
40+
)
41+
.with_variant(
42+
JSONB(),
43+
"sqlite",
44+
)
45+
)
46+
47+
_Integer = sa.Integer().with_variant(
48+
sa.BigInteger(),
49+
"postgresql",
50+
)
51+
52+
53+
def upgrade() -> None:
54+
op.create_table(
55+
"project_session_annotations",
56+
sa.Column("id", _Integer, primary_key=True),
57+
sa.Column(
58+
"project_session_id",
59+
_Integer,
60+
sa.ForeignKey("project_sessions.id", ondelete="CASCADE"),
61+
nullable=False,
62+
index=True,
63+
),
64+
sa.Column("name", sa.String, nullable=False),
65+
sa.Column("label", sa.String),
66+
sa.Column("score", sa.Float),
67+
sa.Column("explanation", sa.String),
68+
sa.Column("metadata", JSON_, nullable=False),
69+
sa.Column(
70+
"annotator_kind",
71+
sa.String,
72+
sa.CheckConstraint(
73+
"annotator_kind IN ('LLM', 'CODE', 'HUMAN')",
74+
name="valid_annotator_kind",
75+
),
76+
nullable=False,
77+
),
78+
sa.Column(
79+
"user_id",
80+
_Integer,
81+
sa.ForeignKey("users.id", ondelete="SET NULL"),
82+
),
83+
sa.Column("identifier", sa.String, server_default="", nullable=False),
84+
sa.Column(
85+
"source",
86+
sa.String,
87+
sa.CheckConstraint("source IN ('API', 'APP')", name="valid_source"),
88+
nullable=False,
89+
),
90+
sa.Column(
91+
"created_at", sa.TIMESTAMP(timezone=True), server_default=sa.func.now(), nullable=False
92+
),
93+
sa.Column(
94+
"updated_at",
95+
sa.TIMESTAMP(timezone=True),
96+
server_default=sa.func.now(),
97+
onupdate=sa.func.now(),
98+
nullable=False,
99+
),
100+
sa.UniqueConstraint("name", "project_session_id", "identifier"),
101+
)
102+
103+
104+
def downgrade() -> None:
105+
op.drop_table("project_session_annotations")

src/phoenix/db/models.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,43 @@ class DocumentAnnotation(Base):
10121012
)
10131013

10141014

1015+
class ProjectSessionAnnotation(Base):
1016+
__tablename__ = "project_session_annotations"
1017+
project_session_id: Mapped[int] = mapped_column(
1018+
ForeignKey("project_sessions.id", ondelete="CASCADE"),
1019+
index=True,
1020+
)
1021+
name: Mapped[str]
1022+
label: Mapped[Optional[str]] = mapped_column(String, index=True)
1023+
score: Mapped[Optional[float]] = mapped_column(Float, index=True)
1024+
explanation: Mapped[Optional[str]]
1025+
metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
1026+
annotator_kind: Mapped[Literal["LLM", "CODE", "HUMAN"]] = mapped_column(
1027+
CheckConstraint("annotator_kind IN ('LLM', 'CODE', 'HUMAN')", name="valid_annotator_kind"),
1028+
)
1029+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1030+
updated_at: Mapped[datetime] = mapped_column(
1031+
UtcTimeStamp, server_default=func.now(), onupdate=func.now()
1032+
)
1033+
identifier: Mapped[str] = mapped_column(
1034+
String,
1035+
server_default="",
1036+
nullable=False,
1037+
)
1038+
source: Mapped[Literal["API", "APP"]] = mapped_column(
1039+
CheckConstraint("source IN ('API', 'APP')", name="valid_source"),
1040+
)
1041+
user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
1042+
1043+
__table_args__ = (
1044+
UniqueConstraint(
1045+
"name",
1046+
"project_session_id",
1047+
"identifier",
1048+
),
1049+
)
1050+
1051+
10151052
class Dataset(Base):
10161053
__tablename__ = "datasets"
10171054
name: Mapped[str] = mapped_column(unique=True)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Optional
2+
3+
import strawberry
4+
from strawberry.relay import GlobalID
5+
from strawberry.scalars import JSON
6+
7+
from phoenix.server.api.exceptions import BadRequest
8+
from phoenix.server.api.types.AnnotationSource import AnnotationSource
9+
from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
10+
11+
12+
@strawberry.input
13+
class CreateProjectSessionAnnotationInput:
14+
project_session_id: GlobalID
15+
name: str
16+
annotator_kind: AnnotatorKind = AnnotatorKind.HUMAN
17+
label: Optional[str] = None
18+
score: Optional[float] = None
19+
explanation: Optional[str] = None
20+
metadata: JSON = strawberry.field(default_factory=dict)
21+
source: AnnotationSource = AnnotationSource.APP
22+
identifier: Optional[str] = strawberry.UNSET
23+
24+
def __post_init__(self) -> None:
25+
self.name = self.name.strip()
26+
if isinstance(self.label, str):
27+
self.label = self.label.strip()
28+
if not self.label:
29+
self.label = None
30+
if isinstance(self.explanation, str):
31+
self.explanation = self.explanation.strip()
32+
if not self.explanation:
33+
self.explanation = None
34+
if isinstance(self.identifier, str):
35+
self.identifier = self.identifier.strip()
36+
if self.score is None and not self.label and not self.explanation:
37+
raise BadRequest("At least one of score, label, or explanation must be not null/empty.")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Optional
2+
3+
import strawberry
4+
from strawberry.relay import GlobalID
5+
from strawberry.scalars import JSON
6+
7+
from phoenix.server.api.exceptions import BadRequest
8+
from phoenix.server.api.types.AnnotationSource import AnnotationSource
9+
from phoenix.server.api.types.AnnotatorKind import AnnotatorKind
10+
11+
12+
@strawberry.input
13+
class UpdateAnnotationInput:
14+
id: GlobalID
15+
name: str
16+
annotator_kind: AnnotatorKind = AnnotatorKind.HUMAN
17+
label: Optional[str] = None
18+
score: Optional[float] = None
19+
explanation: Optional[str] = None
20+
metadata: JSON = strawberry.field(default_factory=dict)
21+
source: AnnotationSource = AnnotationSource.APP
22+
23+
def __post_init__(self) -> None:
24+
self.name = self.name.strip()
25+
if isinstance(self.label, str):
26+
self.label = self.label.strip()
27+
if not self.label:
28+
self.label = None
29+
if isinstance(self.explanation, str):
30+
self.explanation = self.explanation.strip()
31+
if not self.explanation:
32+
self.explanation = None
33+
if self.score is None and not self.label and not self.explanation:
34+
raise BadRequest("At least one of score, label, or explanation must be not null/empty.")

src/phoenix/server/api/mutations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from phoenix.server.api.mutations.export_events_mutations import ExportEventsMutationMixin
1111
from phoenix.server.api.mutations.model_mutations import ModelMutationMixin
1212
from phoenix.server.api.mutations.project_mutations import ProjectMutationMixin
13+
from phoenix.server.api.mutations.project_session_annotations_mutations import (
14+
ProjectSessionAnnotationMutationMixin,
15+
)
1316
from phoenix.server.api.mutations.project_trace_retention_policy_mutations import (
1417
ProjectTraceRetentionPolicyMutationMixin,
1518
)
@@ -37,6 +40,7 @@ class Mutation(
3740
PromptVersionTagMutationMixin,
3841
PromptLabelMutationMixin,
3942
SpanAnnotationMutationMixin,
43+
ProjectSessionAnnotationMutationMixin,
4044
TraceAnnotationMutationMixin,
4145
TraceMutationMixin,
4246
UserMutationMixin,

0 commit comments

Comments
 (0)