ci: Integrate Braintrust eval suite CLOUDP-367319#1274
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Adds a Braintrust eval CI pipeline to run the existing eval suite against a local MongoDB instance, generate a markdown report (including historical baseline comparison), and publish results back to PRs via a sticky comment.
Changes:
- Add
Braintrust EvalsGitHub Actions workflow gated by a PR label, with steps to start MongoDB, run evals, and post a sticky PR comment. - Add a CI reporting script (
tests/eval/scripts/reportCi.ts) plus helpers to fetch Braintrust history and render a markdown report with a mermaid chart. - Wire baseline comparison via
EVAL_BASE_EXPERIMENT_NAME, and improve eval teardown/prompt behavior.
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/braintrust-evals.yml |
New workflow to run evals in CI and (when labeled) comment results on PRs. |
.gitignore |
Ignore generated .eval/ report output. |
package.json |
Add eval:ci:run script and add @braintrust/api dependency. |
pnpm-lock.yaml |
Lockfile updates for @braintrust/api and transitive deps. |
tests/eval/lib/judge.ts |
Adjust judge system prompt “Rules of Engagement”. |
tests/eval/lib/shared.ts |
Teardown now also closes the read-only MCP client singleton and resets factory. |
tests/eval/mongodb.eval.ts |
Honor EVAL_BASE_EXPERIMENT_NAME for baseline comparison. |
tests/eval/scripts/reportCi.ts |
New CI helper script to fetch baseline history, run evals, and write .eval/ci-report.md. |
tests/eval/scripts/reportCi/braintrustHistory.ts |
New helper to page Braintrust experiments and compute a score timeline. |
tests/eval/scripts/reportCi/renderMarkdown.ts |
New markdown + mermaid report renderer. |
tests/eval/scripts/reportCi/types.ts |
New shared types for parsed summaries and timeline points. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
fd522c1 to
36fc4dd
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
765dbf5 to
6e9155e
Compare
6e9155e to
03abcc4
Compare
a9d5683 to
84530a0
Compare
nirinchev
left a comment
There was a problem hiding this comment.
Great job overall, a few minor questions/suggestions.
| if: always() | ||
| run: pnpm run eval:db-stop | ||
| - name: Comment summary on PR | ||
| if: github.event_name == 'pull_request' && github.event.label.name == 'braintrust-evals' |
There was a problem hiding this comment.
Do we need the label check in this condition?
There was a problem hiding this comment.
Good catch, dropped it. ✅ I admit that I had copy-pasta this snippet from accuracy-tests.yml.
| "eval:db-stop": "sh tests/eval/scripts/evalDb.sh stop", | ||
| "eval:run": "bt eval tests/eval/mongodb.eval.ts", | ||
| "eval:debug": "tsx tests/eval/mongodb.eval.ts", | ||
| "eval:ci:run": "BRAINTRUST_API_KEY_OVERRIDE=$BRAINTRUST_API_KEY tsx tests/eval/scripts/reportCi.ts -- bt eval --jsonl tests/eval/mongodb.eval.ts", |
There was a problem hiding this comment.
Not sure I understand why we have the override here?
There was a problem hiding this comment.
Good question! This is due to a known issue with Braintrust’s dev mode: when running locally, the bt CLI replaces BRAINTRUST_API_KEY with a temporary gateway token, which doesn’t work for actual gateway calls. The Braintrust team has documented this here:
https://www.braintrust.dev/docs/kb/gateway-calls-fail-in-bt-eval-dev-mode
To workaround this, I copy the supplied BRAINTRUST_API_KEY value to BRAINTRUST_API_KEY_OVERRIDE. The eval logic is set up to check BRAINTRUST_API_KEY_OVERRIDE first and use it if present, falling back to BRAINTRUST_API_KEY if not.
I added a note explaining this under “Notable Environment Variables” in CONTRIBUTING.md for anyone running into this in the future. 🫡
| const defaultParameters = { | ||
| ...staticDefaultParameters, | ||
| ...(process.env.BT_EVAL_PARAMS_JSON | ||
| ? (JSON.parse(process.env.BT_EVAL_PARAMS_JSON) as typeof staticDefaultParameters) |
There was a problem hiding this comment.
This JSON.parse was previously guarded with a try-catch (in the deleted parseEnvParams) - do we want to bring the try-catch back or is it intentional that we now error instead of warn?
There was a problem hiding this comment.
This method felt pretty yucky, so I removed it. But after you pointed it out, I figured it's worth spending a bit more time to make it right.
I've now moved the parameter definition into lib/evalTypes.ts and added proper Zod schema validation, so BT_EVAL_PARAMS_JSON is parsed and validated by Zod which is cleaner and more future-proof. 👍
| const listParams = { | ||
| project_name: projectName, | ||
| org_name: orgName, | ||
| limit: MAX_PAGE_SIZE, | ||
| }; |
There was a problem hiding this comment.
| const listParams = { | |
| project_name: projectName, | |
| org_name: orgName, | |
| limit: MAX_PAGE_SIZE, | |
| }; | |
| const listParams = { | |
| project_name: projectName, | |
| org_name: orgName, | |
| limit: MAX_PAGE_SIZE, | |
| } satisfies ExperimentListParams; |
That way we get autocomplete and object shape validation
| logHistoryProgress( | ||
| `found ${rows.length} experiments on branch \`${gitBranchName}\`: ${experiment.name ?? experiment.id}` | ||
| ); | ||
| if (rows.length >= BRANCH_HISTORY_CHART_CAP) break; |
There was a problem hiding this comment.
Is listExperiments returning elements in chronological or reverse chronological order? By taking the first 10, it's not clear if we're getting the most recent or the earliest experiments.
There was a problem hiding this comment.
They're returned in reverse-chronological order, as documented here:
https://www.braintrust.dev/docs/api-reference/experiments/list-experiments
| if (!(t && t.startsWith("{") && t.endsWith("}"))) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const obj = JSON.parse(t) as Record<string, unknown>; |
There was a problem hiding this comment.
Rather than check for braces, which is somewhat brittle, can we wrap the JSON.parse in a try-catch instead?
There was a problem hiding this comment.
I initially went with your suggestion to wrap JSON.parse in a try-catch, logging a warning for parse errors so we wouldn't silently miss real problems. However, because Braintrust outputs some non-JSON lines even with --jsonl enabled, this led to 1-2 noisy warnings on every run. 🤦
That's why I ultimately changed it to only attempt to parse lines that look like JSON. Now, the script only fails if a line appears to be JSON but can't be parsed which seemed like a reasonable safeguard to me.
There was a problem hiding this comment.
Got it. Should we still wrap the json.parse in a try-catch? Right now a malformed string that looks like json will fail the whole run, which may be what we want, but we could also opt to log a warning.
| "", | ||
| "### Scoring", | ||
| "- 1.0 = every criterion fully satisfied; partial credit proportional to how many are satisfied; 0.0 = none.", | ||
| "If no scoring criteria are provided, score the assistant's response based on the following criteria:", |
There was a problem hiding this comment.
What's the case when no scoring criteria would be provided?
There was a problem hiding this comment.
Here’s some context:
In the 'llm_judge' samples I’ve drafted so far, the prompt includes two sections: "success criterion" and "scoring." Here's an example:
Success criterion
A search index exists on the 'movies' collection that stores only
titleandgenresviastoredSource.Scoring
- Score 1.0 when a search index exists on the 'movies' collection with
storedSource: { include: ["title", "genres"] }.- Score 0 otherwise.
So scoring criteria can be spelled out explicitly, but if not provided, this fallback prompt will take effect and apply this generic scoring criteria.
There was a problem hiding this comment.
Ah, I see - yeah, it was a bit confusing as the code in judgeUsingLLM has criteria as string, but I guess this is a bit overloaded to mean both success and scoring criteria. Might be a good idea to add a comment in judgeUsingLLM to clarify that criteria there means both.
| - main | ||
| pull_request: | ||
| types: | ||
| - labeled |
There was a problem hiding this comment.
This is a problem with the other accuracy workflow, but the labeled trigger only executes when the labels change, so adding commits to a labeled PR will not run the evals - consider if we should also trigger on synchronize to make sure that once we add a label all commits trigger the evals suite.
There was a problem hiding this comment.
Great point! added synchronize to triggers as well.
| llm_judge: z.union([z.string()]).optional() | ||
| .describe(`Provide a prompt for the LLM judge to evaluate and make assertions about: | ||
| - the state of the database after the assistant completes the prompt. The judge may use any available read-only MCP tools to check and validate these assertions. | ||
| - if prompt references ${GetResponseTool.keyword}, the assistant’s response for this eval case will be made available for evaluation. |
There was a problem hiding this comment.
[super nit] vscode is flagging this quote as confusing:
The character U+2019 "’" could be confused with the ASCII character U+0060 "`", which is more common in source code
Not a big deal at all, but people who get triggered by editor warnings might be annoyed 😅
There was a problem hiding this comment.
Feel ya! Good point, fixed 😄
e6c706f to
4e1f0fa
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
nirinchev
left a comment
There was a problem hiding this comment.
Great job - thanks for taking care of this! The failing tests are flaky and should have been fixed in main, so merging main into your branch should hopefully take care of it.
Thanks Nikola! appreciate your kind words. 🫶 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Braintrust eval CI reportCurrent run
Accuracy timeline (historical + this run)---
config:
xyChart:
showDataLabel: true
showDataLabelOutsideBar: true
themeVariables:
xyChart:
titleColor: 'orange'
---
xychart
title "accuracy"
x-axis ["2026-06-18 22:58", "2026-06-18 23:05", "This run"]
y-axis "Accuracy (%)" 0 --> 100
bar [100, 75, 73.6]
line [100, 75, 73.6]
|
Adds a GitHub Actions workflow that runs the Braintrust eval suite against a local MongoDB instance and posts a sticky PR comment with results 📊
CI workflow (
braintrust-evals.yml)workflow_dispatch, pushes tomain, and PRs labeledbraintrust-evalspnpm eval:ci:run(starts/stops eval MongoDB, executes evals, writes report)main, also runspnpm eval:pushto publish the bundled eval to the Braintrust sandbox 🚀.eval/ci-report.mdas a sticky PR comment viamarocchino/sticky-pull-request-commentCI report script (
tests/eval/scripts/reportCi.ts)main-branch experiment history from Braintrust (@braintrust/api) before the runEVAL_BASE_EXPERIMENT_NAMEso the run's evals compare against the latest main baseline--(expects similar output asbt eval --jsonl)stderrand teesstdoutfor live CI logsllm_judgeaccuracy and a mermaid accuracy-over-time chartSmall fixes bundled in this PR
submitScore🎯baseExperimentNamefrom env for baseline comparison🧪 Documentation and Testing
CONTRIBUTING.mddocs for running and pushing evals 📚