-
Notifications
You must be signed in to change notification settings - Fork 4
feat: eval #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
feat: eval #68
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
4c27c07
First try
SirSimon04 8047147
update impl
SirSimon04 8bfedab
add mocking
SirSimon04 5704a1a
Some changes
SirSimon04 bf53299
.
SirSimon04 25ae909
set header
SirSimon04 39381af
Basic setup
schiwekM 1df9768
Merge branch 'main' into tryout/llm-as-a-judge
schiwekM c3bf8e2
Fixes
schiwekM 52f98fe
Formatting
schiwekM f158f9e
Update .gitignore
schiwekM 3088d5f
Added changelog
schiwekM 8ebcbec
Cleanup
schiwekM 8a67a23
Better metrics
schiwekM 66b1c3c
Docs
schiwekM 4eb770d
Fix for multi turn
schiwekM 118ca09
Fixes
schiwekM e2805f8
Multi turn eval
schiwekM 1914b1d
Renamed to chat
schiwekM 7a25d81
Update package.json
schiwekM 6690eb1
Small cleanup
schiwekM 34be355
Restructure and export prompts
schiwekM caaa620
Fix
schiwekM 6d2f9d5
Cleanup
schiwekM 0835360
fix
schiwekM c668905
lint
schiwekM 641c443
Adjustments based on review
schiwekM 653acd4
Merge branch 'main' into feat/eval
schiwekM 85bf9b0
Update CHANGELOG.md
schiwekM 8ab12ec
Cleanup mlflow.js
schiwekM 10b2ee0
Further cleanup
schiwekM 3b19a30
Cleanup
schiwekM d380fbf
Merge branch 'main' into feat/eval
schiwekM 9435dd0
fix
schiwekM f5d1241
Review
schiwekM f03a1f0
One Judge
schiwekM 12df419
Delete vitest.config.evals.js
schiwekM f558c63
Merge branch 'main' into feat/eval
schiwekM 87cc99c
fix
schiwekM 10955dc
Update bookshop.eval.test.js
schiwekM f793541
vitest as devDep for faster ci
schiwekM a27c522
Update test.yml
schiwekM 2683dc5
Fix
schiwekM fc17bfe
Update package-lock.json
schiwekM 93b2936
Delete telemetry-v1.test.js
schiwekM 13e34b5
Merge branch 'main' into feat/eval
schiwekM 4ca3c1a
Update Judge.js
schiwekM 01a79c2
Fix custom prompt
schiwekM f03c048
Update eval-framework.test.js
schiwekM File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| # Agent Evals | ||
|
|
||
| > [!WARNING] | ||
| > The following features are experimental and may be changed or removed at any time. | ||
|
|
||
| Eval tests run your agent against real LLM calls and can score responses with an LLM-as-judge. Results, metrics, and validation rollups are posted to MLflow automatically if MLflow is enabled. | ||
|
|
||
| ## Setup | ||
|
|
||
| Install optional peer dependencies: | ||
|
|
||
| ```bash | ||
| npm install --save-dev openevals | ||
| ``` | ||
|
|
||
| Run a specific eval test with your project’s binding/profile setup, for example: | ||
|
|
||
| ```bash | ||
| CDS_ENV=test,hybrid,tracing cds bind --exec -- npx vitest run test/eval/<scenario>.test.js | ||
| ``` | ||
|
|
||
| ## Writing eval tests | ||
|
|
||
| ```js | ||
| import cds from "@sap/cds" | ||
| import { test } from "vitest" | ||
| import { Judge, matchToolCall } from "@cap-js/agents/eval" | ||
|
|
||
| cds.test(".") | ||
|
|
||
| const judge = new Judge("Response fully and accurately answers the user's question.") | ||
|
|
||
| describe("catalog-eval", () => { | ||
| test("lists books", async () => { | ||
| const agent = await cds.connect.to("CatalogService") | ||
| const result = await agent.chat("Show me all books") | ||
|
|
||
| // OTel-derived metrics are available in the test profile and posted to MLflow. | ||
| expect(result.metrics.tool_call_count).toBeGreaterThan(0) | ||
|
|
||
| // Deterministic validation; also contributes to eval rollups. | ||
| expect(matchToolCall(result, "query", (args) => !!args.cql)).toBe(true) | ||
|
|
||
| // LLM-as-judge validation; also contributes to eval rollups. | ||
| const { pass, score, comment } = await judge | ||
| .criteria("must list multiple books with titles") | ||
| .evaluate(result) | ||
|
|
||
| expect(pass).toBe(true) | ||
| }) | ||
| }) | ||
| ``` | ||
|
|
||
| For multi-turn or HITL evals, pass the previous `agent.chat()` result to continue the same context. If the previous result has status `input-required`, the same task is resumed. | ||
|
|
||
| ```js | ||
| test("approves an order", async () => { | ||
| const agent = await cds.connect.to("CatalogService") | ||
|
|
||
| const r1 = await agent.chat("Show me all books") | ||
| const r2 = await agent.chat("Order the cheapest one", r1) | ||
| expect(r2.status).toBe("input-required") | ||
|
|
||
| const r3 = await agent.chat("yes", r2) | ||
| expect(r3.status).toBe("completed") | ||
|
|
||
| const { pass } = await new Judge("The final response confirms the order.").evaluate(r3) | ||
| expect(pass).toBe(true) | ||
| }) | ||
| ``` | ||
|
|
||
| ## Eval run lifecycle in MLflow | ||
|
|
||
| Importing from `@cap-js/agents/eval` installs eval test integration for top-level `describe("name", ...)` blocks. The describe name is used as the eval run name. | ||
|
|
||
| Rules: | ||
|
|
||
| - Import from `@cap-js/agents/eval` before declaring the top-level `describe`. | ||
| - Use the global `describe`; do not import `describe` from `vitest`, because imported bindings bypass the global patch. | ||
| - Only top-level `describe` blocks create eval runs; nested `describe` blocks are grouping only. | ||
| - Skipped and todo suites do not create eval runs. | ||
|
|
||
| Validation helpers such as `matchToolCall()` and `judge.evaluate()` contribute to per-test rollups. When MLflow is enabled, those rollups are flushed automatically. | ||
|
|
||
| ## `agent.chat(query, previous?)` | ||
|
|
||
| `agent.chat()` calls the agent in-process. It is registered on `@agent` services by the agent service handlers and is intended for tests and evals. | ||
|
|
||
| ```js | ||
| await agent.chat("Show me all books") | ||
| await agent.chat("Order it", previousResult) | ||
| ``` | ||
|
|
||
| The typical result: | ||
|
|
||
| ```js | ||
| result.text // final text response | ||
| result.status // "completed" | "input-required" | "canceled" | ||
| result.contextId // conversation id — pass to the next chat() for multi-turn | ||
| result.taskId // task id — used for HITL resume | ||
| ``` | ||
|
|
||
| When the `test` profile is active, additional eval details are available: | ||
|
|
||
| ```js | ||
| result.query // original query string, used by judges | ||
| result.traceId // OTel trace id | ||
| result.toolCalls // [{ tool, args, result?, cqn? }] | ||
| result.messages // LangChain messages for the current turn | ||
| result.spans // OTel spans captured for the trace | ||
| result.metrics // input/output tokens, tool count, latency, cost | ||
| ``` | ||
|
|
||
| ## `Judge` | ||
|
|
||
| `Judge` evaluates one `agent.chat()` result and returns `{ score, comment, pass }`. | ||
|
|
||
| ```js | ||
| const { score, comment, pass } = await new Judge( | ||
| "Answer must mention at least one concrete book title.", | ||
| ).evaluate(result) | ||
| ``` | ||
|
|
||
| The constructor accepts zero or one argument: | ||
|
|
||
| ```js | ||
| await new Judge().evaluate(result) | ||
| await new Judge("ANSWER_RELEVANCE_PROMPT").evaluate(result) | ||
| await new Judge({ criteria: "ANSWER_RELEVANCE_PROMPT", continuous: false }).evaluate(result) | ||
| ``` | ||
|
schiwekM marked this conversation as resolved.
|
||
|
|
||
| If no criteria is passed, `Judge` defaults to `ANSWER_RELEVANCE_PROMPT`. | ||
|
|
||
| `criteria` is used as the judge prompt source: | ||
|
|
||
| - if it matches an `openevals` prompt like `ANSWER_RELEVANCE_PROMPT`, the built-in prompt is used | ||
| - otherwise the string is passed as the prompt | ||
|
|
||
| Use `.criteria(text)` to append additional instructions to the existing criteria. It returns a sibling judge and does not replace the original criteria. | ||
|
|
||
| ```js | ||
| const base = new Judge("Response must answer the user question.") | ||
|
|
||
| await base.criteria("Response must include stock information.").evaluate(result) | ||
| ``` | ||
|
|
||
| Other possible prompts are in the [OpenEvals prebuilt prompts](https://github.com/langchain-ai/openevals#prebuilt-prompts). | ||
|
|
||
| ## Trajectory judging with `Judge` | ||
|
|
||
| Trajectory judging evaluates the steps the agent took, not only the final answer. Set `type: "trajectory"` explicitly. | ||
|
|
||
| ```js | ||
| import { Judge } from "@cap-js/agents/eval" | ||
|
|
||
| const { pass, score, comment } = await new Judge({ | ||
| criteria: "TRAJECTORY_ACCURACY_PROMPT", | ||
| type: "trajectory", | ||
| }) | ||
| .criteria("Agent must call getStock before stating a stock level.") | ||
| .evaluate(result) | ||
|
|
||
| expect(pass).toBe(true) | ||
| ``` | ||
|
|
||
| Trajectory prompt keys from OpenEvals can be used through the same constructor. See the [OpenEvals trajectory prompts](https://github.com/langchain-ai/openevals#trajectory-prompts). | ||
|
|
||
| ## Session judging with `Judge` | ||
|
|
||
| To evaluate a full session instead of a single turn, pass all `agent.chat()` results for the conversation in order. | ||
|
|
||
| ```js | ||
| import { Judge } from "@cap-js/agents/eval" | ||
|
|
||
| const r1 = await agent.chat("How many copies of Wuthering Heights are in stock?") | ||
| const r2 = await agent.chat("Tell me more about that book.", r1) | ||
|
|
||
| const { pass, score, comment } = await new Judge("TASK_COMPLETION_PROMPT").evaluate([r1, r2]) | ||
|
|
||
| expect(pass).toBe(true) | ||
| ``` | ||
|
|
||
| Session judging evaluates the collected messages from all passed results and posts the assessment with session metadata when MLflow is enabled. | ||
|
|
||
| Conversation prompt keys from OpenEvals can be used with the same base class. See the [OpenEvals conversation prompts](https://github.com/langchain-ai/openevals#conversation-prompts). | ||
|
|
||
| ## `matchToolCall(result, toolName, matcher?)` | ||
|
|
||
| `matchToolCall()` is a deterministic tool-call assertion. It returns a boolean and contributes to eval rollups. | ||
|
|
||
| ```js | ||
| matchToolCall(result, "query") // any call with that tool name | ||
| matchToolCall(result, "query", { entity: "Books" }) // partial args match | ||
| matchToolCall(result, "getStock", (args) => args.book === 42) // predicate | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,3 +20,4 @@ proposal.md | |
| *.sqlite | ||
| *.sqlite-wal | ||
| *.sqlite-shm | ||
| .playwright-mcp | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.