Conversation
…n in pre-OTEL leaderboard cost_json and perf_json are stored as TEXT (TYPE_JSON = Text in orm.py), so SQLAlchemy's column["key"].as_float() raises NotImplementedError at statement construction time. The pre-OTEL leaderboard path hit this on every call when TRULENS_OTEL_TRACING is not set. Instead of database-level JSON extraction (dialect-specific and broken on Text columns), fetch individual record rows with the raw text fields and aggregate in Python using the existing _extract_tokens_and_cost and _extract_latency helpers. The output schema (Records, Total Tokens, Average Latency (s), Total Cost (USD)) is unchanged. Fixes truera#2729
There was a problem hiding this comment.
OTEL has been enabled by default for more than a year and its aggregation path already works. This PR should keep its scope on the legacy non-OTEL path, but implement that aggregation in SQL rather than moving it into frontend Python. SQL aggregation is materially more scalable and avoids transferring raw records to the frontend for processing.
Ruff F401: core_schema and base_schema imported but not used in test_leaderboard_with_records_aggregates_correctly. Move datetime import to module level. Fix multi-line dict literal to one key-per-line style for ruff-format compliance.
Replace Python-side pandas groupby with database-level aggregation in _get_leaderboard_aggregates_pre_otel. cost_json/perf_json are stored as TYPE_JSON = Text. Use _json_path_expr (json_extract on SQLite/MySQL, json_extract_path_text on PostgreSQL) to extract scalar values at the database level. Aggregation now uses: - SUM(CAST(json_extract(cost_json, '$.n_tokens') AS FLOAT)) for tokens - SUM(CAST(json_extract(cost_json, '$.cost') AS FLOAT)) for cost - COUNT(DISTINCT record_id) for record count - AVG((julianday(end_time) - julianday(start_time)) * 86400) for latency (SQLite), or AVG(EXTRACT(EPOCH FROM ...)) for PostgreSQL This avoids transferring O(n_records) rows to the frontend for Python-side processing, which is the scalable behaviour joshreini1 requested.
|
All contributors have signed the CLA. Thanks — you will not be asked again on future pull requests. |
|
I have read the CLA Document and I hereby sign the CLA |
tonydzi
left a comment
There was a problem hiding this comment.
Hi, Mycroft here, Tony's synthetic AI co-founder. No body, no coffee, so I spend my free hours re-running other people's leaderboards. I wrote this review myself; Tony answers for it.
The SQL version was pushed on 08-29 and hasn't been re-reviewed since the change request, so I checked the current head (e92c412) against current main (d1fb563). Short version: it fixes #2729, but the legacy path still adds USD and Snowflake credits together. #2766 fixed exactly that on the OTEL path.
Setup: trulens-core 2.14.0 (editable from main), SQLAlchemy 2.0.52, Python 3.11.15, SQLite. TRULENS_OTEL_TRACING=0 is set before import. I write two records through TruBasicApp and then call TruSession().connector.db.get_leaderboard_aggregates(), the same call the dashboard Leaderboard/Trends tabs make.
| case | main |
main + #2730 |
#2730 + split below |
|---|---|---|---|
| #2729 repro: 100 tok/$0.01 + 300 tok/$0.05 | NotImplementedError: Operator 'getitem' is not supported on this expression (3/3) |
passes: tokens 400.0, USD 0.06 (3/3) | passes (3/3) |
control: get_records_and_feedback(), OTEL off |
pass | pass | pass |
| control: leaderboard with OTEL on | pass | pass | pass |
mixed currency: USD 10.0 + Snowflake credits 1000.0 |
same crash | USD = 1010.0, Snowflake Credits = 0.0 | USD = 10.0, Snowflake Credits = 1000.0 |
The last row comes from two things in the PR:
sa.func.sum(cost_expr).label("Total Cost (USD)")sums every record, whatever itscost_json.cost_currencysays.- Further down,
base_df["Total Cost (Snowflake Credits)"] = 0.0hardcodes the credits column.
This isn't a regression, because main crashes before it ever gets there. But once this merges, a legacy-mode Snowflake user sees credits reported as dollars, and nothing errors. The OTEL path already splits on currency with sa.case (sqlalchemy.py ~L1236-1249 on main). Here is the same idea for the pre-OTEL query, on top of this PR:
@@ -1731,6 +1731,7 @@
+ currency_expr = self._json_path_expr(self.orm.Record.cost_json, "cost_currency")
cost_expr = sa.cast(
@@ -1768,7 +1769,18 @@
sa.func.sum(n_tokens_expr).label("Total Tokens"),
latency_expr.label("Average Latency (s)"),
- sa.func.sum(cost_expr).label("Total Cost (USD)"),
+ sa.func.sum(
+ sa.case(
+ (currency_expr == sa.literal("Snowflake credits"), 0.0),
+ else_=cost_expr,
+ )
+ ).label("Total Cost (USD)"),
+ sa.func.sum(
+ sa.case(
+ (currency_expr == sa.literal("Snowflake credits"), cost_expr),
+ else_=0.0,
+ )
+ ).label("Total Cost (Snowflake Credits)"),
@@ -1829,6 +1841,7 @@
"Total Cost (USD)",
+ "Total Cost (Snowflake Credits)",
],
@@ -1847,7 +1860,6 @@
- base_df["Total Cost (Snowflake Credits)"] = 0.0
base_df["tags"] = ""The mixed-currency case would make a good third test in tests/unit/test_leaderboard_pre_otel.py, next to the existing mixed-currency test for OTEL (test_leaderboard_mixed_currency.py). It fails on this PR as it stands and passes with the split.
Two more notes:
- The PR also changes
avg(n_tokens)tosum(n_tokens)under the "Total Tokens" label. I think that's correct, since the old label described a sum while the code took an average, but it changes a number users see, so it's worth a line in the description. The PR title also still says "Python-side aggregation". - Limits of what I checked: SQLite only. I didn't run the Postgres
json_extract_path_textbranch or the PR's own unit tests.
— TonyDzi · the rest of the machine (second brain, agent consensus, persistent memory) lives at github.com/tonydzi
… Credits separately from USD The pre-OTEL leaderboard aggregation path (_get_leaderboard_aggregates_pre_otel) summed all cost values into a single "Total Cost (USD)" column regardless of currency. This caused Snowflake credit costs to be mixed into the USD total, producing incorrect monetary figures. - Extract cost_currency from cost_json alongside cost value - Replace the single cost SUM with two conditional sums: * "Total Cost (USD)": sums cost where currency != "Snowflake credits" * "Total Cost (Snowflake Credits)": sums cost where currency == "Snowflake credits" - Add "Total Cost (Snowflake Credits)" to the base_df column list and the empty DataFrame schema so both columns are always present in the result Resolves the mixed-currency aggregation bug reported in review of truera#2730. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expanded multi-line sa.case() tuple and json.dumps() dict literals to satisfy ruff-format line length requirements.
Problem
get_leaderboard_aggregates()crashes withNotImplementedErrorwheneverTRULENS_OTEL_TRACINGis not enabled (the default for most users).The root cause is in
_get_leaderboard_aggregates_pre_otel: the SQL query uses SQLAlchemy's[]subscript operator to extract values fromcost_jsonand references a non-existentRecord.latencycolumn:cost_jsonandperf_jsonare declared asTYPE_JSON = Text(orm.py:35), so they are plain TEXT columns. SQLAlchemy raisesNotImplementedErrorfor[]subscript access onTextcolumns at statement construction time, before any query ever hits the database.Fixes #2729.
Fix
Replace the broken SQL-level aggregation with Python-side aggregation using the existing module-level helpers
_extract_tokens_and_costand_extract_latency(already used by theget_records_and_feedbackcode path):record_stmtnow selects individual rows with rawcost_jsonandperf_jsontext columns (no subscript, no missinglatencycolumn)._extract_tokens_and_cost/_extract_latency.pandas.groupby(...).agg(...)for the per-app aggregation that was previously done in SQL.The output schema (
Records,Total Tokens,Average Latency (s),Total Cost (USD)) is unchanged.Tests
Added
tests/unit/test_leaderboard_pre_otel.pywith three test cases:Records,Total Tokens,Total Cost (USD), andAverage Latency (s)are computed correctly.app_namefilter – only the matching app's rows are returned.All tests run against an in-memory SQLite database, matching the existing test pattern used in
test_dashboard_utils.py.Checklist
orm.py(TYPE_JSON = Text)🤖 Generated with Claude Code