diff --git a/package-lock.json b/package-lock.json index f25675ffda8..12d2d07b50d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1108,6 +1108,24 @@ "node": ">=14.0" } }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -8298,6 +8316,10 @@ "resolved": "packages/compass-app-stores", "link": true }, + "node_modules/@mongodb-js/compass-assistant": { + "resolved": "packages/compass-assistant", + "link": true + }, "node_modules/@mongodb-js/compass-collection": { "resolved": "packages/compass-collection", "link": true @@ -11865,6 +11887,15 @@ "@octokit/openapi-types": "^7.3.4" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -13514,6 +13545,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", @@ -23351,6 +23388,15 @@ "node": ">=0.4.x" } }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -40525,6 +40571,18 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, + "node_modules/throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -43757,6 +43815,7 @@ "@mongodb-js/compass-aggregations": "^9.71.0", "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-app-stores": "^7.55.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-collection": "^4.68.0", "@mongodb-js/compass-components": "^1.47.0", "@mongodb-js/compass-connection-import-export": "^0.65.0", @@ -44107,6 +44166,217 @@ "url": "https://opencollective.com/sinon" } }, + "packages/compass-assistant": { + "name": "@mongodb-js/compass-assistant", + "version": "1.0.0", + "license": "SSPL", + "dependencies": { + "@ai-sdk/openai": "^2.0.4", + "@mongodb-js/atlas-service": "^0.54.0", + "@mongodb-js/compass-app-registry": "^9.4.18", + "@mongodb-js/compass-components": "^1.30.5", + "ai": "^5.0.5", + "compass-preferences-model": "^2.49.0", + "react": "^17.0.2", + "throttleit": "^2.1.0", + "use-sync-external-store": "^1.5.0" + }, + "devDependencies": { + "@mongodb-js/eslint-config-compass": "^1.4.5", + "@mongodb-js/mocha-config-compass": "^1.7.0", + "@mongodb-js/prettier-config-compass": "^1.2.8", + "@mongodb-js/testing-library-compass": "^1.3.8", + "@mongodb-js/tsconfig-compass": "^1.2.9", + "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", + "@types/mocha": "^9.0.0", + "@types/react": "^17.0.5", + "@types/react-dom": "^17.0.10", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "sinon": "^17.0.1", + "typescript": "^5.8.3", + "xvfb-maybe": "^0.2.1" + } + }, + "packages/compass-assistant/node_modules/@ai-sdk/gateway": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.4.tgz", + "integrity": "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "packages/compass-assistant/node_modules/@ai-sdk/openai": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.10.tgz", + "integrity": "sha512-vnB6Jk2Qb245fajaWjG3q6N0QQy/uej7kZ0QR9xxq09x++3Tx/UPOcgAKMhFFA2fnuGpkFSRKoiDCyp/E3RARQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "packages/compass-assistant/node_modules/@ai-sdk/provider-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.1.tgz", + "integrity": "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.3", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "packages/compass-assistant/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/compass-assistant/node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "packages/compass-assistant/node_modules/@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + } + }, + "packages/compass-assistant/node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "packages/compass-assistant/node_modules/ai": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.10.tgz", + "integrity": "sha512-oPvaifsnHZzT3I07qI9jgWDOGpXDAFSXJ54rgpeHSq6qKQlQ3vwaCgQz861wb+5iJ/kk+B/qm3i5Yfghc/+XSw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "1.0.4", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "packages/compass-assistant/node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, + "packages/compass-assistant/node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "packages/compass-assistant/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "packages/compass-assistant/node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "packages/compass-assistant/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/compass-assistant/node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "packages/compass-collection": { "name": "@mongodb-js/compass-collection", "version": "4.68.0", @@ -48031,6 +48301,7 @@ "@mongodb-js/compass-aggregations": "^9.71.0", "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-app-stores": "^7.55.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-collection": "^4.68.0", "@mongodb-js/compass-components": "^1.47.0", "@mongodb-js/compass-connections": "^1.69.0", @@ -51063,6 +51334,21 @@ } } }, + "@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "requires": { + "json-schema": "^0.4.0" + }, + "dependencies": { + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + } + } + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -56747,6 +57033,167 @@ } } }, + "@mongodb-js/compass-assistant": { + "version": "file:packages/compass-assistant", + "requires": { + "@ai-sdk/openai": "^2.0.4", + "@mongodb-js/atlas-service": "^0.54.0", + "@mongodb-js/compass-app-registry": "^9.4.18", + "@mongodb-js/compass-components": "^1.30.5", + "@mongodb-js/eslint-config-compass": "^1.4.5", + "@mongodb-js/mocha-config-compass": "^1.7.0", + "@mongodb-js/prettier-config-compass": "^1.2.8", + "@mongodb-js/testing-library-compass": "^1.3.8", + "@mongodb-js/tsconfig-compass": "^1.2.9", + "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", + "@types/mocha": "^9.0.0", + "@types/react": "^17.0.5", + "@types/react-dom": "^17.0.10", + "@types/sinon-chai": "^3.2.5", + "ai": "^5.0.5", + "chai": "^4.3.6", + "compass-preferences-model": "^2.49.0", + "depcheck": "^1.4.1", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "react": "^17.0.2", + "sinon": "^17.0.1", + "throttleit": "^2.1.0", + "typescript": "^5.8.3", + "use-sync-external-store": "^1.5.0", + "xvfb-maybe": "^0.2.1" + }, + "dependencies": { + "@ai-sdk/gateway": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.4.tgz", + "integrity": "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ==", + "requires": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1" + } + }, + "@ai-sdk/openai": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-2.0.10.tgz", + "integrity": "sha512-vnB6Jk2Qb245fajaWjG3q6N0QQy/uej7kZ0QR9xxq09x++3Tx/UPOcgAKMhFFA2fnuGpkFSRKoiDCyp/E3RARQ==", + "requires": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1" + } + }, + "@ai-sdk/provider-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.1.tgz", + "integrity": "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g==", + "requires": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.3", + "zod-to-json-schema": "^3.24.1" + } + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, + "ai": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.10.tgz", + "integrity": "sha512-oPvaifsnHZzT3I07qI9jgWDOGpXDAFSXJ54rgpeHSq6qKQlQ3vwaCgQz861wb+5iJ/kk+B/qm3i5Yfghc/+XSw==", + "requires": { + "@ai-sdk/gateway": "1.0.4", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.1", + "@opentelemetry/api": "1.9.0" + } + }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + } + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "peer": true + }, + "zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "requires": {} + } + } + }, "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { @@ -59811,6 +60258,7 @@ "@mongodb-js/compass-aggregations": "^9.71.0", "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-app-stores": "^7.55.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-collection": "^4.68.0", "@mongodb-js/compass-components": "^1.47.0", "@mongodb-js/compass-connections": "^1.69.0", @@ -63717,6 +64165,11 @@ "@octokit/openapi-types": "^7.3.4" } }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" + }, "@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -65040,6 +65493,11 @@ } } }, + "@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, "@szmarczak/http-timer": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", @@ -73027,6 +73485,11 @@ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" }, + "eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==" + }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -79780,6 +80243,7 @@ "@mongodb-js/compass-aggregations": "^9.71.0", "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-app-stores": "^7.55.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-collection": "^4.68.0", "@mongodb-js/compass-components": "^1.47.0", "@mongodb-js/compass-connection-import-export": "^0.65.0", @@ -87202,6 +87666,11 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, + "throttleit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", + "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/packages/atlas-service/src/atlas-service.spec.ts b/packages/atlas-service/src/atlas-service.spec.ts index b63d7a9a1d6..b23a0418398 100644 --- a/packages/atlas-service/src/atlas-service.spec.ts +++ b/packages/atlas-service/src/atlas-service.spec.ts @@ -15,6 +15,7 @@ const ATLAS_CONFIG = { issuer: 'http://example.com/oauth2/default', }, authPortalUrl: 'http://example.com/account/login', + assistantApiBaseUrl: 'http://example.com/assistant', }; function getAtlasService( diff --git a/packages/atlas-service/src/atlas-service.ts b/packages/atlas-service/src/atlas-service.ts index c7e5bbd7203..e6684657f58 100644 --- a/packages/atlas-service/src/atlas-service.ts +++ b/packages/atlas-service/src/atlas-service.ts @@ -67,6 +67,9 @@ export class AtlasService { cloudEndpoint(path?: string): string { return `${this.config.cloudBaseUrl}${normalizePath(path)}`; } + assistantApiEndpoint(path?: string): string { + return `${this.config.assistantApiBaseUrl}${normalizePath(path)}`; + } regionalizedCloudEndpoint( _atlasMetadata: Pick, path?: string diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index 5f9bec8c48f..cb361830be0 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -56,6 +56,7 @@ describe('CompassAuthServiceMain', function () { clientId: '1234abcd', }, authPortalUrl: 'http://example.com', + assistantApiBaseUrl: 'http://example.com/assistant', }; const fetch = CompassAuthService['fetch']; diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index 48abca07cd4..7f8d711c986 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -116,6 +116,10 @@ export type AtlasServiceConfig = { * Atlas Account Portal UI base url */ authPortalUrl: string; + /** + * Assistant API base url + */ + assistantApiBaseUrl: string; }; /** @@ -139,6 +143,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'atlas-dev': { wsBaseUrl: '', @@ -149,6 +154,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'atlas-qa': { wsBaseUrl: '', @@ -159,6 +165,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-qa.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, atlas: { wsBaseUrl: '', @@ -169,6 +176,7 @@ const config = { issuer: 'https://auth.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'web-sandbox-atlas-local': { wsBaseUrl: '/ccs', @@ -179,6 +187,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'web-sandbox-atlas-dev': { wsBaseUrl: '/ccs', @@ -189,6 +198,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'web-sandbox-atlas-qa': { wsBaseUrl: '/ccs', @@ -199,6 +209,7 @@ const config = { issuer: 'https://auth-qa.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account-dev.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, 'web-sandbox-atlas': { wsBaseUrl: '/ccs', @@ -209,6 +220,7 @@ const config = { issuer: 'https://auth.mongodb.com/oauth2/default', }, authPortalUrl: 'https://account.mongodb.com/account/login', + assistantApiBaseUrl: 'https://knowledge.staging.corp.mongodb.com/api/v1', }, } as const; @@ -223,6 +235,7 @@ export function getAtlasConfig( issuer: process.env.COMPASS_OIDC_ISSUER_OVERRIDE, }, authPortalUrl: process.env.COMPASS_ATLAS_AUTH_PORTAL_URL_OVERRIDE, + assistantApiBaseUrl: process.env.COMPASS_ASSISTANT_BASE_URL_OVERRIDE, }; return defaultsDeep( envConfig, diff --git a/packages/compass-assistant/.depcheckrc b/packages/compass-assistant/.depcheckrc new file mode 100644 index 00000000000..ae7c8273e41 --- /dev/null +++ b/packages/compass-assistant/.depcheckrc @@ -0,0 +1,11 @@ +ignores: + - '@mongodb-js/prettier-config-compass' + - '@mongodb-js/tsconfig-compass' + - '@types/chai' + - '@types/sinon-chai' + - 'sinon' + - '@types/chai-dom' + - '@types/react' + - '@types/react-dom' +ignore-patterns: + - 'dist' diff --git a/packages/compass-assistant/.eslintignore b/packages/compass-assistant/.eslintignore new file mode 100644 index 00000000000..85a8a75e68c --- /dev/null +++ b/packages/compass-assistant/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/compass-assistant/.eslintrc.js b/packages/compass-assistant/.eslintrc.js new file mode 100644 index 00000000000..f06f8fc6013 --- /dev/null +++ b/packages/compass-assistant/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-compass/plugin'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/compass-assistant/.mocharc.js b/packages/compass-assistant/.mocharc.js new file mode 100644 index 00000000000..a7e53abc444 --- /dev/null +++ b/packages/compass-assistant/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-compass/compass-plugin'); diff --git a/packages/compass-assistant/package.json b/packages/compass-assistant/package.json new file mode 100644 index 00000000000..b606a3dac53 --- /dev/null +++ b/packages/compass-assistant/package.json @@ -0,0 +1,83 @@ +{ + "name": "@mongodb-js/compass-assistant", + "description": "Compass plugin for the AI Assistant", + "author": { + "name": "MongoDB Inc", + "email": "compass@mongodb.com" + }, + "private": true, + "bugs": { + "url": "https://jira.mongodb.org/projects/COMPASS/issues", + "email": "compass@mongodb.com" + }, + "homepage": "https://github.com/mongodb-js/compass", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/compass.git" + }, + "files": [ + "dist" + ], + "license": "SSPL", + "main": "dist/index.js", + "compass:main": "src/index.tsx", + "exports": { + "import": "./dist/.esm-wrapper.mjs", + "require": "./dist/index.js" + }, + "compass:exports": { + ".": "./src/index.tsx" + }, + "types": "./dist/index.d.ts", + "scripts": { + "bootstrap": "npm run compile", + "compile": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig-lint.json --noEmit", + "eslint": "eslint-compass", + "prettier": "prettier-compass", + "lint": "npm run eslint . && npm run prettier -- --check .", + "depcheck": "compass-scripts check-peer-deps && depcheck", + "check": "npm run typecheck && npm run lint && npm run depcheck", + "check-ci": "npm run check", + "test": "mocha", + "test-electron": "xvfb-maybe electron-mocha --no-sandbox", + "test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", + "test-watch": "npm run test -- --watch", + "test-ci": "npm run test-cov", + "test-ci-electron": "npm run test-electron", + "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." + }, + "dependencies": { + "@ai-sdk/openai": "^2.0.4", + "@mongodb-js/atlas-service": "^0.54.0", + "@mongodb-js/compass-app-registry": "^9.4.18", + "@mongodb-js/compass-components": "^1.30.5", + "ai": "^5.0.5", + "compass-preferences-model": "^2.49.0", + "react": "^17.0.2", + "throttleit": "^2.1.0", + "use-sync-external-store": "^1.5.0" + }, + "devDependencies": { + "@mongodb-js/eslint-config-compass": "^1.4.5", + "@mongodb-js/mocha-config-compass": "^1.7.0", + "@mongodb-js/prettier-config-compass": "^1.2.8", + "@mongodb-js/testing-library-compass": "^1.3.8", + "@mongodb-js/tsconfig-compass": "^1.2.9", + "@types/chai": "^4.2.21", + "@types/chai-dom": "^0.0.10", + "@types/mocha": "^9.0.0", + "@types/react": "^17.0.5", + "@types/react-dom": "^17.0.10", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "sinon": "^17.0.1", + "typescript": "^5.8.3", + "xvfb-maybe": "^0.2.1" + }, + "is_compass_plugin": true +} diff --git a/packages/compass-assistant/src/@ai-sdk/react/chat-react.ts b/packages/compass-assistant/src/@ai-sdk/react/chat-react.ts new file mode 100644 index 00000000000..dfa4ede2c6e --- /dev/null +++ b/packages/compass-assistant/src/@ai-sdk/react/chat-react.ts @@ -0,0 +1,149 @@ +// Copyright 2023 Vercel, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { + AbstractChat, + type ChatInit, + type ChatState, + type ChatStatus, + type UIMessage, +} from 'ai'; +import { throttle } from './throttle'; + +class ReactChatState + implements ChatState +{ + #messages: UI_MESSAGE[]; + #status: ChatStatus = 'ready'; + #error: Error | undefined = undefined; + + #messagesCallbacks = new Set<() => void>(); + #statusCallbacks = new Set<() => void>(); + #errorCallbacks = new Set<() => void>(); + + constructor(initialMessages: UI_MESSAGE[] = []) { + this.#messages = initialMessages; + } + + get status(): ChatStatus { + return this.#status; + } + + set status(newStatus: ChatStatus) { + this.#status = newStatus; + this.#callStatusCallbacks(); + } + + get error(): Error | undefined { + return this.#error; + } + + set error(newError: Error | undefined) { + this.#error = newError; + this.#callErrorCallbacks(); + } + + get messages(): UI_MESSAGE[] { + return this.#messages; + } + + set messages(newMessages: UI_MESSAGE[]) { + this.#messages = [...newMessages]; + this.#callMessagesCallbacks(); + } + + pushMessage = (message: UI_MESSAGE) => { + this.#messages = this.#messages.concat(message); + this.#callMessagesCallbacks(); + }; + + popMessage = () => { + this.#messages = this.#messages.slice(0, -1); + this.#callMessagesCallbacks(); + }; + + replaceMessage = (index: number, message: UI_MESSAGE) => { + this.#messages = [ + ...this.#messages.slice(0, index), + // We deep clone the message here to ensure the new React Compiler (currently in RC) detects deeply nested parts/metadata changes: + this.snapshot(message), + ...this.#messages.slice(index + 1), + ]; + this.#callMessagesCallbacks(); + }; + + snapshot = (value: T): T => structuredClone(value); + + '~registerMessagesCallback' = ( + onChange: () => void, + throttleWaitMs?: number + ): (() => void) => { + const callback = throttleWaitMs + ? throttle(onChange, throttleWaitMs) + : onChange; + this.#messagesCallbacks.add(callback); + return () => { + this.#messagesCallbacks.delete(callback); + }; + }; + + '~registerStatusCallback' = (onChange: () => void): (() => void) => { + this.#statusCallbacks.add(onChange); + return () => { + this.#statusCallbacks.delete(onChange); + }; + }; + + '~registerErrorCallback' = (onChange: () => void): (() => void) => { + this.#errorCallbacks.add(onChange); + return () => { + this.#errorCallbacks.delete(onChange); + }; + }; + + #callMessagesCallbacks = () => { + this.#messagesCallbacks.forEach((callback) => callback()); + }; + + #callStatusCallbacks = () => { + this.#statusCallbacks.forEach((callback) => callback()); + }; + + #callErrorCallbacks = () => { + this.#errorCallbacks.forEach((callback) => callback()); + }; +} + +export class Chat< + UI_MESSAGE extends UIMessage +> extends AbstractChat { + #state: ReactChatState; + + constructor({ messages, ...init }: ChatInit) { + const state = new ReactChatState(messages); + super({ ...init, state }); + this.#state = state; + } + + '~registerMessagesCallback' = ( + onChange: () => void, + throttleWaitMs?: number + ): (() => void) => + this.#state['~registerMessagesCallback'](onChange, throttleWaitMs); + + '~registerStatusCallback' = (onChange: () => void): (() => void) => + this.#state['~registerStatusCallback'](onChange); + + '~registerErrorCallback' = (onChange: () => void): (() => void) => + this.#state['~registerErrorCallback'](onChange); +} diff --git a/packages/compass-assistant/src/@ai-sdk/react/throttle.ts b/packages/compass-assistant/src/@ai-sdk/react/throttle.ts new file mode 100644 index 00000000000..8090bf15b83 --- /dev/null +++ b/packages/compass-assistant/src/@ai-sdk/react/throttle.ts @@ -0,0 +1,22 @@ +// Copyright 2023 Vercel, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import throttleFunction from 'throttleit'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function throttle any>( + fn: T, + waitMs: number | undefined +): T { + return waitMs !== undefined ? throttleFunction(fn, waitMs) : fn; +} diff --git a/packages/compass-assistant/src/@ai-sdk/react/use-chat.ts b/packages/compass-assistant/src/@ai-sdk/react/use-chat.ts new file mode 100644 index 00000000000..6dfb36fda40 --- /dev/null +++ b/packages/compass-assistant/src/@ai-sdk/react/use-chat.ts @@ -0,0 +1,125 @@ +// Copyright 2023 Vercel, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import type { AbstractChat, ChatInit, CreateUIMessage, UIMessage } from 'ai'; +import { useCallback, useEffect, useRef } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { Chat } from './chat-react'; + +export type { CreateUIMessage, UIMessage }; + +export type UseChatHelpers = { + /** + * The id of the chat. + */ + readonly id: string; + + /** + * Update the `messages` state locally. This is useful when you want to + * edit the messages on the client, and then trigger the `reload` method + * manually to regenerate the AI response. + */ + setMessages: ( + messages: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]) + ) => void; + + error: Error | undefined; +} & Pick< + AbstractChat, + | 'sendMessage' + | 'regenerate' + | 'stop' + | 'resumeStream' + | 'addToolResult' + | 'status' + | 'messages' +>; + +export type UseChatOptions = + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + ({ chat: Chat } | ChatInit) & { + /** +Custom throttle wait in ms for the chat messages and data updates. +Default is undefined, which disables throttling. + */ + experimental_throttle?: number; + + /** + * Whether to resume an ongoing chat generation stream. + */ + resume?: boolean; + }; + +export function useChat({ + experimental_throttle: throttleWaitMs, + resume = false, + ...options +}: UseChatOptions = {}): UseChatHelpers { + const chatRef = useRef('chat' in options ? options.chat : new Chat(options)); + + const subscribeToMessages = useCallback( + (update: () => void) => + chatRef.current['~registerMessagesCallback'](update, throttleWaitMs), + [throttleWaitMs] + ); + + const messages = useSyncExternalStore( + subscribeToMessages, + () => chatRef.current.messages, + () => chatRef.current.messages + ); + + const status = useSyncExternalStore( + chatRef.current['~registerStatusCallback'], + () => chatRef.current.status, + () => chatRef.current.status + ); + + const error = useSyncExternalStore( + chatRef.current['~registerErrorCallback'], + () => chatRef.current.error, + () => chatRef.current.error + ); + + const setMessages = useCallback( + ( + messagesParam: UI_MESSAGE[] | ((messages: UI_MESSAGE[]) => UI_MESSAGE[]) + ) => { + if (typeof messagesParam === 'function') { + messagesParam = messagesParam(messages); + } + + chatRef.current.messages = messagesParam; + }, + [messages, chatRef] + ); + + useEffect(() => { + if (resume) { + void chatRef.current.resumeStream(); + } + }, [resume, chatRef]); + + return { + id: chatRef.current.id, + messages, + setMessages, + sendMessage: chatRef.current.sendMessage, + regenerate: chatRef.current.regenerate, + stop: chatRef.current.stop, + error, + resumeStream: chatRef.current.resumeStream, + status, + addToolResult: chatRef.current.addToolResult, + }; +} diff --git a/packages/compass-assistant/src/assistant-chat.spec.tsx b/packages/compass-assistant/src/assistant-chat.spec.tsx new file mode 100644 index 00000000000..c281a590f19 --- /dev/null +++ b/packages/compass-assistant/src/assistant-chat.spec.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import { render, screen, userEvent } from '@mongodb-js/testing-library-compass'; +import { AssistantChat } from './assistant-chat'; +import { expect } from 'chai'; +import type { UIMessage } from './@ai-sdk/react/use-chat'; + +describe('AssistantChat', function () { + const mockMessages: UIMessage[] = [ + { + id: '1', + role: 'user', + parts: [{ type: 'text', text: 'Hello, MongoDB Assistant!' }], + }, + { + id: '2', + role: 'assistant', + parts: [ + { + type: 'text', + text: 'Hello! How can I help you with MongoDB today?', + }, + ], + }, + ]; + + it('renders input field and send button', function () { + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + const sendButton = screen.getByTestId('assistant-chat-send-button'); + + expect(inputField).to.exist; + expect(sendButton).to.exist; + }); + + it('input field accepts text input', function () { + render(); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const inputField = screen.getByTestId( + 'assistant-chat-input' + ) as HTMLInputElement; + + userEvent.type(inputField, 'What is MongoDB?'); + + expect(inputField.value).to.equal('What is MongoDB?'); + }); + + it('send button is disabled when input is empty', function () { + render(); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const sendButton = screen.getByTestId( + 'assistant-chat-send-button' + ) as HTMLButtonElement; + + expect(sendButton.disabled).to.be.true; + }); + + it('send button is enabled when input has text', function () { + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const sendButton = screen.getByTestId( + 'assistant-chat-send-button' + ) as HTMLButtonElement; + + userEvent.type(inputField, 'What is MongoDB?'); + + expect(sendButton.disabled).to.be.false; + }); + + it('send button is disabled for whitespace-only input', function () { + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const sendButton = screen.getByTestId( + 'assistant-chat-send-button' + ) as HTMLButtonElement; + + userEvent.type(inputField, ' '); + + expect(sendButton.disabled).to.be.true; + }); + + it('displays messages in the chat feed', function () { + render(); + + expect(screen.getByTestId('assistant-message-user')).to.exist; + expect(screen.getByTestId('assistant-message-assistant')).to.exist; + expect(screen.getByText('Hello, MongoDB Assistant!')).to.exist; + expect(screen.getByText('Hello! How can I help you with MongoDB today?')).to + .exist; + }); + + it('calls onSendMessage when form is submitted', function () { + let sentMessage = ''; + const handleSendMessage = (message: string) => { + sentMessage = message; + }; + + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + const sendButton = screen.getByTestId('assistant-chat-send-button'); + + userEvent.type(inputField, 'What is aggregation?'); + userEvent.click(sendButton); + + expect(sentMessage).to.equal('What is aggregation?'); + }); + + it('clears input field after successful submission', function () { + const handleSendMessage = () => {}; + + render(); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const inputField = screen.getByTestId( + 'assistant-chat-input' + ) as HTMLInputElement; + + userEvent.type(inputField, 'Test message'); + expect(inputField.value).to.equal('Test message'); + + userEvent.click(screen.getByTestId('assistant-chat-send-button')); + expect(inputField.value).to.equal(''); + }); + + it('trims whitespace from input before sending', function () { + let sentMessage = ''; + const handleSendMessage = (message: string) => { + sentMessage = message; + }; + + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + + userEvent.type(inputField, ' What is sharding? '); + userEvent.click(screen.getByTestId('assistant-chat-send-button')); + + expect(sentMessage).to.equal('What is sharding?'); + }); + + it('does not call onSendMessage when input is empty or whitespace-only', function () { + let messageSent = false; + const handleSendMessage = () => { + messageSent = true; + }; + + render(); + + const inputField = screen.getByTestId('assistant-chat-input'); + const chatForm = screen.getByTestId('assistant-chat-form'); + + // Test empty input + userEvent.click(chatForm); + expect(messageSent).to.be.false; + + // Test whitespace-only input + userEvent.type(inputField, ' '); + userEvent.click(chatForm); + expect(messageSent).to.be.false; + }); + + it('displays user and assistant messages with different styling', function () { + render(); + + const userMessage = screen.getByTestId('assistant-message-user'); + const assistantMessage = screen.getByTestId('assistant-message-assistant'); + + // User messages should have different background color than assistant messages + expect(userMessage).to.exist; + expect(assistantMessage).to.exist; + + const userStyle = window.getComputedStyle(userMessage); + const assistantStyle = window.getComputedStyle(assistantMessage); + + expect(userStyle.backgroundColor).to.not.equal( + assistantStyle.backgroundColor + ); + }); + + it('handles messages with multiple text parts', function () { + const messagesWithMultipleParts: UIMessage[] = [ + { + id: '1', + role: 'assistant', + parts: [ + { type: 'text', text: 'Here is part 1. ' }, + { type: 'text', text: 'And here is part 2.' }, + ], + }, + ]; + + render(); + + expect(screen.getByText('Here is part 1. And here is part 2.')).to.exist; + }); + + it('handles messages with mixed part types (filters to text only)', function () { + const messagesWithMixedParts: UIMessage[] = [ + { + id: '1', + role: 'assistant', + parts: [ + { type: 'text', text: 'This is text content.' }, + // @ts-expect-error - tool-call is not a valid part type + { type: 'tool-call', text: 'This should be filtered out.' }, + { type: 'text', text: ' More text content.' }, + ], + }, + ]; + + render(); + + expect(screen.getByText('This is text content. More text content.')).to + .exist; + expect(screen.queryByText('This should be filtered out.')).to.not.exist; + }); +}); diff --git a/packages/compass-assistant/src/assistant-chat.tsx b/packages/compass-assistant/src/assistant-chat.tsx new file mode 100644 index 00000000000..39dc889493b --- /dev/null +++ b/packages/compass-assistant/src/assistant-chat.tsx @@ -0,0 +1,124 @@ +import React, { useCallback, useState } from 'react'; +import type { UIMessage } from './@ai-sdk/react/use-chat'; + +interface AssistantChatProps { + messages: UIMessage[]; + onSendMessage?: (message: string) => void; +} + +/** + * This component is currently using placeholders as Leafygreen UI updates are not available yet. + * Before release, we will replace this with the actual Leafygreen chat components. + */ +export const AssistantChat: React.FunctionComponent = ({ + messages, + onSendMessage, +}) => { + const [inputValue, setInputValue] = useState(''); + + const handleInputSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + if (inputValue.trim() && onSendMessage) { + onSendMessage(inputValue.trim()); + setInputValue(''); + } + }, + [inputValue, onSendMessage] + ); + + return ( +
+ {/* Message Feed */} +
+ {messages.map((message) => ( +
+ {message.parts + ?.filter((part) => part.type === 'text') + .map((part) => part.text) + .join('') || ''} +
+ ))} +
+ + {/* Input Bar */} +
+ setInputValue(e.target.value)} + placeholder="Ask MongoDB Assistant a question" + style={{ + flex: 1, + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px', + }} + /> + +
+
+ ); +}; diff --git a/packages/compass-assistant/src/assistant-provider.tsx b/packages/compass-assistant/src/assistant-provider.tsx new file mode 100644 index 00000000000..296cfa6b623 --- /dev/null +++ b/packages/compass-assistant/src/assistant-provider.tsx @@ -0,0 +1,60 @@ +import { DrawerSection } from '@mongodb-js/compass-components'; +import React, { type PropsWithChildren, useCallback, useRef } from 'react'; +import { type UIMessage, useChat } from './@ai-sdk/react/use-chat'; +import type { Chat } from './@ai-sdk/react/chat-react'; +import { AssistantChat } from './assistant-chat'; +import { usePreference } from 'compass-preferences-model/provider'; + +export const ASSISTANT_DRAWER_ID = 'compass-assistant-drawer'; + +import { createContext, useContext } from 'react'; + +type AssistantActions = unknown; + +export const AssistantActionsContext = createContext({}); + +export function useAssistantActions(): AssistantActions { + return useContext(AssistantActionsContext); +} + +export const AssistantProvider: React.FunctionComponent< + PropsWithChildren<{ + chat: Chat; + }> +> = ({ chat, children }) => { + const enableAIAssistant = usePreference('enableAIAssistant'); + + const { messages, sendMessage } = useChat({ + chat, + }); + + const contextActions = useRef({}); + + const handleMessageSend = useCallback( + (messageBody: string) => { + void sendMessage({ text: messageBody }); + }, + [sendMessage] + ); + + if (!enableAIAssistant) { + return <>{children}; + } + + return ( + + + + + {children} + + ); +}; + +// Keep the old component name for backward compatibility during transition +export const AssistantDrawerSection = AssistantProvider; diff --git a/packages/compass-assistant/src/docs-provider-transport.ts b/packages/compass-assistant/src/docs-provider-transport.ts new file mode 100644 index 00000000000..44261e1124e --- /dev/null +++ b/packages/compass-assistant/src/docs-provider-transport.ts @@ -0,0 +1,37 @@ +import { + type ChatTransport, + type UIMessage, + type UIMessageChunk, + convertToModelMessages, + streamText, +} from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; + +export class DocsProviderTransport implements ChatTransport { + private openai: ReturnType; + + constructor({ baseUrl }: { baseUrl: string }) { + this.openai = createOpenAI({ + baseURL: baseUrl, + apiKey: '', + }); + } + + sendMessages({ + messages, + abortSignal, + }: Parameters['sendMessages']>[0]) { + const result = streamText({ + model: this.openai.responses('mongodb-chat-latest'), + messages: convertToModelMessages(messages), + abortSignal: abortSignal, + }); + + return Promise.resolve(result.toUIMessageStream()); + } + + reconnectToStream(): Promise | null> { + // For this implementation, we don't support reconnecting to streams + return Promise.resolve(null); + } +} diff --git a/packages/compass-assistant/src/index.spec.tsx b/packages/compass-assistant/src/index.spec.tsx new file mode 100644 index 00000000000..1041ddabba6 --- /dev/null +++ b/packages/compass-assistant/src/index.spec.tsx @@ -0,0 +1,45 @@ +import { render } from '@mongodb-js/testing-library-compass'; +import { CompassAssistantProvider } from '.'; +import React from 'react'; +import sinon from 'sinon'; +import { expect } from 'chai'; +import { + DrawerAnchor, + DrawerContentProvider, +} from '@mongodb-js/compass-components'; +import { Chat } from './@ai-sdk/react/chat-react'; +import type { AtlasService } from '@mongodb-js/atlas-service/provider'; + +describe('CompassAssistantProvider', function () { + beforeEach(function () { + process.env.COMPASS_ASSISTANT_USE_ATLAS_SERVICE_URL = 'true'; + }); + + afterEach(function () { + delete process.env.COMPASS_ASSISTANT_USE_ATLAS_SERVICE_URL; + }); + + it('uses the Atlas Service assistantApiEndpoint', function () { + const mockAtlasService = { + assistantApiEndpoint: sinon + .stub() + .returns('https://atlas-assistant-api.example.com/api/v1'), + }; + + const MockedProvider = CompassAssistantProvider.withMockServices({ + atlasService: mockAtlasService as unknown as AtlasService, + }); + + render( + + + + , + { + preferences: { enableAIAssistant: true }, + } + ); + + expect(mockAtlasService.assistantApiEndpoint.calledOnce).to.be.true; + }); +}); diff --git a/packages/compass-assistant/src/index.tsx b/packages/compass-assistant/src/index.tsx new file mode 100644 index 00000000000..d786fe06d9a --- /dev/null +++ b/packages/compass-assistant/src/index.tsx @@ -0,0 +1,44 @@ +import { registerCompassPlugin } from '@mongodb-js/compass-app-registry'; +import { AssistantProvider } from './assistant-provider'; +import { Chat } from './@ai-sdk/react/chat-react'; +import { DocsProviderTransport } from './docs-provider-transport'; +import { atlasServiceLocator } from '@mongodb-js/atlas-service/provider'; +import type { PropsWithChildren } from 'react'; +import type { UIMessage } from 'ai'; +import React from 'react'; + +const CompassAssistantProvider = registerCompassPlugin( + { + name: 'CompassAssistant', + component: ({ + chat, + children, + }: PropsWithChildren<{ + chat?: Chat; + }>) => { + if (!chat) { + throw new Error('Chat was not provided by the state'); + } + return {children}; + }, + activate: (initialProps, { atlasService }) => { + const chat = new Chat({ + transport: new DocsProviderTransport({ + baseUrl: atlasService.assistantApiEndpoint(), + }), + }); + return { + store: { state: { chat } }, + deactivate: () => {}, + }; + }, + }, + { + atlasService: atlasServiceLocator, + } +); + +export { CompassAssistantProvider }; + +// Export hooks and components for external use +export { AssistantProvider, useAssistantActions } from './assistant-provider'; diff --git a/packages/compass-assistant/tsconfig-lint.json b/packages/compass-assistant/tsconfig-lint.json new file mode 100644 index 00000000000..6bdef84f322 --- /dev/null +++ b/packages/compass-assistant/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/compass-assistant/tsconfig.json b/packages/compass-assistant/tsconfig.json new file mode 100644 index 00000000000..79bc84584ce --- /dev/null +++ b/packages/compass-assistant/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mongodb-js/tsconfig-compass/tsconfig.react.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["./src/**/*.spec.*"] +} diff --git a/packages/compass-preferences-model/src/feature-flags.ts b/packages/compass-preferences-model/src/feature-flags.ts index b45be8e0e61..216d9518b76 100644 --- a/packages/compass-preferences-model/src/feature-flags.ts +++ b/packages/compass-preferences-model/src/feature-flags.ts @@ -29,6 +29,7 @@ export type FeatureFlags = { enableContextMenus: boolean; enableSearchActivationProgramP1: boolean; enableUnauthenticatedGenAI: boolean; + enableAIAssistant: boolean; }; export const featureFlags: Required<{ @@ -167,4 +168,14 @@ export const featureFlags: Required<{ short: 'Enable interface to view and modify search indexes', }, }, + + /** + * Feature flag for AI Assistant. + */ + enableAIAssistant: { + stage: 'development', + description: { + short: 'Enable AI Assistant', + }, + }, }; diff --git a/packages/compass-web/package.json b/packages/compass-web/package.json index c38a7a03ed7..0ac63cbffee 100644 --- a/packages/compass-web/package.json +++ b/packages/compass-web/package.json @@ -67,6 +67,7 @@ "@mongodb-js/compass-aggregations": "^9.71.0", "@mongodb-js/compass-app-registry": "^9.4.18", "@mongodb-js/compass-app-stores": "^7.55.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-collection": "^4.68.0", "@mongodb-js/compass-components": "^1.47.0", "@mongodb-js/compass-connections": "^1.69.0", diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index f7e4fa05e82..b370edd0ab2 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -58,6 +58,7 @@ import { WebWorkspaceTab as WelcomeWorkspaceTab } from '@mongodb-js/compass-welc import { useCompassWebPreferences } from './preferences'; import { DataModelingWorkspaceTab as DataModelingWorkspace } from '@mongodb-js/compass-data-modeling'; import { DataModelStorageServiceProviderInMemory } from '@mongodb-js/compass-data-modeling/web'; +import { CompassAssistantProvider } from '@mongodb-js/compass-assistant'; export type TrackFunction = ( event: string, @@ -410,20 +411,22 @@ const CompassWeb = ({ }} > - - - - - - + + + + + + + + diff --git a/packages/compass/package.json b/packages/compass/package.json index 28e358a0112..d05c13681b6 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -203,6 +203,7 @@ "@mongodb-js/compass-connection-import-export": "^0.65.0", "@mongodb-js/compass-connections": "^1.69.0", "@mongodb-js/compass-crud": "^13.69.0", + "@mongodb-js/compass-assistant": "^1.0.0", "@mongodb-js/compass-data-modeling": "^1.20.0", "@mongodb-js/compass-databases-collections": "^1.68.0", "@mongodb-js/compass-explain-plan": "^6.69.0", diff --git a/packages/compass/src/app/components/home.tsx b/packages/compass/src/app/components/home.tsx index bc5be4406d5..3ca5b5cd396 100644 --- a/packages/compass/src/app/components/home.tsx +++ b/packages/compass/src/app/components/home.tsx @@ -34,6 +34,7 @@ import { ConnectionStorageProvider } from '@mongodb-js/connection-storage/provid import { ConnectionImportExportProvider } from '@mongodb-js/compass-connection-import-export'; import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; import { usePreference } from 'compass-preferences-model/provider'; +import { CompassAssistantProvider } from '@mongodb-js/compass-assistant'; resetGlobalCSS(); @@ -107,22 +108,27 @@ function Home({ return ( - -
- - - -
- - - - - - -
+ + +
+ + + +
+ + + + + + +
+
); diff --git a/packages/compass/src/app/utils/csp.ts b/packages/compass/src/app/utils/csp.ts index 939d81991a3..0b76da8dfa3 100644 --- a/packages/compass/src/app/utils/csp.ts +++ b/packages/compass/src/app/utils/csp.ts @@ -56,6 +56,8 @@ const defaultCSP = { 'https://cloud-qa.mongodb.com', 'https://compass.mongodb.com', 'https://ip-ranges.amazonaws.com', + 'https://knowledge.staging.corp.mongodb.com', + 'https://knowledge.corp.mongodb.com', ], 'child-src': [ 'blob:',