diff --git a/package-lock.json b/package-lock.json index 8af855f12..e469e94e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,9 @@ "@anthropic-ai/mcpb": "^1.0.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.25.1", - "@openai/agents": "^0.1.9", + "@langchain/core": "^1.1.31", + "@langchain/mcp-adapters": "^1.1.3", + "@langchain/openai": "^1.2.12", "@types/cors": "^2.8.19", "@types/eslint__js": "^8.42.3", "@types/express": "^5.0.3", @@ -43,8 +45,9 @@ "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-simple-import-sort": "^12.1.1", + "langchain": "^1.2.30", + "langsmith": "^0.5.7", "npm-run-all": "^4.1.5", - "openai": "^5.23.2", "prettier": "^3.5.3", "supertest": "^7.1.4", "tsx": "^4.21.0", @@ -155,6 +158,13 @@ "node": ">=18" } }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -1133,6 +1143,240 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.31.tgz", + "integrity": "sha512-FxsgIUONjKaRpjx59sISgmb0OMCbAetPGyhzjGa2kX0y1f8LZ5xm9VB2db7W9HYWyLvzRWcMA51Uu4OSTJmtZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "@standard-schema/spec": "^1.1.0", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": ">=0.5.0 <1.0.0", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.1.tgz", + "integrity": "sha512-OeLMejye1DZeZBPnurus2bqvjRi+pyrqfAXX77hYdUqKeQ3hAG7pLG04xdrvMs0pl/F57ZtwywqoE2oVqcI6JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.6.5", + "@standard-schema/spec": "1.1.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.1.16", + "zod": "^3.25.32 || ^4.2.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "dev": true, + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1" + } + }, + "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.6.5.tgz", + "integrity": "sha512-JjprmbhgCnoNJ9DUKcvrEU+C9FfKsNGyT3ooqWxAY5Cx2qofhXmDJOpTCqqbxfDHPKG0RjTs5HgVK3WW5M6Big==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "p-queue": "^9.0.1", + "p-retry": "^7.1.1", + "uuid": "^13.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.1.16", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", + "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@langchain/langgraph/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/mcp-adapters": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-1.1.3.tgz", + "integrity": "sha512-OPHIQNkTUJjnRj1pr+cp2nguMBZeF3Q1pVT1hCbgU7BrHgV7lov99wbU8po8Cm4zZzmeRtVO/T9X1SrDD1ogtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "debug": "^4.4.3", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20.10.0" + }, + "optionalDependencies": { + "extended-eventsource": "^1.7.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0", + "@langchain/langgraph": "^1.0.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": false + }, + "@langchain/langgraph": { + "optional": false + } + } + }, + "node_modules/@langchain/openai": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.2.12.tgz", + "integrity": "sha512-Im6PPNujrfkZk4vpc9JAjbeERg+RbNtWRe3KSFOP7aNGa/yZ+XD69lxXwbsZGaZkbiUN/hwe9RYeisUfThb5wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^6.24.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.30" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", @@ -1251,76 +1495,6 @@ "node": ">= 8" } }, - "node_modules/@openai/agents": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@openai/agents/-/agents-0.1.9.tgz", - "integrity": "sha512-f0MLi8N9tVqJqxR3oJj7XmXq0roE+lllRaBG3MeMNP0edOC99+Le/24u3lM7HE6/FyqaCdrRuEbv9QEEf0RDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@openai/agents-core": "0.1.8", - "@openai/agents-openai": "0.1.9", - "@openai/agents-realtime": "0.1.8", - "debug": "^4.4.0", - "openai": "^5.20.2" - }, - "peerDependencies": { - "zod": "^3.25.40" - } - }, - "node_modules/@openai/agents-core": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@openai/agents-core/-/agents-core-0.1.8.tgz", - "integrity": "sha512-6tNFU/oR7EXzYyv6E6xuCVaVFr9ixO3zsPRGb1d+67obGN2V9QftbfXkYN8bobIaVgI7FD5i4d9VQQesL8Odkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "openai": "^5.20.2" - }, - "optionalDependencies": { - "@modelcontextprotocol/sdk": "^1.17.2" - }, - "peerDependencies": { - "zod": "^3.25.40" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@openai/agents-openai": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@openai/agents-openai/-/agents-openai-0.1.9.tgz", - "integrity": "sha512-NNca4HSLDQ9J/xVM05+rzS6QvPN4bscZmZTIgPix1V5wFs8buiDd5sR1jFkEXZ3HppFhm2P8cndV2Ab46it2zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@openai/agents-core": "0.1.8", - "debug": "^4.4.0", - "openai": "^5.20.2" - }, - "peerDependencies": { - "zod": "^3.25.40" - } - }, - "node_modules/@openai/agents-realtime": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.1.8.tgz", - "integrity": "sha512-i9+0G3P640l8LmBkDanJ+YvVTfMnQvLvjqj3h7TnVBjNiXBeFi1G5uaCae5e/xH+igciKgvS8VyQ/Mftel0BQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@openai/agents-core": "0.1.8", - "@types/ws": "^8.18.1", - "debug": "^4.4.0", - "ws": "^8.18.1" - }, - "peerDependencies": { - "zod": "^3.25.40" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1703,6 +1877,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1920,6 +2101,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -1927,16 +2115,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.33.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", @@ -2596,6 +2774,27 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2714,6 +2913,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -2839,6 +3051,16 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "dev": true, + "license": "MIT", + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -2977,6 +3199,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3548,6 +3780,13 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -3637,6 +3876,14 @@ "express": ">= 4.11" } }, + "node_modules/extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4641,6 +4888,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4900,6 +5160,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -4972,6 +5242,88 @@ "json-buffer": "3.0.1" } }, + "node_modules/langchain": { + "version": "1.2.30", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.30.tgz", + "integrity": "sha512-Ofsk7LTGvIkyy3uesv7hpyerpTghdjNpYFJfIxJRGGLjd+4JgTVkT/Ax6Cjg0F6doEuO7VRID0NK2QwmT3A/bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@langchain/langgraph": "^1.1.2", + "@langchain/langgraph-checkpoint": "^1.0.0", + "langsmith": ">=0.5.0 <1.0.0", + "uuid": "^11.1.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.1.31" + } + }, + "node_modules/langsmith": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.7.tgz", + "integrity": "sha512-FjYf2oBGMoSXnaT4SRaFguIiGJaonZ5VKWKJDPl9awLZjz2RkN29AcQWceecSINVzXzTvtRWPOjAWT+XggqNNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^5.6.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5190,6 +5542,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -5519,9 +5881,9 @@ } }, "node_modules/openai": { - "version": "5.23.2", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", - "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.27.0.tgz", + "integrity": "sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5529,7 +5891,7 @@ }, "peerDependencies": { "ws": "^8.18.0", - "zod": "^3.23.8" + "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { "ws": { @@ -5592,6 +5954,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5622,6 +5994,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6433,6 +6851,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7704,6 +8129,20 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -8156,28 +8595,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 522a8bb6e..0f0e6995f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "build:inspect:docker:http": "run-s build:docker inspect:docker:http", "test": "vitest --config ./vitest.config.ts", "test:e2e": "vitest --config ./vitest.config.e2e.ts", - "test:eval": "vitest --config ./vitest.config.eval.ts", + "test:eval": "tsx tests/eval/runEval.ts", "test:oauth:embedded": "vitest --config ./vitest.config.oauth.embedded.ts", "coverage": "vitest run --config ./vitest.config.ts --coverage", "version:major": "npm version major --no-git-tag-version", @@ -68,7 +68,9 @@ "@anthropic-ai/mcpb": "^1.0.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.25.1", - "@openai/agents": "^0.1.9", + "@langchain/core": "^1.1.31", + "@langchain/mcp-adapters": "^1.1.3", + "@langchain/openai": "^1.2.12", "@types/cors": "^2.8.19", "@types/eslint__js": "^8.42.3", "@types/express": "^5.0.3", @@ -83,8 +85,9 @@ "eslint-config-prettier": "^10.1.2", "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-simple-import-sort": "^12.1.1", + "langchain": "^1.2.30", + "langsmith": "^0.5.7", "npm-run-all": "^4.1.5", - "openai": "^5.23.2", "prettier": "^3.5.3", "supertest": "^7.1.4", "tsx": "^4.21.0", diff --git a/tests/eval/base.ts b/tests/eval/base.ts index a1a4ff0a3..2176da96b 100644 --- a/tests/eval/base.ts +++ b/tests/eval/base.ts @@ -1,27 +1,13 @@ -import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; -import { - Agent, - getAllMcpTools, - MCPServerStdio, - OpenAIChatCompletionsModel, - StreamedRunResult, - withTrace, -} from '@openai/agents'; -import { OpenAI } from 'openai/client.js'; -import { Err, Ok, Result } from 'ts-results-es'; -import z from 'zod'; - -import invariant from '../../src/utils/invariant.js'; - -type ToolExecution = { - name: string; - arguments: Record; - output: string; -}; +import { MultiServerMCPClient, StdioConnection } from '@langchain/mcp-adapters'; +import { ChatOpenAI } from '@langchain/openai'; +import { AIMessage, createAgent, HumanMessage, SystemMessage, ToolMessage } from 'langchain'; const DEFAULT_MODEL = 'claude-sonnet-4-5-20250929'; -function getApiKey(): string { +type Message = AIMessage | HumanMessage | SystemMessage | ToolMessage; +type Agent = ReturnType; + +export function getApiKey(): string { const { OPENAI_API_KEY } = process.env; if (!OPENAI_API_KEY) { @@ -35,125 +21,64 @@ export function getModel(): string { return process.env.EVAL_TEST_MODEL || DEFAULT_MODEL; } -export async function getMcpServer(env?: Record): Promise { - const mcpServer = new MCPServerStdio({ +export function getMcpServer(env?: Record): StdioConnection { + const mcpServer: StdioConnection = { command: 'node', args: ['build/index.js'], env, - cacheToolsList: true, - }); + }; - await mcpServer.connect(); return mcpServer; } export async function getAgent({ - systemPrompt, model, mcpServer, }: { - systemPrompt: string; model: string; - mcpServer?: MCPServerStdio; -}): Promise { - return await withTrace('get_agent', async () => { - const agentOptions = { - name: 'Assistant with Tableau MCP tools', - instructions: systemPrompt, - model: new OpenAIChatCompletionsModel( - new OpenAI({ - baseURL: process.env.OPENAI_BASE_URL, - apiKey: getApiKey(), - }), - model, - ), - }; - - if (!mcpServer) { - return new Agent(agentOptions); - } - - const tools = await getAllMcpTools([mcpServer]); - tools.forEach((tool) => { - tool.name = `tableau_${tool.name}`; - }); - - return new Agent({ - ...agentOptions, - mcpServers: [mcpServer], - tools, - modelSettings: { toolChoice: 'required' }, - }); + mcpServer?: StdioConnection; +}): Promise<{ agent: Agent; client?: MultiServerMCPClient }> { + const agentOptions = { + name: 'Assistant with Tableau MCP tools', + model: new ChatOpenAI({ + model, + configuration: { + apiKey: getApiKey(), + baseURL: process.env.OPENAI_BASE_URL, + }, + }), + }; + + if (!mcpServer) { + return { agent: createAgent({ ...agentOptions }) }; + } + + const client = new MultiServerMCPClient({ + tableau: mcpServer, }); -} -export async function getToolExecutions( - result: StreamedRunResult, -): Promise> { - const toolExecutions: Map = new Map(); - - for (const item of result.history) { - if (item.type === 'function_call') { - toolExecutions.set(item.callId, { - name: item.name, - arguments: JSON.parse(item.arguments) as Record, - output: '', - }); - } - } + const tools = await client.getTools(); + return { + agent: createAgent({ ...agentOptions, tools }), + client, + }; +} - for (const item of result.history) { - if (item.type === 'function_call_result') { - const call = toolExecutions.get(item.callId); - if (!call) { - throw new Error(`Could not find tool execution for callId ${item.callId}`); - } - - call.output = - item.output.type === 'text' - ? item.output.text - : item.output.type === 'image' - ? item.output.data - : ''; - } - } +export async function prompt(agent: Agent, content: string): Promise> { + console.log(`Invoking agent with input: ${content}...`); - log('šŸ› ļø tool executions:'); - const executions = [...toolExecutions.values()]; - for (const execution of executions) { - log(` šŸ”Ø ${execution.name}`); - log(` šŸ‘‰ arguments: ${JSON.stringify(execution.arguments)}`); - log(` šŸ‘ˆ output: ${execution.output}`); - log('\n'); - } + const start = performance.now(); - return executions; -} + const { messages } = (await agent.invoke({ + messages: [{ role: 'user', content }], + })) as { messages: Array }; -export function getCallToolResult( - toolExecution: ToolExecution, - schema: Z, -): z.infer { - const callToolResult = CallToolResultSchema.parse(JSON.parse(toolExecution.output)); - invariant(callToolResult.type === 'text'); - invariant(typeof callToolResult.text === 'string'); - const result = schema.parse(JSON.parse(callToolResult.text)); - return result; -} + const end = performance.now(); -export function getCallToolResultSafe( - toolExecution: ToolExecution, - schema: Z, -): Result, Error> { - const callToolResult = CallToolResultSchema.safeParse(JSON.parse(toolExecution.output)); - if (!callToolResult.success) { - return Err(callToolResult.error); - } + const duration = Math.round(end - start); + console.log(`Agent response received after ${duration}ms`); - invariant(callToolResult.data.type === 'text'); - invariant(typeof callToolResult.data.text === 'string'); - const result = schema.parse(JSON.parse(callToolResult.data.text)); - return Ok(result); + return messages; } export function log(message?: any, force?: boolean): void { diff --git a/tests/eval/ensureDatasetExists.ts b/tests/eval/ensureDatasetExists.ts new file mode 100644 index 000000000..443d85627 --- /dev/null +++ b/tests/eval/ensureDatasetExists.ts @@ -0,0 +1,119 @@ +import { Client } from 'langsmith'; +import { ExampleCreate } from 'langsmith/schemas'; + +import { evalInputSchema, evalReferenceSchema } from './evaluators'; +import { EvalExample, examples } from './examples'; + +export async function ensureDatasetExists({ + client, + datasetName, + datasetDescription, +}: { + client: Client; + datasetName: string; + datasetDescription: string; +}): Promise { + console.log(`Ensuring dataset "${datasetName}" exists`); + console.log('Getting current datasets...'); + const datasets = client.listDatasets({ datasetName }); + + let dataset = await first(datasets); + + if (dataset) { + console.log(`Dataset "${datasetName}" already exists`); + } else { + console.log(`Creating dataset "${datasetName}"...`); + dataset = await client.createDataset(datasetName, { + description: datasetDescription, + }); + console.log(`Created dataset "${datasetName}"`); + } + + console.log('Getting current examples...'); + const datasetExamples = await toArray(client.listExamples({ datasetId: dataset.id })); + + const datasetExamplesStr = datasetExamples.length === 1 ? 'example' : 'examples'; + console.log(`Found ${datasetExamples.length} ${datasetExamplesStr} in dataset`); + + const missingExamples: Array = examples.filter((example) => { + return !datasetExamples.some((e) => e.inputs.question === example.inputs.question); + }); + + const mismatchedExamples: Array = []; + for (const datasetExample of datasetExamples) { + const inputResult = evalInputSchema.safeParse(datasetExample.inputs); + if (!inputResult.success) { + console.log("Dataset example inputs don't match expected schema"); + console.log(`Deleting example: ${datasetExample.id}...`); + await client.deleteExample(datasetExample.id); + continue; + } + + const datasetInputs = inputResult.data; + const example = examples.find((e) => e.inputs.question === datasetInputs.question); + if (!example) { + console.log(`Dataset example not found: ${datasetInputs.question}`); + console.log(`Deleting example: ${datasetExample.id}...`); + await client.deleteExample(datasetExample.id); + continue; + } + + const outputResult = evalReferenceSchema.safeParse(datasetExample.outputs); + if (!outputResult.success) { + console.log(`Example outputs don't match expected schema: ${datasetExample.inputs.question}`); + console.log(`Deleting example: ${datasetExample.id}...`); + await client.deleteExample(datasetExample.id); + mismatchedExamples.push(example); + continue; + } + + const datasetOutputs = outputResult.data; + if ( + datasetOutputs.rubric !== example.outputs.rubric || + datasetOutputs.mustContain.length !== example.outputs.mustContain.length || + !datasetOutputs.mustContain.every((mustContain) => + example.outputs.mustContain.includes(mustContain), + ) || + datasetOutputs.expectedTools.length !== example.outputs.expectedTools.length || + !datasetOutputs.expectedTools.every((tool) => example.outputs.expectedTools.includes(tool)) + ) { + console.log(`Example mismatch: ${datasetExample.inputs.question}`); + console.log('Deleting example...'); + await client.deleteExample(datasetExample.id); + mismatchedExamples.push(example); + } + } + + const uploads: Array = [...missingExamples, ...mismatchedExamples].map( + (example) => ({ + inputs: example.inputs, + outputs: example.outputs, + dataset_id: dataset.id, + }), + ); + + if (uploads.length === 0) { + console.log('Dataset examples are up to date'); + return; + } + + const examplesStr = uploads.length === 1 ? 'example' : 'examples'; + console.log(`Creating ${uploads.length} ${examplesStr}...`); + await client.createExamples(uploads); + console.log(`Created ${uploads.length} ${examplesStr}`); +} + +async function first(iter: AsyncIterable): Promise { + for await (const item of iter) { + return item; + } +} + +async function toArray(iter: AsyncIterable): Promise { + const result: Array = []; + for await (const item of iter) { + result.push(item); + } + + return result; +} diff --git a/tests/eval/evaluationResult.ts b/tests/eval/evaluationResult.ts deleted file mode 100644 index e79390c62..000000000 --- a/tests/eval/evaluationResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -import z from 'zod'; - -export const evaluationSchema = z.object({ - accuracy: z.number(), - completeness: z.number(), - relevance: z.number(), - clarity: z.number(), - reasoning: z.number(), - comments: z.string(), -}); diff --git a/tests/eval/evaluators.ts b/tests/eval/evaluators.ts new file mode 100644 index 000000000..0bc96b067 --- /dev/null +++ b/tests/eval/evaluators.ts @@ -0,0 +1,134 @@ +import { AIMessage } from 'langchain'; +import { EvaluationResult, EvaluatorT } from 'langsmith/evaluation'; +import { KVMap } from 'langsmith/schemas'; +import z from 'zod'; + +import { getAgent, getModel, prompt } from './base'; + +export const evalInputSchema = z.object({ + question: z.string(), +}); + +export const evalOutputSchema = z.object({ + toolsUsed: z.array( + z.object({ + name: z.string(), + content: z.string(), + }), + ), +}); + +export const evalReferenceSchema = z.object({ + expectedTools: z.array(z.string()), + mustContain: z.array(z.string()), + rubric: z.string(), +}); + +export type EvalInput = z.infer; +export type EvalOutput = z.infer; +export type EvalReference = z.infer; + +/** Checks that every expected tool was actually invoked */ +export const toolSelectionGrader: EvaluatorT = async ({ + outputs, + referenceOutputs, +}: { + outputs: KVMap; + referenceOutputs?: KVMap; +}): Promise => { + const { toolsUsed } = evalOutputSchema.parse(outputs); + const { expectedTools } = evalReferenceSchema.parse(referenceOutputs ?? { expectedTools: [] }); + + if (expectedTools.length === 0) { + return { key: 'tool_selection', score: 1 }; + } + + const hits = expectedTools.filter((expectedTool) => + toolsUsed.some((actualTool) => actualTool.name === expectedTool), + ); + + const score = hits.length / expectedTools.length; + + return { + key: 'tool_selection', + score, + comment: `Expected [${expectedTools.join(', ')}] - got [${toolsUsed.map((tool) => tool.name).join(', ')}]`, + }; +}; + +/** Checks that the output contains required substrings */ +export const contentPresenceGrader: EvaluatorT = async ({ + outputs, + referenceOutputs, +}: { + outputs: KVMap; + referenceOutputs?: KVMap; +}): Promise => { + const { toolsUsed } = evalOutputSchema.parse(outputs); + const { mustContain } = evalReferenceSchema.parse(referenceOutputs ?? { mustContain: [] }); + + if (mustContain.length === 0) { + return { key: 'content_presence', score: 1 }; + } + + const hits = toolsUsed.filter((tool) => + mustContain.some((s) => tool.content.toLowerCase().includes(s.toLowerCase())), + ); + const score = hits.length / mustContain.length; + + return { + key: 'content_presence', + score, + comment: `Found ${hits.length}/${mustContain.length} required terms`, + }; +}; + +/** LLM-as-judge for open-ended rubric grading */ +export const rubricGrader: EvaluatorT = async ({ + inputs, + outputs, + referenceOutputs, +}: { + inputs: KVMap; + outputs: KVMap; + referenceOutputs?: KVMap; +}): Promise => { + const { question } = evalInputSchema.parse(inputs); + const { toolsUsed } = evalOutputSchema.parse(outputs); + const { rubric } = evalReferenceSchema.parse(referenceOutputs ?? { rubric: '' }); + + if (!rubric) { + return { key: 'rubric', score: 1 }; + } + + const judgePrompt = `You are evaluating an AI agent's response to a user question. + +Question: ${question} +Agent Response: ${toolsUsed.map((tool) => tool.content).join('\n') || 'none'} +Tools Used: ${toolsUsed.map((tool) => tool.name).join(', ') || 'none'} + +Rubric: ${rubric} + +Score the response from 0.0 to 1.0. +Respond ONLY with JSON: {"score": , "comment": ""}`; + + const { agent } = await getAgent({ + model: getModel(), + }); + + const messages = await prompt(agent, judgePrompt); + + const text = + messages.find((message) => AIMessage.isInstance(message))?.content?.toString() ?? ''; + + try { + const parsed = JSON.parse(text.replace(/```json|```/g, '').trim()); + return { + key: 'rubric', + score: parsed.score, + comment: parsed.comment, + }; + } catch { + return { key: 'rubric', score: 0, comment: 'Judge failed to parse' }; + } +}; diff --git a/tests/eval/examples.ts b/tests/eval/examples.ts new file mode 100644 index 000000000..c0bc9a2aa --- /dev/null +++ b/tests/eval/examples.ts @@ -0,0 +1,18 @@ +import { EvalInput, EvalReference } from './evaluators'; + +export type EvalExample = { + inputs: EvalInput; + outputs: EvalReference; +}; + +export const examples: Array = [ + { + inputs: { question: 'Show me my datasources' }, + outputs: { + expectedTools: ['list-datasources'], + mustContain: ['Superstore Datasource'], + rubric: + 'Response should list datasource names and luid values. A JSON blob should be returned.', + }, + }, +]; diff --git a/tests/eval/getDatasourceMetadata.test.ts b/tests/eval/getDatasourceMetadata.test.ts deleted file mode 100644 index 6f60d4e89..000000000 --- a/tests/eval/getDatasourceMetadata.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { MCPServerStdio } from '@openai/agents'; -import dotenv from 'dotenv'; -import z from 'zod'; - -import { dataSourceSchema } from '../../src/sdks/tableau/types/dataSource.js'; -import { fieldsResultSchema } from '../../src/tools/getDatasourceMetadata/datasourceMetadataUtils.js'; -import invariant from '../../src/utils/invariant.js'; -import { Datasource } from '../constants.js'; -import { getDefaultEnv, getSuperstoreDatasource, resetEnv, setEnv } from '../testEnv.js'; -import { - getCallToolResult, - getCallToolResultSafe, - getMcpServer, - getModel, - getToolExecutions, -} from './base.js'; -import { grade } from './grade.js'; - -describe('get-datasource-metadata', () => { - let mcpServer: MCPServerStdio; - let superstore: Datasource; - - beforeAll(setEnv); - afterAll(resetEnv); - - beforeAll(async () => { - dotenv.config({ path: 'tests/eval/.env' }); - }); - - beforeEach(async () => { - const env = getDefaultEnv(); - superstore = getSuperstoreDatasource(env); - mcpServer = await getMcpServer(env); - }); - - afterEach(async () => { - await mcpServer.close(); - }); - - it('should call get_datasource_metadata tool', async () => { - const prompt = - 'For the Superstore data source, get its metadata. Do not perform any analysis on the metadata, just show it.'; - - const { agentResult } = await grade({ - mcpServer, - model: getModel(), - prompt, - }); - - const toolExecutions = await getToolExecutions(agentResult); - expect(toolExecutions.length).toBeGreaterThanOrEqual(2); - - const listDatasourcesToolExecution = toolExecutions.find((toolExecution) => { - if (toolExecution.name !== 'list_datasources') { - return false; - } - - const result = getCallToolResultSafe(toolExecution, z.array(dataSourceSchema)); - if (result.isErr()) { - return false; - } - - if (result.value.length === 0) { - return false; - } - - return true; - }); - - invariant(listDatasourcesToolExecution, 'list_datasources tool execution not found'); - - const datasources = getCallToolResult(listDatasourcesToolExecution, z.array(dataSourceSchema)); - expect(datasources.length).greaterThan(0); - const datasource = datasources.find( - (datasource) => datasource.name === 'Superstore Datasource', - ); - - expect(datasource).toMatchObject({ - id: superstore.id, - name: 'Superstore Datasource', - }); - - const getDatasourceMetadataToolExecution = toolExecutions.find((toolExecution) => { - return ( - toolExecution.name === 'get_datasource_metadata' && - toolExecution.arguments.datasourceLuid === superstore.id - ); - }); - - invariant( - getDatasourceMetadataToolExecution, - 'get_datasource_metadata tool execution not found', - ); - - const { fields } = getCallToolResult(getDatasourceMetadataToolExecution, fieldsResultSchema); - expect(fields.length).toBeGreaterThan(0); - - const fieldNames = fields.map((field) => field.name); - expect(fieldNames).toContain('Postal Code'); - expect(fieldNames).toContain('Product Name'); - }); -}); diff --git a/tests/eval/grade.ts b/tests/eval/grade.ts deleted file mode 100644 index 4dfd32170..000000000 --- a/tests/eval/grade.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { MCPServerStdio, run, StreamedRunResult, withTrace } from '@openai/agents'; - -import { getAgent, log } from './base.js'; -import { evaluationSchema } from './evaluationResult.js'; - -type GradeInput = { - mcpServer: MCPServerStdio; - model: string; - prompt: string; -}; - -const agentSystemPrompt = ` - You are an assistant responsible for evaluating the results of calling various tools. - Given the user's query, use the tools available to you to answer the question.`; - -const evalSystemPrompt = ` - You are an expert evaluator assessing how well an LLM answers a given question. - Review the provided answer and score it from 1 to 5 in each of the following categories: - Accuracy - Does the answer contain factual errors or hallucinations? - Completeness - Does the answer fully address all parts of the question? - Relevance - Is the information directly related to the question? - Clarity - Is the explanation easy to understand and well-structured? - Reasoning - Does the answer show logical thinking or provide evidence or rationale? - Return your evaluation as a JSON object in the format: - { - "accuracy": 1-5, - "completeness": 1-5, - "relevance": 1-5, - "clarity": 1-5, - "reasoning": 1-5, - "comments": "A short paragraph summarizing the strengths and weaknesses of the answer." - }`; - -export async function grade({ - mcpServer, - model, - prompt, -}: GradeInput): Promise<{ agentResult: StreamedRunResult }> { - const evals = await promptAgent({ mcpServer, model, prompt }); - log('\n'); - - const evalAgentPrompt = ` - Here is the user input: ${prompt} - Here is the LLM's answer: ${evals.finalOutput}`; - - const evalAgent = await getAgent({ - model, - systemPrompt: evalSystemPrompt, - }); - - const result = await withTrace('run_eval_agent', async () => { - const stream = await run(evalAgent, evalAgentPrompt, { stream: true }); - if (process.env.ENABLE_LOGGING === 'true') { - stream.toTextStream({ compatibleWithNodeStreams: true }).pipe(process.stdout); - } - - await stream.completed; - return stream; - }); - - log('\n'); - - const jsonRegexes = [/(?\{[^}]+\})/]; - for (const jsonRegex of jsonRegexes) { - const match = result.finalOutput?.match(jsonRegex); - if (match) { - const evaluationResult = evaluationSchema.safeParse(JSON.parse(match.groups?.JSON ?? '{}')); - if (!evaluationResult.success) { - throw new Error( - `Could not parse agent output as an evaluation result:\n${result.finalOutput}`, - ); - } - - const evaluation = evaluationResult.data; - expect(evaluation.accuracy).toBeGreaterThanOrEqual(4); - expect(evaluation.completeness).toBeGreaterThanOrEqual(4); - expect(evaluation.relevance).toBeGreaterThanOrEqual(4); - expect(evaluation.clarity).toBeGreaterThanOrEqual(4); - expect(evaluation.reasoning).toBeGreaterThanOrEqual(4); - - return { - agentResult: evals, - }; - } - } - throw new Error('Could not parse JSON from agent output'); -} - -async function promptAgent({ - mcpServer, - model, - prompt, -}: GradeInput): Promise> { - log(`Evaluating prompt: ${prompt}`, true); - - const agentWithTools = await getAgent({ - mcpServer, - model, - systemPrompt: agentSystemPrompt, - }); - - const result = await withTrace('run_agent_with_tools', async () => { - const stream = await run(agentWithTools, prompt, { stream: true }); - if (process.env.ENABLE_LOGGING === 'true') { - stream.toTextStream({ compatibleWithNodeStreams: true }).pipe(process.stdout); - } - - await stream.completed; - return stream; - }); - - return result; -} diff --git a/tests/eval/listDatasources.test.ts b/tests/eval/listDatasources.test.ts deleted file mode 100644 index c09a2cf09..000000000 --- a/tests/eval/listDatasources.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MCPServerStdio } from '@openai/agents'; -import dotenv from 'dotenv'; -import z from 'zod'; - -import { dataSourceSchema } from '../../src/sdks/tableau/types/dataSource.js'; -import { Datasource } from '../constants.js'; -import { getDefaultEnv, getSuperstoreDatasource, resetEnv, setEnv } from '../testEnv.js'; -import { getCallToolResult, getMcpServer, getModel, getToolExecutions } from './base.js'; -import { grade } from './grade.js'; - -describe('list-datasources', () => { - let mcpServer: MCPServerStdio; - let superstore: Datasource; - - beforeAll(setEnv); - afterAll(resetEnv); - - beforeAll(async () => { - dotenv.config({ path: 'tests/eval/.env' }); - }); - - beforeEach(async () => { - const env = getDefaultEnv(); - superstore = getSuperstoreDatasource(env); - mcpServer = await getMcpServer(env); - }); - - afterEach(async () => { - await mcpServer?.close(); - }); - - it('should call list_datasources tool', async () => { - const prompt = - 'List the data sources that are available on my Tableau site. Do not perform any analysis on the the list of data sources, just show the list.'; - - const { agentResult } = await grade({ - mcpServer, - model: getModel(), - prompt, - }); - - const toolExecutions = await getToolExecutions(agentResult); - - expect(toolExecutions.length).toBe(1); - expect(toolExecutions[0].name).toBe('list_datasources'); - expect(toolExecutions[0].arguments.filter).toBeFalsy(); - - const datasources = getCallToolResult(toolExecutions[0], z.array(dataSourceSchema)); - expect(datasources.length).greaterThan(0); - const datasource = datasources.find( - (datasource) => datasource.name === 'Superstore Datasource', - ); - - expect(datasource).toMatchObject({ - id: superstore.id, - name: 'Superstore Datasource', - }); - }); -}); diff --git a/tests/eval/runEval.ts b/tests/eval/runEval.ts new file mode 100644 index 000000000..e13189cc8 --- /dev/null +++ b/tests/eval/runEval.ts @@ -0,0 +1,70 @@ +import dotenv from 'dotenv'; +import { Client } from 'langsmith'; +import { evaluate, type EvaluateOptions } from 'langsmith/evaluation'; + +import { setEnv } from '../testEnv'; +import { ensureDatasetExists } from './ensureDatasetExists'; +import { + contentPresenceGrader, + evalInputSchema, + rubricGrader, + toolSelectionGrader, +} from './evaluators'; +import { target } from './target'; + +async function runEval(): Promise { + setEnv(); + dotenv.config({ path: 'tests/eval/.env' }); + + const client = new Client(); + + const datasetName = 'Tableau MCP: Development'; + await ensureDatasetExists({ + client, + datasetName, + datasetDescription: 'Tableau MCP evaluation dataset', + }); + + console.log('\nšŸš€ Starting evaluation run...\n'); + + const options: EvaluateOptions = { + data: datasetName, + evaluators: [toolSelectionGrader, contentPresenceGrader, rubricGrader], + experimentPrefix: 'tableau-mcp', + maxConcurrency: 2, + }; + + const { results } = await evaluate(target, options); + + // Print summary + console.log('\nšŸ“Š Results Summary\n' + '-'.repeat(50)); + + const scores: Record = {}; + + for await (const result of results) { + const { question } = evalInputSchema.parse(result.example.inputs); + console.log(`\nšŸ”¹ "${question}"`); + + for (const evalResult of result.evaluationResults.results) { + const { key, score, comment } = evalResult; + + const numScore = typeof score === 'number' ? score : null; + const pct = numScore != null ? `${(numScore * 100).toFixed(0)}%` : 'N/A'; + console.log(` ${key}: ${pct}${comment ? ` - ${comment}` : ''}`); + + if (numScore !== null) { + scores[key] = [...(scores[key] ?? []), numScore]; + } + } + } + + console.log('\nšŸ“ˆ Aggregate Scores\n' + '-'.repeat(50)); + for (const [key, vals] of Object.entries(scores)) { + const avg = vals.reduce((a, b) => a + b, 0) / vals.length; + console.log(` ${key}: ${(avg * 100).toFixed(1)}% avg (n=${vals.length})`); + } + + console.log('\nāœ… Done. View results at https://smith.langchain.com'); +} + +runEval().catch(console.error); diff --git a/tests/eval/target.ts b/tests/eval/target.ts new file mode 100644 index 000000000..8d51eafec --- /dev/null +++ b/tests/eval/target.ts @@ -0,0 +1,32 @@ +import { ToolMessage } from 'langchain'; + +import { getDefaultEnv } from '../testEnv'; +import { getAgent, getMcpServer, getModel, prompt } from './base'; +import { EvalInput, EvalOutput } from './evaluators'; + +export async function target(inputs: EvalInput): Promise { + const env = getDefaultEnv(); + + const { agent, client } = await getAgent({ + model: getModel(), + mcpServer: getMcpServer(env), + }); + + try { + const messages = await prompt(agent, inputs.question); + + const toolsUsed = messages.reduce((acc, message) => { + if (ToolMessage.isInstance(message)) { + acc.push({ + name: message.name ?? 'unknown tool', + content: message.content.toString(), + }); + } + return acc; + }, []); + + return { toolsUsed }; + } finally { + await client?.close(); + } +} diff --git a/vitest.config.eval.ts b/vitest.config.eval.ts deleted file mode 100644 index 721c87513..000000000 --- a/vitest.config.eval.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; - -import { configShared } from './configShared'; - -export default mergeConfig( - defineConfig(configShared), - defineConfig({ - test: { - dir: 'tests/eval', - testTimeout: 60 * 1000, - fileParallelism: false, - outputFile: 'junit/eval.xml', - }, - }), -);