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

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (29) hide show
  1. {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/METADATA +2 -1
  2. {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/RECORD +29 -22
  3. phoenix/db/bulk_inserter.py +135 -3
  4. phoenix/db/helpers.py +23 -1
  5. phoenix/db/insertion/constants.py +2 -0
  6. phoenix/db/insertion/document_annotation.py +157 -0
  7. phoenix/db/insertion/helpers.py +13 -0
  8. phoenix/db/insertion/span_annotation.py +144 -0
  9. phoenix/db/insertion/trace_annotation.py +144 -0
  10. phoenix/db/insertion/types.py +261 -0
  11. phoenix/experiments/types.py +3 -3
  12. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  13. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  14. phoenix/server/api/routers/v1/evaluations.py +90 -4
  15. phoenix/server/api/routers/v1/spans.py +36 -46
  16. phoenix/server/api/routers/v1/traces.py +36 -48
  17. phoenix/server/api/types/Span.py +22 -3
  18. phoenix/server/api/types/Trace.py +21 -4
  19. phoenix/server/app.py +2 -0
  20. phoenix/server/static/.vite/manifest.json +14 -14
  21. phoenix/server/static/assets/{components-kGgeFkHp.js → components-Ci5kMOk5.js} +119 -126
  22. phoenix/server/static/assets/{index-BctFO6S7.js → index-BQG5WVX7.js} +2 -2
  23. phoenix/server/static/assets/{pages-DabDCmVd.js → pages-BrevprVW.js} +289 -213
  24. phoenix/server/static/assets/{vendor-arizeai-B5Hti8OB.js → vendor-arizeai-DTbiPGp6.js} +1 -1
  25. phoenix/trace/dsl/filter.py +2 -6
  26. phoenix/version.py +1 -1
  27. {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/WHEEL +0 -0
  28. {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/licenses/IP_NOTICE +0 -0
  29. {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,144 @@
1
+ from datetime import datetime
2
+ from typing import Any, List, Mapping, NamedTuple, Optional, Tuple
3
+
4
+ from sqlalchemy import Row, Select, and_, select, tuple_
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from typing_extensions import TypeAlias
7
+
8
+ from phoenix.db import models
9
+ from phoenix.db.helpers import dedup
10
+ from phoenix.db.insertion.types import (
11
+ Insertables,
12
+ Postponed,
13
+ Precursors,
14
+ QueueInserter,
15
+ Received,
16
+ )
17
+
18
+ _Name: TypeAlias = str
19
+ _SpanId: TypeAlias = str
20
+ _SpanRowId: TypeAlias = int
21
+ _AnnoRowId: TypeAlias = int
22
+
23
+ _Key: TypeAlias = Tuple[_Name, _SpanId]
24
+ _UniqueBy: TypeAlias = Tuple[_Name, _SpanRowId]
25
+ _Existing: TypeAlias = Tuple[
26
+ _SpanRowId,
27
+ _SpanId,
28
+ Optional[_AnnoRowId],
29
+ Optional[_Name],
30
+ Optional[datetime],
31
+ ]
32
+
33
+
34
+ class SpanAnnotationQueueInserter(
35
+ QueueInserter[
36
+ Precursors.SpanAnnotation,
37
+ Insertables.SpanAnnotation,
38
+ models.SpanAnnotation,
39
+ ],
40
+ table=models.SpanAnnotation,
41
+ unique_by=("name", "span_rowid"),
42
+ ):
43
+ async def _partition(
44
+ self,
45
+ session: AsyncSession,
46
+ *parcels: Received[Precursors.SpanAnnotation],
47
+ ) -> Tuple[
48
+ List[Received[Insertables.SpanAnnotation]],
49
+ List[Postponed[Precursors.SpanAnnotation]],
50
+ List[Received[Precursors.SpanAnnotation]],
51
+ ]:
52
+ to_insert: List[Received[Insertables.SpanAnnotation]] = []
53
+ to_postpone: List[Postponed[Precursors.SpanAnnotation]] = []
54
+ to_discard: List[Received[Precursors.SpanAnnotation]] = []
55
+
56
+ stmt = self._select_existing(*map(_key, parcels))
57
+ existing: List[Row[_Existing]] = [_ async for _ in await session.stream(stmt)]
58
+ existing_spans: Mapping[str, _SpanAttr] = {
59
+ e.span_id: _SpanAttr(e.span_rowid) for e in existing
60
+ }
61
+ existing_annos: Mapping[_Key, _AnnoAttr] = {
62
+ (e.name, e.span_id): _AnnoAttr(e.span_rowid, e.id, e.updated_at)
63
+ for e in existing
64
+ if e.id is not None and e.name is not None and e.updated_at is not None
65
+ }
66
+
67
+ for p in parcels:
68
+ if (anno := existing_annos.get(_key(p))) is not None:
69
+ if p.received_at <= anno.updated_at:
70
+ to_discard.append(p)
71
+ else:
72
+ to_insert.append(
73
+ Received(
74
+ received_at=p.received_at,
75
+ item=p.item.as_insertable(
76
+ span_rowid=anno.span_rowid,
77
+ id_=anno.id_,
78
+ ),
79
+ )
80
+ )
81
+ elif (span := existing_spans.get(p.item.span_id)) is not None:
82
+ to_insert.append(
83
+ Received(
84
+ received_at=p.received_at,
85
+ item=p.item.as_insertable(
86
+ span_rowid=span.span_rowid,
87
+ ),
88
+ )
89
+ )
90
+ elif isinstance(p, Postponed):
91
+ if p.retries_left > 1:
92
+ to_postpone.append(p.postpone(p.retries_left - 1))
93
+ else:
94
+ to_discard.append(p)
95
+ elif isinstance(p, Received):
96
+ to_postpone.append(p.postpone(self._retry_allowance))
97
+ else:
98
+ to_discard.append(p)
99
+
100
+ assert len(to_insert) + len(to_postpone) + len(to_discard) == len(parcels)
101
+ to_insert = dedup(sorted(to_insert, key=_time, reverse=True), _unique_by)[::-1]
102
+ return to_insert, to_postpone, to_discard
103
+
104
+ def _select_existing(self, *keys: _Key) -> Select[_Existing]:
105
+ anno = self.table
106
+ span = (
107
+ select(models.Span.id, models.Span.span_id)
108
+ .where(models.Span.span_id.in_({span_id for _, span_id in keys}))
109
+ .cte()
110
+ )
111
+ onclause = and_(
112
+ span.c.id == anno.span_rowid,
113
+ anno.name.in_({name for name, _ in keys}),
114
+ tuple_(anno.name, span.c.span_id).in_(keys),
115
+ )
116
+ return select(
117
+ span.c.id.label("span_rowid"),
118
+ span.c.span_id,
119
+ anno.id,
120
+ anno.name,
121
+ anno.updated_at,
122
+ ).outerjoin_from(span, anno, onclause)
123
+
124
+
125
+ class _SpanAttr(NamedTuple):
126
+ span_rowid: _SpanRowId
127
+
128
+
129
+ class _AnnoAttr(NamedTuple):
130
+ span_rowid: _SpanRowId
131
+ id_: _AnnoRowId
132
+ updated_at: datetime
133
+
134
+
135
+ def _key(p: Received[Precursors.SpanAnnotation]) -> _Key:
136
+ return p.item.obj.name, p.item.span_id
137
+
138
+
139
+ def _unique_by(p: Received[Insertables.SpanAnnotation]) -> _UniqueBy:
140
+ return p.item.obj.name, p.item.span_rowid
141
+
142
+
143
+ def _time(p: Received[Any]) -> datetime:
144
+ return p.received_at
@@ -0,0 +1,144 @@
1
+ from datetime import datetime
2
+ from typing import Any, List, Mapping, NamedTuple, Optional, Tuple
3
+
4
+ from sqlalchemy import Row, Select, and_, select, tuple_
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from typing_extensions import TypeAlias
7
+
8
+ from phoenix.db import models
9
+ from phoenix.db.helpers import dedup
10
+ from phoenix.db.insertion.types import (
11
+ Insertables,
12
+ Postponed,
13
+ Precursors,
14
+ QueueInserter,
15
+ Received,
16
+ )
17
+
18
+ _Name: TypeAlias = str
19
+ _TraceId: TypeAlias = str
20
+ _TraceRowId: TypeAlias = int
21
+ _AnnoRowId: TypeAlias = int
22
+
23
+ _Key: TypeAlias = Tuple[_Name, _TraceId]
24
+ _UniqueBy: TypeAlias = Tuple[_Name, _TraceRowId]
25
+ _Existing: TypeAlias = Tuple[
26
+ _TraceRowId,
27
+ _TraceId,
28
+ Optional[_AnnoRowId],
29
+ Optional[_Name],
30
+ Optional[datetime],
31
+ ]
32
+
33
+
34
+ class TraceAnnotationQueueInserter(
35
+ QueueInserter[
36
+ Precursors.TraceAnnotation,
37
+ Insertables.TraceAnnotation,
38
+ models.TraceAnnotation,
39
+ ],
40
+ table=models.TraceAnnotation,
41
+ unique_by=("name", "trace_rowid"),
42
+ ):
43
+ async def _partition(
44
+ self,
45
+ session: AsyncSession,
46
+ *parcels: Received[Precursors.TraceAnnotation],
47
+ ) -> Tuple[
48
+ List[Received[Insertables.TraceAnnotation]],
49
+ List[Postponed[Precursors.TraceAnnotation]],
50
+ List[Received[Precursors.TraceAnnotation]],
51
+ ]:
52
+ to_insert: List[Received[Insertables.TraceAnnotation]] = []
53
+ to_postpone: List[Postponed[Precursors.TraceAnnotation]] = []
54
+ to_discard: List[Received[Precursors.TraceAnnotation]] = []
55
+
56
+ stmt = self._select_existing(*map(_key, parcels))
57
+ existing: List[Row[_Existing]] = [_ async for _ in await session.stream(stmt)]
58
+ existing_traces: Mapping[str, _TraceAttr] = {
59
+ e.trace_id: _TraceAttr(e.trace_rowid) for e in existing
60
+ }
61
+ existing_annos: Mapping[_Key, _AnnoAttr] = {
62
+ (e.name, e.trace_id): _AnnoAttr(e.trace_rowid, e.id, e.updated_at)
63
+ for e in existing
64
+ if e.id is not None and e.name is not None and e.updated_at is not None
65
+ }
66
+
67
+ for p in parcels:
68
+ if (anno := existing_annos.get(_key(p))) is not None:
69
+ if p.received_at <= anno.updated_at:
70
+ to_discard.append(p)
71
+ else:
72
+ to_insert.append(
73
+ Received(
74
+ received_at=p.received_at,
75
+ item=p.item.as_insertable(
76
+ trace_rowid=anno.trace_rowid,
77
+ id_=anno.id_,
78
+ ),
79
+ )
80
+ )
81
+ elif (trace := existing_traces.get(p.item.trace_id)) is not None:
82
+ to_insert.append(
83
+ Received(
84
+ received_at=p.received_at,
85
+ item=p.item.as_insertable(
86
+ trace_rowid=trace.trace_rowid,
87
+ ),
88
+ )
89
+ )
90
+ elif isinstance(p, Postponed):
91
+ if p.retries_left > 1:
92
+ to_postpone.append(p.postpone(p.retries_left - 1))
93
+ else:
94
+ to_discard.append(p)
95
+ elif isinstance(p, Received):
96
+ to_postpone.append(p.postpone(self._retry_allowance))
97
+ else:
98
+ to_discard.append(p)
99
+
100
+ assert len(to_insert) + len(to_postpone) + len(to_discard) == len(parcels)
101
+ to_insert = dedup(sorted(to_insert, key=_time, reverse=True), _unique_by)[::-1]
102
+ return to_insert, to_postpone, to_discard
103
+
104
+ def _select_existing(self, *keys: _Key) -> Select[_Existing]:
105
+ anno = self.table
106
+ trace = (
107
+ select(models.Trace.id, models.Trace.trace_id)
108
+ .where(models.Trace.trace_id.in_({trace_id for _, trace_id in keys}))
109
+ .cte()
110
+ )
111
+ onclause = and_(
112
+ trace.c.id == anno.trace_rowid,
113
+ anno.name.in_({name for name, _ in keys}),
114
+ tuple_(anno.name, trace.c.trace_id).in_(keys),
115
+ )
116
+ return select(
117
+ trace.c.id.label("trace_rowid"),
118
+ trace.c.trace_id,
119
+ anno.id,
120
+ anno.name,
121
+ anno.updated_at,
122
+ ).outerjoin_from(trace, anno, onclause)
123
+
124
+
125
+ class _TraceAttr(NamedTuple):
126
+ trace_rowid: _TraceRowId
127
+
128
+
129
+ class _AnnoAttr(NamedTuple):
130
+ trace_rowid: _TraceRowId
131
+ id_: _AnnoRowId
132
+ updated_at: datetime
133
+
134
+
135
+ def _key(p: Received[Precursors.TraceAnnotation]) -> _Key:
136
+ return p.item.obj.name, p.item.trace_id
137
+
138
+
139
+ def _unique_by(p: Received[Insertables.TraceAnnotation]) -> _UniqueBy:
140
+ return p.item.obj.name, p.item.trace_rowid
141
+
142
+
143
+ def _time(p: Received[Any]) -> datetime:
144
+ return p.received_at
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from abc import ABC, abstractmethod
6
+ from copy import copy
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import (
10
+ Any,
11
+ Generic,
12
+ List,
13
+ Mapping,
14
+ Optional,
15
+ Protocol,
16
+ Sequence,
17
+ Tuple,
18
+ Type,
19
+ TypeVar,
20
+ cast,
21
+ )
22
+
23
+ from sqlalchemy.ext.asyncio import AsyncSession
24
+ from sqlalchemy.sql.dml import ReturningInsert
25
+
26
+ from phoenix.db import models
27
+ from phoenix.db.insertion.constants import DEFAULT_RETRY_ALLOWANCE, DEFAULT_RETRY_DELAY_SEC
28
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
29
+ from phoenix.server.types import DbSessionFactory
30
+
31
+ logger = logging.getLogger("__name__")
32
+
33
+
34
+ class Insertable(Protocol):
35
+ @property
36
+ def row(self) -> models.Base: ...
37
+
38
+
39
+ _AnyT = TypeVar("_AnyT")
40
+ _PrecursorT = TypeVar("_PrecursorT")
41
+ _InsertableT = TypeVar("_InsertableT", bound=Insertable)
42
+ _RowT = TypeVar("_RowT", bound=models.Base)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class Received(Generic[_AnyT]):
47
+ item: _AnyT
48
+ received_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
49
+
50
+ def postpone(self, retries_left: int = DEFAULT_RETRY_ALLOWANCE) -> Postponed[_AnyT]:
51
+ return Postponed(item=self.item, received_at=self.received_at, retries_left=retries_left)
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Postponed(Received[_AnyT]):
56
+ retries_left: int = field(default=DEFAULT_RETRY_ALLOWANCE)
57
+
58
+
59
+ class QueueInserter(ABC, Generic[_PrecursorT, _InsertableT, _RowT]):
60
+ table: Type[_RowT]
61
+ unique_by: Sequence[str]
62
+
63
+ def __init_subclass__(
64
+ cls,
65
+ table: Type[_RowT],
66
+ unique_by: Sequence[str],
67
+ ) -> None:
68
+ cls.table = table
69
+ cls.unique_by = unique_by
70
+
71
+ def __init__(
72
+ self,
73
+ db: DbSessionFactory,
74
+ retry_delay_sec: float = DEFAULT_RETRY_DELAY_SEC,
75
+ retry_allowance: int = DEFAULT_RETRY_ALLOWANCE,
76
+ ) -> None:
77
+ self._queue: List[Received[_PrecursorT]] = []
78
+ self._db = db
79
+ self._retry_delay_sec = retry_delay_sec
80
+ self._retry_allowance = retry_allowance
81
+
82
+ @property
83
+ def empty(self) -> bool:
84
+ return not bool(self._queue)
85
+
86
+ async def enqueue(self, *items: _PrecursorT) -> None:
87
+ self._queue.extend([Received(item) for item in items])
88
+
89
+ @abstractmethod
90
+ async def _partition(
91
+ self,
92
+ session: AsyncSession,
93
+ *parcels: Received[_PrecursorT],
94
+ ) -> Tuple[
95
+ List[Received[_InsertableT]],
96
+ List[Postponed[_PrecursorT]],
97
+ List[Received[_PrecursorT]],
98
+ ]: ...
99
+
100
+ async def insert(self) -> Tuple[Type[_RowT], List[int]]:
101
+ if not self._queue:
102
+ return self.table, []
103
+ parcels = self._queue
104
+ self._queue = []
105
+ inserted_ids: List[int] = []
106
+ async with self._db() as session:
107
+ to_insert, to_postpone, _ = await self._partition(session, *parcels)
108
+ if to_insert:
109
+ inserted_ids, to_retry, _ = await self._insert(session, *to_insert)
110
+ to_postpone.extend(to_retry)
111
+ if to_postpone:
112
+ loop = asyncio.get_running_loop()
113
+ loop.call_later(self._retry_delay_sec, self._queue.extend, to_postpone)
114
+ return self.table, inserted_ids
115
+
116
+ def _stmt(self, *records: Mapping[str, Any]) -> ReturningInsert[Tuple[int]]:
117
+ pk = next(c for c in self.table.__table__.c if c.primary_key)
118
+ return insert_on_conflict(
119
+ *records,
120
+ table=self.table,
121
+ unique_by=self.unique_by,
122
+ dialect=self._db.dialect,
123
+ ).returning(pk)
124
+
125
+ async def _insert(
126
+ self,
127
+ session: AsyncSession,
128
+ *insertions: Received[_InsertableT],
129
+ ) -> Tuple[List[int], List[Postponed[_PrecursorT]], List[Received[_InsertableT]]]:
130
+ records = [dict(as_kv(ins.item.row)) for ins in insertions]
131
+ inserted_ids: List[int] = []
132
+ to_retry: List[Postponed[_PrecursorT]] = []
133
+ failures: List[Received[_InsertableT]] = []
134
+ stmt = self._stmt(*records)
135
+ try:
136
+ async with session.begin_nested():
137
+ ids = [id_ async for id_ in await session.stream_scalars(stmt)]
138
+ inserted_ids.extend(ids)
139
+ except BaseException:
140
+ logger.exception(
141
+ f"Failed to bulk insert for {self.table.__name__}. "
142
+ f"Will try to insert ({len(records)} records) individually instead."
143
+ )
144
+ for i, record in enumerate(records):
145
+ stmt = self._stmt(record)
146
+ try:
147
+ async with session.begin_nested():
148
+ ids = [id_ async for id_ in await session.stream_scalars(stmt)]
149
+ inserted_ids.extend(ids)
150
+ except BaseException:
151
+ logger.exception(f"Failed to insert for {self.table.__name__}.")
152
+ p = insertions[i]
153
+ if isinstance(p, Postponed) and p.retries_left == 1:
154
+ failures.append(p)
155
+ else:
156
+ to_retry.append(
157
+ Postponed(
158
+ item=cast(_PrecursorT, p.item),
159
+ received_at=p.received_at,
160
+ retries_left=(p.retries_left - 1)
161
+ if isinstance(p, Postponed)
162
+ else self._retry_allowance,
163
+ )
164
+ )
165
+ return inserted_ids, to_retry, failures
166
+
167
+
168
+ class Precursors(ABC):
169
+ @dataclass(frozen=True)
170
+ class SpanAnnotation:
171
+ span_id: str
172
+ obj: models.SpanAnnotation
173
+
174
+ def as_insertable(
175
+ self,
176
+ span_rowid: int,
177
+ id_: Optional[int] = None,
178
+ ) -> Insertables.SpanAnnotation:
179
+ return Insertables.SpanAnnotation(
180
+ span_id=self.span_id,
181
+ obj=self.obj,
182
+ span_rowid=span_rowid,
183
+ id_=id_,
184
+ )
185
+
186
+ @dataclass(frozen=True)
187
+ class TraceAnnotation:
188
+ trace_id: str
189
+ obj: models.TraceAnnotation
190
+
191
+ def as_insertable(
192
+ self,
193
+ trace_rowid: int,
194
+ id_: Optional[int] = None,
195
+ ) -> Insertables.TraceAnnotation:
196
+ return Insertables.TraceAnnotation(
197
+ trace_id=self.trace_id,
198
+ obj=self.obj,
199
+ trace_rowid=trace_rowid,
200
+ id_=id_,
201
+ )
202
+
203
+ @dataclass(frozen=True)
204
+ class DocumentAnnotation:
205
+ span_id: str
206
+ document_position: int
207
+ obj: models.DocumentAnnotation
208
+
209
+ def as_insertable(
210
+ self,
211
+ span_rowid: int,
212
+ id_: Optional[int] = None,
213
+ ) -> Insertables.DocumentAnnotation:
214
+ return Insertables.DocumentAnnotation(
215
+ span_id=self.span_id,
216
+ document_position=self.document_position,
217
+ obj=self.obj,
218
+ span_rowid=span_rowid,
219
+ id_=id_,
220
+ )
221
+
222
+
223
+ class Insertables(ABC):
224
+ @dataclass(frozen=True)
225
+ class SpanAnnotation(Precursors.SpanAnnotation):
226
+ span_rowid: int
227
+ id_: Optional[int] = None
228
+
229
+ @property
230
+ def row(self) -> models.SpanAnnotation:
231
+ obj = copy(self.obj)
232
+ obj.span_rowid = self.span_rowid
233
+ if self.id_ is not None:
234
+ obj.id = self.id_
235
+ return obj
236
+
237
+ @dataclass(frozen=True)
238
+ class TraceAnnotation(Precursors.TraceAnnotation):
239
+ trace_rowid: int
240
+ id_: Optional[int] = None
241
+
242
+ @property
243
+ def row(self) -> models.TraceAnnotation:
244
+ obj = copy(self.obj)
245
+ obj.trace_rowid = self.trace_rowid
246
+ if self.id_ is not None:
247
+ obj.id = self.id_
248
+ return obj
249
+
250
+ @dataclass(frozen=True)
251
+ class DocumentAnnotation(Precursors.DocumentAnnotation):
252
+ span_rowid: int
253
+ id_: Optional[int] = None
254
+
255
+ @property
256
+ def row(self) -> models.DocumentAnnotation:
257
+ obj = copy(self.obj)
258
+ obj.span_rowid = self.span_rowid
259
+ if self.id_ is not None:
260
+ obj.id = self.id_
261
+ return obj
@@ -226,7 +226,7 @@ class ExperimentRun:
226
226
 
227
227
  def __post_init__(self) -> None:
228
228
  if bool(self.output) == bool(self.error):
229
- ValueError("Must specify exactly one of experiment_run_output or error")
229
+ raise ValueError("Must specify exactly one of experiment_run_output or error")
230
230
 
231
231
 
232
232
  @dataclass(frozen=True)
@@ -249,7 +249,7 @@ class EvaluationResult:
249
249
 
250
250
  def __post_init__(self) -> None:
251
251
  if self.score is None and not self.label:
252
- ValueError("Must specify score or label, or both")
252
+ raise ValueError("Must specify score or label, or both")
253
253
  if self.score is None and not self.label:
254
254
  object.__setattr__(self, "score", 0)
255
255
  for k in ("label", "explanation"):
@@ -285,7 +285,7 @@ class ExperimentEvaluationRun:
285
285
 
286
286
  def __post_init__(self) -> None:
287
287
  if bool(self.result) == bool(self.error):
288
- ValueError("Must specify either result or error")
288
+ raise ValueError("Must specify either result or error")
289
289
 
290
290
 
291
291
  ExperimentTask: TypeAlias = Union[
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+
3
+ import strawberry
4
+
5
+ from phoenix.server.api.types.SortDir import SortDir
6
+
7
+
8
+ @strawberry.enum
9
+ class SpanAnnotationColumn(Enum):
10
+ createdAt = "created_at"
11
+ name = "name"
12
+
13
+
14
+ @strawberry.input(description="The sort key and direction for SpanAnnotation connections")
15
+ class SpanAnnotationSort:
16
+ col: SpanAnnotationColumn
17
+ dir: SortDir
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+
3
+ import strawberry
4
+
5
+ from phoenix.server.api.types.SortDir import SortDir
6
+
7
+
8
+ @strawberry.enum
9
+ class TraceAnnotationColumn(Enum):
10
+ createdAt = "created_at"
11
+ name = "name"
12
+
13
+
14
+ @strawberry.input(description="The sort key and direction for TraceAnnotation connections")
15
+ class TraceAnnotationSort:
16
+ col: TraceAnnotationColumn
17
+ dir: SortDir