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.
- {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/METADATA +2 -1
- {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/RECORD +29 -22
- phoenix/db/bulk_inserter.py +135 -3
- phoenix/db/helpers.py +23 -1
- phoenix/db/insertion/constants.py +2 -0
- phoenix/db/insertion/document_annotation.py +157 -0
- phoenix/db/insertion/helpers.py +13 -0
- phoenix/db/insertion/span_annotation.py +144 -0
- phoenix/db/insertion/trace_annotation.py +144 -0
- phoenix/db/insertion/types.py +261 -0
- phoenix/experiments/types.py +3 -3
- phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
- phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
- phoenix/server/api/routers/v1/evaluations.py +90 -4
- phoenix/server/api/routers/v1/spans.py +36 -46
- phoenix/server/api/routers/v1/traces.py +36 -48
- phoenix/server/api/types/Span.py +22 -3
- phoenix/server/api/types/Trace.py +21 -4
- phoenix/server/app.py +2 -0
- phoenix/server/static/.vite/manifest.json +14 -14
- phoenix/server/static/assets/{components-kGgeFkHp.js → components-Ci5kMOk5.js} +119 -126
- phoenix/server/static/assets/{index-BctFO6S7.js → index-BQG5WVX7.js} +2 -2
- phoenix/server/static/assets/{pages-DabDCmVd.js → pages-BrevprVW.js} +289 -213
- phoenix/server/static/assets/{vendor-arizeai-B5Hti8OB.js → vendor-arizeai-DTbiPGp6.js} +1 -1
- phoenix/trace/dsl/filter.py +2 -6
- phoenix/version.py +1 -1
- {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.15.0.dist-info → arize_phoenix-4.16.1.dist-info}/licenses/IP_NOTICE +0 -0
- {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
|
phoenix/experiments/types.py
CHANGED
|
@@ -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
|