Skip to content

feat(evaluations): run generation-only evaluations from code - #39

Draft
donei003 wants to merge 7 commits into
mainfrom
devin/1786604824-evaluations-scaffold
Draft

feat(evaluations): run generation-only evaluations from code#39
donei003 wants to merge 7 commits into
mainfrom
devin/1786604824-evaluations-scaffold

Conversation

@donei003

@donei003 donei003 commented Aug 13, 2026

Copy link
Copy Markdown

Intent

Update existing draft PR #39 in place by moving into its independently reviewable diff the generation-result publishing/ingestion feature-flag gate currently contributed by stacked draft PR #44. Preserve the captain-selected current PR 44 HEAD contract exactly: use the single canonical flag key/constant enable-batch-ingest-in-evals-from-code; when LD_SDK_KEY is configured, only a variation that is strictly true enables generation-result ingest/publish, while false/default, malformed values, or evaluation errors safely skip ingest/publish; when no SDK key is configured and the gate cannot be evaluated, retain PR 39 existing generation-result ingestion behavior. Integrate the gate naturally with PR 39 generation-only evaluation publishing, with executable no-network coverage for enabled, disabled/default, no-SDK, malformed, evaluation-error-safe, and publishing/no-publishing behavior. Do not move PR 44 judge/scorer functionality or unrelated changes. Avoid duplicate constants, evaluations, tests, or documentation, preserve PR 39 existing generation-only SDK evaluation behavior outside the selected gate contract, and document the behavior accurately. Keep PR 39 draft, update its existing remote branch without opening a new PR, never force-push, and ensure the stacked outcome leaves the gate in PR 39 independent responsibility rather than PR 44.

What Changed

  • Added a new launchdarkly_ai_server.evaluations module — init_evaluations(), an LDApiClient for the management API, and EvaluationsModule.run() which resolves tools, fetches the dataset, creates the evaluation + client-source run, invokes the handler once per row, ingests generations, and returns LaunchDarkly's stored verdict.
  • Gated generation-result batch ingest on the enable-batch-ingest-in-evals-from-code flag: when LD_SDK_KEY is set, only a strictly true variation publishes results, while false/default, malformed, or failed evaluations skip publish; without an SDK key the gate is unevaluable and ingest runs unconditionally.
  • Re-exported init_evaluations and result types from the package barrel and documented the new entry point, env vars, and gate contract in the client and launchdarkly-ai-python READMEs and agents.md, alongside no-network tests covering the runner, flag gate, and end-to-end run.

Risk Assessment

✅ Low: The flag gate is a small, well-bounded change: a single canonical flag constant, fail-safe evaluation (identity check on True; broad exception catch returning False), and one call site that only runs when an SDK key is configured; executable no-network tests exercise enabled, disabled, malformed, evaluation-error, no-SDK, and publishing/no-publishing paths.

Testing

Targeted packages/client/tests/test_evaluation_flags.py and packages/client/tests/test_evaluations_run.py under uv run pytest: 16/16 pass in 0.06s with a SequencedTransport mock that raises on any unexpected HTTP call, so all scenarios are strictly no-network. The parametrized ingest test proves the enable-batch-ingest-in-evals-from-code gate governs the /generation-results POST (enabled → POST; disabled/malformed/eval-error → no POST), and the existing order-preservation test with LD_SDK_KEY unset confirms the pre-PR-39 ingest behavior is preserved when no SDK key is present. Flag-helper unit tests directly cover the malformed and evaluation-error safety branches. This is behavior-level evidence of the intent working end-to-end for CLI callers; no worktree artifacts were created and no source files modified. Coverage is sufficient; no findings.

Evidence: gate-test-output

16 passed in 0.06s — flags helper + run() gate scenarios (enabled, disabled-default, malformed, evaluation-error, no-SDK, publishing/no-publishing) all exercise the public interface with SequencedTransport (no network).

============================= test session starts ==============================
platform darwin -- Python 3.12.9, pytest-9.1.1, pluggy-1.6.0 -- /Users/dylanoneill/.no-mistakes/worktrees/00b1855f4236/01M0K380M84NNSP0707B1VTMCK/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/dylanoneill/.no-mistakes/worktrees/00b1855f4236/01M0K380M84NNSP0707B1VTMCK
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 16 items

packages/client/tests/test_evaluation_flags.py::test_enabled_flag_enables_generation_result_batch_ingest PASSED [  6%]
packages/client/tests/test_evaluation_flags.py::test_disabled_flag_disables_generation_result_batch_ingest PASSED [ 12%]
packages/client/tests/test_evaluation_flags.py::test_malformed_flag_disables_generation_result_batch_ingest[None] PASSED [ 18%]
packages/client/tests/test_evaluation_flags.py::test_malformed_flag_disables_generation_result_batch_ingest[1] PASSED [ 25%]
packages/client/tests/test_evaluation_flags.py::test_malformed_flag_disables_generation_result_batch_ingest[true] PASSED [ 31%]
packages/client/tests/test_evaluation_flags.py::test_malformed_flag_disables_generation_result_batch_ingest[malformed_value3] PASSED [ 37%]
packages/client/tests/test_evaluation_flags.py::test_flag_evaluation_error_disables_generation_result_batch_ingest PASSED [ 43%]
packages/client/tests/test_evaluations_run.py::test_run_calls_private_operations_in_order_and_returns_server_verdict PASSED [ 50%]
packages/client/tests/test_evaluations_run.py::test_batch_ingest_flag_controls_generation_result_publishing[enabled] PASSED [ 56%]
packages/client/tests/test_evaluations_run.py::test_batch_ingest_flag_controls_generation_result_publishing[disabled-default] PASSED [ 62%]
packages/client/tests/test_evaluations_run.py::test_batch_ingest_flag_controls_generation_result_publishing[malformed] PASSED [ 68%]
packages/client/tests/test_evaluations_run.py::test_batch_ingest_flag_controls_generation_result_publishing[evaluation-error] PASSED [ 75%]
packages/client/tests/test_evaluations_run.py::test_run_rejects_instructions_and_messages_before_network_io PASSED [ 81%]
packages/client/tests/test_evaluations_run.py::test_missing_tool_aborts_before_any_mutating_request PASSED [ 87%]
packages/client/tests/test_evaluations_run.py::test_empty_dataset_fails_before_evaluation_or_run_creation PASSED [ 93%]
packages/client/tests/test_evaluations_run.py::test_handler_error_is_ingested_and_other_rows_continue PASSED [100%]

============================== 16 passed in 0.06s ==============================

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • uv run pytest packages/client/tests/test_evaluation_flags.py packages/client/tests/test_evaluations_run.py -v (16 passed)
  • test_batch_ingest_flag_controls_generation_result_publishing[enabled|disabled-default|malformed|evaluation-error] — asserts /generation-results POST presence/absence with LD_SDK_KEY set
  • test_run_calls_private_operations_in_order_and_returns_server_verdict — no-SDK path: init_client.assert_not_awaited() and existing ingest POST still fires
  • test_enabled_flag_enables_generation_result_batch_ingest — asserts canonical flag key and project context are used with default False
  • test_disabled_flag_disables_generation_result_batch_ingest, test_malformed_flag_disables_generation_result_batch_ingest[None|1|'true'|{}], test_flag_evaluation_error_disables_generation_result_batch_ingest
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

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

Copy link
Copy Markdown

🤖 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 devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +72 to +76
api_client = LDApiClient(
api_token=token,
base_uri=base_uri or _env("LD_BASE_URI") or DEFAULT_BASE_URI,
transport=transport,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Custom LaunchDarkly host setting for flag delivery is reused for management API calls, sending requests to the wrong server

The evaluations client picks up the same host setting already used for flag delivery (_env("LD_BASE_URI") at packages/client/src/launchdarkly_ai_server/evaluations/module.py:74) even though the two point at different LaunchDarkly services, so anyone who configured a streaming/relay host will have their evaluation requests sent to a server that cannot answer them.
Impact: Users with a relay proxy or staging streaming endpoint configured get failing or misdirected evaluation API calls instead of reaching the LaunchDarkly management API.

Env var collision between SDK polling base URI and /api/v2 base URI

packages/client/src/launchdarkly_ai_server/lifecycle.py:175 already consumes LD_BASE_URI as the SDK polling/streaming base URI (documented in packages/client/README.md:36 as "Override the LaunchDarkly polling base URI"). The evaluations module reuses the same variable but appends /api/v2/... (packages/client/src/launchdarkly_ai_server/evaluations/api.py:93), which targets the public management API (default https://app.launchdarkly.com). A user pointing LD_BASE_URI at e.g. a relay proxy or https://sdk.launchdarkly.com would silently redirect all evaluations API traffic there. A distinct variable (e.g. LD_API_BASE_URI) avoids the conflict.

Suggested change
api_client = LDApiClient(
api_token=token,
base_uri=base_uri or _env("LD_BASE_URI") or DEFAULT_BASE_URI,
transport=transport,
)
api_client = LDApiClient(
api_token=token,
base_uri=base_uri or _env("LD_API_BASE_URI") or DEFAULT_BASE_URI,
transport=transport,
)
Open in Devin Review

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

@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants