Skip to content

feat(intake): add column sorting to evaluation sessions endpoint [ASE-591]#780

Merged
walston merged 9 commits into
mainfrom
nwalston/ase-591-column-sorting
Jul 20, 2026
Merged

feat(intake): add column sorting to evaluation sessions endpoint [ASE-591]#780
walston merged 9 commits into
mainfrom
nwalston/ase-591-column-sorting

Conversation

@walston

@walston walston commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds multi-field column sorting to the GET /v2/workspaces/{workspace}/evaluations/{name}/sessions endpoint. Sort composes globally with pagination (ORDER BY runs in ClickHouse before LIMIT/OFFSET).

Changes

Endpoint (endpoints.py)

  • New _SESSION_SORT_FIELDS whitelist constant (module-level, per codebase convention)
  • New _parse_session_sort_keys(sort) parser: comma-separated -field format, 422 on invalid field
  • Added sort: str | None = Query(default=None) param to list_evaluation_sessions
  • Parsed sort_keys threaded to list_sessions (raw string never reaches the repository)

Repository + SQL (evaluation_session_repository.py)

  • list_sessions accepts sort_keys: list[tuple[str, bool]] | None = None
  • _build_order_by: builds a comma-separated ORDER BY clause from sort keys + tiebreaker; NULLS LAST on all user keys
  • _pre_page_metrics_cte_sql: computes cost/tokens for all sessions before pagination when sorting by those fields — necessary because those values are not in trace_index and would otherwise only be computed for the current page
  • _list_sql now accepts sort_keys and generates dynamic ORDER BY in both page_sessions and the final SELECT; conditionally injects pre_page_metrics CTE for cost/tokens sorts
  • Default (no sort param): preserves original start_time ASC, root_span_id ASC behaviour

Sortable fields

test_case_id, started_at, ended_at, latency_ms, status, cost_total_usd, tokens

Tests

  • Updated existing payload-mode tests to pass the now-required sort_keys arg
  • Added: default order, single-field asc/desc, multi-field, pre_page_metrics injection for cost/tokens, tiebreaker presence, _build_order_by unit tests

OpenAPI

  • make refresh-openapi run; ListEvaluationSessionsParams gains sort
  • make stainless (Python SDK sync) requires STAINLESS_API_KEY — run separately

Out of scope

Frontend wiring — separate PR on a separate branch (ASE-591 FE)

Closes ASE-591 (partial — BE only)

Summary by CodeRabbit

  • New Features
    • Added an optional sort query parameter for evaluation sessions, supporting comma-separated multi-field ordering with - for descending and default ordering by started_at ascending when omitted.
    • Sorting supports specific session fields and metric-based ordering (for example, cost and token totals).
    • Updated the endpoint’s labeling to align with the “Experiments” category.
  • Bug Fixes
    • Ensured deterministic, stable ordering across pagination with consistent tie-breaking.
    • Expanded validation and error behavior for unsupported/empty sort (400) and requests too large to sort by metrics (413), while retaining 503 for backend unavailability.

walston added 3 commits July 20, 2026 07:42
Add _SESSION_SORT_FIELDS whitelist and _parse_session_sort_keys parser.
The parser validates field names and converts the comma-separated sort
string to an ordered list of (field, descending) tuples, raising 422
on an unknown field. Parsed sort_keys are threaded through to
list_sessions (repository layer) rather than passing the raw string,
keeping validation at the HTTP boundary.

Signed-off-by: Nathan Walston <[email protected]>
Replace hardcoded ORDER BY with user-driven multi-field sort in
_list_sql. Sort composes with LIMIT/OFFSET in ClickHouse so it is
global (not per-page).

For trace_index columns (started_at, ended_at, latency_ms, status,
test_case_id): swap the ORDER BY in page_sessions directly — the
values exist in scoped_sessions without any join.

For cost_total_usd and tokens: inject a pre_page_metrics CTE that
aggregates spans for ALL scoped sessions before pagination runs. This
makes the ORDER BY see the full result set, not just the current page.

NULLS LAST on every user key; root_span_id always appended as a stable
tiebreaker. Default (no sort param) preserves the original start_time
ASC behaviour.

Tests: default order preserved, single-field asc/desc, multi-field,
pre_page_metrics injection for cost/tokens, tiebreaker presence,
_build_order_by unit tests.

Signed-off-by: Nathan Walston <[email protected]>
make refresh-openapi adds sort to ListEvaluationSessionsParams.
make stainless (Python SDK sync) requires STAINLESS_API_KEY — run
separately once the key is available.

Signed-off-by: Nathan Walston <[email protected]>
@walston
walston requested review from a team as code owners July 20, 2026 14:45
@github-actions github-actions Bot added the feat label Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Evaluation session listing now supports validated multi-field sorting, including cost and token metrics, with updated API documentation, paginated ClickHouse ordering, size-limit handling, and SQL-generation tests.

Changes

Evaluation session sorting

Layer / File(s) Summary
API sort contract and wiring
openapi/ga/..., services/intake/.../experiments/endpoints.py
Adds the optional sort parameter, documents supported fields and responses, validates ordered fields, and passes parsed keys to the repository.
Repository ordering and pagination
services/intake/.../spans/evaluation_session_repository.py
Builds dynamic ordering, adds deterministic tie-breaking and NULLS LAST, computes metrics for cost/token sorting, and enforces a session limit.
Sorting query coverage
services/intake/tests/test_evaluation_session_clickhouse_repository.py
Tests default, single-field, multi-field, metric, tie-breaker, and generated ORDER BY behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SessionsEndpoint
  participant EvaluationSessionRepository
  participant ClickHouse
  Client->>SessionsEndpoint: Request sessions with sort
  SessionsEndpoint->>SessionsEndpoint: Validate and parse sort fields
  SessionsEndpoint->>EvaluationSessionRepository: Pass sort_keys
  EvaluationSessionRepository->>ClickHouse: Execute paginated ordered query
  ClickHouse-->>Client: Return sorted sessions
Loading

Possibly related PRs

Suggested reviewers: briannewsom, shanaiabuggy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding column sorting to the evaluation sessions endpoint.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nwalston/ase-591-column-sorting

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
services/intake/src/nmp/intake/spans/evaluation_session_repository.py (1)

229-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cross-file allowlist coupling is fragile.

_build_order_by does a bare expr_map[field] lookup. Correctness depends on _SESSION_SORT_FIELDS in endpoints.py staying exactly in sync with _SORT_EXPR_PAGE/_SORT_EXPR_FINAL here. A future field added to one but not the other becomes an unhandled KeyError (500) instead of the intended 400. Consider deriving the endpoint allowlist from _SORT_EXPR_PAGE.keys() & _SORT_EXPR_FINAL.keys(), or asserting parity in a test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/intake/src/nmp/intake/spans/evaluation_session_repository.py` around
lines 229 - 241, The sort-field allowlist is duplicated across endpoint and
repository modules, allowing unsupported fields to reach _build_order_by and
raise KeyError. Derive _SESSION_SORT_FIELDS from the intersection of
_SORT_EXPR_PAGE and _SORT_EXPR_FINAL keys, or add an explicit parity test, so
only fields supported by both expression maps are accepted and invalid input
remains a 400.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/intake/src/nmp/intake/api/v2/experiments/endpoints.py`:
- Around line 687-690: The session endpoint’s sort parameter uses a truncated
placeholder description. Update _parse_session_sort_keys’s endpoint definition
in services/intake/src/nmp/intake/api/v2/experiments/endpoints.py:687-690 with a
complete description covering all 7 supported fields, “-” descending prefixes,
comma-separated multi-field ordering, and omitted-value defaults; then
regenerate the corresponding descriptions in
openapi/ga/individual/platform.openapi.yaml:3688-3695,
openapi/ga/openapi.yaml:3688-3695, and openapi/openapi.yaml:3685-3695.
- Around line 1044-1067: Reconcile the sort-validation contract around
_parse_session_sort_keys: keep its existing 400 behavior consistent with sibling
validators, update the PR description if 422 is unintended, and extend the
endpoint’s OpenAPI 400 response documentation to include invalid or empty sort
fields.

In `@services/intake/src/nmp/intake/spans/evaluation_session_repository.py`:
- Around line 54-63: Update _pre_page_metrics_cte_sql’s total_tokens calculation
and _SORT_EXPR_FINAL["tokens"] to coalesce each guarded input/output token sum
to zero before addition, while preserving NULL only when both sums are NULL.
Ensure sessions with only one recorded token type receive that value for sorting
rather than a NULL result.
- Around line 243-271: The _pre_page_metrics_cte_sql path performs unbounded
aggregation for cost/token sorting; add the same size guard used by the sibling
evaluations metric-sort flow. In list_sessions, use the already-available total
before invoking this query, and return the established 413 response when it
exceeds the configured threshold, while preserving normal pagination for
eligible result sizes.

---

Nitpick comments:
In `@services/intake/src/nmp/intake/spans/evaluation_session_repository.py`:
- Around line 229-241: The sort-field allowlist is duplicated across endpoint
and repository modules, allowing unsupported fields to reach _build_order_by and
raise KeyError. Derive _SESSION_SORT_FIELDS from the intersection of
_SORT_EXPR_PAGE and _SORT_EXPR_FINAL keys, or add an explicit parity test, so
only fields supported by both expression maps are accepted and invalid input
remains a 400.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ec4517ec-787d-403a-8722-cc0aaf9dcfc3

📥 Commits

Reviewing files that changed from the base of the PR and between 0f5a0b2 and c4d0a55.

📒 Files selected for processing (6)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • services/intake/src/nmp/intake/spans/evaluation_session_repository.py
  • services/intake/tests/test_evaluation_session_clickhouse_repository.py

Comment thread services/intake/src/nmp/intake/api/v2/experiments/endpoints.py Outdated
Comment thread services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
Comment thread services/intake/src/nmp/intake/spans/evaluation_session_repository.py Outdated
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 26385/34036 77.5% 61.8%
Integration Tests 15176/32661 46.5% 18.7%

@shanaiabuggy shanaiabuggy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving with 1 comment! also tagging in @BrianNewsom to do a quick review as the clickhouse connoisseur

Comment thread services/intake/src/nmp/intake/api/v2/experiments/endpoints.py Outdated
- Sort param description: replace placeholder with full docs covering
  all 7 sortable fields, '-' prefix semantics, and default behaviour.
- Response codes: add 413 and 422 to endpoint responses dict; add
  'Invalid sort field' note to 400 description.
- Token NULL propagation: coalesce each guarded sum to 0 before adding
  so sessions with only input or only output tokens sort by their
  partial usage rather than NULL-ing to the bottom via NULLS LAST.
  NULL is preserved only when both sides are absent.
- Pre-metrics size cap: raise MetricSortTooLargeError (domain exception)
  when a cost/tokens sort is requested on more than 10 000 sessions.
  The endpoint catches it and returns 413, mirroring the evaluations
  list cap. The limit and upgrade path are documented in comments.

Signed-off-by: Nathan Walston <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openapi/openapi.yaml`:
- Around line 3692-3704: The sort documentation in the OpenAPI schema conflicts
with _parse_session_sort_keys, which returns 400 for unsupported or empty sort
values. Align both sort descriptions and the endpoint behavior by either
changing _parse_session_sort_keys to return 422 as documented or updating the
OpenAPI descriptions to state 400, then regenerate the specs consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f65b298d-0911-4716-a4b2-03341c3957b5

📥 Commits

Reviewing files that changed from the base of the PR and between c4d0a55 and 7a3824a.

⛔ Files ignored due to path filters (5)
  • sdk/python/nemo-platform/.github/workflows/ci.yml is excluded by !sdk/**
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/evaluations/sessions.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/evaluations/session_list_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/evaluations/test_sessions.py is excluded by !sdk/**
📒 Files selected for processing (6)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • services/intake/src/nmp/intake/spans/evaluation_session_repository.py
  • services/intake/tests/test_evaluation_session_clickhouse_repository.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • openapi/ga/openapi.yaml
  • services/intake/src/nmp/intake/api/v2/experiments/endpoints.py
  • openapi/ga/individual/platform.openapi.yaml
  • services/intake/src/nmp/intake/spans/evaluation_session_repository.py
  • services/intake/tests/test_evaluation_session_clickhouse_repository.py

Comment thread openapi/openapi.yaml Outdated
walston added 2 commits July 20, 2026 10:13
_parse_session_sort_keys raises 400 (matching sibling _parse_sort_keys).
Remove 422 from the endpoint responses dict and fold the sort-error
case into the existing 400 description.

Signed-off-by: Nathan Walston <[email protected]>
@BrianNewsom

Copy link
Copy Markdown
Contributor

Performance concern for cost_total_usd/tokens sorts: ClickHouse 24.3 inlines CTEs rather than materializing them. page_sessions is referenced by current_page_spans, session_metrics, session_scores, and the
final SELECT; because page_sessions depends on pre_page_metrics, the all-session span aggregation may execute once per reference (it will do the expensive queries 4x)

Suggested direction:

Query 1: count scoped sessions

Query 2: compute sorted page
scoped sessions
→ aggregate token/cost metric across their spans once
→ ORDER BY + LIMIT/OFFSET
→ ordered page of session IDs

Query 3: hydrate page
ordered session IDs (≤ 1000)
→ fetch session fields
→ aggregate page spans
→ fetch evaluator scores
→ restore Query 2 ordering in Python

This should preserve global metric sorting while ensuring the expensive all-session aggregation runs once per request.

ClickHouse 24.3 inlines CTEs rather than materialising them, so the
previous single-query approach re-executed the all-session span
aggregation (pre_page_metrics) once per downstream CTE reference
(current_page_spans, session_metrics, session_scores, final SELECT) —
4x per request.

Split the pre-metrics sort into two separate queries:

  Query 2 (_metric_sort_page_ids_sql): aggregate cost/tokens once
  across all scoped sessions, apply ORDER BY + LIMIT/OFFSET, return
  ordered (workspace, session_id) pairs only.

  Query 3 (_hydrate_by_ids_sql): given the page session IDs, fetch
  session fields, page-bounded span metrics, and evaluator scores.
  No ORDER BY — caller restores Query 2's ordering in Python.

This ensures the expensive all-session aggregation runs exactly once
per request. The single-query path (_list_sql) is unchanged and still
used for trace_index column sorts where CTE inlining is harmless.

Signed-off-by: Nathan Walston <[email protected]>
@walston
walston enabled auto-merge July 20, 2026 19:55
@walston
walston added this pull request to the merge queue Jul 20, 2026
Merged via the queue into main with commit 0ec73d9 Jul 20, 2026
58 checks passed
@walston
walston deleted the nwalston/ase-591-column-sorting branch July 20, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants