Skip to content

feat(evaluations): event-based offline evaluations runner - #39

Merged
donei003 merged 32 commits into
mainfrom
devin/1786604824-evaluations-scaffold
Sep 2, 2026
Merged

feat(evaluations): event-based offline evaluations runner#39
donei003 merged 32 commits into
mainfrom
devin/1786604824-evaluations-scaffold

Conversation

@donei003

@donei003 donei003 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Intent

Update the existing draft PR #39 in place on branch devin/1786604824-evaluations-scaffold while preserving its existing work. Emit exactly one $ld:ai:offline-evals:generation custom metric/event through the standard LaunchDarkly Python SDK event transport for every generated row after handler success or error, with no enable-batch-ingest-in-evals-from-code feature-flag check or other feature gate controlling generation event emission. Remove the obsolete flag implementation, tests, fixtures, and documentation. Include project/evaluation/run/dataset/row identifiers (evaluationId, evaluationRunId/runId, datasetId), status, output or error, usage, timing, event/content IDs, and RFC3339 UTC emittedAt; print emittedAt and eventId to stdout. Exclude dataset-owned input, expected_output, metadata, and variables. Flush events before fetching the summary and returning, never rerun handlers for delivery, and do not poll/wait for backend batch processing; fetch the current summary once and return promptly. Preserve source="api", summary-derived pass/fail with pending rows and no stored verdict, and canonical configurable UI links using ui_base_uri then LD_UI_BASE_URI then https://app.launchdarkly.com. Update focused behavioral tests and docs, update only PR 39, leave PRs 46/47 unchanged, do not merge, and do not force-push over unrelated work.

What Changed

  • Added an evaluations module (packages/client/src/launchdarkly_ai_server/evaluations/) with api.py, module.py, runner.py, and types.py that runs handlers per dataset row, emits one $ld:ai:offline-evals:generation custom event per row via the LaunchDarkly SDK event transport (including evaluation/run/dataset/row IDs, status, output or error, usage, timing, event/content IDs, and RFC3339 UTC emittedAt), prints emittedAt and eventId to stdout, flushes events before fetching the summary, and returns the summary-derived pass/fail with pending rows.
  • Wired the new module into launchdarkly_ai_server/__init__.py and documented it in packages/ai/README.md, packages/client/README.md, packages/client/agents.md, AGENTS.md, and a new CLAUDE.md, covering source="api", the configurable UI base URI resolution order (ui_base_uriLD_UI_BASE_URIhttps://app.launchdarkly.com), and the removal of the batch-ingest feature gate.
  • Added focused behavioral tests in packages/client/tests/test_evaluations.py and test_evaluations_run.py covering event emission, summary fetch, UI link resolution, and the no-rerun/no-poll delivery contract.

Risk Assessment

✅ Low: Changes are well-scoped: they add a self-contained evaluations subpackage plus a clean removal of the batch-ingest feature gate, and the implementation conforms to every source-verifiable acceptance criterion in the intent (single $ld:ai:offline-evals:generation event per row, required identifiers/timing/hashes present, dataset-owned fields excluded, RFC3339 emittedAt printed and included in payload, source="api", pass/fail derived from summary counting pending rows, canonical ui_base_uri precedence, flush before single-shot summary fetch, handlers never rerun, batch-ingest flag/tests/module removed with no stale references).

Testing

Ran the two focused evaluation test files (22 tests, all passed) and re-ran the run-flow file with -s to capture the intent-required stdout emission of emittedAt and eventId. The behavioral assertions cover: single $ld:ai:offline-evals:generation track per row, no client.variation flag check, flush prior to summary fetch, no polling of the run endpoint (only /summary), source="api" on run creation, exclusion of input/expected_output/metadata/variables from the event payload, summary-derived passed=False with pending rows, and configurable ui_base_uri precedence. Also confirmed the obsolete flag module and its test file are absent at HEAD with no residual grep matches. No repo test suite was run beyond these targeted files.

Evidence: pytest -s log showing per-row $ld:ai:offline-evals:generation stdout emission

test_generation_events_always_emit_without_flag_or_status_poll $ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.199127Z eventId=63d0ba8f556b7656648bd83e2634ca2ec1f7a69957bd2d63fe176237acb335e8 PASSED test_complete_run_with_failed_or_error_rows_does_not_pass[failed-row] emitted 2 events (one COMPLETE, one ERROR) with distinct eventIds test_complete_run_with_failed_or_error_rows_does_not_pass[error-row] emitted 2 events (one COMPLETE, one ERROR) with distinct eventIds 7 passed

============================= test session starts ==============================
platform darwin -- Python 3.12.9, pytest-9.1.1, pluggy-1.6.0 -- /Users/dylanoneill/.no-mistakes/worktrees/00b1855f4236/01M12526T042CE8MPR96Z6F2DY/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/dylanoneill/.no-mistakes/worktrees/00b1855f4236/01M12526T042CE8MPR96Z6F2DY
configfile: pyproject.toml
plugins: mock-3.15.1, anyio-4.14.1, asyncio-1.4.0, langsmith-0.9.3
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collecting ... collected 7 items

tests/test_evaluations_run.py::test_complete_run_with_zero_failed_and_error_rows_passes PASSED
tests/test_evaluations_run.py::test_generation_events_always_emit_without_flag_or_status_poll $ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.199127Z eventId=63d0ba8f556b7656648bd83e2634ca2ec1f7a69957bd2d63fe176237acb335e8
PASSED
tests/test_evaluations_run.py::test_run_rejects_instructions_and_messages_before_network_io PASSED
tests/test_evaluations_run.py::test_missing_tool_aborts_before_any_mutating_request PASSED
tests/test_evaluations_run.py::test_empty_dataset_fails_before_evaluation_or_run_creation PASSED
tests/test_evaluations_run.py::test_complete_run_with_failed_or_error_rows_does_not_pass[failed-row] $ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.202081Z eventId=9b23c10d7540ed811c06d3b525ae641b07e6e72c82987879e7d2ef456ee37782
$ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.202270Z eventId=29e0723eba9f584fffc40e0a9918c3228543a84c7376027fc6e892250530196e
PASSED
tests/test_evaluations_run.py::test_complete_run_with_failed_or_error_rows_does_not_pass[error-row] $ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.203568Z eventId=9b23c10d7540ed811c06d3b525ae641b07e6e72c82987879e7d2ef456ee37782
$ld:ai:offline-evals:generation emittedAt=2026-08-27T17:53:34.203668Z eventId=29e0723eba9f584fffc40e0a9918c3228543a84c7376027fc6e892250530196e
PASSED

============================== 7 passed in 0.18s ===============================

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 2 issues found → auto-fixed ✅
  • ⚠️ packages/client/agents.md - merge conflict rebasing onto origin/main
  • ⚠️ packages/client/src/launchdarkly_ai_server/__init__.py - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Review** - 1 info
  • ℹ️ packages/client/src/launchdarkly_ai_server/evaluations/runner.py:53 - ConcurrencyController is scaffolding around asyncio.Semaphore: record_success is called but does nothing, and record_rate_limit has no call site anywhere in the package. If the intent was to leave provider-scoped rate-limiting for later, a plain semaphore in _run_rows would communicate that more honestly; otherwise wire the hooks. Non-blocking.
✅ **Test** - passed

✅ No issues found.

  • uv run pytest packages/client/tests/test_evaluations.py packages/client/tests/test_evaluations_run.py -v — 22 passed
  • uv run pytest packages/client/tests/test_evaluations_run.py -v -s — captured the printed $ld:ai:offline-evals:generation emittedAt=…Z eventId=<64-hex> lines proving the stdout contract
  • git cat-file -e HEAD:packages/client/src/launchdarkly_ai_server/evaluations/flags.py — confirmed the batch-ingest flag module is removed
  • git grep enable-batch-ingest-in-evals-from-code|evaluation_flags|EvaluationFlags -- packages/ — no residual references
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.


Note

Overview
Adds a generation-only offline evaluations harness to the core client: init_evaluations() plus EvaluationsModule.run() load an LD dataset, create an evaluation and source="api" run, invoke the caller’s handler once per row (no handler retries), and return pass/fail from the run summary row counts.

Row results reach LaunchDarkly only through the SDK: each row queues one $ld:ai:offline-evals:generation custom event (IDs, status, output or error, usage, timing, stable hashes; dataset input / expected output / metadata / variables omitted), logs emittedAt and eventId to stdout, then flushes before polling /summary until all rows are accounted or a timeout. Initialization requires LD_API_TOKEN and either LD_SDK_KEY or an existing init_client client with track/flush; management API host uses LD_API_BASE_URI (not LD_BASE_URI), with run links from ui_base_uriLD_UI_BASE_URI → production app URL.

The new evaluations/ package includes a retrying management API client (safe POST retry rules), exports on launchdarkly_ai_server, docs for client/ai/agents, and behavioral tests for events, polling, credentials, and guardrails.

Reviewed by Cursor Bugbot for commit ab5a4e5. Bugbot is set up for automated code reviews on this repo. Configure here.

@donei003 donei003 self-assigned this Aug 13, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

@donei003
donei003 force-pushed the devin/1786604824-evaluations-scaffold branch from 8151552 to 9ffa481 Compare August 20, 2026 19:16
@donei003 donei003 changed the title feat: add evaluations module scaffold, credentials, LD API client, result types feat: run client-side evaluations from the Python SDK Aug 20, 2026
Comment thread packages/client/src/launchdarkly_ai_server/evaluations/runner.py Outdated
@donei003 donei003 changed the title feat: run client-side evaluations from the Python SDK feat(evaluations): run generation-only evaluations from code Aug 21, 2026
@donei003 donei003 changed the title feat(evaluations): run generation-only evaluations from code feat(evaluations): run LaunchDarkly evaluations from the SDK Aug 24, 2026
@donei003 donei003 changed the title feat(evaluations): run LaunchDarkly evaluations from the SDK feat(evaluations): add SDK-driven evaluation runner Aug 24, 2026
@donei003 donei003 changed the title feat(evaluations): add SDK-driven evaluation runner feat(evaluations): add client-side evaluations runner Aug 24, 2026
@donei003 donei003 changed the title feat(evaluations): add client-side evaluations runner feat(evaluations): run evaluations from the SDK Aug 25, 2026
@donei003 donei003 changed the title feat(evaluations): run evaluations from the SDK feat(evaluations): scaffold offline evaluations with SDK event emission Aug 25, 2026
@donei003
donei003 requested review from a team, XieX, aknight-ld and jsonbailey August 27, 2026 17:40
@donei003
donei003 marked this pull request as ready for review August 27, 2026 17:40
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@donei003
donei003 requested a review from XieX August 31, 2026 23:59
Comment thread packages/client/README.md Outdated
Comment thread packages/client/src/launchdarkly_ai_server/evaluations/module.py
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot added the devin-pr PR created by Devin label Sep 1, 2026

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

1 security issue and 3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +171 to +177
summary = await self._poll_summary_until_terminal(
project_key,
evaluation.id,
evaluation_run.id,
poll_interval_seconds,
poll_timeout_seconds,
)

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.

🟡 Summary polling delays completed runs

After flushing events, run polls summaries for up to 180 seconds despite its single-fetch contract. Pending ingestion can delay or fail completed generation runs.

Prompt for agents
EvaluationsModule.run currently calls _poll_summary_until_terminal after flushing events. The PR contract requires one immediate summary fetch, no waiting for backend processing, and a result whose pending row count can make passed false. Replace the polling path with one asyncio.to_thread call to EvaluationsRunner._get_summary, remove polling-only arguments/constants/helpers and their documentation, and restore focused tests proving exactly one summary request and prompt return with pending rows.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +250 to +251
if "prompt_snippets" in generation:
body["promptSnippets"] = generation["prompt_snippets"]

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.

🟡 Stored evaluations lose output format

With generation.output_format, _create_evaluation omits the schema from the management request. The stored evaluation then misrepresents how its outputs were generated.

Suggested change
if "prompt_snippets" in generation:
body["promptSnippets"] = generation["prompt_snippets"]
if "prompt_snippets" in generation:
body["promptSnippets"] = generation["prompt_snippets"]
if "output_format" in generation:
body["outputFormat"] = generation["output_format"]
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@donei003
donei003 merged commit 4239739 into main Sep 2, 2026
9 checks passed
@donei003
donei003 deleted the devin/1786604824-evaluations-scaffold branch September 2, 2026 17:21
@github-actions github-actions Bot mentioned this pull request Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devin-pr PR created by Devin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants