arize-phoenix 3.25.0__py3-none-any.whl → 4.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.

Files changed (113) hide show
  1. {arize_phoenix-3.25.0.dist-info → arize_phoenix-4.0.0.dist-info}/METADATA +26 -4
  2. {arize_phoenix-3.25.0.dist-info → arize_phoenix-4.0.0.dist-info}/RECORD +80 -75
  3. phoenix/__init__.py +9 -5
  4. phoenix/config.py +109 -53
  5. phoenix/datetime_utils.py +18 -1
  6. phoenix/db/README.md +25 -0
  7. phoenix/db/__init__.py +4 -0
  8. phoenix/db/alembic.ini +119 -0
  9. phoenix/db/bulk_inserter.py +206 -0
  10. phoenix/db/engines.py +152 -0
  11. phoenix/db/helpers.py +47 -0
  12. phoenix/db/insertion/evaluation.py +209 -0
  13. phoenix/db/insertion/helpers.py +54 -0
  14. phoenix/db/insertion/span.py +142 -0
  15. phoenix/db/migrate.py +71 -0
  16. phoenix/db/migrations/env.py +121 -0
  17. phoenix/db/migrations/script.py.mako +26 -0
  18. phoenix/db/migrations/versions/cf03bd6bae1d_init.py +280 -0
  19. phoenix/db/models.py +371 -0
  20. phoenix/exceptions.py +5 -1
  21. phoenix/server/api/context.py +40 -3
  22. phoenix/server/api/dataloaders/__init__.py +97 -0
  23. phoenix/server/api/dataloaders/cache/__init__.py +3 -0
  24. phoenix/server/api/dataloaders/cache/two_tier_cache.py +67 -0
  25. phoenix/server/api/dataloaders/document_evaluation_summaries.py +152 -0
  26. phoenix/server/api/dataloaders/document_evaluations.py +37 -0
  27. phoenix/server/api/dataloaders/document_retrieval_metrics.py +98 -0
  28. phoenix/server/api/dataloaders/evaluation_summaries.py +151 -0
  29. phoenix/server/api/dataloaders/latency_ms_quantile.py +198 -0
  30. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +93 -0
  31. phoenix/server/api/dataloaders/record_counts.py +125 -0
  32. phoenix/server/api/dataloaders/span_descendants.py +64 -0
  33. phoenix/server/api/dataloaders/span_evaluations.py +37 -0
  34. phoenix/server/api/dataloaders/token_counts.py +138 -0
  35. phoenix/server/api/dataloaders/trace_evaluations.py +37 -0
  36. phoenix/server/api/input_types/SpanSort.py +138 -68
  37. phoenix/server/api/routers/v1/__init__.py +11 -0
  38. phoenix/server/api/routers/v1/evaluations.py +275 -0
  39. phoenix/server/api/routers/v1/spans.py +126 -0
  40. phoenix/server/api/routers/v1/traces.py +82 -0
  41. phoenix/server/api/schema.py +112 -48
  42. phoenix/server/api/types/DocumentEvaluationSummary.py +1 -1
  43. phoenix/server/api/types/Evaluation.py +29 -12
  44. phoenix/server/api/types/EvaluationSummary.py +29 -44
  45. phoenix/server/api/types/MimeType.py +2 -2
  46. phoenix/server/api/types/Model.py +9 -9
  47. phoenix/server/api/types/Project.py +240 -171
  48. phoenix/server/api/types/Span.py +87 -131
  49. phoenix/server/api/types/Trace.py +29 -20
  50. phoenix/server/api/types/pagination.py +151 -10
  51. phoenix/server/app.py +263 -35
  52. phoenix/server/grpc_server.py +93 -0
  53. phoenix/server/main.py +75 -60
  54. phoenix/server/openapi/docs.py +218 -0
  55. phoenix/server/prometheus.py +23 -7
  56. phoenix/server/static/index.js +662 -643
  57. phoenix/server/telemetry.py +68 -0
  58. phoenix/services.py +4 -0
  59. phoenix/session/client.py +34 -30
  60. phoenix/session/data_extractor.py +8 -3
  61. phoenix/session/session.py +176 -155
  62. phoenix/settings.py +13 -0
  63. phoenix/trace/attributes.py +349 -0
  64. phoenix/trace/dsl/README.md +116 -0
  65. phoenix/trace/dsl/filter.py +660 -192
  66. phoenix/trace/dsl/helpers.py +24 -5
  67. phoenix/trace/dsl/query.py +562 -185
  68. phoenix/trace/fixtures.py +69 -7
  69. phoenix/trace/otel.py +33 -199
  70. phoenix/trace/schemas.py +14 -8
  71. phoenix/trace/span_evaluations.py +5 -2
  72. phoenix/utilities/__init__.py +0 -26
  73. phoenix/utilities/span_store.py +0 -23
  74. phoenix/version.py +1 -1
  75. phoenix/core/project.py +0 -773
  76. phoenix/core/traces.py +0 -96
  77. phoenix/datasets/dataset.py +0 -214
  78. phoenix/datasets/fixtures.py +0 -24
  79. phoenix/datasets/schema.py +0 -31
  80. phoenix/experimental/evals/__init__.py +0 -73
  81. phoenix/experimental/evals/evaluators.py +0 -413
  82. phoenix/experimental/evals/functions/__init__.py +0 -4
  83. phoenix/experimental/evals/functions/classify.py +0 -453
  84. phoenix/experimental/evals/functions/executor.py +0 -353
  85. phoenix/experimental/evals/functions/generate.py +0 -138
  86. phoenix/experimental/evals/functions/processing.py +0 -76
  87. phoenix/experimental/evals/models/__init__.py +0 -14
  88. phoenix/experimental/evals/models/anthropic.py +0 -175
  89. phoenix/experimental/evals/models/base.py +0 -170
  90. phoenix/experimental/evals/models/bedrock.py +0 -221
  91. phoenix/experimental/evals/models/litellm.py +0 -134
  92. phoenix/experimental/evals/models/openai.py +0 -453
  93. phoenix/experimental/evals/models/rate_limiters.py +0 -246
  94. phoenix/experimental/evals/models/vertex.py +0 -173
  95. phoenix/experimental/evals/models/vertexai.py +0 -186
  96. phoenix/experimental/evals/retrievals.py +0 -96
  97. phoenix/experimental/evals/templates/__init__.py +0 -50
  98. phoenix/experimental/evals/templates/default_templates.py +0 -472
  99. phoenix/experimental/evals/templates/template.py +0 -195
  100. phoenix/experimental/evals/utils/__init__.py +0 -172
  101. phoenix/experimental/evals/utils/threads.py +0 -27
  102. phoenix/server/api/routers/evaluation_handler.py +0 -110
  103. phoenix/server/api/routers/span_handler.py +0 -70
  104. phoenix/server/api/routers/trace_handler.py +0 -60
  105. phoenix/storage/span_store/__init__.py +0 -23
  106. phoenix/storage/span_store/text_file.py +0 -85
  107. phoenix/trace/dsl/missing.py +0 -60
  108. {arize_phoenix-3.25.0.dist-info → arize_phoenix-4.0.0.dist-info}/WHEEL +0 -0
  109. {arize_phoenix-3.25.0.dist-info → arize_phoenix-4.0.0.dist-info}/licenses/IP_NOTICE +0 -0
  110. {arize_phoenix-3.25.0.dist-info → arize_phoenix-4.0.0.dist-info}/licenses/LICENSE +0 -0
  111. /phoenix/{datasets → db/insertion}/__init__.py +0 -0
  112. /phoenix/{experimental → db/migrations}/__init__.py +0 -0
  113. /phoenix/{storage → server/openapi}/__init__.py +0 -0
@@ -0,0 +1,280 @@
1
+ """init
2
+
3
+ Revision ID: cf03bd6bae1d
4
+ Revises:
5
+ Create Date: 2024-04-03 19:41:48.871555
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 = "cf03bd6bae1d"
19
+ down_revision: Union[str, None] = None
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") # type: ignore
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(), # type: ignore
39
+ "postgresql",
40
+ )
41
+ .with_variant(
42
+ JSONB(),
43
+ "sqlite",
44
+ )
45
+ )
46
+
47
+
48
+ def upgrade() -> None:
49
+ projects_table = op.create_table(
50
+ "projects",
51
+ sa.Column("id", sa.Integer, primary_key=True),
52
+ sa.Column("name", sa.String, nullable=False, unique=True),
53
+ sa.Column("description", sa.String, nullable=True),
54
+ sa.Column(
55
+ "gradient_start_color",
56
+ sa.String,
57
+ nullable=False,
58
+ server_default=sa.text("'#5bdbff'"),
59
+ ),
60
+ sa.Column(
61
+ "gradient_end_color",
62
+ sa.String,
63
+ nullable=False,
64
+ server_default=sa.text("'#1c76fc'"),
65
+ ),
66
+ sa.Column(
67
+ "created_at",
68
+ sa.TIMESTAMP(timezone=True),
69
+ nullable=False,
70
+ server_default=sa.func.now(),
71
+ ),
72
+ sa.Column(
73
+ "updated_at",
74
+ sa.TIMESTAMP(timezone=True),
75
+ nullable=False,
76
+ server_default=sa.func.now(),
77
+ onupdate=sa.func.now(),
78
+ ),
79
+ )
80
+ op.create_table(
81
+ "traces",
82
+ sa.Column("id", sa.Integer, primary_key=True),
83
+ sa.Column(
84
+ "project_rowid",
85
+ sa.Integer,
86
+ sa.ForeignKey("projects.id", ondelete="CASCADE"),
87
+ nullable=False,
88
+ index=True,
89
+ ),
90
+ sa.Column("trace_id", sa.String, nullable=False, unique=True),
91
+ sa.Column("start_time", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
92
+ sa.Column("end_time", sa.TIMESTAMP(timezone=True), nullable=False),
93
+ )
94
+
95
+ op.create_table(
96
+ "spans",
97
+ sa.Column("id", sa.Integer, primary_key=True),
98
+ sa.Column(
99
+ "trace_rowid",
100
+ sa.Integer,
101
+ sa.ForeignKey("traces.id", ondelete="CASCADE"),
102
+ nullable=False,
103
+ index=True,
104
+ ),
105
+ sa.Column("span_id", sa.String, nullable=False, unique=True),
106
+ sa.Column("parent_id", sa.String, nullable=True, index=True),
107
+ sa.Column("name", sa.String, nullable=False),
108
+ sa.Column("span_kind", sa.String, nullable=False),
109
+ sa.Column("start_time", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
110
+ sa.Column("end_time", sa.TIMESTAMP(timezone=True), nullable=False),
111
+ sa.Column("attributes", JSON_, nullable=False),
112
+ sa.Column("events", JSON_, nullable=False),
113
+ sa.Column(
114
+ "status_code",
115
+ sa.String,
116
+ # TODO(mikeldking): this doesn't seem to work...
117
+ sa.CheckConstraint("status_code IN ('OK', 'ERROR', 'UNSET')", "valid_status"),
118
+ nullable=False,
119
+ default="UNSET",
120
+ server_default="UNSET",
121
+ ),
122
+ sa.Column("status_message", sa.String, nullable=False),
123
+ sa.Column("cumulative_error_count", sa.Integer, nullable=False),
124
+ sa.Column("cumulative_llm_token_count_prompt", sa.Integer, nullable=False),
125
+ sa.Column("cumulative_llm_token_count_completion", sa.Integer, nullable=False),
126
+ )
127
+ op.create_index("ix_latency", "spans", [sa.text("(end_time - start_time)")], unique=False)
128
+ op.create_index(
129
+ "ix_cumulative_llm_token_count_total",
130
+ "spans",
131
+ [sa.text("(cumulative_llm_token_count_prompt + cumulative_llm_token_count_completion)")],
132
+ unique=False,
133
+ )
134
+
135
+ op.create_table(
136
+ "span_annotations",
137
+ sa.Column("id", sa.Integer, primary_key=True),
138
+ sa.Column(
139
+ "span_rowid",
140
+ sa.Integer,
141
+ sa.ForeignKey("spans.id", ondelete="CASCADE"),
142
+ nullable=False,
143
+ index=True,
144
+ ),
145
+ sa.Column("name", sa.String, nullable=False),
146
+ sa.Column("label", sa.String, nullable=True, index=True),
147
+ sa.Column("score", sa.Float, nullable=True, index=True),
148
+ sa.Column("explanation", sa.String, nullable=True),
149
+ sa.Column("metadata", JSON_, nullable=False),
150
+ sa.Column(
151
+ "annotator_kind",
152
+ sa.String,
153
+ sa.CheckConstraint(
154
+ "annotator_kind IN ('LLM', 'HUMAN')",
155
+ name="valid_annotator_kind",
156
+ ),
157
+ nullable=False,
158
+ ),
159
+ sa.Column(
160
+ "created_at",
161
+ sa.TIMESTAMP(timezone=True),
162
+ nullable=False,
163
+ server_default=sa.func.now(),
164
+ ),
165
+ sa.Column(
166
+ "updated_at",
167
+ sa.TIMESTAMP(timezone=True),
168
+ nullable=False,
169
+ server_default=sa.func.now(),
170
+ onupdate=sa.func.now(),
171
+ ),
172
+ sa.UniqueConstraint(
173
+ "name",
174
+ "span_rowid",
175
+ ),
176
+ )
177
+
178
+ op.create_table(
179
+ "trace_annotations",
180
+ sa.Column("id", sa.Integer, primary_key=True),
181
+ sa.Column(
182
+ "trace_rowid",
183
+ sa.Integer,
184
+ sa.ForeignKey("traces.id", ondelete="CASCADE"),
185
+ nullable=False,
186
+ index=True,
187
+ ),
188
+ sa.Column("name", sa.String, nullable=False),
189
+ sa.Column("label", sa.String, nullable=True, index=True),
190
+ sa.Column("score", sa.Float, nullable=True, index=True),
191
+ sa.Column("explanation", sa.String, nullable=True),
192
+ sa.Column("metadata", JSON_, nullable=False),
193
+ sa.Column(
194
+ "annotator_kind",
195
+ sa.String,
196
+ sa.CheckConstraint(
197
+ "annotator_kind IN ('LLM', 'HUMAN')",
198
+ name="valid_annotator_kind",
199
+ ),
200
+ nullable=False,
201
+ ),
202
+ sa.Column(
203
+ "created_at",
204
+ sa.TIMESTAMP(timezone=True),
205
+ nullable=False,
206
+ server_default=sa.func.now(),
207
+ ),
208
+ sa.Column(
209
+ "updated_at",
210
+ sa.TIMESTAMP(timezone=True),
211
+ nullable=False,
212
+ server_default=sa.func.now(),
213
+ onupdate=sa.func.now(),
214
+ ),
215
+ sa.UniqueConstraint(
216
+ "name",
217
+ "trace_rowid",
218
+ ),
219
+ )
220
+
221
+ op.create_table(
222
+ "document_annotations",
223
+ sa.Column("id", sa.Integer, primary_key=True),
224
+ sa.Column(
225
+ "span_rowid",
226
+ sa.Integer,
227
+ sa.ForeignKey("spans.id", ondelete="CASCADE"),
228
+ nullable=False,
229
+ index=True,
230
+ ),
231
+ sa.Column("document_position", sa.Integer, nullable=False),
232
+ sa.Column("name", sa.String, nullable=False),
233
+ sa.Column("label", sa.String, nullable=True, index=True),
234
+ sa.Column("score", sa.Float, nullable=True, index=True),
235
+ sa.Column("explanation", sa.String, nullable=True),
236
+ sa.Column("metadata", JSON_, nullable=False),
237
+ sa.Column(
238
+ "annotator_kind",
239
+ sa.String,
240
+ sa.CheckConstraint(
241
+ "annotator_kind IN ('LLM', 'HUMAN')",
242
+ name="valid_annotator_kind",
243
+ ),
244
+ nullable=False,
245
+ ),
246
+ sa.Column(
247
+ "created_at",
248
+ sa.TIMESTAMP(timezone=True),
249
+ nullable=False,
250
+ server_default=sa.func.now(),
251
+ ),
252
+ sa.Column(
253
+ "updated_at",
254
+ sa.TIMESTAMP(timezone=True),
255
+ nullable=False,
256
+ server_default=sa.func.now(),
257
+ onupdate=sa.func.now(),
258
+ ),
259
+ sa.UniqueConstraint(
260
+ "name",
261
+ "span_rowid",
262
+ "document_position",
263
+ ),
264
+ )
265
+
266
+ op.bulk_insert(
267
+ projects_table,
268
+ [
269
+ {"name": "default", "description": "Default project"},
270
+ ],
271
+ )
272
+
273
+
274
+ def downgrade() -> None:
275
+ op.drop_table("span_annotations")
276
+ op.drop_table("trace_annotations")
277
+ op.drop_table("document_annotations")
278
+ op.drop_table("spans")
279
+ op.drop_table("traces")
280
+ op.drop_table("projects")
phoenix/db/models.py ADDED
@@ -0,0 +1,371 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from sqlalchemy import (
5
+ JSON,
6
+ TIMESTAMP,
7
+ CheckConstraint,
8
+ ColumnElement,
9
+ Dialect,
10
+ Float,
11
+ ForeignKey,
12
+ Index,
13
+ MetaData,
14
+ String,
15
+ TypeDecorator,
16
+ UniqueConstraint,
17
+ func,
18
+ insert,
19
+ text,
20
+ )
21
+ from sqlalchemy.dialects import postgresql
22
+ from sqlalchemy.ext.asyncio import AsyncEngine
23
+ from sqlalchemy.ext.compiler import compiles
24
+ from sqlalchemy.ext.hybrid import hybrid_property
25
+ from sqlalchemy.orm import (
26
+ DeclarativeBase,
27
+ Mapped,
28
+ WriteOnlyMapped,
29
+ mapped_column,
30
+ relationship,
31
+ )
32
+ from sqlalchemy.sql import expression
33
+
34
+ from phoenix.datetime_utils import normalize_datetime
35
+
36
+
37
+ class JSONB(JSON):
38
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
39
+ __visit_name__ = "JSONB"
40
+
41
+
42
+ @compiles(JSONB, "sqlite") # type: ignore
43
+ def _(*args: Any, **kwargs: Any) -> str:
44
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
45
+ return "JSONB"
46
+
47
+
48
+ JSON_ = (
49
+ JSON()
50
+ .with_variant(
51
+ postgresql.JSONB(), # type: ignore
52
+ "postgresql",
53
+ )
54
+ .with_variant(
55
+ JSONB(),
56
+ "sqlite",
57
+ )
58
+ )
59
+
60
+
61
+ class UtcTimeStamp(TypeDecorator[datetime]):
62
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
63
+ cache_ok = True
64
+ impl = TIMESTAMP(timezone=True)
65
+
66
+ def process_bind_param(self, value: Optional[datetime], _: Dialect) -> Optional[datetime]:
67
+ return normalize_datetime(value)
68
+
69
+ def process_result_value(self, value: Optional[Any], _: Dialect) -> Optional[datetime]:
70
+ return normalize_datetime(value, timezone.utc)
71
+
72
+
73
+ class Base(DeclarativeBase):
74
+ # Enforce best practices for naming constraints
75
+ # https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
76
+ metadata = MetaData(
77
+ naming_convention={
78
+ "ix": "ix_%(table_name)s_%(column_0_N_name)s",
79
+ "uq": "uq_%(table_name)s_%(column_0_N_name)s",
80
+ "ck": "ck_%(table_name)s_`%(constraint_name)s`",
81
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
82
+ "pk": "pk_%(table_name)s",
83
+ }
84
+ )
85
+ type_annotation_map = {
86
+ Dict[str, Any]: JSON_,
87
+ List[Dict[str, Any]]: JSON_,
88
+ }
89
+
90
+
91
+ class Project(Base):
92
+ __tablename__ = "projects"
93
+ id: Mapped[int] = mapped_column(primary_key=True)
94
+ name: Mapped[str]
95
+ description: Mapped[Optional[str]]
96
+ gradient_start_color: Mapped[str] = mapped_column(
97
+ String,
98
+ server_default=text("'#5bdbff'"),
99
+ )
100
+
101
+ gradient_end_color: Mapped[str] = mapped_column(
102
+ String,
103
+ server_default=text("'#1c76fc'"),
104
+ )
105
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
106
+ updated_at: Mapped[datetime] = mapped_column(
107
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
108
+ )
109
+
110
+ traces: WriteOnlyMapped[List["Trace"]] = relationship(
111
+ "Trace",
112
+ back_populates="project",
113
+ cascade="all, delete-orphan",
114
+ passive_deletes=True,
115
+ uselist=True,
116
+ )
117
+ __table_args__ = (
118
+ UniqueConstraint(
119
+ "name",
120
+ ),
121
+ )
122
+
123
+
124
+ class Trace(Base):
125
+ __tablename__ = "traces"
126
+ id: Mapped[int] = mapped_column(primary_key=True)
127
+ project_rowid: Mapped[int] = mapped_column(
128
+ ForeignKey("projects.id", ondelete="CASCADE"),
129
+ index=True,
130
+ )
131
+ trace_id: Mapped[str]
132
+ start_time: Mapped[datetime] = mapped_column(UtcTimeStamp, index=True)
133
+ end_time: Mapped[datetime] = mapped_column(UtcTimeStamp)
134
+
135
+ @hybrid_property
136
+ def latency_ms(self) -> float:
137
+ # See https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html
138
+ return (self.end_time - self.start_time).total_seconds() * 1000
139
+
140
+ @latency_ms.inplace.expression
141
+ @classmethod
142
+ def _latency_ms_expression(cls) -> ColumnElement[float]:
143
+ # See https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html
144
+ return LatencyMs(cls.start_time, cls.end_time)
145
+
146
+ project: Mapped["Project"] = relationship(
147
+ "Project",
148
+ back_populates="traces",
149
+ )
150
+ spans: Mapped[List["Span"]] = relationship(
151
+ "Span",
152
+ back_populates="trace",
153
+ cascade="all, delete-orphan",
154
+ uselist=True,
155
+ )
156
+ __table_args__ = (
157
+ UniqueConstraint(
158
+ "trace_id",
159
+ ),
160
+ )
161
+
162
+
163
+ class Span(Base):
164
+ __tablename__ = "spans"
165
+ id: Mapped[int] = mapped_column(primary_key=True)
166
+ trace_rowid: Mapped[int] = mapped_column(
167
+ ForeignKey("traces.id", ondelete="CASCADE"),
168
+ index=True,
169
+ )
170
+ span_id: Mapped[str]
171
+ parent_id: Mapped[Optional[str]] = mapped_column(index=True)
172
+ name: Mapped[str]
173
+ span_kind: Mapped[str]
174
+ start_time: Mapped[datetime] = mapped_column(UtcTimeStamp, index=True)
175
+ end_time: Mapped[datetime] = mapped_column(UtcTimeStamp)
176
+ attributes: Mapped[Dict[str, Any]]
177
+ events: Mapped[List[Dict[str, Any]]]
178
+ status_code: Mapped[str] = mapped_column(
179
+ CheckConstraint("status_code IN ('OK', 'ERROR', 'UNSET')", name="valid_status")
180
+ )
181
+ status_message: Mapped[str]
182
+
183
+ # TODO(mikeldking): is computed columns possible here
184
+ cumulative_error_count: Mapped[int]
185
+ cumulative_llm_token_count_prompt: Mapped[int]
186
+ cumulative_llm_token_count_completion: Mapped[int]
187
+
188
+ @hybrid_property
189
+ def latency_ms(self) -> float:
190
+ # See https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html
191
+ return (self.end_time - self.start_time).total_seconds() * 1000
192
+
193
+ @latency_ms.inplace.expression
194
+ @classmethod
195
+ def _latency_ms_expression(cls) -> ColumnElement[float]:
196
+ # See https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html
197
+ return LatencyMs(cls.start_time, cls.end_time)
198
+
199
+ @hybrid_property
200
+ def cumulative_llm_token_count_total(self) -> int:
201
+ return self.cumulative_llm_token_count_prompt + self.cumulative_llm_token_count_completion
202
+
203
+ trace: Mapped["Trace"] = relationship("Trace", back_populates="spans")
204
+ document_annotations: Mapped[List["DocumentAnnotation"]] = relationship(back_populates="span")
205
+
206
+ __table_args__ = (
207
+ UniqueConstraint(
208
+ "span_id",
209
+ sqlite_on_conflict="IGNORE",
210
+ ),
211
+ Index("ix_latency", text("(end_time - start_time)")),
212
+ Index(
213
+ "ix_cumulative_llm_token_count_total",
214
+ text("(cumulative_llm_token_count_prompt + cumulative_llm_token_count_completion)"),
215
+ ),
216
+ )
217
+
218
+
219
+ class LatencyMs(expression.FunctionElement[float]):
220
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
221
+ inherit_cache = True
222
+ type = Float()
223
+ name = "latency_ms"
224
+
225
+
226
+ @compiles(LatencyMs) # type: ignore
227
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
228
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
229
+ start_time, end_time = list(element.clauses)
230
+ return compiler.process(
231
+ func.round((func.extract("EPOCH", end_time) - func.extract("EPOCH", start_time)) * 1000, 1),
232
+ **kw,
233
+ )
234
+
235
+
236
+ @compiles(LatencyMs, "sqlite") # type: ignore
237
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
238
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
239
+ start_time, end_time = list(element.clauses)
240
+ return compiler.process(
241
+ # We don't know why sqlite returns a slightly different value.
242
+ # postgresql is correct because it matches the value computed by Python.
243
+ func.round(
244
+ (func.unixepoch(end_time, "subsec") - func.unixepoch(start_time, "subsec")) * 1000, 1
245
+ ),
246
+ **kw,
247
+ )
248
+
249
+
250
+ class TextContains(expression.FunctionElement[str]):
251
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
252
+ inherit_cache = True
253
+ type = String()
254
+ name = "text_contains"
255
+
256
+
257
+ @compiles(TextContains) # type: ignore
258
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
259
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
260
+ string, substring = list(element.clauses)
261
+ return compiler.process(string.contains(substring), **kw)
262
+
263
+
264
+ @compiles(TextContains, "postgresql") # type: ignore
265
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
266
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
267
+ string, substring = list(element.clauses)
268
+ return compiler.process(func.strpos(string, substring) > 0, **kw)
269
+
270
+
271
+ @compiles(TextContains, "sqlite") # type: ignore
272
+ def _(element: Any, compiler: Any, **kw: Any) -> Any:
273
+ # See https://docs.sqlalchemy.org/en/20/core/compiler.html
274
+ string, substring = list(element.clauses)
275
+ return compiler.process(func.text_contains(string, substring) > 0, **kw)
276
+
277
+
278
+ async def init_models(engine: AsyncEngine) -> None:
279
+ async with engine.begin() as conn:
280
+ await conn.run_sync(Base.metadata.create_all)
281
+ await conn.execute(
282
+ insert(Project).values(
283
+ name="default",
284
+ description="default project",
285
+ )
286
+ )
287
+
288
+
289
+ class SpanAnnotation(Base):
290
+ __tablename__ = "span_annotations"
291
+ id: Mapped[int] = mapped_column(primary_key=True)
292
+ span_rowid: Mapped[int] = mapped_column(
293
+ ForeignKey("spans.id", ondelete="CASCADE"),
294
+ index=True,
295
+ )
296
+ name: Mapped[str]
297
+ label: Mapped[Optional[str]] = mapped_column(String, index=True)
298
+ score: Mapped[Optional[float]] = mapped_column(Float, index=True)
299
+ explanation: Mapped[Optional[str]]
300
+ metadata_: Mapped[Dict[str, Any]] = mapped_column("metadata")
301
+ annotator_kind: Mapped[str] = mapped_column(
302
+ CheckConstraint("annotator_kind IN ('LLM', 'HUMAN')", name="valid_annotator_kind"),
303
+ )
304
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
305
+ updated_at: Mapped[datetime] = mapped_column(
306
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
307
+ )
308
+ __table_args__ = (
309
+ UniqueConstraint(
310
+ "name",
311
+ "span_rowid",
312
+ ),
313
+ )
314
+
315
+
316
+ class TraceAnnotation(Base):
317
+ __tablename__ = "trace_annotations"
318
+ id: Mapped[int] = mapped_column(primary_key=True)
319
+ trace_rowid: Mapped[int] = mapped_column(
320
+ ForeignKey("traces.id", ondelete="CASCADE"),
321
+ index=True,
322
+ )
323
+ name: Mapped[str]
324
+ label: Mapped[Optional[str]] = mapped_column(String, index=True)
325
+ score: Mapped[Optional[float]] = mapped_column(Float, index=True)
326
+ explanation: Mapped[Optional[str]]
327
+ metadata_: Mapped[Dict[str, Any]] = mapped_column("metadata")
328
+ annotator_kind: Mapped[str] = mapped_column(
329
+ CheckConstraint("annotator_kind IN ('LLM', 'HUMAN')", name="valid_annotator_kind"),
330
+ )
331
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
332
+ updated_at: Mapped[datetime] = mapped_column(
333
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
334
+ )
335
+ __table_args__ = (
336
+ UniqueConstraint(
337
+ "name",
338
+ "trace_rowid",
339
+ ),
340
+ )
341
+
342
+
343
+ class DocumentAnnotation(Base):
344
+ __tablename__ = "document_annotations"
345
+ id: Mapped[int] = mapped_column(primary_key=True)
346
+ span_rowid: Mapped[int] = mapped_column(
347
+ ForeignKey("spans.id", ondelete="CASCADE"),
348
+ index=True,
349
+ )
350
+ document_position: Mapped[int]
351
+ name: Mapped[str]
352
+ label: Mapped[Optional[str]] = mapped_column(String, index=True)
353
+ score: Mapped[Optional[float]] = mapped_column(Float, index=True)
354
+ explanation: Mapped[Optional[str]]
355
+ metadata_: Mapped[Dict[str, Any]] = mapped_column("metadata")
356
+ annotator_kind: Mapped[str] = mapped_column(
357
+ CheckConstraint("annotator_kind IN ('LLM', 'HUMAN')", name="valid_annotator_kind"),
358
+ )
359
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
360
+ updated_at: Mapped[datetime] = mapped_column(
361
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
362
+ )
363
+ span: Mapped["Span"] = relationship(back_populates="document_annotations")
364
+
365
+ __table_args__ = (
366
+ UniqueConstraint(
367
+ "name",
368
+ "span_rowid",
369
+ "document_position",
370
+ ),
371
+ )
phoenix/exceptions.py CHANGED
@@ -2,5 +2,9 @@ class PhoenixException(Exception):
2
2
  pass
3
3
 
4
4
 
5
- class PhoenixContextLimitExceeded(PhoenixException):
5
+ class PhoenixEvaluationNameIsMissing(PhoenixException):
6
+ pass
7
+
8
+
9
+ class PhoenixMigrationError(PhoenixException):
6
10
  pass