arize-phoenix 10.14.0__py3-none-any.whl → 11.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.
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/METADATA +3 -2
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/RECORD +82 -50
- phoenix/config.py +5 -2
- phoenix/datetime_utils.py +8 -1
- phoenix/db/bulk_inserter.py +40 -1
- phoenix/db/facilitator.py +263 -4
- phoenix/db/insertion/helpers.py +15 -0
- phoenix/db/insertion/span.py +3 -1
- phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
- phoenix/db/models.py +267 -9
- phoenix/db/types/model_provider.py +1 -0
- phoenix/db/types/token_price_customization.py +29 -0
- phoenix/server/api/context.py +38 -4
- phoenix/server/api/dataloaders/__init__.py +41 -5
- phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
- phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
- phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +58 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +140 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
- phoenix/server/api/dataloaders/span_costs.py +35 -0
- phoenix/server/api/dataloaders/types.py +29 -0
- phoenix/server/api/helpers/playground_clients.py +562 -12
- phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
- phoenix/server/api/helpers/prompts/models.py +67 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +2 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +3 -0
- phoenix/server/api/input_types/SpanSort.py +17 -0
- phoenix/server/api/mutations/__init__.py +2 -0
- phoenix/server/api/mutations/chat_mutations.py +17 -0
- phoenix/server/api/mutations/model_mutations.py +208 -0
- phoenix/server/api/queries.py +82 -41
- phoenix/server/api/routers/v1/traces.py +11 -4
- phoenix/server/api/subscriptions.py +36 -2
- phoenix/server/api/types/CostBreakdown.py +15 -0
- phoenix/server/api/types/Experiment.py +59 -1
- phoenix/server/api/types/ExperimentRun.py +58 -4
- phoenix/server/api/types/GenerativeModel.py +143 -2
- phoenix/server/api/types/GenerativeProvider.py +33 -20
- phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
- phoenix/server/api/types/ModelInterface.py +11 -0
- phoenix/server/api/types/PlaygroundModel.py +10 -0
- phoenix/server/api/types/Project.py +42 -0
- phoenix/server/api/types/ProjectSession.py +44 -0
- phoenix/server/api/types/Span.py +137 -0
- phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
- phoenix/server/api/types/SpanCostSummary.py +10 -0
- phoenix/server/api/types/TokenPrice.py +16 -0
- phoenix/server/api/types/TokenUsage.py +3 -3
- phoenix/server/api/types/Trace.py +41 -0
- phoenix/server/app.py +59 -0
- phoenix/server/cost_tracking/cost_details_calculator.py +190 -0
- phoenix/server/cost_tracking/cost_model_lookup.py +151 -0
- phoenix/server/cost_tracking/helpers.py +68 -0
- phoenix/server/cost_tracking/model_cost_manifest.json +59 -329
- phoenix/server/cost_tracking/regex_specificity.py +397 -0
- phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
- phoenix/server/daemons/__init__.py +0 -0
- phoenix/server/daemons/generative_model_store.py +51 -0
- phoenix/server/daemons/span_cost_calculator.py +103 -0
- phoenix/server/dml_event_handler.py +1 -0
- phoenix/server/static/.vite/manifest.json +36 -36
- phoenix/server/static/assets/components-BnK9kodr.js +5055 -0
- phoenix/server/static/assets/{index-qiubV_74.js → index-S3YKLmbo.js} +13 -13
- phoenix/server/static/assets/{pages-C4V07ozl.js → pages-BW6PBHZb.js} +809 -417
- phoenix/server/static/assets/{vendor-Bfsiga8H.js → vendor-DqQvHbPa.js} +147 -147
- phoenix/server/static/assets/{vendor-arizeai-CQOWsrzm.js → vendor-arizeai-CLX44PFA.js} +1 -1
- phoenix/server/static/assets/{vendor-codemirror-CrcGVhB2.js → vendor-codemirror-Du3XyJnB.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-Yyg3G-Rq.js → vendor-recharts-B2PJDrnX.js} +25 -25
- phoenix/server/static/assets/{vendor-shiki-OPjag7Hm.js → vendor-shiki-CNbrFjf9.js} +1 -1
- phoenix/version.py +1 -1
- phoenix/server/cost_tracking/cost_lookup.py +0 -255
- phoenix/server/static/assets/components-CUUWyAMo.js +0 -4509
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-10.14.0.dist-info → arize_phoenix-11.0.0.dist-info}/licenses/LICENSE +0 -0
phoenix/db/models.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from datetime import datetime, timezone
|
|
2
3
|
from typing import Any, Iterable, Literal, Optional, Sequence, TypedDict, cast
|
|
3
4
|
|
|
5
|
+
import sqlalchemy as sa
|
|
4
6
|
import sqlalchemy.sql as sql
|
|
5
7
|
from openinference.semconv.trace import RerankerAttributes, SpanAttributes
|
|
6
8
|
from sqlalchemy import (
|
|
7
9
|
JSON,
|
|
8
10
|
NUMERIC,
|
|
9
11
|
TIMESTAMP,
|
|
12
|
+
Boolean,
|
|
10
13
|
CheckConstraint,
|
|
11
14
|
ColumnElement,
|
|
12
15
|
Dialect,
|
|
@@ -52,6 +55,10 @@ from phoenix.db.types.annotation_configs import (
|
|
|
52
55
|
)
|
|
53
56
|
from phoenix.db.types.identifier import Identifier
|
|
54
57
|
from phoenix.db.types.model_provider import ModelProvider
|
|
58
|
+
from phoenix.db.types.token_price_customization import (
|
|
59
|
+
TokenPriceCustomization,
|
|
60
|
+
TokenPriceCustomizationParser,
|
|
61
|
+
)
|
|
55
62
|
from phoenix.db.types.trace_retention import TraceRetentionCronExpression, TraceRetentionRule
|
|
56
63
|
from phoenix.server.api.helpers.prompts.models import (
|
|
57
64
|
PromptInvocationParameters,
|
|
@@ -391,12 +398,49 @@ class _AnnotationConfig(TypeDecorator[AnnotationConfigType]):
|
|
|
391
398
|
return AnnotationConfigModel.model_validate(value).root if value is not None else None
|
|
392
399
|
|
|
393
400
|
|
|
401
|
+
class _TokenCustomization(TypeDecorator[TokenPriceCustomization]):
|
|
402
|
+
# See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
|
|
403
|
+
cache_ok = True
|
|
404
|
+
impl = JSON
|
|
405
|
+
|
|
406
|
+
def process_bind_param(
|
|
407
|
+
self, value: Optional[TokenPriceCustomization], _: Dialect
|
|
408
|
+
) -> Optional[dict[str, Any]]:
|
|
409
|
+
return value.model_dump() if value is not None else None
|
|
410
|
+
|
|
411
|
+
def process_result_value(
|
|
412
|
+
self, value: Optional[dict[str, Any]], _: Dialect
|
|
413
|
+
) -> Optional[TokenPriceCustomization]:
|
|
414
|
+
return TokenPriceCustomizationParser.parse(value)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class _RegexStr(TypeDecorator[re.Pattern[str]]):
|
|
418
|
+
# See https://docs.sqlalchemy.org/en/20/core/custom_types.html
|
|
419
|
+
cache_ok = True
|
|
420
|
+
impl = String
|
|
421
|
+
|
|
422
|
+
def process_bind_param(self, value: Optional[re.Pattern[str]], _: Dialect) -> Optional[str]:
|
|
423
|
+
if value is None:
|
|
424
|
+
return None
|
|
425
|
+
if not isinstance(value, re.Pattern):
|
|
426
|
+
raise TypeError(f"Expected a regex pattern, got {type(value)}")
|
|
427
|
+
pattern = value.pattern
|
|
428
|
+
if not isinstance(pattern, str):
|
|
429
|
+
raise ValueError(f"Expected a string, got {type(pattern)}")
|
|
430
|
+
return pattern
|
|
431
|
+
|
|
432
|
+
def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[re.Pattern[str]]:
|
|
433
|
+
if value is None:
|
|
434
|
+
return None
|
|
435
|
+
return re.compile(value)
|
|
436
|
+
|
|
437
|
+
|
|
394
438
|
class ExperimentRunOutput(TypedDict, total=False):
|
|
395
439
|
task_output: Any
|
|
396
440
|
|
|
397
441
|
|
|
398
442
|
class Base(DeclarativeBase):
|
|
399
|
-
id: Mapped[int] = mapped_column(
|
|
443
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
400
444
|
# Enforce best practices for naming constraints
|
|
401
445
|
# https://alembic.sqlalchemy.org/en/latest/naming.html#integration-of-naming-conventions-into-operations-autogenerate
|
|
402
446
|
metadata = MetaData(
|
|
@@ -418,7 +462,6 @@ class Base(DeclarativeBase):
|
|
|
418
462
|
|
|
419
463
|
class ProjectTraceRetentionPolicy(Base):
|
|
420
464
|
__tablename__ = "project_trace_retention_policies"
|
|
421
|
-
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
422
465
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
423
466
|
cron_expression: Mapped[TraceRetentionCronExpression] = mapped_column(
|
|
424
467
|
_TraceRetentionCronExpression, nullable=False
|
|
@@ -528,6 +571,12 @@ class Trace(Base):
|
|
|
528
571
|
primaryjoin="foreign(ExperimentRun.trace_id) == Trace.trace_id",
|
|
529
572
|
back_populates="trace",
|
|
530
573
|
)
|
|
574
|
+
span_costs: Mapped[list["SpanCost"]] = relationship(
|
|
575
|
+
"SpanCost",
|
|
576
|
+
back_populates="trace",
|
|
577
|
+
cascade="all, delete-orphan",
|
|
578
|
+
uselist=True,
|
|
579
|
+
)
|
|
531
580
|
__table_args__ = (
|
|
532
581
|
UniqueConstraint(
|
|
533
582
|
"trace_id",
|
|
@@ -685,6 +734,7 @@ class Span(Base):
|
|
|
685
734
|
span_annotations: Mapped[list["SpanAnnotation"]] = relationship(back_populates="span")
|
|
686
735
|
document_annotations: Mapped[list["DocumentAnnotation"]] = relationship(back_populates="span")
|
|
687
736
|
dataset_examples: Mapped[list["DatasetExample"]] = relationship(back_populates="span")
|
|
737
|
+
span_cost: Mapped[Optional["SpanCost"]] = relationship(back_populates="span")
|
|
688
738
|
|
|
689
739
|
__table_args__ = (
|
|
690
740
|
UniqueConstraint(
|
|
@@ -1303,9 +1353,86 @@ class ApiKey(Base):
|
|
|
1303
1353
|
__table_args__ = (dict(sqlite_autoincrement=True),)
|
|
1304
1354
|
|
|
1305
1355
|
|
|
1356
|
+
CostType: TypeAlias = Literal["DEFAULT", "OVERRIDE"]
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
class GenerativeModel(Base):
|
|
1360
|
+
__tablename__ = "generative_models"
|
|
1361
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
1362
|
+
provider: Mapped[str]
|
|
1363
|
+
start_time: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
|
|
1364
|
+
name_pattern: Mapped[re.Pattern[str]] = mapped_column(_RegexStr, nullable=False)
|
|
1365
|
+
is_built_in: Mapped[bool] = mapped_column(
|
|
1366
|
+
Boolean,
|
|
1367
|
+
nullable=False,
|
|
1368
|
+
)
|
|
1369
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
1370
|
+
UtcTimeStamp,
|
|
1371
|
+
server_default=func.now(),
|
|
1372
|
+
)
|
|
1373
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
1374
|
+
UtcTimeStamp,
|
|
1375
|
+
server_default=func.now(),
|
|
1376
|
+
onupdate=func.now(),
|
|
1377
|
+
)
|
|
1378
|
+
deleted_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp)
|
|
1379
|
+
|
|
1380
|
+
token_prices: Mapped[list["TokenPrice"]] = relationship(
|
|
1381
|
+
"TokenPrice",
|
|
1382
|
+
back_populates="model",
|
|
1383
|
+
cascade="all, delete-orphan",
|
|
1384
|
+
uselist=True,
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
__table_args__ = (
|
|
1388
|
+
Index(
|
|
1389
|
+
"ix_generative_models_match_criteria",
|
|
1390
|
+
"name_pattern",
|
|
1391
|
+
"provider",
|
|
1392
|
+
"is_built_in",
|
|
1393
|
+
postgresql_where=sa.text("deleted_at IS NULL"),
|
|
1394
|
+
sqlite_where=sa.text("deleted_at IS NULL"),
|
|
1395
|
+
unique=True,
|
|
1396
|
+
),
|
|
1397
|
+
Index(
|
|
1398
|
+
"ix_generative_models_name_is_built_in",
|
|
1399
|
+
"name",
|
|
1400
|
+
"is_built_in",
|
|
1401
|
+
postgresql_where=sa.text("deleted_at IS NULL"),
|
|
1402
|
+
sqlite_where=sa.text("deleted_at IS NULL"),
|
|
1403
|
+
unique=True,
|
|
1404
|
+
),
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
class TokenPrice(Base):
|
|
1409
|
+
__tablename__ = "token_prices"
|
|
1410
|
+
model_id: Mapped[int] = mapped_column(
|
|
1411
|
+
ForeignKey("generative_models.id", ondelete="CASCADE"),
|
|
1412
|
+
nullable=False,
|
|
1413
|
+
index=True,
|
|
1414
|
+
)
|
|
1415
|
+
token_type: Mapped[str]
|
|
1416
|
+
is_prompt: Mapped[bool]
|
|
1417
|
+
base_rate: Mapped[float]
|
|
1418
|
+
customization: Mapped[TokenPriceCustomization] = mapped_column(_TokenCustomization)
|
|
1419
|
+
|
|
1420
|
+
model: Mapped["GenerativeModel"] = relationship(
|
|
1421
|
+
"GenerativeModel",
|
|
1422
|
+
back_populates="token_prices",
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
__table_args__ = (
|
|
1426
|
+
UniqueConstraint(
|
|
1427
|
+
"model_id",
|
|
1428
|
+
"token_type",
|
|
1429
|
+
"is_prompt",
|
|
1430
|
+
),
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
|
|
1306
1434
|
class PromptLabel(Base):
|
|
1307
1435
|
__tablename__ = "prompt_labels"
|
|
1308
|
-
|
|
1309
1436
|
name: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
|
|
1310
1437
|
description: Mapped[Optional[str]]
|
|
1311
1438
|
color: Mapped[str] = mapped_column(String, nullable=True)
|
|
@@ -1320,7 +1447,6 @@ class PromptLabel(Base):
|
|
|
1320
1447
|
|
|
1321
1448
|
class Prompt(Base):
|
|
1322
1449
|
__tablename__ = "prompts"
|
|
1323
|
-
|
|
1324
1450
|
source_prompt_id: Mapped[Optional[int]] = mapped_column(
|
|
1325
1451
|
ForeignKey("prompts.id", ondelete="SET NULL"),
|
|
1326
1452
|
index=True,
|
|
@@ -1358,7 +1484,6 @@ class Prompt(Base):
|
|
|
1358
1484
|
|
|
1359
1485
|
class PromptPromptLabel(Base):
|
|
1360
1486
|
__tablename__ = "prompts_prompt_labels"
|
|
1361
|
-
|
|
1362
1487
|
prompt_label_id: Mapped[int] = mapped_column(
|
|
1363
1488
|
ForeignKey("prompt_labels.id", ondelete="CASCADE"),
|
|
1364
1489
|
index=True,
|
|
@@ -1458,16 +1583,12 @@ class PromptVersionTag(Base):
|
|
|
1458
1583
|
|
|
1459
1584
|
class AnnotationConfig(Base):
|
|
1460
1585
|
__tablename__ = "annotation_configs"
|
|
1461
|
-
|
|
1462
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
1463
1586
|
name: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
|
1464
1587
|
config: Mapped[AnnotationConfigType] = mapped_column(_AnnotationConfig, nullable=False)
|
|
1465
1588
|
|
|
1466
1589
|
|
|
1467
1590
|
class ProjectAnnotationConfig(Base):
|
|
1468
1591
|
__tablename__ = "project_annotation_configs"
|
|
1469
|
-
|
|
1470
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
1471
1592
|
project_id: Mapped[int] = mapped_column(
|
|
1472
1593
|
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
|
1473
1594
|
)
|
|
@@ -1476,3 +1597,140 @@ class ProjectAnnotationConfig(Base):
|
|
|
1476
1597
|
)
|
|
1477
1598
|
|
|
1478
1599
|
__table_args__ = (UniqueConstraint("project_id", "annotation_config_id"),)
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
class SpanCost(Base):
|
|
1603
|
+
__tablename__ = "span_costs"
|
|
1604
|
+
|
|
1605
|
+
span_rowid: Mapped[int] = mapped_column(
|
|
1606
|
+
ForeignKey("spans.id", ondelete="CASCADE"),
|
|
1607
|
+
nullable=False,
|
|
1608
|
+
)
|
|
1609
|
+
trace_rowid: Mapped[int] = mapped_column(
|
|
1610
|
+
ForeignKey("traces.id", ondelete="CASCADE"),
|
|
1611
|
+
nullable=False,
|
|
1612
|
+
)
|
|
1613
|
+
span_start_time: Mapped[datetime] = mapped_column(
|
|
1614
|
+
UtcTimeStamp,
|
|
1615
|
+
nullable=False,
|
|
1616
|
+
index=True,
|
|
1617
|
+
)
|
|
1618
|
+
model_id: Mapped[Optional[int]] = mapped_column(
|
|
1619
|
+
sa.Integer,
|
|
1620
|
+
ForeignKey(
|
|
1621
|
+
"generative_models.id",
|
|
1622
|
+
ondelete="RESTRICT",
|
|
1623
|
+
),
|
|
1624
|
+
nullable=True,
|
|
1625
|
+
)
|
|
1626
|
+
total_cost: Mapped[Optional[float]]
|
|
1627
|
+
total_tokens: Mapped[Optional[float]]
|
|
1628
|
+
|
|
1629
|
+
@hybrid_property
|
|
1630
|
+
def total_cost_per_token(self) -> Optional[float]:
|
|
1631
|
+
return ((self.total_cost or 0) / self.total_tokens) if self.total_tokens else None
|
|
1632
|
+
|
|
1633
|
+
@total_cost_per_token.inplace.expression
|
|
1634
|
+
@classmethod
|
|
1635
|
+
def _total_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
|
|
1636
|
+
return sql.case(
|
|
1637
|
+
(
|
|
1638
|
+
sa.and_(cls.total_tokens.isnot(None), cls.total_tokens != 0),
|
|
1639
|
+
cls.total_cost / cls.total_tokens,
|
|
1640
|
+
)
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
prompt_cost: Mapped[Optional[float]]
|
|
1644
|
+
prompt_tokens: Mapped[Optional[float]]
|
|
1645
|
+
|
|
1646
|
+
@hybrid_property
|
|
1647
|
+
def prompt_cost_per_token(self) -> Optional[float]:
|
|
1648
|
+
return ((self.prompt_cost or 0) / self.prompt_tokens) if self.prompt_tokens else None
|
|
1649
|
+
|
|
1650
|
+
@prompt_cost_per_token.inplace.expression
|
|
1651
|
+
@classmethod
|
|
1652
|
+
def _prompt_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
|
|
1653
|
+
return sql.case(
|
|
1654
|
+
(
|
|
1655
|
+
sa.and_(cls.prompt_tokens.isnot(None), cls.prompt_tokens != 0),
|
|
1656
|
+
cls.prompt_cost / cls.prompt_tokens,
|
|
1657
|
+
)
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
completion_cost: Mapped[Optional[float]]
|
|
1661
|
+
completion_tokens: Mapped[Optional[float]]
|
|
1662
|
+
|
|
1663
|
+
@hybrid_property
|
|
1664
|
+
def completion_cost_per_token(self) -> Optional[float]:
|
|
1665
|
+
return (
|
|
1666
|
+
((self.completion_cost or 0) / self.completion_tokens)
|
|
1667
|
+
if self.completion_tokens
|
|
1668
|
+
else None
|
|
1669
|
+
)
|
|
1670
|
+
|
|
1671
|
+
@completion_cost_per_token.inplace.expression
|
|
1672
|
+
@classmethod
|
|
1673
|
+
def _completion_cost_per_token_expression(cls) -> ColumnElement[Optional[float]]:
|
|
1674
|
+
return sql.case(
|
|
1675
|
+
(
|
|
1676
|
+
sa.and_(cls.completion_tokens.isnot(None), cls.completion_tokens != 0),
|
|
1677
|
+
cls.completion_cost / cls.completion_tokens,
|
|
1678
|
+
)
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
span: Mapped["Span"] = relationship("Span", back_populates="span_cost")
|
|
1682
|
+
trace: Mapped["Trace"] = relationship("Trace", back_populates="span_costs")
|
|
1683
|
+
span_cost_details: Mapped[list["SpanCostDetail"]] = relationship(
|
|
1684
|
+
"SpanCostDetail",
|
|
1685
|
+
back_populates="span_cost",
|
|
1686
|
+
cascade="all, delete-orphan",
|
|
1687
|
+
uselist=True,
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
__table_args__ = (
|
|
1691
|
+
Index(
|
|
1692
|
+
"ix_span_costs_model_id_span_start_time",
|
|
1693
|
+
"model_id",
|
|
1694
|
+
"span_start_time",
|
|
1695
|
+
),
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
def append_detail(self, detail: "SpanCostDetail") -> None:
|
|
1699
|
+
self.span_cost_details.append(detail)
|
|
1700
|
+
if cost := detail.cost:
|
|
1701
|
+
if detail.is_prompt:
|
|
1702
|
+
self.prompt_cost = (self.prompt_cost or 0) + cost
|
|
1703
|
+
else:
|
|
1704
|
+
self.completion_cost = (self.completion_cost or 0) + cost
|
|
1705
|
+
self.total_cost = (self.total_cost or 0) + cost
|
|
1706
|
+
if tokens := detail.tokens:
|
|
1707
|
+
if detail.is_prompt:
|
|
1708
|
+
self.prompt_tokens = (self.prompt_tokens or 0) + tokens
|
|
1709
|
+
else:
|
|
1710
|
+
self.completion_tokens = (self.completion_tokens or 0) + tokens
|
|
1711
|
+
self.total_tokens = (self.total_tokens or 0) + tokens
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
class SpanCostDetail(Base):
|
|
1715
|
+
__tablename__ = "span_cost_details"
|
|
1716
|
+
span_cost_id: Mapped[int] = mapped_column(
|
|
1717
|
+
ForeignKey("span_costs.id", ondelete="CASCADE"),
|
|
1718
|
+
nullable=False,
|
|
1719
|
+
index=True,
|
|
1720
|
+
)
|
|
1721
|
+
token_type: Mapped[str]
|
|
1722
|
+
is_prompt: Mapped[bool]
|
|
1723
|
+
|
|
1724
|
+
cost: Mapped[Optional[float]]
|
|
1725
|
+
tokens: Mapped[Optional[float]]
|
|
1726
|
+
cost_per_token: Mapped[Optional[float]]
|
|
1727
|
+
|
|
1728
|
+
span_cost: Mapped["SpanCost"] = relationship("SpanCost", back_populates="span_cost_details")
|
|
1729
|
+
|
|
1730
|
+
__table_args__ = (
|
|
1731
|
+
UniqueConstraint(
|
|
1732
|
+
"span_cost_id",
|
|
1733
|
+
"token_type",
|
|
1734
|
+
"is_prompt",
|
|
1735
|
+
),
|
|
1736
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, Literal, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenPriceCustomization(BaseModel, ABC):
|
|
8
|
+
model_config = {"extra": "allow"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ThresholdBasedTokenPriceCustomization(TokenPriceCustomization):
|
|
12
|
+
type: Literal["threshold_based"] = "threshold_based"
|
|
13
|
+
key: str
|
|
14
|
+
threshold: float
|
|
15
|
+
new_rate: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TokenPriceCustomizationParser:
|
|
19
|
+
"""Intended to be forward-compatible while maintaining the ability to round-trip."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def parse(data: Optional[dict[str, Any]]) -> Optional[TokenPriceCustomization]:
|
|
23
|
+
if not data:
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
return ThresholdBasedTokenPriceCustomization.model_validate(data)
|
|
27
|
+
except ValidationError:
|
|
28
|
+
pass
|
|
29
|
+
return TokenPriceCustomization.model_validate(data)
|
phoenix/server/api/context.py
CHANGED
|
@@ -28,6 +28,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
28
28
|
ExperimentRunAnnotations,
|
|
29
29
|
ExperimentRunCountsDataLoader,
|
|
30
30
|
ExperimentSequenceNumberDataLoader,
|
|
31
|
+
LastUsedTimesByGenerativeModelIdDataLoader,
|
|
31
32
|
LatencyMsQuantileDataLoader,
|
|
32
33
|
MinStartOrMaxEndTimeDataLoader,
|
|
33
34
|
NumChildSpansDataLoader,
|
|
@@ -43,6 +44,18 @@ from phoenix.server.api.dataloaders import (
|
|
|
43
44
|
SessionTraceLatencyMsQuantileDataLoader,
|
|
44
45
|
SpanAnnotationsDataLoader,
|
|
45
46
|
SpanByIdDataLoader,
|
|
47
|
+
SpanCostBySpanDataLoader,
|
|
48
|
+
SpanCostDetailsBySpanCostDataLoader,
|
|
49
|
+
SpanCostDetailSummaryEntriesByGenerativeModelDataLoader,
|
|
50
|
+
SpanCostDetailSummaryEntriesByProjectSessionDataLoader,
|
|
51
|
+
SpanCostDetailSummaryEntriesBySpanDataLoader,
|
|
52
|
+
SpanCostDetailSummaryEntriesByTraceDataLoader,
|
|
53
|
+
SpanCostSummaryByExperimentDataLoader,
|
|
54
|
+
SpanCostSummaryByExperimentRunDataLoader,
|
|
55
|
+
SpanCostSummaryByGenerativeModelDataLoader,
|
|
56
|
+
SpanCostSummaryByProjectDataLoader,
|
|
57
|
+
SpanCostSummaryByProjectSessionDataLoader,
|
|
58
|
+
SpanCostSummaryByTraceDataLoader,
|
|
46
59
|
SpanDatasetExamplesDataLoader,
|
|
47
60
|
SpanDescendantsDataLoader,
|
|
48
61
|
SpanProjectsDataLoader,
|
|
@@ -55,6 +68,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
55
68
|
UsersDataLoader,
|
|
56
69
|
)
|
|
57
70
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
71
|
+
from phoenix.server.daemons.span_cost_calculator import SpanCostCalculator
|
|
58
72
|
from phoenix.server.dml_event import DmlEvent
|
|
59
73
|
from phoenix.server.email.types import EmailSender
|
|
60
74
|
from phoenix.server.types import (
|
|
@@ -68,23 +82,26 @@ from phoenix.server.types import (
|
|
|
68
82
|
|
|
69
83
|
@dataclass
|
|
70
84
|
class DataLoaders:
|
|
85
|
+
annotation_summaries: AnnotationSummaryDataLoader
|
|
71
86
|
average_experiment_run_latency: AverageExperimentRunLatencyDataLoader
|
|
72
87
|
dataset_example_revisions: DatasetExampleRevisionsDataLoader
|
|
73
88
|
dataset_example_spans: DatasetExampleSpansDataLoader
|
|
74
89
|
document_evaluation_summaries: DocumentEvaluationSummaryDataLoader
|
|
75
90
|
document_evaluations: DocumentEvaluationsDataLoader
|
|
76
91
|
document_retrieval_metrics: DocumentRetrievalMetricsDataLoader
|
|
77
|
-
annotation_summaries: AnnotationSummaryDataLoader
|
|
78
92
|
experiment_annotation_summaries: ExperimentAnnotationSummaryDataLoader
|
|
79
93
|
experiment_error_rates: ExperimentErrorRatesDataLoader
|
|
80
94
|
experiment_run_annotations: ExperimentRunAnnotations
|
|
81
95
|
experiment_run_counts: ExperimentRunCountsDataLoader
|
|
82
96
|
experiment_sequence_number: ExperimentSequenceNumberDataLoader
|
|
97
|
+
last_used_times_by_generative_model_id: LastUsedTimesByGenerativeModelIdDataLoader
|
|
83
98
|
latency_ms_quantile: LatencyMsQuantileDataLoader
|
|
84
99
|
min_start_or_max_end_times: MinStartOrMaxEndTimeDataLoader
|
|
85
100
|
num_child_spans: NumChildSpansDataLoader
|
|
86
101
|
num_spans_per_trace: NumSpansPerTraceDataLoader
|
|
102
|
+
project_by_name: ProjectByNameDataLoader
|
|
87
103
|
project_fields: TableFieldsDataLoader
|
|
104
|
+
project_trace_retention_policy_fields: TableFieldsDataLoader
|
|
88
105
|
projects_by_trace_retention_policy_id: ProjectIdsByTraceRetentionPolicyIdDataLoader
|
|
89
106
|
prompt_version_sequence_number: PromptVersionSequenceNumberDataLoader
|
|
90
107
|
record_counts: RecordCountDataLoader
|
|
@@ -96,6 +113,24 @@ class DataLoaders:
|
|
|
96
113
|
session_trace_latency_ms_quantile: SessionTraceLatencyMsQuantileDataLoader
|
|
97
114
|
span_annotations: SpanAnnotationsDataLoader
|
|
98
115
|
span_by_id: SpanByIdDataLoader
|
|
116
|
+
span_cost_by_span: SpanCostBySpanDataLoader
|
|
117
|
+
span_cost_detail_fields: TableFieldsDataLoader
|
|
118
|
+
span_cost_detail_summary_entries_by_generative_model: (
|
|
119
|
+
SpanCostDetailSummaryEntriesByGenerativeModelDataLoader
|
|
120
|
+
)
|
|
121
|
+
span_cost_detail_summary_entries_by_project_session: (
|
|
122
|
+
SpanCostDetailSummaryEntriesByProjectSessionDataLoader
|
|
123
|
+
)
|
|
124
|
+
span_cost_detail_summary_entries_by_span: SpanCostDetailSummaryEntriesBySpanDataLoader
|
|
125
|
+
span_cost_detail_summary_entries_by_trace: SpanCostDetailSummaryEntriesByTraceDataLoader
|
|
126
|
+
span_cost_details_by_span_cost: SpanCostDetailsBySpanCostDataLoader
|
|
127
|
+
span_cost_fields: TableFieldsDataLoader
|
|
128
|
+
span_cost_summary_by_experiment: SpanCostSummaryByExperimentDataLoader
|
|
129
|
+
span_cost_summary_by_experiment_run: SpanCostSummaryByExperimentRunDataLoader
|
|
130
|
+
span_cost_summary_by_generative_model: SpanCostSummaryByGenerativeModelDataLoader
|
|
131
|
+
span_cost_summary_by_project: SpanCostSummaryByProjectDataLoader
|
|
132
|
+
span_cost_summary_by_project_session: SpanCostSummaryByProjectSessionDataLoader
|
|
133
|
+
span_cost_summary_by_trace: SpanCostSummaryByTraceDataLoader
|
|
99
134
|
span_dataset_examples: SpanDatasetExamplesDataLoader
|
|
100
135
|
span_descendants: SpanDescendantsDataLoader
|
|
101
136
|
span_fields: TableFieldsDataLoader
|
|
@@ -104,11 +139,9 @@ class DataLoaders:
|
|
|
104
139
|
trace_by_trace_ids: TraceByTraceIdsDataLoader
|
|
105
140
|
trace_fields: TableFieldsDataLoader
|
|
106
141
|
trace_retention_policy_id_by_project_id: TraceRetentionPolicyIdByProjectIdDataLoader
|
|
107
|
-
project_trace_retention_policy_fields: TableFieldsDataLoader
|
|
108
142
|
trace_root_spans: TraceRootSpansDataLoader
|
|
109
|
-
project_by_name: ProjectByNameDataLoader
|
|
110
|
-
users: UsersDataLoader
|
|
111
143
|
user_roles: UserRolesDataLoader
|
|
144
|
+
users: UsersDataLoader
|
|
112
145
|
|
|
113
146
|
|
|
114
147
|
class _NoOp:
|
|
@@ -123,6 +156,7 @@ class Context(BaseContext):
|
|
|
123
156
|
cache_for_dataloaders: Optional[CacheForDataLoaders]
|
|
124
157
|
model: Model
|
|
125
158
|
export_path: Path
|
|
159
|
+
span_cost_calculator: SpanCostCalculator
|
|
126
160
|
last_updated_at: CanGetLastUpdatedAt = _NoOp()
|
|
127
161
|
event_queue: CanPutItem[DmlEvent] = _NoOp()
|
|
128
162
|
corpus: Optional[Model] = None
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
|
|
3
|
+
from phoenix.server.api.dataloaders.span_cost_detail_summary_entries_by_project_session import (
|
|
4
|
+
SpanCostDetailSummaryEntriesByProjectSessionDataLoader,
|
|
5
|
+
)
|
|
6
|
+
|
|
3
7
|
from .annotation_summaries import AnnotationSummaryCache, AnnotationSummaryDataLoader
|
|
4
8
|
from .average_experiment_run_latency import AverageExperimentRunLatencyDataLoader
|
|
5
9
|
from .dataset_example_revisions import DatasetExampleRevisionsDataLoader
|
|
@@ -15,6 +19,7 @@ from .experiment_error_rates import ExperimentErrorRatesDataLoader
|
|
|
15
19
|
from .experiment_run_annotations import ExperimentRunAnnotations
|
|
16
20
|
from .experiment_run_counts import ExperimentRunCountsDataLoader
|
|
17
21
|
from .experiment_sequence_number import ExperimentSequenceNumberDataLoader
|
|
22
|
+
from .last_used_times_by_generative_model_id import LastUsedTimesByGenerativeModelIdDataLoader
|
|
18
23
|
from .latency_ms_quantile import LatencyMsQuantileCache, LatencyMsQuantileDataLoader
|
|
19
24
|
from .min_start_or_max_end_times import MinStartOrMaxEndTimeCache, MinStartOrMaxEndTimeDataLoader
|
|
20
25
|
from .num_child_spans import NumChildSpansDataLoader
|
|
@@ -30,6 +35,20 @@ from .session_token_usages import SessionTokenUsagesDataLoader
|
|
|
30
35
|
from .session_trace_latency_ms_quantile import SessionTraceLatencyMsQuantileDataLoader
|
|
31
36
|
from .span_annotations import SpanAnnotationsDataLoader
|
|
32
37
|
from .span_by_id import SpanByIdDataLoader
|
|
38
|
+
from .span_cost_by_span import SpanCostBySpanDataLoader
|
|
39
|
+
from .span_cost_detail_summary_entries_by_generative_model import (
|
|
40
|
+
SpanCostDetailSummaryEntriesByGenerativeModelDataLoader,
|
|
41
|
+
)
|
|
42
|
+
from .span_cost_detail_summary_entries_by_span import SpanCostDetailSummaryEntriesBySpanDataLoader
|
|
43
|
+
from .span_cost_detail_summary_entries_by_trace import SpanCostDetailSummaryEntriesByTraceDataLoader
|
|
44
|
+
from .span_cost_details_by_span_cost import SpanCostDetailsBySpanCostDataLoader
|
|
45
|
+
from .span_cost_summary_by_experiment import SpanCostSummaryByExperimentDataLoader
|
|
46
|
+
from .span_cost_summary_by_experiment_run import SpanCostSummaryByExperimentRunDataLoader
|
|
47
|
+
from .span_cost_summary_by_generative_model import SpanCostSummaryByGenerativeModelDataLoader
|
|
48
|
+
from .span_cost_summary_by_project import SpanCostSummaryByProjectDataLoader, SpanCostSummaryCache
|
|
49
|
+
from .span_cost_summary_by_project_session import SpanCostSummaryByProjectSessionDataLoader
|
|
50
|
+
from .span_cost_summary_by_trace import SpanCostSummaryByTraceDataLoader
|
|
51
|
+
from .span_costs import SpanCostsDataLoader
|
|
33
52
|
from .span_dataset_examples import SpanDatasetExamplesDataLoader
|
|
34
53
|
from .span_descendants import SpanDescendantsDataLoader
|
|
35
54
|
from .span_projects import SpanProjectsDataLoader
|
|
@@ -42,23 +61,25 @@ from .user_roles import UserRolesDataLoader
|
|
|
42
61
|
from .users import UsersDataLoader
|
|
43
62
|
|
|
44
63
|
__all__ = [
|
|
45
|
-
"
|
|
64
|
+
"AnnotationSummaryDataLoader",
|
|
46
65
|
"AverageExperimentRunLatencyDataLoader",
|
|
66
|
+
"CacheForDataLoaders",
|
|
47
67
|
"DatasetExampleRevisionsDataLoader",
|
|
48
68
|
"DatasetExampleSpansDataLoader",
|
|
49
69
|
"DocumentEvaluationSummaryDataLoader",
|
|
50
70
|
"DocumentEvaluationsDataLoader",
|
|
51
71
|
"DocumentRetrievalMetricsDataLoader",
|
|
52
|
-
"AnnotationSummaryDataLoader",
|
|
53
72
|
"ExperimentAnnotationSummaryDataLoader",
|
|
54
73
|
"ExperimentErrorRatesDataLoader",
|
|
55
74
|
"ExperimentRunAnnotations",
|
|
56
75
|
"ExperimentRunCountsDataLoader",
|
|
57
76
|
"ExperimentSequenceNumberDataLoader",
|
|
77
|
+
"LastUsedTimesByGenerativeModelIdDataLoader",
|
|
58
78
|
"LatencyMsQuantileDataLoader",
|
|
59
79
|
"MinStartOrMaxEndTimeDataLoader",
|
|
60
80
|
"NumChildSpansDataLoader",
|
|
61
81
|
"NumSpansPerTraceDataLoader",
|
|
82
|
+
"ProjectByNameDataLoader",
|
|
62
83
|
"ProjectIdsByTraceRetentionPolicyIdDataLoader",
|
|
63
84
|
"PromptVersionSequenceNumberDataLoader",
|
|
64
85
|
"RecordCountDataLoader",
|
|
@@ -67,7 +88,21 @@ __all__ = [
|
|
|
67
88
|
"SessionNumTracesWithErrorDataLoader",
|
|
68
89
|
"SessionTokenUsagesDataLoader",
|
|
69
90
|
"SessionTraceLatencyMsQuantileDataLoader",
|
|
91
|
+
"SpanAnnotationsDataLoader",
|
|
70
92
|
"SpanByIdDataLoader",
|
|
93
|
+
"SpanCostBySpanDataLoader",
|
|
94
|
+
"SpanCostDetailSummaryEntriesByGenerativeModelDataLoader",
|
|
95
|
+
"SpanCostDetailSummaryEntriesByProjectSessionDataLoader",
|
|
96
|
+
"SpanCostDetailSummaryEntriesBySpanDataLoader",
|
|
97
|
+
"SpanCostDetailSummaryEntriesByTraceDataLoader",
|
|
98
|
+
"SpanCostDetailsBySpanCostDataLoader",
|
|
99
|
+
"SpanCostSummaryByExperimentDataLoader",
|
|
100
|
+
"SpanCostSummaryByExperimentRunDataLoader",
|
|
101
|
+
"SpanCostSummaryByGenerativeModelDataLoader",
|
|
102
|
+
"SpanCostSummaryByProjectDataLoader",
|
|
103
|
+
"SpanCostSummaryByProjectSessionDataLoader",
|
|
104
|
+
"SpanCostSummaryByTraceDataLoader",
|
|
105
|
+
"SpanCostsDataLoader",
|
|
71
106
|
"SpanDatasetExamplesDataLoader",
|
|
72
107
|
"SpanDescendantsDataLoader",
|
|
73
108
|
"SpanProjectsDataLoader",
|
|
@@ -76,10 +111,8 @@ __all__ = [
|
|
|
76
111
|
"TraceByTraceIdsDataLoader",
|
|
77
112
|
"TraceRetentionPolicyIdByProjectIdDataLoader",
|
|
78
113
|
"TraceRootSpansDataLoader",
|
|
79
|
-
"ProjectByNameDataLoader",
|
|
80
|
-
"SpanAnnotationsDataLoader",
|
|
81
|
-
"UsersDataLoader",
|
|
82
114
|
"UserRolesDataLoader",
|
|
115
|
+
"UsersDataLoader",
|
|
83
116
|
]
|
|
84
117
|
|
|
85
118
|
|
|
@@ -103,3 +136,6 @@ class CacheForDataLoaders:
|
|
|
103
136
|
token_count: TokenCountCache = field(
|
|
104
137
|
default_factory=TokenCountCache,
|
|
105
138
|
)
|
|
139
|
+
token_cost: SpanCostSummaryCache = field(
|
|
140
|
+
default_factory=SpanCostSummaryCache,
|
|
141
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import func, select
|
|
5
|
+
from strawberry.dataloader import DataLoader
|
|
6
|
+
from typing_extensions import TypeAlias
|
|
7
|
+
|
|
8
|
+
from phoenix.db import models
|
|
9
|
+
from phoenix.server.types import DbSessionFactory
|
|
10
|
+
|
|
11
|
+
GenerativeModelID: TypeAlias = int
|
|
12
|
+
Key: TypeAlias = GenerativeModelID
|
|
13
|
+
Result: TypeAlias = Optional[datetime]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LastUsedTimesByGenerativeModelIdDataLoader(DataLoader[Key, Result]):
|
|
17
|
+
def __init__(self, db: DbSessionFactory) -> None:
|
|
18
|
+
super().__init__(load_fn=self._load_fn)
|
|
19
|
+
self._db = db
|
|
20
|
+
|
|
21
|
+
async def _load_fn(self, keys: list[Key]) -> list[Result]:
|
|
22
|
+
async with self._db() as session:
|
|
23
|
+
last_used_times_by_model_id: dict[Key, Result] = {
|
|
24
|
+
model_id: last_used_time
|
|
25
|
+
async for model_id, last_used_time in await session.stream(
|
|
26
|
+
select(
|
|
27
|
+
models.SpanCost.model_id,
|
|
28
|
+
func.max(models.SpanCost.span_start_time).label("last_used_time"),
|
|
29
|
+
)
|
|
30
|
+
.select_from(models.SpanCost)
|
|
31
|
+
.where(models.SpanCost.model_id.in_(keys))
|
|
32
|
+
.group_by(models.SpanCost.model_id)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return [last_used_times_by_model_id.get(model_id) for model_id in keys]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import select
|
|
4
|
+
from strawberry.dataloader import DataLoader
|
|
5
|
+
from typing_extensions import TypeAlias
|
|
6
|
+
|
|
7
|
+
from phoenix.db import models
|
|
8
|
+
from phoenix.server.types import DbSessionFactory
|
|
9
|
+
|
|
10
|
+
SpanRowId: TypeAlias = int
|
|
11
|
+
Key: TypeAlias = SpanRowId
|
|
12
|
+
Result: TypeAlias = Optional[models.SpanCost]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SpanCostBySpanDataLoader(DataLoader[Key, Result]):
|
|
16
|
+
def __init__(self, db: DbSessionFactory) -> None:
|
|
17
|
+
super().__init__(load_fn=self._load_fn)
|
|
18
|
+
self._db = db
|
|
19
|
+
|
|
20
|
+
async def _load_fn(self, keys: list[Key]) -> list[Result]:
|
|
21
|
+
stmt = select(models.SpanCost).where(models.SpanCost.span_rowid.in_(keys))
|
|
22
|
+
async with self._db() as session:
|
|
23
|
+
result = {sc.span_rowid: sc async for sc in await session.stream_scalars(stmt)}
|
|
24
|
+
return list(map(result.get, keys))
|