-
Notifications
You must be signed in to change notification settings - Fork 19
Evals for the CAP MCP server #129
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
Open
larsPlessing
wants to merge
47
commits into
main
Choose a base branch
from
evals-mcp-server
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
8831720
init
larsPlessing a5e0a5a
Add deterministic retrieval eval suite for the CAP MCP server
larsPlessing a12cc34
Use oldest run as baseline; drop baseline.json
larsPlessing 36c1e86
remove offline
larsPlessing 5360890
.
larsPlessing 7ba5c98
Score search_docs as a black box via URL#breadcrumb doc ids
larsPlessing 6ef3ff2
improvments
larsPlessing d149a97
comments
larsPlessing 3165171
evals: replace inline node -e scripts with bin/ entry files; drop eva…
larsPlessing c130f2f
evals: fix review findings
larsPlessing 339af42
evals: address review findings
larsPlessing 00557b7
evals: revert golden set to the original 10 questions
larsPlessing fbf047f
evals: satisfy no-console lint like the rest of the server
larsPlessing f937925
evals: set CDS_MCP_OFFLINE at the entry point, not per-run
larsPlessing 611e44a
evals: trim comments to essentials on recently-changed code
larsPlessing bc56abe
evals: id = Source URL, throw on missing URL; trim comments
larsPlessing f6ddcaa
evals: add optional run label (EVAL_LABEL) shown in console + compare…
larsPlessing 8ae4c39
evals: run label, warn-based checks, generated-anker ids, expandable …
larsPlessing b3f520f
evals: drop post-run console block, dedupe compare helpers
larsPlessing 848d7ba
evals: snapshot retrieved chunk text per slot into the report
larsPlessing a01fd28
make embedding model configurable via CDS_MCP_MODEL / EVAL_MODEL
larsPlessing ec2d2cb
evals: add tests for model config (CDS_MCP_MODEL propagation + proven…
larsPlessing 93092bd
evals: always include model name in the run label
larsPlessing 7364e5a
evals: test that model switch uses the correct corpus for id resolution
larsPlessing 760e1bb
support ONNX external-data files for models like pplx-embed
larsPlessing 79321b0
use onnxruntime-node for models with external ONNX data files (pplx-e…
larsPlessing 138f549
compare.html: leaderboard, short x-axis labels, rank replaces delta/b…
larsPlessing 94bb3e3
compare.html: rotate x-axis labels -45° to prevent overlap
larsPlessing f1d622e
compare.html: one label per dot (no skipping), full label on hover
larsPlessing e3d642c
compare.html: 3-column grid (max 2 metrics per column)
larsPlessing 1ba386e
compare.html: 2-column grid (max 2 charts per row)
larsPlessing f8f51b4
leaderboard: show all 5 metrics, not just gated ones
larsPlessing c4b055b
tests: cover leaderboard (all 5 metrics), ranking, rank column, x-axi…
larsPlessing c1d8baa
ids: fallback synthetic capire:// URL for breadcrumb-only chunks (no …
larsPlessing fbb92c7
evals: add embed-history.js to re-embed a corpus history with any model
larsPlessing 9819eb5
config
larsPlessing 460ab30
evals:compare: accept --runs <path> and --out <path> CLI args
larsPlessing 3ef7322
docs: expand Configuration with a useful commands subsection
larsPlessing 16b1579
remove EVAL_MODEL and EVAL_RUNS; use CDS_MCP_MODEL directly, run once…
larsPlessing c54e968
Add progress logging to createEmbeddings (every 50 chunks by default)
larsPlessing 620dbcd
parseId: search full chunk body for Source: URL, not just first line
larsPlessing d664610
evals: credit retrieved chunk as hit when golden URL appears in its b…
larsPlessing 08c08b9
golden set: add capire-11 — destinations in multitenant deployment (b…
larsPlessing 978f443
golden set: capire-11 — cds watch + inner loop (tests multi-Source ch…
larsPlessing eaf5fd0
better golden set
larsPlessing 926c6ee
Address PR #129 review
larsPlessing 9409496
evals: resolve doc ids via a Source tree from llms-full.txt
larsPlessing 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
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,102 @@ | ||
| /* eslint-disable no-console */ | ||
| // Build a Source tree from docs-resources/llms-full.txt so the eval can resolve | ||
| // EVERY doc section a corpus chunk covers — not just the one on its first | ||
| // `> Source:` line. A chunk is a slice of llms-full.txt spanning several | ||
| // headings; deeper headings carry their own `> Source:` line, but a chunk | ||
| // boundary can split a heading from its Source line. The tree recovers those. | ||
| // | ||
| // Output (evals/data/source-tree.json): | ||
| // { | ||
| // source: "<abs path to llms-full.txt>", | ||
| // tree: { "<page-path>": ["<anchor-url>", ...] }, // page → its anchors | ||
| // byHeadingInPage: { "<page-path>": { "<heading-lc>": "<source-url>" } }, | ||
| // byBreadcrumb: { "<a > b > c lc>": "<source-url>" }, // full heading path → url | ||
| // byLeaf: { "<heading-lc>": "<source-url>" } // leaf heading → url | ||
| // } | ||
| // | ||
| // byBreadcrumb / byLeaf let the eval score the live LLM-summary corpus, whose | ||
| // chunks are keyed by a breadcrumb ("The Bookshop Sample > Databases") instead of | ||
| // a `> Source:` URL. Regenerate with `npm run evals:build-source-tree`. Do not hand-edit. | ||
| import fs from 'fs/promises' | ||
| import path from 'path' | ||
| import { fileURLToPath } from 'url' | ||
|
|
||
| const HERE = path.dirname(fileURLToPath(import.meta.url)) | ||
| const OUT = path.join(HERE, '..', 'data', 'source-tree.json') | ||
|
|
||
| // Default location of the docs export; override with argv[2]. | ||
| const DEFAULT_SRC = '/Users/i543501/SAPDevelop/docs-resources/llms-full.txt' | ||
|
|
||
| const HEADING = /^(#{1,6})\s+(.*\S)\s*$/ | ||
| const SOURCE = /^>\s*Source:\s*(\S+)/ | ||
|
|
||
| // Strip a trailing VitePress heading attribute like `{.subtitle}`. | ||
| function cleanHeading(t) { | ||
| return t.replace(/\s*\{[^}]*\}\s*$/, '').trim() | ||
| } | ||
|
|
||
| export function buildSourceTree(text) { | ||
| const tree = {} | ||
| const byHeadingInPage = {} | ||
| const byBreadcrumb = {} // full heading path ("A > B > C") → url | ||
| const byLeaf = {} // leaf heading ("C") → url (first occurrence wins) | ||
| const stack = [] // [{ level, text }] heading ancestry | ||
| let lastHeading = null // leaf text of the most recent heading | ||
| let lastCrumb = null // full path of the most recent heading | ||
| for (const line of text.split('\n')) { | ||
| const h = HEADING.exec(line) | ||
| if (h) { | ||
| const level = h[1].length | ||
| const txt = cleanHeading(h[2]) | ||
| while (stack.length && stack[stack.length - 1].level >= level) stack.pop() | ||
| stack.push({ level, text: txt }) | ||
| lastHeading = txt | ||
| lastCrumb = stack.map(s => s.text).join(' > ') | ||
| continue | ||
| } | ||
| const s = SOURCE.exec(line) | ||
| if (s) { | ||
| const url = s[1] | ||
| const page = url.split('#')[0] | ||
| const anchors = (tree[page] ||= []) | ||
| if (url.includes('#') && !anchors.includes(url)) anchors.push(url) | ||
| if (lastHeading !== null) { | ||
| const map = (byHeadingInPage[page] ||= {}) | ||
| // Page-scoped, so cross-page heading collisions can't occur; a heading | ||
| // repeated on ONE page keeps the first occurrence (the un-suffixed anchor). | ||
| const key = lastHeading.toLowerCase() | ||
| if (!(key in map)) map[key] = url | ||
| const crumbKey = lastCrumb.toLowerCase() | ||
| if (!(crumbKey in byBreadcrumb)) byBreadcrumb[crumbKey] = url | ||
| if (!(key in byLeaf)) byLeaf[key] = url | ||
| } | ||
| lastHeading = null | ||
| lastCrumb = null | ||
| } | ||
| } | ||
| return { tree, byHeadingInPage, byBreadcrumb, byLeaf } | ||
| } | ||
|
|
||
| async function main() { | ||
| const srcPath = process.argv[2] ? path.resolve(process.argv[2]) : DEFAULT_SRC | ||
| const text = await fs.readFile(srcPath, 'utf8') | ||
| const { tree, byHeadingInPage, byBreadcrumb, byLeaf } = buildSourceTree(text) | ||
| const pages = Object.keys(tree).length | ||
| const anchors = Object.values(tree).reduce((n, a) => n + a.length, 0) | ||
| await fs.writeFile( | ||
| OUT, | ||
| JSON.stringify({ source: srcPath, tree, byHeadingInPage, byBreadcrumb, byLeaf }, null, 2) | ||
| ) | ||
| console.log( | ||
| `Wrote ${path.relative(process.cwd(), OUT)} — ${pages} pages, ${anchors} anchors, ` + | ||
| `${Object.keys(byBreadcrumb).length} breadcrumbs` | ||
| ) | ||
| } | ||
|
|
||
| // Run as a script; importable for tests. | ||
| if (import.meta.url === `file://${process.argv[1]}`) { | ||
| main().catch(e => { | ||
| console.error(e) | ||
| process.exit(1) | ||
| }) | ||
| } |
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,40 @@ | ||
| /* eslint-disable no-console */ | ||
| // Entry point for `npm run evals:compare`: (re)build the comparison report. | ||
| // | ||
| // Optional CLI args: | ||
| // --runs <path> path to a result.jsonl file OR a runs dir | ||
| // --out <path> output path for compare.html / compare.md | ||
| // | ||
| // Examples: | ||
| // npm run evals:compare | ||
| // node evals/bin/compare.js --runs runs-xenova/result.jsonl | ||
| // node evals/bin/compare.js --runs runs-pplx/ --out runs-pplx/compare.html | ||
| import path from 'path' | ||
| import { compare } from '../lib/compare.js' | ||
|
|
||
| const args = process.argv.slice(2) | ||
| const get = flag => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : null } | ||
| const runsArg = get('--runs') | ||
| const outArg = get('--out') | ||
|
|
||
| // runsArg may be a result.jsonl file or a directory — normalise to a dir. | ||
| let overrides = {} | ||
| let outPath = outArg || undefined | ||
|
|
||
| if (runsArg) { | ||
| const abs = path.resolve(runsArg) | ||
| const isJsonl = abs.endsWith('.jsonl') | ||
| const runsDir = isJsonl ? path.dirname(abs) : abs | ||
| const resultsName = isJsonl ? path.basename(abs) : undefined | ||
| overrides.paths = { runsDir } | ||
| if (resultsName) overrides.output = { resultsName } | ||
| // default output alongside the jsonl when --out not given | ||
| if (!outPath && isJsonl) outPath = path.join(runsDir, 'compare.html') | ||
| } | ||
|
|
||
| compare({ overrides, outPath }) | ||
| .then(r => process.exit(r.code)) | ||
| .catch(e => { | ||
| console.error(e) | ||
| process.exit(3) | ||
| }) | ||
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,19 @@ | ||
| /* eslint-disable no-console */ | ||
| // Entry point for `npm run evals`: evaluate once, then build the comparison report. | ||
| // | ||
| // This thin wrapper exists for one reason: search_docs reads CDS_MCP_OFFLINE at | ||
| // MODULE LOAD. It must be set before ./evaluate.js (which transitively imports | ||
| // the search tool) is loaded — hence the env assignment followed by a dynamic | ||
| // import. A static import here, or setting the env inside evaluate.js, would run | ||
| // too late. Offline scoring against the already-downloaded corpus keeps runs | ||
| // deterministic (no mid-run re-download). | ||
| process.env.CDS_MCP_OFFLINE = 'true' | ||
|
|
||
| const { evaluateAndCompare } = await import('../lib/evaluate.js') | ||
|
|
||
| evaluateAndCompare() | ||
| .then(r => process.exit(r.code)) | ||
| .catch(e => { | ||
| console.error(e) | ||
| process.exit(3) | ||
| }) |
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,20 @@ | ||
| { | ||
| "k": 5, | ||
| "capire_version": "2026.5.0", | ||
| "paths": { | ||
| "goldenSet": "data/golden-set.json", | ||
| "runsDir": "runs" | ||
| }, | ||
| "gates": { | ||
| "recall_at_k": 0.8, | ||
| "mrr": 0.5, | ||
| "hit_rate_at_k": 0.8, | ||
| "precision_at_k": null, | ||
| "ndcg_at_k": null | ||
| }, | ||
| "output": { | ||
| "keepRuns": 100, | ||
| "resultsName": "result.jsonl", | ||
| "compareFormat": "html" | ||
| } | ||
| } |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need the binary?