arize-phoenix 11.8.0__py3-none-any.whl → 11.10.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-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/METADATA +1 -1
- {arize_phoenix-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/RECORD +26 -25
- phoenix/db/insertion/span.py +12 -10
- phoenix/db/insertion/types.py +9 -2
- phoenix/server/api/input_types/CreateProjectInput.py +27 -0
- phoenix/server/api/mutations/project_mutations.py +37 -1
- phoenix/server/api/mutations/trace_mutations.py +45 -1
- phoenix/server/api/types/Project.py +589 -11
- phoenix/server/cost_tracking/model_cost_manifest.json +85 -0
- phoenix/server/dml_event.py +4 -0
- phoenix/server/static/.vite/manifest.json +41 -41
- phoenix/server/static/assets/{components-5M9nebi4.js → components-XAeml0-1.js} +400 -326
- phoenix/server/static/assets/{index-OU2WTnGN.js → index-D7EtHUpz.js} +37 -9
- phoenix/server/static/assets/{pages-DF8rqxJ4.js → pages-CPfaxiKa.js} +642 -437
- phoenix/server/static/assets/vendor-CqDb5u4o.css +1 -0
- phoenix/server/static/assets/vendor-DhvamIr8.js +939 -0
- phoenix/server/static/assets/vendor-arizeai-4fVwwnrI.js +168 -0
- phoenix/server/static/assets/{vendor-codemirror-vlcH1_iR.js → vendor-codemirror-DRfFHb57.js} +1 -1
- phoenix/server/static/assets/vendor-recharts-w6bSawXG.js +37 -0
- phoenix/server/static/assets/{vendor-shiki-BsknB7bv.js → vendor-shiki-CplrhwOk.js} +1 -1
- phoenix/server/templates/index.html +3 -4
- phoenix/version.py +1 -1
- phoenix/server/static/assets/vendor-Bl7CyFDw.js +0 -911
- phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-B_viEUUA.js +0 -180
- phoenix/server/static/assets/vendor-recharts-C9cQu72o.js +0 -59
- {arize_phoenix-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-11.8.0.dist-info → arize_phoenix-11.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,10 +7,11 @@ from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, Optional, c
|
|
|
7
7
|
import strawberry
|
|
8
8
|
from aioitertools.itertools import groupby, islice
|
|
9
9
|
from openinference.semconv.trace import SpanAttributes
|
|
10
|
-
from sqlalchemy import and_, desc, distinct, exists, func, or_, select
|
|
10
|
+
from sqlalchemy import and_, case, desc, distinct, exists, func, or_, select
|
|
11
11
|
from sqlalchemy.dialects import postgresql, sqlite
|
|
12
12
|
from sqlalchemy.sql.elements import ColumnElement
|
|
13
13
|
from sqlalchemy.sql.expression import tuple_
|
|
14
|
+
from sqlalchemy.sql.functions import percentile_cont
|
|
14
15
|
from strawberry import ID, UNSET, Private, lazy
|
|
15
16
|
from strawberry.relay import Connection, Edge, Node, NodeID, PageInfo
|
|
16
17
|
from strawberry.types import Info
|
|
@@ -718,6 +719,7 @@ class Project(Node):
|
|
|
718
719
|
info: Info[Context, None],
|
|
719
720
|
time_range: TimeRange,
|
|
720
721
|
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
722
|
+
filter_condition: Optional[str] = UNSET,
|
|
721
723
|
) -> SpanCountTimeSeries:
|
|
722
724
|
if time_range.start is None:
|
|
723
725
|
raise BadRequest("Start time is required")
|
|
@@ -741,7 +743,17 @@ class Project(Node):
|
|
|
741
743
|
field = "year"
|
|
742
744
|
bucket = date_trunc(dialect, field, models.Span.start_time, utc_offset_minutes)
|
|
743
745
|
stmt = (
|
|
744
|
-
select(
|
|
746
|
+
select(
|
|
747
|
+
bucket,
|
|
748
|
+
func.count(models.Span.id).label("total_count"),
|
|
749
|
+
func.sum(case((models.Span.status_code == "OK", 1), else_=0)).label("ok_count"),
|
|
750
|
+
func.sum(case((models.Span.status_code == "ERROR", 1), else_=0)).label(
|
|
751
|
+
"error_count"
|
|
752
|
+
),
|
|
753
|
+
func.sum(case((models.Span.status_code == "UNSET", 1), else_=0)).label(
|
|
754
|
+
"unset_count"
|
|
755
|
+
),
|
|
756
|
+
)
|
|
745
757
|
.join_from(models.Span, models.Trace)
|
|
746
758
|
.where(models.Trace.project_rowid == self.project_rowid)
|
|
747
759
|
.group_by(bucket)
|
|
@@ -751,21 +763,31 @@ class Project(Node):
|
|
|
751
763
|
stmt = stmt.where(time_range.start <= models.Span.start_time)
|
|
752
764
|
if time_range.end:
|
|
753
765
|
stmt = stmt.where(models.Span.start_time < time_range.end)
|
|
766
|
+
if filter_condition:
|
|
767
|
+
span_filter = SpanFilter(condition=filter_condition)
|
|
768
|
+
stmt = span_filter(stmt)
|
|
754
769
|
|
|
755
770
|
data = {}
|
|
756
771
|
async with info.context.db() as session:
|
|
757
|
-
async for t,
|
|
772
|
+
async for t, total_count, ok_count, error_count, unset_count in await session.stream(
|
|
773
|
+
stmt
|
|
774
|
+
):
|
|
758
775
|
timestamp = _as_datetime(t)
|
|
759
|
-
data[timestamp] =
|
|
776
|
+
data[timestamp] = SpanCountTimeSeriesDataPoint(
|
|
777
|
+
timestamp=timestamp,
|
|
778
|
+
ok_count=ok_count,
|
|
779
|
+
error_count=error_count,
|
|
780
|
+
unset_count=unset_count,
|
|
781
|
+
total_count=total_count,
|
|
782
|
+
)
|
|
760
783
|
|
|
761
784
|
data_timestamps: list[datetime] = [data_point.timestamp for data_point in data.values()]
|
|
762
785
|
min_time = min([*data_timestamps, time_range.start])
|
|
763
786
|
max_time = max(
|
|
764
787
|
[
|
|
765
788
|
*data_timestamps,
|
|
766
|
-
*([time_range.end] if time_range.end else []),
|
|
789
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
767
790
|
],
|
|
768
|
-
default=datetime.now(timezone.utc),
|
|
769
791
|
)
|
|
770
792
|
for timestamp in get_timestamp_range(
|
|
771
793
|
start_time=min_time,
|
|
@@ -774,7 +796,7 @@ class Project(Node):
|
|
|
774
796
|
utc_offset_minutes=utc_offset_minutes,
|
|
775
797
|
):
|
|
776
798
|
if timestamp not in data:
|
|
777
|
-
data[timestamp] =
|
|
799
|
+
data[timestamp] = SpanCountTimeSeriesDataPoint(timestamp=timestamp)
|
|
778
800
|
return SpanCountTimeSeries(data=sorted(data.values(), key=lambda x: x.timestamp))
|
|
779
801
|
|
|
780
802
|
@strawberry.field
|
|
@@ -827,9 +849,8 @@ class Project(Node):
|
|
|
827
849
|
max_time = max(
|
|
828
850
|
[
|
|
829
851
|
*data_timestamps,
|
|
830
|
-
*([time_range.end] if time_range.end else []),
|
|
852
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
831
853
|
],
|
|
832
|
-
default=datetime.now(timezone.utc),
|
|
833
854
|
)
|
|
834
855
|
for timestamp in get_timestamp_range(
|
|
835
856
|
start_time=min_time,
|
|
@@ -841,10 +862,483 @@ class Project(Node):
|
|
|
841
862
|
data[timestamp] = TimeSeriesDataPoint(timestamp=timestamp)
|
|
842
863
|
return TraceCountTimeSeries(data=sorted(data.values(), key=lambda x: x.timestamp))
|
|
843
864
|
|
|
865
|
+
@strawberry.field
|
|
866
|
+
async def trace_count_by_status_time_series(
|
|
867
|
+
self,
|
|
868
|
+
info: Info[Context, None],
|
|
869
|
+
time_range: TimeRange,
|
|
870
|
+
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
871
|
+
) -> TraceCountByStatusTimeSeries:
|
|
872
|
+
if time_range.start is None:
|
|
873
|
+
raise BadRequest("Start time is required")
|
|
874
|
+
|
|
875
|
+
dialect = info.context.db.dialect
|
|
876
|
+
utc_offset_minutes = 0
|
|
877
|
+
field: Literal["minute", "hour", "day", "week", "month", "year"] = "hour"
|
|
878
|
+
if time_bin_config:
|
|
879
|
+
utc_offset_minutes = time_bin_config.utc_offset_minutes
|
|
880
|
+
if time_bin_config.scale is TimeBinScale.MINUTE:
|
|
881
|
+
field = "minute"
|
|
882
|
+
elif time_bin_config.scale is TimeBinScale.HOUR:
|
|
883
|
+
field = "hour"
|
|
884
|
+
elif time_bin_config.scale is TimeBinScale.DAY:
|
|
885
|
+
field = "day"
|
|
886
|
+
elif time_bin_config.scale is TimeBinScale.WEEK:
|
|
887
|
+
field = "week"
|
|
888
|
+
elif time_bin_config.scale is TimeBinScale.MONTH:
|
|
889
|
+
field = "month"
|
|
890
|
+
elif time_bin_config.scale is TimeBinScale.YEAR:
|
|
891
|
+
field = "year"
|
|
892
|
+
bucket = date_trunc(dialect, field, models.Trace.start_time, utc_offset_minutes)
|
|
893
|
+
trace_error_status_counts = (
|
|
894
|
+
select(
|
|
895
|
+
models.Span.trace_rowid,
|
|
896
|
+
)
|
|
897
|
+
.where(models.Span.parent_id.is_(None))
|
|
898
|
+
.group_by(models.Span.trace_rowid)
|
|
899
|
+
.having(func.max(models.Span.cumulative_error_count) > 0)
|
|
900
|
+
).subquery()
|
|
901
|
+
stmt = (
|
|
902
|
+
select(
|
|
903
|
+
bucket,
|
|
904
|
+
func.count(models.Trace.id).label("total_count"),
|
|
905
|
+
func.coalesce(func.count(trace_error_status_counts.c.trace_rowid), 0).label(
|
|
906
|
+
"error_count"
|
|
907
|
+
),
|
|
908
|
+
)
|
|
909
|
+
.join_from(
|
|
910
|
+
models.Trace,
|
|
911
|
+
trace_error_status_counts,
|
|
912
|
+
onclause=trace_error_status_counts.c.trace_rowid == models.Trace.id,
|
|
913
|
+
isouter=True,
|
|
914
|
+
)
|
|
915
|
+
.where(models.Trace.project_rowid == self.project_rowid)
|
|
916
|
+
.group_by(bucket)
|
|
917
|
+
.order_by(bucket)
|
|
918
|
+
)
|
|
919
|
+
if time_range:
|
|
920
|
+
if time_range.start:
|
|
921
|
+
stmt = stmt.where(time_range.start <= models.Trace.start_time)
|
|
922
|
+
if time_range.end:
|
|
923
|
+
stmt = stmt.where(models.Trace.start_time < time_range.end)
|
|
924
|
+
data: dict[datetime, TraceCountByStatusTimeSeriesDataPoint] = {}
|
|
925
|
+
async with info.context.db() as session:
|
|
926
|
+
async for t, total_count, error_count in await session.stream(stmt):
|
|
927
|
+
timestamp = _as_datetime(t)
|
|
928
|
+
data[timestamp] = TraceCountByStatusTimeSeriesDataPoint(
|
|
929
|
+
timestamp=timestamp,
|
|
930
|
+
ok_count=total_count - error_count,
|
|
931
|
+
error_count=error_count,
|
|
932
|
+
total_count=total_count,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
data_timestamps: list[datetime] = [data_point.timestamp for data_point in data.values()]
|
|
936
|
+
min_time = min([*data_timestamps, time_range.start])
|
|
937
|
+
max_time = max(
|
|
938
|
+
[
|
|
939
|
+
*data_timestamps,
|
|
940
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
941
|
+
],
|
|
942
|
+
)
|
|
943
|
+
for timestamp in get_timestamp_range(
|
|
944
|
+
start_time=min_time,
|
|
945
|
+
end_time=max_time,
|
|
946
|
+
stride=field,
|
|
947
|
+
utc_offset_minutes=utc_offset_minutes,
|
|
948
|
+
):
|
|
949
|
+
if timestamp not in data:
|
|
950
|
+
data[timestamp] = TraceCountByStatusTimeSeriesDataPoint(
|
|
951
|
+
timestamp=timestamp,
|
|
952
|
+
ok_count=0,
|
|
953
|
+
error_count=0,
|
|
954
|
+
total_count=0,
|
|
955
|
+
)
|
|
956
|
+
return TraceCountByStatusTimeSeries(data=sorted(data.values(), key=lambda x: x.timestamp))
|
|
957
|
+
|
|
958
|
+
@strawberry.field
|
|
959
|
+
async def trace_latency_ms_percentile_time_series(
|
|
960
|
+
self,
|
|
961
|
+
info: Info[Context, None],
|
|
962
|
+
time_range: TimeRange,
|
|
963
|
+
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
964
|
+
) -> TraceLatencyPercentileTimeSeries:
|
|
965
|
+
if time_range.start is None:
|
|
966
|
+
raise BadRequest("Start time is required")
|
|
967
|
+
|
|
968
|
+
dialect = info.context.db.dialect
|
|
969
|
+
utc_offset_minutes = 0
|
|
970
|
+
field: Literal["minute", "hour", "day", "week", "month", "year"] = "hour"
|
|
971
|
+
if time_bin_config:
|
|
972
|
+
utc_offset_minutes = time_bin_config.utc_offset_minutes
|
|
973
|
+
if time_bin_config.scale is TimeBinScale.MINUTE:
|
|
974
|
+
field = "minute"
|
|
975
|
+
elif time_bin_config.scale is TimeBinScale.HOUR:
|
|
976
|
+
field = "hour"
|
|
977
|
+
elif time_bin_config.scale is TimeBinScale.DAY:
|
|
978
|
+
field = "day"
|
|
979
|
+
elif time_bin_config.scale is TimeBinScale.WEEK:
|
|
980
|
+
field = "week"
|
|
981
|
+
elif time_bin_config.scale is TimeBinScale.MONTH:
|
|
982
|
+
field = "month"
|
|
983
|
+
elif time_bin_config.scale is TimeBinScale.YEAR:
|
|
984
|
+
field = "year"
|
|
985
|
+
bucket = date_trunc(dialect, field, models.Trace.start_time, utc_offset_minutes)
|
|
986
|
+
|
|
987
|
+
stmt = select(bucket).where(models.Trace.project_rowid == self.project_rowid)
|
|
988
|
+
if time_range.start:
|
|
989
|
+
stmt = stmt.where(time_range.start <= models.Trace.start_time)
|
|
990
|
+
if time_range.end:
|
|
991
|
+
stmt = stmt.where(models.Trace.start_time < time_range.end)
|
|
992
|
+
|
|
993
|
+
if dialect is SupportedSQLDialect.POSTGRESQL:
|
|
994
|
+
stmt = stmt.add_columns(
|
|
995
|
+
percentile_cont(0.50).within_group(models.Trace.latency_ms.asc()).label("p50"),
|
|
996
|
+
percentile_cont(0.75).within_group(models.Trace.latency_ms.asc()).label("p75"),
|
|
997
|
+
percentile_cont(0.90).within_group(models.Trace.latency_ms.asc()).label("p90"),
|
|
998
|
+
percentile_cont(0.95).within_group(models.Trace.latency_ms.asc()).label("p95"),
|
|
999
|
+
percentile_cont(0.99).within_group(models.Trace.latency_ms.asc()).label("p99"),
|
|
1000
|
+
percentile_cont(0.999).within_group(models.Trace.latency_ms.asc()).label("p999"),
|
|
1001
|
+
func.max(models.Trace.latency_ms).label("max"),
|
|
1002
|
+
)
|
|
1003
|
+
elif dialect is SupportedSQLDialect.SQLITE:
|
|
1004
|
+
stmt = stmt.add_columns(
|
|
1005
|
+
func.percentile(models.Trace.latency_ms, 50).label("p50"),
|
|
1006
|
+
func.percentile(models.Trace.latency_ms, 75).label("p75"),
|
|
1007
|
+
func.percentile(models.Trace.latency_ms, 90).label("p90"),
|
|
1008
|
+
func.percentile(models.Trace.latency_ms, 95).label("p95"),
|
|
1009
|
+
func.percentile(models.Trace.latency_ms, 99).label("p99"),
|
|
1010
|
+
func.percentile(models.Trace.latency_ms, 99.9).label("p999"),
|
|
1011
|
+
func.max(models.Trace.latency_ms).label("max"),
|
|
1012
|
+
)
|
|
1013
|
+
else:
|
|
1014
|
+
assert_never(dialect)
|
|
1015
|
+
|
|
1016
|
+
stmt = stmt.group_by(bucket).order_by(bucket)
|
|
1017
|
+
|
|
1018
|
+
data: dict[datetime, TraceLatencyMsPercentileTimeSeriesDataPoint] = {}
|
|
1019
|
+
async with info.context.db() as session:
|
|
1020
|
+
async for (
|
|
1021
|
+
bucket_time,
|
|
1022
|
+
p50,
|
|
1023
|
+
p75,
|
|
1024
|
+
p90,
|
|
1025
|
+
p95,
|
|
1026
|
+
p99,
|
|
1027
|
+
p999,
|
|
1028
|
+
max_latency,
|
|
1029
|
+
) in await session.stream(stmt):
|
|
1030
|
+
timestamp = _as_datetime(bucket_time)
|
|
1031
|
+
data[timestamp] = TraceLatencyMsPercentileTimeSeriesDataPoint(
|
|
1032
|
+
timestamp=timestamp,
|
|
1033
|
+
p50=p50,
|
|
1034
|
+
p75=p75,
|
|
1035
|
+
p90=p90,
|
|
1036
|
+
p95=p95,
|
|
1037
|
+
p99=p99,
|
|
1038
|
+
p999=p999,
|
|
1039
|
+
max=max_latency,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
data_timestamps: list[datetime] = [data_point.timestamp for data_point in data.values()]
|
|
1043
|
+
min_time = min([*data_timestamps, time_range.start])
|
|
1044
|
+
max_time = max(
|
|
1045
|
+
[
|
|
1046
|
+
*data_timestamps,
|
|
1047
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
1048
|
+
],
|
|
1049
|
+
)
|
|
1050
|
+
for timestamp in get_timestamp_range(
|
|
1051
|
+
start_time=min_time,
|
|
1052
|
+
end_time=max_time,
|
|
1053
|
+
stride=field,
|
|
1054
|
+
utc_offset_minutes=utc_offset_minutes,
|
|
1055
|
+
):
|
|
1056
|
+
if timestamp not in data:
|
|
1057
|
+
data[timestamp] = TraceLatencyMsPercentileTimeSeriesDataPoint(timestamp=timestamp)
|
|
1058
|
+
return TraceLatencyPercentileTimeSeries(
|
|
1059
|
+
data=sorted(data.values(), key=lambda x: x.timestamp)
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
@strawberry.field
|
|
1063
|
+
async def trace_token_count_time_series(
|
|
1064
|
+
self,
|
|
1065
|
+
info: Info[Context, None],
|
|
1066
|
+
time_range: TimeRange,
|
|
1067
|
+
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
1068
|
+
) -> TraceTokenCountTimeSeries:
|
|
1069
|
+
if time_range.start is None:
|
|
1070
|
+
raise BadRequest("Start time is required")
|
|
1071
|
+
|
|
1072
|
+
dialect = info.context.db.dialect
|
|
1073
|
+
utc_offset_minutes = 0
|
|
1074
|
+
field: Literal["minute", "hour", "day", "week", "month", "year"] = "hour"
|
|
1075
|
+
if time_bin_config:
|
|
1076
|
+
utc_offset_minutes = time_bin_config.utc_offset_minutes
|
|
1077
|
+
if time_bin_config.scale is TimeBinScale.MINUTE:
|
|
1078
|
+
field = "minute"
|
|
1079
|
+
elif time_bin_config.scale is TimeBinScale.HOUR:
|
|
1080
|
+
field = "hour"
|
|
1081
|
+
elif time_bin_config.scale is TimeBinScale.DAY:
|
|
1082
|
+
field = "day"
|
|
1083
|
+
elif time_bin_config.scale is TimeBinScale.WEEK:
|
|
1084
|
+
field = "week"
|
|
1085
|
+
elif time_bin_config.scale is TimeBinScale.MONTH:
|
|
1086
|
+
field = "month"
|
|
1087
|
+
elif time_bin_config.scale is TimeBinScale.YEAR:
|
|
1088
|
+
field = "year"
|
|
1089
|
+
bucket = date_trunc(dialect, field, models.Trace.start_time, utc_offset_minutes)
|
|
1090
|
+
stmt = (
|
|
1091
|
+
select(
|
|
1092
|
+
bucket,
|
|
1093
|
+
func.sum(models.SpanCost.total_tokens),
|
|
1094
|
+
func.sum(models.SpanCost.prompt_tokens),
|
|
1095
|
+
func.sum(models.SpanCost.completion_tokens),
|
|
1096
|
+
)
|
|
1097
|
+
.join_from(
|
|
1098
|
+
models.Trace,
|
|
1099
|
+
models.SpanCost,
|
|
1100
|
+
onclause=models.SpanCost.trace_rowid == models.Trace.id,
|
|
1101
|
+
)
|
|
1102
|
+
.where(models.Trace.project_rowid == self.project_rowid)
|
|
1103
|
+
.group_by(bucket)
|
|
1104
|
+
.order_by(bucket)
|
|
1105
|
+
)
|
|
1106
|
+
if time_range:
|
|
1107
|
+
if time_range.start:
|
|
1108
|
+
stmt = stmt.where(time_range.start <= models.Trace.start_time)
|
|
1109
|
+
if time_range.end:
|
|
1110
|
+
stmt = stmt.where(models.Trace.start_time < time_range.end)
|
|
1111
|
+
data: dict[datetime, TraceTokenCountTimeSeriesDataPoint] = {}
|
|
1112
|
+
async with info.context.db() as session:
|
|
1113
|
+
async for (
|
|
1114
|
+
t,
|
|
1115
|
+
total_tokens,
|
|
1116
|
+
prompt_tokens,
|
|
1117
|
+
completion_tokens,
|
|
1118
|
+
) in await session.stream(stmt):
|
|
1119
|
+
timestamp = _as_datetime(t)
|
|
1120
|
+
data[timestamp] = TraceTokenCountTimeSeriesDataPoint(
|
|
1121
|
+
timestamp=timestamp,
|
|
1122
|
+
prompt_token_count=prompt_tokens,
|
|
1123
|
+
completion_token_count=completion_tokens,
|
|
1124
|
+
total_token_count=total_tokens,
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
data_timestamps: list[datetime] = [data_point.timestamp for data_point in data.values()]
|
|
1128
|
+
min_time = min([*data_timestamps, time_range.start])
|
|
1129
|
+
max_time = max(
|
|
1130
|
+
[
|
|
1131
|
+
*data_timestamps,
|
|
1132
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
1133
|
+
],
|
|
1134
|
+
)
|
|
1135
|
+
for timestamp in get_timestamp_range(
|
|
1136
|
+
start_time=min_time,
|
|
1137
|
+
end_time=max_time,
|
|
1138
|
+
stride=field,
|
|
1139
|
+
utc_offset_minutes=utc_offset_minutes,
|
|
1140
|
+
):
|
|
1141
|
+
if timestamp not in data:
|
|
1142
|
+
data[timestamp] = TraceTokenCountTimeSeriesDataPoint(timestamp=timestamp)
|
|
1143
|
+
return TraceTokenCountTimeSeries(data=sorted(data.values(), key=lambda x: x.timestamp))
|
|
1144
|
+
|
|
1145
|
+
@strawberry.field
|
|
1146
|
+
async def trace_token_cost_time_series(
|
|
1147
|
+
self,
|
|
1148
|
+
info: Info[Context, None],
|
|
1149
|
+
time_range: TimeRange,
|
|
1150
|
+
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
1151
|
+
) -> TraceTokenCostTimeSeries:
|
|
1152
|
+
if time_range.start is None:
|
|
1153
|
+
raise BadRequest("Start time is required")
|
|
1154
|
+
|
|
1155
|
+
dialect = info.context.db.dialect
|
|
1156
|
+
utc_offset_minutes = 0
|
|
1157
|
+
field: Literal["minute", "hour", "day", "week", "month", "year"] = "hour"
|
|
1158
|
+
if time_bin_config:
|
|
1159
|
+
utc_offset_minutes = time_bin_config.utc_offset_minutes
|
|
1160
|
+
if time_bin_config.scale is TimeBinScale.MINUTE:
|
|
1161
|
+
field = "minute"
|
|
1162
|
+
elif time_bin_config.scale is TimeBinScale.HOUR:
|
|
1163
|
+
field = "hour"
|
|
1164
|
+
elif time_bin_config.scale is TimeBinScale.DAY:
|
|
1165
|
+
field = "day"
|
|
1166
|
+
elif time_bin_config.scale is TimeBinScale.WEEK:
|
|
1167
|
+
field = "week"
|
|
1168
|
+
elif time_bin_config.scale is TimeBinScale.MONTH:
|
|
1169
|
+
field = "month"
|
|
1170
|
+
elif time_bin_config.scale is TimeBinScale.YEAR:
|
|
1171
|
+
field = "year"
|
|
1172
|
+
bucket = date_trunc(dialect, field, models.Trace.start_time, utc_offset_minutes)
|
|
1173
|
+
stmt = (
|
|
1174
|
+
select(
|
|
1175
|
+
bucket,
|
|
1176
|
+
func.sum(models.SpanCost.total_cost),
|
|
1177
|
+
func.sum(models.SpanCost.prompt_cost),
|
|
1178
|
+
func.sum(models.SpanCost.completion_cost),
|
|
1179
|
+
)
|
|
1180
|
+
.join_from(
|
|
1181
|
+
models.Trace,
|
|
1182
|
+
models.SpanCost,
|
|
1183
|
+
onclause=models.SpanCost.trace_rowid == models.Trace.id,
|
|
1184
|
+
)
|
|
1185
|
+
.where(models.Trace.project_rowid == self.project_rowid)
|
|
1186
|
+
.group_by(bucket)
|
|
1187
|
+
.order_by(bucket)
|
|
1188
|
+
)
|
|
1189
|
+
if time_range:
|
|
1190
|
+
if time_range.start:
|
|
1191
|
+
stmt = stmt.where(time_range.start <= models.Trace.start_time)
|
|
1192
|
+
if time_range.end:
|
|
1193
|
+
stmt = stmt.where(models.Trace.start_time < time_range.end)
|
|
1194
|
+
data: dict[datetime, TraceTokenCostTimeSeriesDataPoint] = {}
|
|
1195
|
+
async with info.context.db() as session:
|
|
1196
|
+
async for (
|
|
1197
|
+
t,
|
|
1198
|
+
total_cost,
|
|
1199
|
+
prompt_cost,
|
|
1200
|
+
completion_cost,
|
|
1201
|
+
) in await session.stream(stmt):
|
|
1202
|
+
timestamp = _as_datetime(t)
|
|
1203
|
+
data[timestamp] = TraceTokenCostTimeSeriesDataPoint(
|
|
1204
|
+
timestamp=timestamp,
|
|
1205
|
+
prompt_cost=prompt_cost,
|
|
1206
|
+
completion_cost=completion_cost,
|
|
1207
|
+
total_cost=total_cost,
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
data_timestamps: list[datetime] = [data_point.timestamp for data_point in data.values()]
|
|
1211
|
+
min_time = min([*data_timestamps, time_range.start])
|
|
1212
|
+
max_time = max(
|
|
1213
|
+
[
|
|
1214
|
+
*data_timestamps,
|
|
1215
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
1216
|
+
],
|
|
1217
|
+
)
|
|
1218
|
+
for timestamp in get_timestamp_range(
|
|
1219
|
+
start_time=min_time,
|
|
1220
|
+
end_time=max_time,
|
|
1221
|
+
stride=field,
|
|
1222
|
+
utc_offset_minutes=utc_offset_minutes,
|
|
1223
|
+
):
|
|
1224
|
+
if timestamp not in data:
|
|
1225
|
+
data[timestamp] = TraceTokenCostTimeSeriesDataPoint(timestamp=timestamp)
|
|
1226
|
+
return TraceTokenCostTimeSeries(data=sorted(data.values(), key=lambda x: x.timestamp))
|
|
1227
|
+
|
|
1228
|
+
@strawberry.field
|
|
1229
|
+
async def span_annotation_score_time_series(
|
|
1230
|
+
self,
|
|
1231
|
+
info: Info[Context, None],
|
|
1232
|
+
time_range: TimeRange,
|
|
1233
|
+
time_bin_config: Optional[TimeBinConfig] = UNSET,
|
|
1234
|
+
) -> SpanAnnotationScoreTimeSeries:
|
|
1235
|
+
if time_range.start is None:
|
|
1236
|
+
raise BadRequest("Start time is required")
|
|
1237
|
+
|
|
1238
|
+
dialect = info.context.db.dialect
|
|
1239
|
+
utc_offset_minutes = 0
|
|
1240
|
+
field: Literal["minute", "hour", "day", "week", "month", "year"] = "hour"
|
|
1241
|
+
if time_bin_config:
|
|
1242
|
+
utc_offset_minutes = time_bin_config.utc_offset_minutes
|
|
1243
|
+
if time_bin_config.scale is TimeBinScale.MINUTE:
|
|
1244
|
+
field = "minute"
|
|
1245
|
+
elif time_bin_config.scale is TimeBinScale.HOUR:
|
|
1246
|
+
field = "hour"
|
|
1247
|
+
elif time_bin_config.scale is TimeBinScale.DAY:
|
|
1248
|
+
field = "day"
|
|
1249
|
+
elif time_bin_config.scale is TimeBinScale.WEEK:
|
|
1250
|
+
field = "week"
|
|
1251
|
+
elif time_bin_config.scale is TimeBinScale.MONTH:
|
|
1252
|
+
field = "month"
|
|
1253
|
+
elif time_bin_config.scale is TimeBinScale.YEAR:
|
|
1254
|
+
field = "year"
|
|
1255
|
+
bucket = date_trunc(dialect, field, models.Trace.start_time, utc_offset_minutes)
|
|
1256
|
+
stmt = (
|
|
1257
|
+
select(
|
|
1258
|
+
bucket,
|
|
1259
|
+
models.SpanAnnotation.name,
|
|
1260
|
+
func.avg(models.SpanAnnotation.score).label("average_score"),
|
|
1261
|
+
)
|
|
1262
|
+
.join_from(
|
|
1263
|
+
models.SpanAnnotation,
|
|
1264
|
+
models.Span,
|
|
1265
|
+
onclause=models.SpanAnnotation.span_rowid == models.Span.id,
|
|
1266
|
+
)
|
|
1267
|
+
.join_from(
|
|
1268
|
+
models.Span,
|
|
1269
|
+
models.Trace,
|
|
1270
|
+
onclause=models.Span.trace_rowid == models.Trace.id,
|
|
1271
|
+
)
|
|
1272
|
+
.where(models.Trace.project_rowid == self.project_rowid)
|
|
1273
|
+
.group_by(bucket, models.SpanAnnotation.name)
|
|
1274
|
+
.order_by(bucket)
|
|
1275
|
+
)
|
|
1276
|
+
if time_range:
|
|
1277
|
+
if time_range.start:
|
|
1278
|
+
stmt = stmt.where(time_range.start <= models.Trace.start_time)
|
|
1279
|
+
if time_range.end:
|
|
1280
|
+
stmt = stmt.where(models.Trace.start_time < time_range.end)
|
|
1281
|
+
scores: dict[datetime, dict[str, float]] = {}
|
|
1282
|
+
unique_names: set[str] = set()
|
|
1283
|
+
async with info.context.db() as session:
|
|
1284
|
+
async for (
|
|
1285
|
+
t,
|
|
1286
|
+
name,
|
|
1287
|
+
average_score,
|
|
1288
|
+
) in await session.stream(stmt):
|
|
1289
|
+
timestamp = _as_datetime(t)
|
|
1290
|
+
if timestamp not in scores:
|
|
1291
|
+
scores[timestamp] = {}
|
|
1292
|
+
scores[timestamp][name] = average_score
|
|
1293
|
+
unique_names.add(name)
|
|
1294
|
+
|
|
1295
|
+
score_timestamps: list[datetime] = [timestamp for timestamp in scores]
|
|
1296
|
+
min_time = min([*score_timestamps, time_range.start])
|
|
1297
|
+
max_time = max(
|
|
1298
|
+
[
|
|
1299
|
+
*score_timestamps,
|
|
1300
|
+
*([time_range.end] if time_range.end else [datetime.now(timezone.utc)]),
|
|
1301
|
+
],
|
|
1302
|
+
)
|
|
1303
|
+
data: dict[datetime, SpanAnnotationScoreTimeSeriesDataPoint] = {
|
|
1304
|
+
timestamp: SpanAnnotationScoreTimeSeriesDataPoint(
|
|
1305
|
+
timestamp=timestamp,
|
|
1306
|
+
scores_with_labels=[
|
|
1307
|
+
SpanAnnotationScoreWithLabel(label=label, score=scores[timestamp][label])
|
|
1308
|
+
for label in scores[timestamp]
|
|
1309
|
+
],
|
|
1310
|
+
)
|
|
1311
|
+
for timestamp in score_timestamps
|
|
1312
|
+
}
|
|
1313
|
+
for timestamp in get_timestamp_range(
|
|
1314
|
+
start_time=min_time,
|
|
1315
|
+
end_time=max_time,
|
|
1316
|
+
stride=field,
|
|
1317
|
+
utc_offset_minutes=utc_offset_minutes,
|
|
1318
|
+
):
|
|
1319
|
+
if timestamp not in data:
|
|
1320
|
+
data[timestamp] = SpanAnnotationScoreTimeSeriesDataPoint(
|
|
1321
|
+
timestamp=timestamp,
|
|
1322
|
+
scores_with_labels=[],
|
|
1323
|
+
)
|
|
1324
|
+
return SpanAnnotationScoreTimeSeries(
|
|
1325
|
+
data=sorted(data.values(), key=lambda x: x.timestamp),
|
|
1326
|
+
names=sorted(list(unique_names)),
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
@strawberry.type
|
|
1331
|
+
class SpanCountTimeSeriesDataPoint:
|
|
1332
|
+
timestamp: datetime
|
|
1333
|
+
ok_count: Optional[int] = None
|
|
1334
|
+
error_count: Optional[int] = None
|
|
1335
|
+
unset_count: Optional[int] = None
|
|
1336
|
+
total_count: Optional[int] = None
|
|
1337
|
+
|
|
844
1338
|
|
|
845
1339
|
@strawberry.type
|
|
846
|
-
class SpanCountTimeSeries
|
|
847
|
-
|
|
1340
|
+
class SpanCountTimeSeries:
|
|
1341
|
+
data: list[SpanCountTimeSeriesDataPoint]
|
|
848
1342
|
|
|
849
1343
|
|
|
850
1344
|
@strawberry.type
|
|
@@ -852,6 +1346,80 @@ class TraceCountTimeSeries(TimeSeries):
|
|
|
852
1346
|
"""A time series of trace count"""
|
|
853
1347
|
|
|
854
1348
|
|
|
1349
|
+
@strawberry.type
|
|
1350
|
+
class TraceCountByStatusTimeSeriesDataPoint:
|
|
1351
|
+
timestamp: datetime
|
|
1352
|
+
ok_count: int
|
|
1353
|
+
error_count: int
|
|
1354
|
+
total_count: int
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
@strawberry.type
|
|
1358
|
+
class TraceCountByStatusTimeSeries:
|
|
1359
|
+
data: list[TraceCountByStatusTimeSeriesDataPoint]
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
@strawberry.type
|
|
1363
|
+
class TraceLatencyMsPercentileTimeSeriesDataPoint:
|
|
1364
|
+
timestamp: datetime
|
|
1365
|
+
p50: Optional[float] = None
|
|
1366
|
+
p75: Optional[float] = None
|
|
1367
|
+
p90: Optional[float] = None
|
|
1368
|
+
p95: Optional[float] = None
|
|
1369
|
+
p99: Optional[float] = None
|
|
1370
|
+
p999: Optional[float] = None
|
|
1371
|
+
max: Optional[float] = None
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
@strawberry.type
|
|
1375
|
+
class TraceLatencyPercentileTimeSeries:
|
|
1376
|
+
data: list[TraceLatencyMsPercentileTimeSeriesDataPoint]
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
@strawberry.type
|
|
1380
|
+
class TraceTokenCountTimeSeriesDataPoint:
|
|
1381
|
+
timestamp: datetime
|
|
1382
|
+
prompt_token_count: Optional[float] = None
|
|
1383
|
+
completion_token_count: Optional[float] = None
|
|
1384
|
+
total_token_count: Optional[float] = None
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
@strawberry.type
|
|
1388
|
+
class TraceTokenCountTimeSeries:
|
|
1389
|
+
data: list[TraceTokenCountTimeSeriesDataPoint]
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
@strawberry.type
|
|
1393
|
+
class TraceTokenCostTimeSeriesDataPoint:
|
|
1394
|
+
timestamp: datetime
|
|
1395
|
+
prompt_cost: Optional[float] = None
|
|
1396
|
+
completion_cost: Optional[float] = None
|
|
1397
|
+
total_cost: Optional[float] = None
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
@strawberry.type
|
|
1401
|
+
class TraceTokenCostTimeSeries:
|
|
1402
|
+
data: list[TraceTokenCostTimeSeriesDataPoint]
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
@strawberry.type
|
|
1406
|
+
class SpanAnnotationScoreWithLabel:
|
|
1407
|
+
label: str
|
|
1408
|
+
score: float
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
@strawberry.type
|
|
1412
|
+
class SpanAnnotationScoreTimeSeriesDataPoint:
|
|
1413
|
+
timestamp: datetime
|
|
1414
|
+
scores_with_labels: list[SpanAnnotationScoreWithLabel]
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
@strawberry.type
|
|
1418
|
+
class SpanAnnotationScoreTimeSeries:
|
|
1419
|
+
data: list[SpanAnnotationScoreTimeSeriesDataPoint]
|
|
1420
|
+
names: list[str]
|
|
1421
|
+
|
|
1422
|
+
|
|
855
1423
|
INPUT_VALUE = SpanAttributes.INPUT_VALUE.split(".")
|
|
856
1424
|
OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE.split(".")
|
|
857
1425
|
|
|
@@ -1060,3 +1628,13 @@ async def _paginate_span_by_trace_start_time(
|
|
|
1060
1628
|
has_next_page=has_next_page,
|
|
1061
1629
|
),
|
|
1062
1630
|
)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def to_gql_project(project: models.Project) -> Project:
|
|
1634
|
+
"""
|
|
1635
|
+
Converts an ORM project to a GraphQL project.
|
|
1636
|
+
"""
|
|
1637
|
+
return Project(
|
|
1638
|
+
project_rowid=project.id,
|
|
1639
|
+
db_project=project,
|
|
1640
|
+
)
|