diff --git a/core-web/yarn.lock b/core-web/yarn.lock index 605040fd867f..fb9eb00b9e27 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -334,10 +334,10 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.8", "@babel/compat-data@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.0.tgz#6b226a5da3a686db3c30519750e071dce292ad95" - integrity sha512-P4fwKI2mjEb3ZU5cnMJzvRsRKGBUcs8jvxIoRmr6ufAY9Xk2Bz7JubRTTivkw55c7WQJfTECeqYVa+HZ0FzREg== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" + integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== "@babel/core@7.23.9": version "7.23.9" @@ -382,20 +382,20 @@ semver "^6.3.1" "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.19.6", "@babel/core@^7.21.3", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.24.6": - version "7.24.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82" - integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.24.9" - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-module-transforms" "^7.24.9" - "@babel/helpers" "^7.24.8" - "@babel/parser" "^7.24.8" - "@babel/template" "^7.24.7" - "@babel/traverse" "^7.24.8" - "@babel/types" "^7.24.9" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -412,7 +412,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.23.0", "@babel/generator@^7.23.6", "@babel/generator@^7.24.9", "@babel/generator@^7.25.0", "@babel/generator@^7.7.2": +"@babel/generator@^7.23.0", "@babel/generator@^7.23.6", "@babel/generator@^7.25.0", "@babel/generator@^7.7.2": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== @@ -444,12 +444,12 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271" - integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== dependencies: - "@babel/compat-data" "^7.24.8" + "@babel/compat-data" "^7.25.2" "@babel/helper-validator-option" "^7.24.8" browserslist "^4.23.1" lru-cache "^5.1.1" @@ -469,9 +469,9 @@ semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.0.tgz#17afe5d23b3a833a90f0fab9c2ae69fea192de5c" - integrity sha512-q0T+dknZS+L5LDazIP+02gEZITG5unzvb6yIjcmj5i0eFrs5ToBV2m2JGH4EsE/gtP8ygEGLGApBgRIZkTm7zg== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" + integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== dependencies: "@babel/helper-annotate-as-pure" "^7.24.7" regexpu-core "^5.3.1" @@ -522,15 +522,15 @@ "@babel/traverse" "^7.24.7" "@babel/types" "^7.24.7" -"@babel/helper-module-transforms@^7.23.3", "@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9", "@babel/helper-module-transforms@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.0.tgz#3ffc23c473a2769a7e40d3274495bd559fdd2ecc" - integrity sha512-bIkOa2ZJYn7FHnepzr5iX9Kmz8FjIz4UKzJ9zhX3dnYuVW0xul9RuR3skBfoLu+FPTQw90EHW9rJsSZhyLQ3fQ== +"@babel/helper-module-transforms@^7.23.3", "@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== dependencies: "@babel/helper-module-imports" "^7.24.7" "@babel/helper-simple-access" "^7.24.7" "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.0" + "@babel/traverse" "^7.25.2" "@babel/helper-optimise-call-expression@^7.24.7": version "7.24.7" @@ -609,7 +609,7 @@ "@babel/traverse" "^7.25.0" "@babel/types" "^7.25.0" -"@babel/helpers@^7.23.9", "@babel/helpers@^7.24.0", "@babel/helpers@^7.24.8": +"@babel/helpers@^7.23.9", "@babel/helpers@^7.24.0", "@babel/helpers@^7.25.0": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== @@ -627,7 +627,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.24.0", "@babel/parser@^7.24.8", "@babel/parser@^7.25.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.24.0", "@babel/parser@^7.25.0": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.0.tgz#9fdc9237504d797b6e7b8f66e78ea7f570d256ad" integrity sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA== @@ -998,11 +998,11 @@ "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-transform-flow-strip-types@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz#ae454e62219288fbb734541ab00389bfb13c063e" - integrity sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.2.tgz#b3aa251db44959b7a7c82abcd6b4225dec7d2258" + integrity sha512-InBZ0O8tew5V0K6cHcQ+wgxlrjOw1W4wDXLkOTjLRD8GYhTSkxTVBtdy3MMtvYBrbAWa1Qm3hNoTc1620Yj+Mg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-syntax-flow" "^7.24.7" "@babel/plugin-transform-for-of@^7.23.6", "@babel/plugin-transform-for-of@^7.24.7": @@ -1013,7 +1013,7 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" -"@babel/plugin-transform-function-name@^7.23.3", "@babel/plugin-transform-function-name@^7.25.0": +"@babel/plugin-transform-function-name@^7.23.3", "@babel/plugin-transform-function-name@^7.25.1": version "7.25.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== @@ -1030,12 +1030,12 @@ "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.23.3", "@babel/plugin-transform-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" - integrity sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ== +"@babel/plugin-transform-literals@^7.23.3", "@babel/plugin-transform-literals@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" + integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-logical-assignment-operators@^7.23.4", "@babel/plugin-transform-logical-assignment-operators@^7.24.7": version "7.24.7" @@ -1207,15 +1207,15 @@ "@babel/plugin-transform-react-jsx" "^7.24.7" "@babel/plugin-transform-react-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" - integrity sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz#e37e8ebfa77e9f0b16ba07fadcb6adb47412227a" + integrity sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA== dependencies: "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/types" "^7.25.2" "@babel/plugin-transform-react-pure-annotations@^7.24.7": version "7.24.7" @@ -1301,9 +1301,9 @@ "@babel/helper-plugin-utils" "^7.24.8" "@babel/plugin-transform-typescript@^7.24.7": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.0.tgz#56f47fb87b86a97caa9c7770920a1967d40ac86e" - integrity sha512-LZicxFzHIw+Sa3pzgMgSz6gdpsdkfiMObHUzhSIrwKF0+/rP/nuR49u79pSS+zIFJ1FeGeqQD2Dq4QGFbOVvSw== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" + integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== dependencies: "@babel/helper-annotate-as-pure" "^7.24.7" "@babel/helper-create-class-features-plugin" "^7.25.0" @@ -1429,12 +1429,12 @@ semver "^6.3.1" "@babel/preset-env@^7.19.4", "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.23.2", "@babel/preset-env@^7.24.6": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.0.tgz#3fe92e470311e91478129efda101816c680f0479" - integrity sha512-vYAA8PrCOeZfG4D87hmw1KJ1BPubghXP1e2MacRFwECGNKL76dkA38JEwYllbvQCpf/kLxsTtir0b8MtxKoVCw== + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.2.tgz#15918e9d050c4713a2ab8fa2fa82514eaf16676e" + integrity sha512-Y2Vkwy3ITW4id9c6KXshVV/x5yCGK7VdJmKkzOzNsDZMojRKfSA/033rRbLqlRozmhRXCejxWHLSJOg/wUHfzw== dependencies: - "@babel/compat-data" "^7.25.0" - "@babel/helper-compilation-targets" "^7.24.8" + "@babel/compat-data" "^7.25.2" + "@babel/helper-compilation-targets" "^7.25.2" "@babel/helper-plugin-utils" "^7.24.8" "@babel/helper-validator-option" "^7.24.8" "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.0" @@ -1478,9 +1478,9 @@ "@babel/plugin-transform-exponentiation-operator" "^7.24.7" "@babel/plugin-transform-export-namespace-from" "^7.24.7" "@babel/plugin-transform-for-of" "^7.24.7" - "@babel/plugin-transform-function-name" "^7.25.0" + "@babel/plugin-transform-function-name" "^7.25.1" "@babel/plugin-transform-json-strings" "^7.24.7" - "@babel/plugin-transform-literals" "^7.24.7" + "@babel/plugin-transform-literals" "^7.25.2" "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" "@babel/plugin-transform-member-expression-literals" "^7.24.7" "@babel/plugin-transform-modules-amd" "^7.24.7" @@ -1597,23 +1597,23 @@ "@babel/parser" "^7.25.0" "@babel/types" "^7.25.0" -"@babel/traverse@^7.16.0", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.9", "@babel/traverse@^7.24.0", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.1.tgz#64dbc31effc5f3fa3cf10d19df0e6310214743f5" - integrity sha512-LrHHoWq08ZpmmFqBAzN+hUdWwy5zt7FGa/hVwMcOqW6OVtwqaoD5utfuGYU87JYxdZgLUvktAsn37j/sYR9siA== +"@babel/traverse@^7.16.0", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.9", "@babel/traverse@^7.24.0", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.2.tgz#1a0a4aef53177bead359ccd0c89f4426c805b2ae" + integrity sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ== dependencies: "@babel/code-frame" "^7.24.7" "@babel/generator" "^7.25.0" "@babel/parser" "^7.25.0" "@babel/template" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/types" "^7.25.2" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.24.0", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.25.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.0.tgz#e6e3656c581f28da8452ed4f69e38008ec0ba41b" - integrity sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg== +"@babel/types@^7.0.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.23.9", "@babel/types@^7.24.0", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" + integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== dependencies: "@babel/helper-string-parser" "^7.24.8" "@babel/helper-validator-identifier" "^7.24.7" @@ -6410,186 +6410,186 @@ tinymce "^6.0.0 || ^5.5.0" tslib "^2.3.0" -"@tiptap/core@^2.0.0-beta.218", "@tiptap/core@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.5.7.tgz#681eefd198f9b7b8ad543ca29c56d46aab4919cf" - integrity sha512-8fBW+yBRSc2rEDOs6P+53kF0EAmSv17M4ruQBABo18Nt5qIyr/Uo4p+/E4NkV30bKgKI1zyq1dPeznDplSseqQ== +"@tiptap/core@^2.0.0-beta.218", "@tiptap/core@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.5.8.tgz#58de366b0d2acb0a6e67a4780de64d619ebd90fa" + integrity sha512-lkWCKyoAoMTxM137MoEsorG7tZ5MZU6O3wMRuZ0P9fcTRY5vd1NWncWuPzuGSJIpL20gwBQOsS6PaQSfR3xjlA== -"@tiptap/extension-blockquote@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.5.7.tgz#378921028a25f39d6a1dcebc86efb73fc4f19cce" - integrity sha512-cSnk5ViQgG6SgKnvJ5qaW47jl5qTN0oADXdcfyaY5XrbCPBGCVq1yRZlUtPU/J0YocZpjNLRRSMPVQ3wya5vtQ== +"@tiptap/extension-blockquote@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.5.8.tgz#95880f0b687790dbff85a1c9e83f2afd0011be67" + integrity sha512-P8vDiagtRrUfIewfCKrJe0ddDSjPgOTKzqoM1UXKS+MenT8C/wT4bjiwopAoWP6zMoV0TfHWXah9emllmCfXFA== -"@tiptap/extension-bold@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.5.7.tgz#58c5b4066571d6e71713f587ba7626d005ddde97" - integrity sha512-1uQjlMVsSo5rIM2ebwmn9LeROBBG6a/EHmCry+H9oKY678sBll5HMRD09o63Q4d4Sg2TB8Dx0VgLOIttVrHWXg== +"@tiptap/extension-bold@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.5.8.tgz#97fcfb3b1bada5d0469f12624a2d188fcc523f03" + integrity sha512-4vEn+U7Y8B4e8izcL7QuEKYJ9thCSdo+UF1K3TOqQWuJTzTrJLPMwTZ4vYOHzvuq5uIXyPLnWzLgnRLgy5mJRg== "@tiptap/extension-bubble-menu@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.7.tgz#24c3f41d93022ed4bebe3610124bb114333c834d" - integrity sha512-gkuBuVGm5YPDRUG5Bscj6IYjDbzM7iJ2aXBGCM1rzuIiwT04twY51dKMIeseXa49uk/AQs/mqt3kGQjgSdSFAw== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.8.tgz#e39b176c574b9fd2f59c6457724f3f22a22fb1b8" + integrity sha512-COmd1Azudu7i281emZFIESECe7FnvWiRoBoQBVjjWSyq5PVzwJaA3PAlnU7GyNZKtVXMZ4xbrckdyNQfDeVQDA== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-bullet-list@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.7.tgz#236018107e43da74662ab017ddaa52a2a1e56324" - integrity sha512-tPhmgFJR8jJ/rJuoHLu1XjnC1A1NjmewggArWnqgKj8gNkZDEgZSgCt8YbEVwz+9tlEGvI2T9D7PLmRVpWr8nw== +"@tiptap/extension-bullet-list@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.8.tgz#fddb0d96ce7902214a1f2cb22f03ebbaae028ce8" + integrity sha512-Wvf0HWBI0ulssoCsCOguxJB1Ntmj9PtE8b/ieFwFvrNptP+sf25XiWgjMs7H1KQrtmpngBu/Bhh5jJRgAmAgeQ== "@tiptap/extension-character-count@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.5.7.tgz#19cb87f76d7d33c855e486772cd7f256e80e6a71" - integrity sha512-Gdy8RULDS3oYIjt7fr4IZhKey/AsSdKddxSYt9khTJLMwboMAdQ2y9CPD6yK4lKmoKjmxuzPFQJgdxKDUVB7hw== - -"@tiptap/extension-code-block@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.5.7.tgz#5cb171ad5e046f1fb71a1572af60b26c4bb016d2" - integrity sha512-Nm1Lx+4CxE3q817WcQXR2Vp1ntG4Let7TRpuuI9YUGBmq/7OSm1N+Y7wdRrUcUV9fHCHQFPWQvEv1uW0kFavng== - -"@tiptap/extension-code@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.5.7.tgz#f8ca1133d2c468ffb5a41e6cd3cfd7c519399d35" - integrity sha512-SMIEamu2y6ROy4mWef+2U5ySwNMYkL25ekFk5O/5ZiB3h3mzt5TMCvsKqJeNy2FcLG0cYXTPK6gHBVT+8bpT9A== - -"@tiptap/extension-document@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.5.7.tgz#cfc608656ab9173334247baf8ddb93135b024260" - integrity sha512-tcK6aleya6pmC/ForF/y2PiwPhN5hK8JSm07pcWV9FmP2Qemx26GWS+1u1EzPDeTTbRBvk+9txHGcq9NYZem0Q== - -"@tiptap/extension-dropcursor@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.7.tgz#f92649bce364d8ebc9a58e937c4ff436be830801" - integrity sha512-cXKMZMa6gt/ArLH02FO0qMkLvgacdobRIg2VRNMSA1a5c1S27KukVwYHoWoo40wgBctB/ctl4S0UyH6GFNgL1A== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-character-count/-/extension-character-count-2.5.8.tgz#ce414de73b1bb015b34b1ce95e79c8cd75ea1b7b" + integrity sha512-uu9FNY9yUMkXEVMfBdTovEyPHOJCZWtEdTVuU+nbOIOpaggNFBG6YcVU4W1NC99USSFnbr45SbCsxP3gySmPIA== + +"@tiptap/extension-code-block@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.5.8.tgz#ac6b34ad174ec4c67bf6b414e2aad2653750a741" + integrity sha512-atMtT1Ddc4hv9+OiH/UCLfQ6Ooo45xpPaaOhqs1Ab509YyqxoyEbfNSOth/yx9DFb8VOenRWE1WV3Z3C0ial0Q== + +"@tiptap/extension-code@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.5.8.tgz#dcb91cada02e6d7554bd86cf3e158644e88ca2d6" + integrity sha512-56lb4NnaYAbIkqBTCIg4ZoITrw86Dj8C2HSi6DrU7f5q9cfvGuH+2057I5n8eEEfASu1AeDN6tSnCz3NR+yiHw== + +"@tiptap/extension-document@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.5.8.tgz#644f042f1d4a8d3f74af057477cc627da7b54dc7" + integrity sha512-r3rP4ihCJAdp3VRIeqd80etHx7jttzZaKNFX8hkQShHK6eTHwrR92VL0jDE4K+NOE3bxjMsOlYizJYWV042BtA== + +"@tiptap/extension-dropcursor@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.8.tgz#5860cfe51c6573d5f317d44cd20bb398f76378b8" + integrity sha512-xPmIfTYqurFF8RukCPlHd8mT8I7hDinWrgq7CQTRROxcJ3DNw8PooWrKWaBYs9HXHe1pbiQ5EK0uOsNvQ1bcDg== "@tiptap/extension-floating-menu@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.7.tgz#b97101d82629335f47663bb4ddbc9231985a2b80" - integrity sha512-tQjNNx0gPb7GxMiozcQ4R1Tl1znmlx/ZkbCF9rqxTzPTD4fnCliqBQAWjtHl98+D8+yEJBcB2DimtP7ztkv2mg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.8.tgz#6af3fa169bf293ab79a671a7b60b5199992a9154" + integrity sha512-qsM6tCyRlXnI/gADrkO/2p0Tldu5aY96CnsXpZMaflMgsO577qhcXD0ReGg17uLXBzJa5xmV8qOik0Ptq3WEWg== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-gapcursor@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.7.tgz#97299e9ae426d5c5ee7f80bed9665e4a297f25d5" - integrity sha512-vpVeQM2KRbfPmZjiEK+Kh7GvuiZSiECezG5Y3pWb0ZG4ExSCgkmEokWXD3jGpAIprlpbbAP92/D+2inBD4e5Kg== +"@tiptap/extension-gapcursor@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.8.tgz#e25aa4dab03f57f98ecae320d727680549b4b939" + integrity sha512-nR7AUOE4xWdp0sDbLbe4uwAhQ/xq+MTLVafvffMLT81U/Hl9R+w0Ap2XF0+c6/JTQwVjZiOalAmg4dobx7rJUQ== -"@tiptap/extension-hard-break@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.5.7.tgz#a832460a610f3ff6c3c4260562c8555e0d0734ec" - integrity sha512-Ki1JV2cz74wo4am8vIY6KWnfiFoE68RVQDIL0/29fNz1oZI46R4VV2Q5IvoVhetXcx7Qe9nTJVqy1vRS//Kcvg== +"@tiptap/extension-hard-break@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.5.8.tgz#95288faad3408b91284d925c3e4dbab66029dd98" + integrity sha512-samZEL0EXzHSmMQ7KyLnfSxdDv3qSjia0JzelfCnFZS6LLcbwjrIjV8ZPxEhJ7UlZqroQdFxPegllkLHZj/MdQ== -"@tiptap/extension-heading@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.5.7.tgz#098530c27cdea3381b554fff451a06456647debb" - integrity sha512-UDUxtaslL1YZyKwoE/GzxDsOoQDoqwqhSWwYNTGJrbNA58fFR2h9UeeotMVaDveQlUwegGjqtBbTQ5aOXjzeFw== +"@tiptap/extension-heading@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.5.8.tgz#a5d14d03d812a7dce821fe46b4b821daffde2ad0" + integrity sha512-fDQoUkTLN+U8MNQ8PI+syKyshS9qFHlKihxzMLf/+tRisJvP47gzHDur99nffTSbXFDnASDqhavhKjI/2xTWlQ== "@tiptap/extension-highlight@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.5.7.tgz#6ce6c61c40ebd0b1b1133e19468b194b31fe3a59" - integrity sha512-VCH38/PqnmUcvnHX8Ws9LA4HcKcwsaAKZFgxfEVh/3k/pY4+3igxOQfxN2Dw/ooN0XUtMIMs4LqhldwVb8CVYg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-highlight/-/extension-highlight-2.5.8.tgz#5945e387f1de4178838626eace7e078b9dfb47b2" + integrity sha512-Auli6YBdUjF8mo0beEYw6Eh1hySukoQVjq+Yz6RKpaRzrrvXjoZUciQ6RoGXS4BHT7sfp8fMw9OIVo9Ifx8d8w== -"@tiptap/extension-history@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.5.7.tgz#ecaf703795f5ef6cc44c9b2b078a82c1ef9b85c0" - integrity sha512-rsCE+pIM2sdcok4uJdq/Z4XK9P4e+2vmzxPadIgOIuXM3z0kYbOVhIpwgyrS5Z9aTE3/TQq5c8/tHP97qwepug== +"@tiptap/extension-history@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.5.8.tgz#5808cfa5488b7ded1ccadf6180c9574e4d48a50a" + integrity sha512-5IrZZfp2Rg9Tov/08aYTKhwoiqdun8v3j3vleuqyW5RB7LU/NKLR19EtSSMh9mVkFZVbhab2zDOFmn5ilsEOhw== -"@tiptap/extension-horizontal-rule@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.7.tgz#28aeabc5aa126235199ec9625f77e1123f97f192" - integrity sha512-pUM+7/Kd2oJ7/QcA0ArbKw6NpN8n0RjdFFudclhdfIe557r0plmEvXSXTD0ZoOIcKgKLHD+pUFB4MgDLPIE9dg== +"@tiptap/extension-horizontal-rule@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.8.tgz#3570fe64c2b97d6e44dd95d7b76cc3fef2e34bda" + integrity sha512-L8Is73WGaP6VNdKrIry+lCIM9W1KaL/Tw2Z6DGMVMU5mr1lLx0xq7nWEStqD7e4zh+n4+3PV15cZSA2F34DZrg== "@tiptap/extension-image@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.5.7.tgz#3cb5d38b2286f7e6649e8859e28baed26fc35df9" - integrity sha512-oBXjMbDpXw+O0OK1sMZKKDm6FPeM5MZadfinIprVaAvrQrgvLF2vAW8hJH0OZUX0t8O2idhOf3n1eZb0MtKYyQ== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.5.8.tgz#c5178addb703777cf82aea250ccc75b17d492f3e" + integrity sha512-xlF3dqzXSN/6vWdmGOaIz0YVUO/B69mPw9vUITg7bQdc4X2pc52tvTGhpAzAc/kbwSVrW33icxAsXx8XH9Bkkg== -"@tiptap/extension-italic@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.5.7.tgz#e68c2870f46e5852f43f276621b5635e469cedc1" - integrity sha512-8tLH4LNWrl8eK741u3Nkrz0ZBqntsU3lvyXKKct1FdfKhLGK6TR0tlFOY86vwvyPzfR3PYBbR319Ye3gzU6w5Q== +"@tiptap/extension-italic@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.5.8.tgz#6d9cb7f0fba561fe77154bd0c231e56432f69894" + integrity sha512-Kh35a7slBai+Qr/tiF9XFXmuWMgUQz4Nt51hmzqVGVuG+QsdWzQE8IZBGypKm8aAzxTGSY0d0QA0rys+YRNq1Q== "@tiptap/extension-link@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.5.7.tgz#91326304a3f4f5fdeb23a8f630d5ae4a1ab64287" - integrity sha512-rxvcdV8H/TiRhR2SZfLHp7hUp5hwBAhkc6PsXEWj8lekG4/5lXGwPSPxLtHMBRtOyeJpXTv9DY6nsCGZuz9x6A== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.5.8.tgz#f9264afed09bd25c37668303151ab80ba82ef044" + integrity sha512-qfeWR7sG2V7bn8z0f3HMyoR68pFlxYJmLs9cbW30diE9/zKClYEd3zTMPCgJ9yMSagCj4PWkqksIuktAhyRqOQ== dependencies: linkifyjs "^4.1.0" -"@tiptap/extension-list-item@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.5.7.tgz#2fac9a74d883f708ac96f96edb2ffdd2c8af0ae5" - integrity sha512-XRngOvLy0Jnz9KORPucQ0qRR3esjzasM9v8oUaShkuzc5s1QzQGsGshDr4TP0ORZ4FehAkKr+i0CAT3LKOaf0g== +"@tiptap/extension-list-item@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.5.8.tgz#7f170233bc39e716d758a645f65fcf55a99a7a8a" + integrity sha512-RFIIzHxxXdPmdf7BL0zhE4VPHoR6BTWtfi3JCTftmNqKoH7o+mLKT0RHMGvF1CGNn2HewHzXAF0iXfKCwmEgHQ== -"@tiptap/extension-ordered-list@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.7.tgz#9ea8b0a9b2b6d44c54f0001594519c4ad021e86f" - integrity sha512-hRaQ4ln/0j/99aviK9DJyjB/TDaTlpOWUvZM4N6gZOrnF5DEYEixnVBYmjfD4QsdWwoyuNmcs7GTUEFghuk0dA== +"@tiptap/extension-ordered-list@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.8.tgz#b2ba61180cc3c6e3f2c8e6e6dd4764291ec2dc1b" + integrity sha512-84gWdWhc8rUCCssn8+6Z1rFKdG7/yIe+gwYkU6WqAtDrcluJdt5jRHrcMOLxb2dbY8ww9pa72EYV/bwOisZlFQ== -"@tiptap/extension-paragraph@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.5.7.tgz#7ce35b365e8222fb8e93f5e7bcdc18ef73c32ac5" - integrity sha512-7zmDE43jv+GMTLuWrztA6oAnYLdUki5fUjYFn0h5FPRHQTuDoxsCD+hX0N/sGQVlc8zl1yn7EYbPNn9rHi7ECw== +"@tiptap/extension-paragraph@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.5.8.tgz#5be7e7c4e5c19bd4f512c72d3dfc4e1e6d6dd876" + integrity sha512-AMfD3lfGSiomfkSE2tUourUjVahLtIfWUQew13NTPuWoxAXaSyoCGO0ULkiou/lO3JVUUUmF9+KJrAHWGIARdA== -"@tiptap/extension-strike@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.5.7.tgz#a00a0cbe307ff13945018919a6b7cf839be5429f" - integrity sha512-8axRRnahDK4ELYy4E1SIh2uj59Wz6fD4r5PM8QgQAnXRGgGDKZMA7Re47oDX3e5t5xa0OrE1WmC0ZqcpvupnZA== +"@tiptap/extension-strike@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.5.8.tgz#a52df8adabd88643ae03530abd8b5237504a74a7" + integrity sha512-uiHhBIEqawX9Up2ofklotVQ5XpGIjwRL6wprZF38s1le3XpsgyhVV7oDnqDkC7ujCsGkOJJfXZtv3LsO3R2nzQ== "@tiptap/extension-subscript@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.5.7.tgz#9b034e30c28ca298a7c1beac1a2860df0b577b8c" - integrity sha512-OXqiiPavw3LqdAPLi568ofKMDRe0W2f9vt/XFNWG5zg0AucJs7f4LrYd6MJJmChEAioV9XQ85Q1lcGt4tymCSA== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-subscript/-/extension-subscript-2.5.8.tgz#bea8486b68a88c7edfabca108bebfabc3929d277" + integrity sha512-pEajsdr/NtTjRiCJZC6XH3JsZCo7z/UO/iQKZbNb/44fsnBMlQoih4jYsIBJmk62j/0dzCJR8AmzfqXJX8qxlA== "@tiptap/extension-superscript@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.5.7.tgz#3db6254b4d6bc7396660499a786ff19e7d8723e4" - integrity sha512-o+aZHuWIVTSPRQWWgJ6woIrRgRLpZgI38HMcWVljt64W7kwUDJipmcCPfOx5Dos4VD6gp/jWCpW41N7skR84ZQ== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-superscript/-/extension-superscript-2.5.8.tgz#578d1d36d9b4abb22452d8028cc91a7eef219e3c" + integrity sha512-NFQD2m11C7w1vHmwzPPDG80PDh+rd98OCEszlc8ZgDFkTFWQMg0TfDZQMTsVsQtxVxMkuy4hl+wdye9xVUh1LA== "@tiptap/extension-table-cell@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.5.7.tgz#6abe557afe2b2a6cad3426a3ade88e63749e1302" - integrity sha512-EraKmWY/eP20d80GL3LW2F4zRp2OMnE4qnQJX9s9bRPfwOAvIx5LG2mp39PTZuJl5bnLM73u1WjvKn4oZiN27A== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.5.8.tgz#f9060f3cd9f156210eb21e4b467a3d3c22ae6306" + integrity sha512-t3fITH/sefWiOMSrqn34fhLRDSIZxTCwWvwvlrXnV0J5zaIjjJyP499JM3gAfB6Kb9+7Hd1VvdyDCeJbgEIgWQ== "@tiptap/extension-table-header@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.5.7.tgz#d9356c23f742ff07744e729dad66a8614e308925" - integrity sha512-f/jj0bINpiM4TKnwLzwdBeh564M0g4v1VcPyNa2ihq7MD6hLKt7EZCLSz2ZLDjsuyhh9+ArzLfTspOgdiM+/6w== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.5.8.tgz#a4902050952483be7452ad755a01cb00bc8f12e8" + integrity sha512-ehR/8IZpeAq8nRfkVMOlrClzTN9ZosGPz48SdhqN0V7aRaHe7MZcVOGbxrAXo9P6/3UTjh21qXFgatBVx8xoTA== "@tiptap/extension-table-row@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.5.7.tgz#76d008759c0fcf3ba2577e41a3bd846a8be4346b" - integrity sha512-+qCb/PhgnMtBNT8G9xw6gLDLxsDwtx0VlZMM+ZSJ72W+zusOvMwU1ZiBAu05yspFYxxQAhBJEbAayNEcEobhPw== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.5.8.tgz#8ed9b0ac4fa86be674798fde5b13323c4d2963bb" + integrity sha512-AESSqAB2XI1X/V8nlJhcNMmzCUmXKM6K0suZPiwdK9LlhPcTrLe8q7V09fPB23ZNL5dEVxVGIREyrdKiZnshIA== "@tiptap/extension-table@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.5.7.tgz#a1178b53576f4ad6035ae995877473bb0a77ef53" - integrity sha512-7ournU7l7g4Hgwm/Z6a6vjGLdrp22PdAEM2IARIswUxZYT4KEISC7pZBXq+foOCqCWmth3WBlABz+y7Ec6jaXg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.5.8.tgz#c992a1be20ce501e135fe042189db5c1da4477f6" + integrity sha512-91LTBn0tVfXYJsTcl8sOeqaoz3XNb2FUmyyQJmaLAoW8XbjnBLMk8V+BnSJdo9/RdhnujL9p9PfUvMdeUMIMJg== "@tiptap/extension-text-align@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.5.7.tgz#c5a5a1bd092a44c2e4bc1a5ca95830827c6ff128" - integrity sha512-GS2nCRQ/10PbMxZKf+EAMJ9kuDFaoi61EMFk7LQqVXziYr8CkkXw+O5QDGvH/9VzBNmRJ54FiblB2vhJ0Hozyg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text-align/-/extension-text-align-2.5.8.tgz#f45cf38c08e6466d474699b8045c9c7039f5a763" + integrity sha512-TtkEUkgHkV6nYwcvx0+vVIpgXkawZhG55IQ9CZI5PnD6tbzHTK8qFnuhnTgmX+ZQkqz4qEg5erFY/fC1gVvQ4g== -"@tiptap/extension-text@^2.5.7": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.5.7.tgz#bc436206ba16214383064672a6ebe3dd0464f368" - integrity sha512-vukhh2K/MsaIIs/UzIAwp44IVxTHPJcAhSsDnmJd4iPlkpjLt1eph77dfxv5awq78bj6mGvnCM0/0F6fW1C6/w== +"@tiptap/extension-text@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.5.8.tgz#a9c4de33eec749c8c01d8bd81fb589f581c30dfc" + integrity sha512-CNkD51jRMdcYCqFVOkrnebqBQ6pCD3ZD5z9kO5bOC5UPZKZBkLsWdlrHGAVwosxcGxdJACbqJ0Nj+fMgIw4tNA== "@tiptap/extension-underline@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.5.7.tgz#502abf03b4829e242a66c7eee7fe5b9bad51f4ce" - integrity sha512-MOGW2ZiKvgUTIg929pNV9yuaLAmmShvNyz+b4B3je/9Ub1Fum7V7Rsqfe+840/OZYx0wprpP3YponPQ8ftQuDg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.5.8.tgz#0f46891e0381417fa0a08ace2586e8c7538a7f4a" + integrity sha512-MxtOcYXVSpiYWNE1hzmBNUJELyH70Y/fFNbOyI5VPOoCZT7a3XjtWIbiZhBvN1lIzXYMHDj8Wacxzt6whK5KJw== "@tiptap/extension-youtube@^2.0.0-beta.220": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/extension-youtube/-/extension-youtube-2.5.7.tgz#455d06c4083d62c167739d3bd5cff01e4802bc48" - integrity sha512-RPAO1QhpeGb39oEj6yHU+R+ftxmvbGZ4zicq74Wn/Pa9utz4d9raQ6jPjfs3QcbVfL8r9TKWUARSpO71MjTEsg== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/extension-youtube/-/extension-youtube-2.5.8.tgz#61e5c9122fafbca14988018145b657d7e83df963" + integrity sha512-TEZ/mZgAIgc4thTO0pbmuNpWhMcCKk4IPgG3ko5HJ6w4lm13aVBroGoTXxHmK50kDzr+yD0g9JaJJTIvJ2cvGA== "@tiptap/pm@^2.1.13": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.5.7.tgz#9661d508fe34f7616b1078becc049baeff75d677" - integrity sha512-4Eb4vA4e4vesBAUmZgx+n3xjgJ58uRKKtnhFDJ3Gg+dfpXvtF8FcEwSIjHJsTlNJ8mSrzX/I7S157qPc5wZXVw== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.5.8.tgz#b18afa77fdf69527b13614a05cfefc8b63e82224" + integrity sha512-CVhHaTG4QNHSkvuh6HHsUR4hE+nbUnk7z+VMUedaqPU8tNqkTwWGCMbiyTc+PCsz0T9Mni7vvBR+EXgEQ3+w4g== dependencies: prosemirror-changeset "^2.2.1" prosemirror-collab "^1.3.1" @@ -6611,34 +6611,34 @@ prosemirror-view "^1.33.9" "@tiptap/starter-kit@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.5.7.tgz#0f3624b8ef4db0650f5a064b3d721922eb278124" - integrity sha512-enurhiMkDkC5J57WdFYUhJzWpw8fCkpJAQkYKZDfRrGXVPn0wiCwXnFtTcsjxZlygabpZA8dQOuCwKF+YS7gxQ== - dependencies: - "@tiptap/core" "^2.5.7" - "@tiptap/extension-blockquote" "^2.5.7" - "@tiptap/extension-bold" "^2.5.7" - "@tiptap/extension-bullet-list" "^2.5.7" - "@tiptap/extension-code" "^2.5.7" - "@tiptap/extension-code-block" "^2.5.7" - "@tiptap/extension-document" "^2.5.7" - "@tiptap/extension-dropcursor" "^2.5.7" - "@tiptap/extension-gapcursor" "^2.5.7" - "@tiptap/extension-hard-break" "^2.5.7" - "@tiptap/extension-heading" "^2.5.7" - "@tiptap/extension-history" "^2.5.7" - "@tiptap/extension-horizontal-rule" "^2.5.7" - "@tiptap/extension-italic" "^2.5.7" - "@tiptap/extension-list-item" "^2.5.7" - "@tiptap/extension-ordered-list" "^2.5.7" - "@tiptap/extension-paragraph" "^2.5.7" - "@tiptap/extension-strike" "^2.5.7" - "@tiptap/extension-text" "^2.5.7" + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.5.8.tgz#0217d342956ef67590e66b78c26aa38bd0ab06f5" + integrity sha512-Beb6Q3cFmJ1pE22WlFrG3wj8XAGXqaGkbqtsGAJDnoyWL4uoSs4vLt5I/UJshK/nQlNqTWFdpd9SxRFsxBYpqg== + dependencies: + "@tiptap/core" "^2.5.8" + "@tiptap/extension-blockquote" "^2.5.8" + "@tiptap/extension-bold" "^2.5.8" + "@tiptap/extension-bullet-list" "^2.5.8" + "@tiptap/extension-code" "^2.5.8" + "@tiptap/extension-code-block" "^2.5.8" + "@tiptap/extension-document" "^2.5.8" + "@tiptap/extension-dropcursor" "^2.5.8" + "@tiptap/extension-gapcursor" "^2.5.8" + "@tiptap/extension-hard-break" "^2.5.8" + "@tiptap/extension-heading" "^2.5.8" + "@tiptap/extension-history" "^2.5.8" + "@tiptap/extension-horizontal-rule" "^2.5.8" + "@tiptap/extension-italic" "^2.5.8" + "@tiptap/extension-list-item" "^2.5.8" + "@tiptap/extension-ordered-list" "^2.5.8" + "@tiptap/extension-paragraph" "^2.5.8" + "@tiptap/extension-strike" "^2.5.8" + "@tiptap/extension-text" "^2.5.8" "@tiptap/suggestion@^2.0.0-beta.218": - version "2.5.7" - resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.5.7.tgz#49d61323a5b9d1d0e3152d8890d9946bcd311b1e" - integrity sha512-js1I1rH8ycDkS9QTJM88W6yihYW9KD28eJcLn6Agh1W2yPo/iJZYAXLdl6oaae/jrT16W6OZRGDaBrNk/7lN3Q== + version "2.5.8" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.5.8.tgz#420a19df48314e1e4b8606f18e52810f07c6178a" + integrity sha512-u0emCyGpzSshKR5mIJVwPwycKikP05137fnD0RFI3+nftO6n/2h54rs2yU6BYA8dc01VZRB00cJ/zHO6DsZWEA== "@tootallnate/once@2": version "2.0.0" @@ -9374,9 +9374,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001591, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001640: - version "1.0.30001643" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz#9c004caef315de9452ab970c3da71085f8241dbd" - integrity sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg== + version "1.0.30001644" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001644.tgz#bcd4212a7a03bdedba1ea850b8a72bfe4bec2395" + integrity sha512-YGvlOZB4QhZuiis+ETS0VXR+MExbFf4fZYYeMTEE0aTQd/RdIjkTyZjLrbYVKnHzppDvnOhritRVv+i7Go6mHw== canvas@^2.11.2: version "2.11.2" @@ -11654,9 +11654,9 @@ ejs@^3.1.10, ejs@^3.1.7, ejs@^3.1.8: jake "^10.8.5" electron-to-chromium@^1.4.820: - version "1.5.2" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz#6126ad229ce45e781ec54ca40db0504787f23d19" - integrity sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ== + version "1.5.3" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.3.tgz#032bbb8661c0449656fd896e805c8f7150229a0f" + integrity sha512-QNdYSS5i8D9axWp/6XIezRObRHqaav/ur9z1VzCDUCH1XIFOr9WQk5xmgunhsTpjjgDy3oLxO/WMOVZlpUQrlA== elegant-spinner@^1.0.1: version "1.0.1" @@ -12965,9 +12965,9 @@ flatted@^3.2.7, flatted@^3.2.9: integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== flow-parser@0.*: - version "0.242.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.242.0.tgz#2ebd8db09a3d6df547303174325c26b463241647" - integrity sha512-qRfeoAH1j32bov4lw62wwjh7MQTLveiQ/nHqgjoAMlUbxA2rY+/Sq2l3m/sYuYSQyR7M2ocARPOWJQEwM6x4WA== + version "0.242.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.242.1.tgz#d95977303d2cca0c1cb39394f5f5098d1ed5fc95" + integrity sha512-E3ml21Q1S5cMAyPbtYslkvI6yZO5oCS/S2EoteeFH8Kx9iKOv/YOJ+dGd/yMf+H3YKfhMKjnOpyNwrO7NdddWA== flush-write-stream@^1.0.0: version "1.1.1" diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java index bfbc6fbda290..3a69c6935a76 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java @@ -1,13 +1,13 @@ package com.dotcms.ai.api; import com.dotcms.ai.AiKeys; +import com.dotcms.ai.app.AIModel; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.db.EmbeddingsDTO; import com.dotcms.ai.rest.forms.CompletionsForm; import com.dotcms.ai.util.EncodingUtil; -import com.dotcms.ai.util.OpenAIModel; import com.dotcms.ai.util.OpenAIRequest; import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.mock.request.FakeHttpRequest; @@ -42,7 +42,7 @@ public class CompletionsAPIImpl implements CompletionsAPI { private final Lazy config; - final Lazy defaultConfig = + private final Lazy defaultConfig = Lazy.of(() -> ConfigService.INSTANCE.config( Try.of(() -> WebAPILocator .getHostWebAPI() @@ -60,7 +60,7 @@ public JSONObject prompt(final String systemPrompt, final String modelIn, final float temperature, final int maxTokens) { - final OpenAIModel model = OpenAIModel.resolveModel(modelIn); + final AIModel model = config.get().resolveModelOrThrow(modelIn); final JSONObject json = new JSONObject(); json.put(AiKeys.TEMPERATURE, temperature); @@ -70,7 +70,7 @@ public JSONObject prompt(final String systemPrompt, json.put(AiKeys.MAX_TOKENS, maxTokens); } - json.put(AiKeys.MODEL, model.modelName); + json.put(AiKeys.MODEL, model.getCurrentModel()); return raw(json); } @@ -91,7 +91,7 @@ public JSONObject summarize(final CompletionsForm summaryRequest) { Try.of(() -> OpenAIRequest.doRequest( config.get().getApiUrl(), HttpMethod.POST, - config.get().getApiKey(), + config.get(), json)) .getOrElseThrow(DotRuntimeException::new); final JSONObject dotCMSResponse = EmbeddingsAPI.impl().reduceChunksToContent(searcher, localResults); @@ -107,7 +107,7 @@ public void summarizeStream(final CompletionsForm summaryRequest, final OutputSt final JSONObject json = buildRequestJson(summaryRequest, localResults); json.put(AiKeys.STREAM, true); - OpenAIRequest.doPost(config.get().getApiUrl(), config.get().getApiKey(), json, out); + OpenAIRequest.doPost(config.get().getApiUrl(), config.get(), json, out); } @Override @@ -119,7 +119,7 @@ public JSONObject raw(final JSONObject json) { final String response = OpenAIRequest.doRequest( config.get().getApiUrl(), HttpMethod.POST, - config.get().getApiKey(), + config.get(), json); if (config.get().getConfigBoolean(AppKeys.DEBUG_LOGGING)) { Logger.info(this.getClass(), "OpenAI response:" + response); @@ -138,7 +138,7 @@ public JSONObject raw(CompletionsForm promptForm) { public void rawStream(final CompletionsForm promptForm, final OutputStream out) { final JSONObject json = buildRequestJson(promptForm); json.put(AiKeys.STREAM, true); - OpenAIRequest.doRequest(config.get().getApiUrl(), HttpMethod.POST, config.get().getApiKey(), json, out); + OpenAIRequest.doRequest(config.get().getApiUrl(), HttpMethod.POST, config.get(), json, out); } private void buildMessages(final String systemPrompt, final String userPrompt, final JSONObject json) { @@ -151,7 +151,7 @@ private void buildMessages(final String systemPrompt, final String userPrompt, f } private JSONObject buildRequestJson(final CompletionsForm form, final List searchResults) { - final OpenAIModel model = OpenAIModel.resolveModel(form.model); + final AIModel model = config.get().resolveModelOrThrow(form.model); // aggregate matching results into text final StringBuilder supportingContent = new StringBuilder(); searchResults.forEach(s -> supportingContent.append(s.extractedText).append(" ")); @@ -162,7 +162,7 @@ private JSONObject buildRequestJson(final CompletionsForm form, final List enc.countTokens(testString)) .orElseThrow(() -> new DotRuntimeException("Encoder not found")); } @@ -244,20 +244,19 @@ private String reduceStringToTokenSize(final String incomingString, final int ma } private JSONObject buildRequestJson(final CompletionsForm form) { - final int maxTokenSize = OpenAIModel.resolveModel(config.get().getConfig(AppKeys.MODEL)).maxTokens; + final AIModel aiModel = config.get().getModel(); final int promptTokens = countTokens(form.prompt); final JSONArray messages = new JSONArray(); final String textPrompt = reduceStringToTokenSize( form.prompt, - maxTokenSize - form.responseLengthTokens - promptTokens); + aiModel.getMaxTokens() - form.responseLengthTokens - promptTokens); messages.add(Map.of(AiKeys.ROLE, AiKeys.USER, AiKeys.CONTENT, textPrompt)); final JSONObject json = new JSONObject(); json.put(AiKeys.MESSAGES, messages); - json.putIfAbsent(AiKeys.MODEL, config.get().getConfig(AppKeys.MODEL)); - + json.putIfAbsent(AiKeys.MODEL, config.get().getConfig(AppKeys.TEXT_MODEL_NAMES)); json.put(AiKeys.TEMPERATURE, form.temperature); json.put(AiKeys.MAX_TOKENS, form.responseLengthTokens); json.put(AiKeys.STREAM, form.stream); diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java index 587382751141..2c49fac5efe2 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java @@ -27,7 +27,6 @@ import com.dotmarketing.exception.DotCorruptedDataException; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.portlets.contentlet.model.Contentlet; -import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.StringUtils; import com.dotmarketing.util.UtilMethods; @@ -37,7 +36,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.liferay.portal.model.User; -import io.vavr.Lazy; import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.Tuple3; @@ -69,9 +67,6 @@ */ class EmbeddingsAPIImpl implements EmbeddingsAPI { - private static final Lazy OPEN_AI_EMBEDDINGS_URL = Lazy.of(() - -> Config.getStringProperty("OPEN_AI_EMBEDDINGS_URL", "https://api.openai.com/v1/embeddings")); - private static final Cache>> EMBEDDING_CACHE = Caffeine.newBuilder() .expireAfterWrite( @@ -348,7 +343,9 @@ public Tuple2> pullOrGenerateEmbeddings(final String conten return Tuple.of(dbEmbeddings._2, dbEmbeddings._3); } - final Tuple2> openAiEmbeddings = Tuple.of(tokens.size(), this.sendTokensToOpenAI(contentId, tokens)); + final Tuple2> openAiEmbeddings = Tuple.of( + tokens.size(), + sendTokensToOpenAI(contentId, tokens)); saveEmbeddingsForCache(content, openAiEmbeddings); EMBEDDING_CACHE.put(hashed, openAiEmbeddings); @@ -424,13 +421,13 @@ private void saveEmbeddingsForCache(final String content, final Tuple2 sendTokensToOpenAI(final String contentId, @NotNull final List tokens) { final JSONObject json = new JSONObject(); - json.put(AiKeys.MODEL, config.getConfig(AppKeys.EMBEDDINGS_MODEL)); + json.put(AiKeys.MODEL, config.getEmbeddingsModel().getCurrentModel()); json.put(AiKeys.INPUT, tokens); debugLogger(this.getClass(), () -> String.format("Content tokens for content ID '%s': %s", contentId, tokens)); final String responseString = OpenAIRequest.doRequest( - OPEN_AI_EMBEDDINGS_URL.get(), + config.getApiEmbeddingsUrl(), HttpMethod.POST, - this.config.getApiKey(), + config, json); debugLogger(this.getClass(), () -> String.format("OpenAI Response for content ID '%s': %s", contentId, responseString.replace("\n", BLANK))); diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java new file mode 100644 index 000000000000..6feaaf24afba --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java @@ -0,0 +1,169 @@ +package com.dotcms.ai.app; + +import com.dotcms.security.apps.AppsUtil; +import com.dotcms.security.apps.Secret; +import com.dotmarketing.util.UtilMethods; +import com.liferay.util.StringPool; +import io.vavr.Lazy; +import io.vavr.control.Try; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Utility class for handling AI application configurations and secrets. + * This class provides methods to resolve secrets, normalize model names, + * split model names, and create AI model instances based on the provided + * configuration and secrets. + * + * @author vico + */ +public class AIAppUtil { + + private static final Lazy INSTANCE = Lazy.of(AIAppUtil::new); + + private AIAppUtil() { + // Private constructor to prevent instantiation + } + + public static AIAppUtil get() { + return INSTANCE.get(); + } + + /** + * Creates a text model instance based on the provided secrets. + * + * @param secrets the map of secrets + * @return the created text model instance + */ + public AIModel createTextModel(final Map secrets) { + return AIModel.builder() + .withType(AIModelType.TEXT) + .withNames(discoverSecret(secrets, AppKeys.TEXT_MODEL_NAMES)) + .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_TOKENS_PER_MINUTE)) + .withApiPerMinute(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_API_PER_MINUTE)) + .withMaxTokens(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_MAX_TOKENS)) + .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.TEXT_MODEL_COMPLETION)) + .build(); + } + + /** + * Creates an image model instance based on the provided secrets. + * + * @param secrets the map of secrets + * @return the created image model instance + */ + public AIModel createImageModel(final Map secrets) { + return AIModel.builder() + .withType(AIModelType.IMAGE) + .withNames(discoverSecret(secrets, AppKeys.IMAGE_MODEL_NAMES)) + .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_TOKENS_PER_MINUTE)) + .withApiPerMinute(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_API_PER_MINUTE)) + .withMaxTokens(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_MAX_TOKENS)) + .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.IMAGE_MODEL_COMPLETION)) + .build(); + } + + /** + * Creates an embeddings model instance based on the provided secrets. + * + * @param secrets the map of secrets + * @return the created embeddings model instance + */ + public AIModel createEmbeddingsModel(final Map secrets) { + return AIModel.builder() + .withType(AIModelType.EMBEDDINGS) + .withNames(splitDiscoveredSecret(secrets, AppKeys.EMBEDDINGS_MODEL_NAMES)) + .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_TOKENS_PER_MINUTE)) + .withApiPerMinute(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_API_PER_MINUTE)) + .withMaxTokens(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_MAX_TOKENS)) + .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.EMBEDDINGS_MODEL_COMPLETION)) + .build(); + } + + /** + * Resolves a secret value from the provided secrets map using the specified key. + * If the secret is not found, the default value is returned. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + * @param defaultValue the default value to return if the secret is not found + * @return the resolved secret value or the default value if the secret is not found + */ + public String discoverSecret(final Map secrets, final AppKeys key, final String defaultValue) { + return Try.of(() -> secrets.get(key.key).getString()).getOrElse(defaultValue); + } + + /** + * Resolves a secret value from the provided secrets map using the specified key. + * If the secret is not found, the default value defined in the key is returned. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + * @return the resolved secret value or the default value defined in the key if the secret is not found + */ + public String discoverSecret(final Map secrets, final AppKeys key) { + return discoverSecret(secrets, key, key.defaultValue); + } + + /** + * Splits a model-specific secret value from the provided secrets map using the specified key. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + * @return the list of split secret values + */ + public List splitDiscoveredSecret(final Map secrets, final AppKeys key) { + return Arrays.stream(discoverSecret(secrets, key).split(",")) + .map(String::trim) + .map(String::toLowerCase) + .collect(Collectors.toList()); + } + + /** + * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + */ + public int discoverIntSecret(final Map secrets, final AppKeys key) { + return toInt(discoverSecret(secrets, key)); + } + + /** + * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + */ + public boolean discoverBooleanSecret(final Map secrets, final AppKeys key) { + return Boolean.parseBoolean(discoverSecret(secrets, key)); + } + + /** + * Resolves an environment-specific secret value from the provided secrets map using the specified key. + * If the secret is not found, it attempts to discover the value from environment variables. + * + * @param secrets the map of secrets + * @param key the key to look up the secret + * @return the resolved environment-specific secret value or an empty string if not found + */ + public String discoverEnvSecret(final Map secrets, final AppKeys key) { + final String secret = discoverSecret(secrets, key, StringPool.BLANK); + if (UtilMethods.isSet(secret)) { + return secret; + } + + return Optional + .ofNullable(AppsUtil.discoverEnvVarValue(AppKeys.APP_KEY, key.key, null)) + .orElse(StringPool.BLANK); + } + + private int toInt(final String value) { + return Try.of(() -> Integer.parseInt(value)).getOrElse(0); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java new file mode 100644 index 000000000000..88b3ef6d58df --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java @@ -0,0 +1,179 @@ +package com.dotcms.ai.app; + +import com.dotcms.util.DotPreconditions; +import com.dotmarketing.util.Logger; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Represents an AI model with various attributes such as type, names, tokens per minute, + * API calls per minute, maximum tokens, and completion status. This class provides methods + * to manage the current model, decommission status, and calculate the minimum interval + * between API calls. It also includes a builder for creating instances of AIModel. + * + * @author vico + */ +public class AIModel { + + private final AIModelType type; + private final List names; + private final int tokensPerMinute; + private final int apiPerMinute; + private final int maxTokens; + private final boolean isCompletion; + private final AtomicInteger current; + private final AtomicBoolean decommissioned; + + private AIModel(final AIModelType type, + final List names, + final int tokensPerMinute, + final int apiPerMinute, + final int maxTokens, + final boolean isCompletion) { + DotPreconditions.checkNotNull(type, "type cannot be null"); + this.type = type; + this.names = Optional.ofNullable(names).orElse(List.of()); + this.tokensPerMinute = tokensPerMinute; + this.apiPerMinute = apiPerMinute; + this.maxTokens = maxTokens; + this.isCompletion = isCompletion; + current = new AtomicInteger(this.names.isEmpty() ? -1 : 0); + decommissioned = new AtomicBoolean(false); + } + + public AIModelType getType() { + return type; + } + + public List getNames() { + return names; + } + + public int getTokensPerMinute() { + return tokensPerMinute; + } + + public int getApiPerMinute() { + return apiPerMinute; + } + + public int getMaxTokens() { + return maxTokens; + } + + public boolean isCompletion() { + return isCompletion; + } + + public int getCurrent() { + return current.get(); + } + + public void setCurrent(final int current) { + if (!isCurrentValid(current)) { + logInvalidModelMessage(); + return; + } + this.current.set(current); + } + + public boolean isDecommissioned() { + return decommissioned.get(); + } + + public void setDecommissioned(final boolean decommissioned) { + this.decommissioned.set(decommissioned); + } + + public String getCurrentModel() { + final int currentIndex = this.current.get(); + if (!isCurrentValid(currentIndex)) { + logInvalidModelMessage(); + return null; + } + return names.get(currentIndex); + } + + public long minIntervalBetweenCalls() { + return 60000 / apiPerMinute; + } + + @Override + public String toString() { + return "AIModel{" + + "name='" + names + '\'' + + ", tokensPerMinute=" + tokensPerMinute + + ", apiPerMinute=" + apiPerMinute + + ", maxTokens=" + maxTokens + + ", isCompletion=" + isCompletion + + '}'; + } + + private boolean isCurrentValid(final int current) { + return !names.isEmpty() && current >= 0 && current < names.size(); + } + + private void logInvalidModelMessage() { + Logger.debug(getClass(), String.format("Current model index must be between 0 and %d", names.size())); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private AIModelType type; + private List names; + private int tokensPerMinute; + private int apiPerMinute; + private int maxTokens; + private boolean isCompletion; + + private Builder() { + } + + public Builder withType(final AIModelType type) { + this.type = type; + return this; + } + + public Builder withNames(final List names) { + this.names = names; + return this; + } + + public Builder withNames(final String... names) { + return withNames(List.of(names)); + } + + public Builder withTokensPerMinute(final int tokensPerMinute) { + this.tokensPerMinute = tokensPerMinute; + return this; + } + + public Builder withApiPerMinute(final int apiPerMinute) { + this.apiPerMinute = apiPerMinute; + return this; + } + + public Builder withMaxTokens(final int maxTokens) { + this.maxTokens = maxTokens; + return this; + } + + public Builder withIsCompletion(final boolean isCompletion) { + this.isCompletion = isCompletion; + return this; + } + + public AIModel build() { + return new AIModel(type, names, tokensPerMinute, apiPerMinute, maxTokens, isCompletion); + } + + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModelType.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModelType.java new file mode 100644 index 000000000000..5f25c015e428 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIModelType.java @@ -0,0 +1,19 @@ +package com.dotcms.ai.app; + +/** + * Enum representing different types of AI models used in the application. + * The types include: + *
    + *
  • TEXT: Models used for text generation and processing.
  • + *
  • IMAGE: Models used for image generation and processing.
  • + *
  • EMBEDDINGS: Models used for generating vector embeddings from text or other data.
  • + *
  • UNKNOWN: Represents an unknown or unsupported model type.
  • + *
+ * + * @author vico + */ +public enum AIModelType { + + TEXT, IMAGE, EMBEDDINGS, UNKNOWN + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java new file mode 100644 index 000000000000..0773d0de5711 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java @@ -0,0 +1,215 @@ +package com.dotcms.ai.app; + +import com.dotcms.ai.model.OpenAIModel; +import com.dotcms.ai.model.OpenAIModels; +import com.dotcms.http.CircuitBreakerUrl; +import com.dotmarketing.beans.Host; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.vavr.Lazy; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.control.Try; +import org.apache.commons.collections4.CollectionUtils; + +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + * Manages the AI models used in the application. This class handles loading, caching, + * and retrieving AI models based on the host and model type. It also fetches supported + * models from external sources and maintains a cache of these models. + * + * @author vico + */ +public class AIModels { + + private static final String SUPPORTED_MODELS_KEY = "supportedModels"; + private static final String AI_MODELS_FETCH_ATTEMPTS_KEY = "ai.models.fetch.attempts"; + private static final int AI_MODELS_FETCH_ATTEMPTS = Config.getIntProperty(AI_MODELS_FETCH_ATTEMPTS_KEY, 3); + private static final String AI_MODELS_FETCH_TIMEOUT_KEY = "ai.models.fetch.timeout"; + private static final int AI_MODELS_FETCH_TIMEOUT = Config.getIntProperty(AI_MODELS_FETCH_TIMEOUT_KEY, 4000); + private static final Lazy INSTANCE = Lazy.of(AIModels::new); + private static final String OPEN_AI_MODELS_URL = Config.getStringProperty( + "OPEN_AI_MODELS_URL", + "https://api.openai.com/v1/models"); + private static final int AI_MODELS_CACHE_TTL = 28800; // 8 hours + private static final int AI_MODELS_CACHE_SIZE = 128; + + public static final AIModel NOOP_MODEL = AIModel.builder() + .withType(AIModelType.UNKNOWN) + .withNames(List.of()) + .build(); + + private final ConcurrentMap>> internalModels = new ConcurrentHashMap<>(); + private final ConcurrentMap, AIModel> modelsByName = new ConcurrentHashMap<>(); + private final Cache> supportedModelsCache = + Caffeine.newBuilder() + .expireAfterWrite(Duration.ofSeconds(AI_MODELS_CACHE_TTL)) + .maximumSize(AI_MODELS_CACHE_SIZE) + .build(); + + public static AIModels get() { + return INSTANCE.get(); + } + + private AIModels() { + } + + /** + * Loads the given list of AI models for the specified host. If models for the host + * are already loaded, this method does nothing. It also maps model names to their + * corresponding AIModel instances. + * + * @param host the host for which the models are being loaded + * @param loading the list of AI models to load + */ + public void loadModels(final String host, final List loading) { + Optional.ofNullable(internalModels.get(host)) + .ifPresentOrElse( + model -> {}, + () -> internalModels.putIfAbsent( + host, + loading.stream() + .map(model -> Tuple.of(model.getType(), model)) + .collect(Collectors.toList()))); + loading.forEach(model -> model + .getNames() + .forEach(name -> { + final Tuple2 key = Tuple.of( + host, + name.toLowerCase().trim()); + if (modelsByName.containsKey(key)) { + Logger.debug( + this, + String.format( + "Model [%s] already exists for host [%s], ignoring it", + name, + host)); + return; + } + modelsByName.putIfAbsent(key, model); + })); + } + + /** + * Finds an AI model by the host and model name. The search is case-insensitive. + * + * @param host the host for which the model is being searched + * @param modelName the name of the model to find + * @return an Optional containing the found AIModel, or an empty Optional if not found + */ + public Optional findModel(final String host, final String modelName) { + return Optional.ofNullable(modelsByName.get(Tuple.of(host, modelName.toLowerCase()))); + } + + /** + * Finds an AI model by the host and model type. + * + * @param host the host for which the model is being searched + * @param type the type of the model to find + * @return an Optional containing the found AIModel, or an empty Optional if not found + */ + public Optional findModel(final String host, final AIModelType type) { + return Optional.ofNullable(internalModels.get(host)) + .flatMap(tuples -> tuples.stream() + .filter(tuple -> tuple._1 == type) + .map(Tuple2::_2) + .findFirst()); + } + + /** + * Resets the internal models cache for the specified host. + * + * @param host the host for which the models are being reset + */ + public void resetModels(final Host host) { + final String hostKey = host.getHostname(); + synchronized (AIModels.class) { + Optional.ofNullable(internalModels.get(hostKey)).ifPresent(models -> { + models.clear(); + internalModels.remove(hostKey); + }); + modelsByName.keySet() + .stream() + .filter(key -> key._1.equals(hostKey)) + .collect(Collectors.toSet()) + .forEach(modelsByName::remove); + ConfigService.INSTANCE.config(host); + } + } + + /** + * Retrieves the list of supported models, either from the cache or by fetching them + * from an external source if the cache is empty or expired. + * + * @return a list of supported model names + */ + public List getOrPullSupportedModels() { + final List cached = supportedModelsCache.getIfPresent(SUPPORTED_MODELS_KEY); + if (CollectionUtils.isNotEmpty(cached)) { + return cached; + } + + final AppConfig appConfig = ConfigService.INSTANCE.config(); + final List supported = Try.of(() -> + fetchOpenAIModels(appConfig) + .getResponse() + .getData() + .stream() + .map(OpenAIModel::getId) + .map(String::toLowerCase) + .collect(Collectors.toList())) + .getOrElse(Optional.ofNullable(cached).orElse(List.of())); + supportedModelsCache.put(SUPPORTED_MODELS_KEY, supported); + + return supported; + } + + /** + * Retrieves the list of available models that are both configured and supported. + * + * @return a list of available model names + */ + public List getAvailableModels() { + final Set configured = internalModels.entrySet().stream().flatMap(entry -> entry.getValue().stream()) + .map(Tuple2::_2) + .flatMap(model -> model.getNames().stream()) + .collect(Collectors.toSet()); + final Set supported = new HashSet<>(getOrPullSupportedModels()); + configured.retainAll(supported); + return configured.stream().sorted().collect(Collectors.toList()); + } + + private static CircuitBreakerUrl.Response fetchOpenAIModels(final AppConfig appConfig) { + + final CircuitBreakerUrl.Response response = CircuitBreakerUrl.builder() + .setMethod(CircuitBreakerUrl.Method.GET) + .setUrl(OPEN_AI_MODELS_URL) + .setTimeout(AI_MODELS_FETCH_TIMEOUT) + .setTryAgainAttempts(AI_MODELS_FETCH_ATTEMPTS) + .setHeaders(CircuitBreakerUrl.authHeaders("Bearer " + appConfig.getApiKey())) + .setThrowWhenNot2xx(false) + .build() + .doResponse(OpenAIModels.class); + + if (!CircuitBreakerUrl.isSuccessResponse(response)) { + Logger.debug( + AIModels.class, + String.format( + "Error fetching OpenAI supported models from [%s] (status code: [%d])", + OPEN_AI_MODELS_URL, + response.getStatusCode())); + } + + return response; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java index d5a8105d3895..d3a161daa746 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java @@ -1,15 +1,16 @@ package com.dotcms.ai.app; -import com.dotcms.security.apps.AppsUtil; import com.dotcms.security.apps.Secret; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; -import com.liferay.util.StringPool; import io.vavr.control.Try; +import org.apache.commons.lang3.StringUtils; import java.io.Serializable; +import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -20,12 +21,16 @@ */ public class AppConfig implements Serializable { - public static final Pattern SPLITTER= Pattern.compile("\\s?,\\s?"); + private static final String OPEN_AI_EMBEDDINGS_URL_KEY = "OPEN_AI_EMBEDDINGS_URL"; + public static final Pattern SPLITTER = Pattern.compile("\\s?,\\s?"); - public final String model; - public final String imageModel; + private final String host; + private final transient AIModel model; + private final transient AIModel imageModel; + private final transient AIModel embeddingsModel; private final String apiUrl; private final String apiImageUrl; + private final String apiEmbeddingsUrl; private final String apiKey; private final String rolePrompt; private final String textPrompt; @@ -34,47 +39,45 @@ public class AppConfig implements Serializable { private final String listenerIndexer; private final Map configValues; - public AppConfig(final Map secrets) { - this.configValues = secrets.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - apiUrl = resolveEnvSecret(secrets, AppKeys.API_URL); - apiImageUrl = resolveEnvSecret(secrets, AppKeys.API_IMAGE_URL); - apiKey = resolveEnvSecret(secrets, AppKeys.API_KEY); - rolePrompt = resolveSecretOrBlank(secrets, AppKeys.ROLE_PROMPT); - textPrompt = resolveSecretOrBlank(secrets, AppKeys.TEXT_PROMPT); - imagePrompt = resolveSecretOrBlank(secrets, AppKeys.IMAGE_PROMPT); - imageSize = resolveSecret(secrets, AppKeys.IMAGE_SIZE, AppKeys.IMAGE_SIZE.defaultValue); - model = resolveSecretOrBlank(secrets, AppKeys.MODEL); - imageModel = resolveSecret(secrets, AppKeys.IMAGE_MODEL, "dall-e-3"); - listenerIndexer = resolveSecretOrBlank(secrets, AppKeys.LISTENER_INDEXER); - Logger.debug(this.getClass().getName(), () -> "apiUrl: " + apiUrl); - Logger.debug(this.getClass().getName(), () -> "apiImageUrl: " + apiImageUrl); - Logger.debug(this.getClass().getName(), () -> "apiKey: " + apiKey); - Logger.debug(this.getClass().getName(), () -> "rolePrompt: " + rolePrompt); - Logger.debug(this.getClass().getName(), () -> "textPrompt: " + textPrompt); - Logger.debug(this.getClass().getName(), () -> "imagePrompt: " + imagePrompt); - Logger.debug(this.getClass().getName(), () -> "imageModel: " + imageModel); - Logger.debug(this.getClass().getName(), () -> "imageSize: " + imageSize); - Logger.debug(this.getClass().getName(), () -> "model: " + model); - Logger.debug(this.getClass().getName(), () -> "listerIndexer: " + listenerIndexer); - } - - private String resolveSecret(final Map secrets, final AppKeys key, final String defaultValue) { - return Try.of(() -> secrets.get(key.key).getString()).getOrElse(defaultValue); - } - - private String resolveSecretOrBlank(final Map secrets, final AppKeys key) { - return resolveSecret(secrets, key, StringPool.BLANK); - } - - private String resolveEnvSecret(final Map secrets, final AppKeys key) { - final String secret = resolveSecretOrBlank(secrets, key); - if (UtilMethods.isSet(secret)) { - return secret; - } + public AppConfig(final String host, final Map secrets) { + this.host = host; + + final AIAppUtil aiAppUtil = AIAppUtil.get(); + AIModels.get().loadModels( + this.host, + List.of( + aiAppUtil.createTextModel(secrets), + aiAppUtil.createImageModel(secrets), + aiAppUtil.createEmbeddingsModel(secrets))); + + model = resolveModel(AIModelType.TEXT); + imageModel = resolveModel(AIModelType.IMAGE); + embeddingsModel = resolveModel(AIModelType.EMBEDDINGS); + + apiUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_URL); + apiImageUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_IMAGE_URL); + apiEmbeddingsUrl = discoverEmbeddingsApiUrl(secrets); + apiKey = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_KEY); + rolePrompt = aiAppUtil.discoverSecret(secrets, AppKeys.ROLE_PROMPT); + textPrompt = aiAppUtil.discoverSecret(secrets, AppKeys.TEXT_PROMPT); + imagePrompt = aiAppUtil.discoverSecret(secrets, AppKeys.IMAGE_PROMPT); + imageSize = aiAppUtil.discoverSecret(secrets, AppKeys.IMAGE_SIZE); + listenerIndexer = aiAppUtil.discoverSecret(secrets, AppKeys.LISTENER_INDEXER); - return Optional - .ofNullable(AppsUtil.discoverEnvVarValue(AppKeys.APP_KEY, key.key, null)) - .orElse(StringPool.BLANK); + configValues = secrets.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Logger.debug(getClass(), () -> "apiUrl: " + apiUrl); + Logger.debug(getClass(), () -> "apiImageUrl: " + apiImageUrl); + Logger.debug(getClass(), () -> "embeddingsUrl: " + apiEmbeddingsUrl); + Logger.debug(getClass(), () -> "apiKey: " + apiKey); + Logger.debug(getClass(), () -> "model: " + model); + Logger.debug(getClass(), () -> "imageModel: " + imageModel); + Logger.debug(getClass(), () -> "embeddingsModel: " + embeddingsModel); + Logger.debug(getClass(), () -> "rolePrompt: " + rolePrompt); + Logger.debug(getClass(), () -> "textPrompt: " + textPrompt); + Logger.debug(getClass(), () -> "imagePrompt: " + imagePrompt); + Logger.debug(getClass(), () -> "imageSize: " + imageSize); + Logger.debug(getClass(), () -> "listerIndexer: " + listenerIndexer); } /** @@ -83,7 +86,7 @@ private String resolveEnvSecret(final Map secrets, final AppKeys * @return the API URL */ public String getApiUrl() { - return UtilMethods.isEmpty(apiUrl) ? "https://api.openai.com/v1/chat/completions" : apiUrl; + return UtilMethods.isEmpty(apiUrl) ? AppKeys.API_URL.defaultValue : apiUrl; } /** @@ -92,7 +95,16 @@ public String getApiUrl() { * @return the API Image URL */ public String getApiImageUrl() { - return UtilMethods.isEmpty(apiImageUrl)? "https://api.openai.com/v1/images/generations" : apiImageUrl; + return UtilMethods.isEmpty(apiImageUrl) ? AppKeys.API_IMAGE_URL.defaultValue : apiImageUrl; + } + + /** + * Retrieves the API Embeddings URL. + * + * @return + */ + public String getApiEmbeddingsUrl() { + return UtilMethods.isEmpty(apiEmbeddingsUrl) ? AppKeys.API_EMBEDDINGS_URL.defaultValue : apiEmbeddingsUrl; } /** @@ -105,12 +117,12 @@ public String getApiKey() { } /** - * Retrieves the Role Prompt. + * Retrieves the Model. * - * @return the Role Prompt + * @return the Model */ - public String getRolePrompt() { - return rolePrompt; + public AIModel getModel() { + return model; } /** @@ -118,7 +130,27 @@ public String getRolePrompt() { * * @return the Image Model */ - public String getImageModel() {return imageModel;} + public AIModel getImageModel() { + return imageModel; + } + + /** + * Retrieves the Embeddings Model. + * + * @return the Embeddings Model + */ + public AIModel getEmbeddingsModel() { + return embeddingsModel; + } + + /** + * Retrieves the Role Prompt. + * + * @return the Role Prompt + */ + public String getRolePrompt() { + return rolePrompt; + } /** * Retrieves the Text Prompt. @@ -147,15 +179,6 @@ public String getImageSize() { return imageSize; } - /** - * Retrieves the Model. - * - * @return the Model - */ - public String getModel() { - return model; - } - /** * Retrieves the Listener Indexer. * @@ -171,9 +194,9 @@ public String getListenerIndexer() { * @param appKey the key to retrieve the configuration value for * @return the integer configuration value */ - public int getConfigInteger(AppKeys appKey) { - String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); - return Try.of(()->Integer.parseInt(value)).getOrElse(0); + public int getConfigInteger(final AppKeys appKey) { + String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); + return Try.of(() -> Integer.parseInt(value)).getOrElse(0); } /** @@ -182,9 +205,9 @@ public int getConfigInteger(AppKeys appKey) { * @param appKey the key to retrieve the configuration value for * @return the float configuration value */ - public float getConfigFloat(AppKeys appKey) { - String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); - return Try.of(()->Float.parseFloat(value)).getOrElse(0f); + public float getConfigFloat(final AppKeys appKey) { + String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); + return Try.of(() -> Float.parseFloat(value)).getOrElse(0f); } /** @@ -193,9 +216,9 @@ public float getConfigFloat(AppKeys appKey) { * @param appKey the key to retrieve the configuration value for * @return the boolean configuration value */ - public boolean getConfigBoolean(AppKeys appKey) { - String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); - return Try.of(()->Boolean.parseBoolean(value)).getOrElse(false); + public boolean getConfigBoolean(final AppKeys appKey) { + final String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); + return Try.of(() -> Boolean.parseBoolean(value)).getOrElse(false); } /** @@ -204,9 +227,8 @@ public boolean getConfigBoolean(AppKeys appKey) { * @param appKey the key to retrieve the configuration value for * @return the array configuration value */ - public String[] getConfigArray(AppKeys appKey) { - String returnValue = getConfig(appKey); - + public String[] getConfigArray(final AppKeys appKey) { + final String returnValue = getConfig(appKey); return returnValue != null ? SPLITTER.split(returnValue) : new String[0]; } @@ -216,13 +238,37 @@ public String[] getConfigArray(AppKeys appKey) { * @param appKey the key to retrieve the configuration value for * @return the configuration value */ - public String getConfig(AppKeys appKey) { + public String getConfig(final AppKeys appKey) { if (configValues.containsKey(appKey.key)) { return Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue); } return appKey.defaultValue; } + /** + * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. + * + * @param type the type of the model to find + */ + public AIModel resolveModel(final AIModelType type) { + return AIModels.get().findModel(host, type).orElse(AIModels.NOOP_MODEL); + } + + /** + * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. + * + * @param modelName the name of the model to find + */ + public AIModel resolveModelOrThrow(final String modelName) { + return AIModels.get() + .findModel(host, modelName) + .orElseThrow(() -> { + final String supported = String.join(", ", AIModels.get().getOrPullSupportedModels()); + return new DotRuntimeException( + "Unable to find model: [" + modelName + "]. Only [" + supported + "] are supported "); + }); + } + /** * Prints a specific error message to the log, based on the {@link AppKeys#DEBUG_LOGGING} * property instead of the usual Log4j configuration. @@ -236,4 +282,11 @@ public static void debugLogger(final Class clazz, final Supplier mess } } -} \ No newline at end of file + private String discoverEmbeddingsApiUrl(final Map secrets) { + final String url = AIAppUtil.get().discoverEnvSecret(secrets, AppKeys.API_EMBEDDINGS_URL); + return StringUtils.isBlank(url) + ? Config.getStringProperty(OPEN_AI_EMBEDDINGS_URL_KEY, "https://api.openai.com/v1/embeddings") + : url; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java b/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java index 7f79b831d966..947c0bf2a831 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java @@ -1,22 +1,33 @@ package com.dotcms.ai.app; public enum AppKeys { + API_URL("apiUrl", "https://api.openai.com/v1/chat/completions"), API_IMAGE_URL("apiImageUrl", "https://api.openai.com/v1/images/generations"), + API_EMBEDDINGS_URL("apiEmbeddingsUrl", null), API_KEY("apiKey", null), - ROLE_PROMPT("rolePrompt", "You are dotCMSbot, and AI assistant to help content" + - " creators generate and rewrite content in their content management system."), + ROLE_PROMPT( + "rolePrompt", + "You are dotCMSbot, and AI assistant to help content" + + " creators generate and rewrite content in their content management system."), TEXT_PROMPT("textPrompt", "Use Descriptive writing style."), IMAGE_PROMPT("imagePrompt", "Use 16:9 aspect ratio."), IMAGE_SIZE("imageSize", "1024x1024"), - MODEL("model", "gpt-3.5-turbo-16k"), - IMAGE_MODEL("imageModel", "dall-e-3"), - DEBUG_LOGGING("com.dotcms.ai.debug.logging", "false"), - COMPLETION_TEMPERATURE("com.dotcms.ai.completion.default.temperature", "1"), - COMPLETION_ROLE_PROMPT("com.dotcms.ai.completion.role.prompt", - "You are a helpful assistant with a descriptive writing style."), - COMPLETION_TEXT_PROMPT("com.dotcms.ai.completion.text.prompt", "Answer this question\\n\\\"$!{prompt}?\\\"\\n\\nby using only the information in the following text:\\n\"\"\"\\n$!{supportingContent} \\n\"\"\"\\n"), - EMBEDDINGS_MODEL("com.dotcms.ai.embeddings.model", "text-embedding-ada-002"), + TEXT_MODEL_NAMES("textModelNames", "gpt-3.5-turbo-16k"), + TEXT_MODEL_TOKENS_PER_MINUTE("textModelTokensPerMinute", "1000"), + TEXT_MODEL_API_PER_MINUTE("textModelApiPerMinute", "1000"), + TEXT_MODEL_MAX_TOKENS("textModelMaxTokens", "1000"), + TEXT_MODEL_COMPLETION("textModelCompletion", "true"), + IMAGE_MODEL_NAMES("imageModelNames", "dall-e-3"), + IMAGE_MODEL_TOKENS_PER_MINUTE("imageModelTokensPerMinute", "1000"), + IMAGE_MODEL_API_PER_MINUTE("imageModelApiPerMinute", "1000"), + IMAGE_MODEL_MAX_TOKENS("imageModelMaxTokens", "1000"), + IMAGE_MODEL_COMPLETION("imageModelCompletion", "true"), + EMBEDDINGS_MODEL_NAMES("embeddingsModelNames", "text-embedding-ada-002"), + EMBEDDINGS_MODEL_TOKENS_PER_MINUTE("embeddingsModelTokensPerMinute", "1000"), + EMBEDDINGS_MODEL_API_PER_MINUTE("embeddingsModelApiPerMinute", "1000"), + EMBEDDINGS_MODEL_MAX_TOKENS("embeddingsModelMaxTokens", "1000"), + EMBEDDINGS_MODEL_COMPLETION("embeddingsModelCompletion", "true"), EMBEDDINGS_SPLIT_AT_TOKENS("com.dotcms.ai.embeddings.split.at.tokens", "512"), EMBEDDINGS_MINIMUM_TEXT_LENGTH_TO_INDEX("com.dotcms.ai.embeddings.minimum.text.length", "64"), EMBEDDINGS_MINIMUM_FILE_SIZE_TO_INDEX("com.dotcms.ai.embeddings.minimum.file.size", "1024"), @@ -27,8 +38,19 @@ public enum AppKeys { EMBEDDINGS_THREADS_QUEUE("com.dotcms.ai.embeddings.threads.queue", "10000"), EMBEDDINGS_CACHE_TTL_SECONDS("com.dotcms.ai.embeddings.cache.ttl.seconds", "600"), EMBEDDINGS_CACHE_SIZE("com.dotcms.ai.embeddings.cache.size", "1000"), + EMBEDDINGS_DB_DELETE_OLD_ON_UPDATE("com.dotcms.ai.embeddings.delete.old.on.update", "true"), + DEBUG_LOGGING("com.dotcms.ai.debug.logging", "false"), + COMPLETION_TEMPERATURE("com.dotcms.ai.completion.default.temperature", "1"), + COMPLETION_ROLE_PROMPT( + "com.dotcms.ai.completion.role.prompt", + "You are a helpful assistant with a descriptive writing style."), + COMPLETION_TEXT_PROMPT( + "com.dotcms.ai.completion.text.prompt", + "Answer this question\\n\\\"$!{prompt}?\\\"\\n\\nby using only the information in" + + " the following text:\\n\"\"\"\\n$!{supportingContent} \\n\"\"\"\\n"), LISTENER_INDEXER("listenerIndexer", "{}"), - EMBEDDINGS_DB_DELETE_OLD_ON_UPDATE("com.dotcms.ai.embeddings.delete.old.on.update", "true"); + AI_MODELS_CACHE_TTL("com.dotcms.ai.models.supported.ttl", "28800"), + AI_MODELS_CACHE_SIZE("com.dotcms.ai.models.supported.size", "64"); public static final String APP_KEY = "dotAI"; @@ -39,4 +61,5 @@ public enum AppKeys { this.key = key; this.defaultValue = defaultValue; } + } diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/ConfigService.java b/dotCMS/src/main/java/com/dotcms/ai/app/ConfigService.java index 3e035e277bb6..ca1e9d7eb91c 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/ConfigService.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/ConfigService.java @@ -17,8 +17,7 @@ public class ConfigService { public static final ConfigService INSTANCE = new ConfigService(); - public AppConfig config() { - return config(null); + private ConfigService() { } /** @@ -26,12 +25,21 @@ public AppConfig config() { * by dotCMS. */ public AppConfig config(final Host host) { + final Host resolved = resolveHost(host); final Optional appSecrets = Try.of(() -> APILocator - .getAppsAPI() - .getSecrets(AppKeys.APP_KEY, true, resolveHost(host), APILocator.systemUser())) + .getAppsAPI() + .getSecrets(AppKeys.APP_KEY, true, resolved, APILocator.systemUser())) .getOrElse(Optional.empty()); - return new AppConfig(appSecrets.map(AppSecrets::getSecrets).orElse(Map.of())); + return new AppConfig(resolved.getHostname(), appSecrets.map(AppSecrets::getSecrets).orElse(Map.of())); + } + + /** + * Gets the secrets from the App - this will check the current host then the SYSTEM_HOST for a valid configuration. This lookup is low overhead and cached + * by dotCMS. + */ + public AppConfig config() { + return config(null); } /** diff --git a/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java b/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java new file mode 100644 index 000000000000..226be03607e8 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java @@ -0,0 +1,70 @@ +package com.dotcms.ai.listener; + +import com.dotcms.ai.app.AIModels; +import com.dotcms.ai.app.AppKeys; +import com.dotcms.security.apps.AppSecretSavedEvent; +import com.dotcms.system.event.local.model.EventSubscriber; +import com.dotcms.system.event.local.model.KeyFilterable; +import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.portlets.contentlet.business.HostAPI; +import com.dotmarketing.util.Logger; +import io.vavr.control.Try; +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; +import java.util.Optional; + +/** + * This class listens to events related to the AI application and performs actions based on those events. + * It implements the EventSubscriber interface and overrides its methods to provide custom functionality. + * The class also implements the KeyFilterable interface to filter events based on a specific key. + * + * @author vico + */ +public final class AIAppListener implements EventSubscriber, KeyFilterable { + + private final HostAPI hostAPI; + + public AIAppListener(final HostAPI hostAPI) { + this.hostAPI = hostAPI; + } + + public AIAppListener() { + this(APILocator.getHostAPI()); + } + + @Override + public void notify(final AppSecretSavedEvent event) { + if (Objects.isNull(event)) { + Logger.debug(this, "Missing event, aborting"); + return; + } + + if (StringUtils.isBlank(event.getHostIdentifier())) { + Logger.debug(this, "Missing event's host id, aborting"); + return; + } + + final String hostId = event.getHostIdentifier(); + final Host host = Try.of(() -> hostAPI.find(hostId, APILocator.systemUser(), false)).getOrNull(); + + Optional.ofNullable(host).ifPresent(found -> AIModels.get().resetModels(found)); + } + + @Override + public Comparable getKey() { + return AppKeys.APP_KEY; + } + + public enum Instance { + SINGLETON; + + private final AIAppListener provider = new AIAppListener(); + + public static AIAppListener get() { + return AIAppListener.Instance.SINGLETON.provider; + } + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/model/AIImageRequestDTO.java b/dotCMS/src/main/java/com/dotcms/ai/model/AIImageRequestDTO.java index 0e5ba4cbe1f1..53f83c3ab149 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/model/AIImageRequestDTO.java +++ b/dotCMS/src/main/java/com/dotcms/ai/model/AIImageRequestDTO.java @@ -27,16 +27,17 @@ public String getSize() { return size; } - public int getNumberOfImages() { return numberOfImages; } - public String getPrompt() { return prompt; } + public String getModel() { + return model; + } public static class Builder { @JsonSetter(nulls = Nulls.SKIP) @@ -46,7 +47,7 @@ public static class Builder { @JsonSetter(nulls = Nulls.SKIP) private String size = ConfigService.INSTANCE.config().getImageSize(); @JsonSetter(nulls = Nulls.SKIP) - private String model = ConfigService.INSTANCE.config().getImageModel(); + private String model = ConfigService.INSTANCE.config().getImageModel().getCurrentModel(); public AIImageRequestDTO build() { return new AIImageRequestDTO(this); @@ -72,4 +73,5 @@ public Builder size(String size) { return this; } } + } diff --git a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java new file mode 100644 index 000000000000..3ff86d8ad44c --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java @@ -0,0 +1,43 @@ +package com.dotcms.ai.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +public class OpenAIModel implements Serializable { + + private final String id; + private final String object; + private final long created; + private final String ownedBy; + + @JsonCreator + public OpenAIModel(@JsonProperty("id") final String id, + @JsonProperty("object") final String object, + @JsonProperty("created") final long created, + @JsonProperty("owned_by") final String ownedBy) { + this.id = id; + this.object = object; + this.created = created; + this.ownedBy = ownedBy; + } + + public String getId() { + return id; + } + + public String getObject() { + return object; + } + + public long getCreated() { + return created; + } + + @JsonProperty("owned_by") + public String getOwnedBy() { + return ownedBy; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java new file mode 100644 index 000000000000..faa691b6a9c1 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java @@ -0,0 +1,29 @@ +package com.dotcms.ai.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.List; + +public class OpenAIModels implements Serializable { + + private final String object; + private final List data; + + @JsonCreator + public OpenAIModels(@JsonProperty("object") final String object, + @JsonProperty("data") final List data) { + this.object = object; + this.data = data; + } + + public String getObject() { + return object; + } + + public List getData() { + return data; + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java b/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java index 6a9e3065fd32..d56f4857870f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java @@ -2,12 +2,12 @@ import com.dotcms.ai.AiKeys; import com.dotcms.ai.api.CompletionsAPI; +import com.dotcms.ai.app.AIModels; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.rest.forms.CompletionsForm; import com.dotcms.ai.util.LineReadingOutputStream; -import com.dotcms.ai.util.OpenAIModel; import com.dotcms.rest.WebResource; import com.dotmarketing.beans.Host; import com.dotmarketing.business.web.WebAPILocator; @@ -28,13 +28,11 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.io.OutputStream; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; -import java.util.stream.Collectors; /** * The CompletionsResource class provides REST endpoints for interacting with the AI completions service. @@ -101,7 +99,7 @@ public final Response rawPrompt(@Context final HttpServletRequest request, @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON}) public final Response getConfig(@Context final HttpServletRequest request, @Context final HttpServletResponse response) { - // get user if we have one (this is allow anon) + // get user if we have one (this allows anon) new WebResource .InitBuilder(request, response) .requiredBackendUser(true) @@ -120,10 +118,7 @@ public final Response getConfig(@Context final HttpServletRequest request, final String apiKey = UtilMethods.isSet(app.getApiKey()) ? "*****" : "NOT SET"; map.put(AppKeys.API_KEY.key, apiKey); - final List models = Arrays.stream(OpenAIModel.values()) - .filter(m->m.completionModel) - .map(m-> m.modelName) - .collect(Collectors.toList()); + final List models = AIModels.get().getAvailableModels(); map.put(AiKeys.AVAILABLE_MODELS, models); return Response.ok(map).build(); @@ -145,7 +140,10 @@ private static CompletionsForm resolveForm(final HttpServletRequest request, .getUser(); final Host host = WebAPILocator.getHostWebAPI().getCurrentHostNoThrow(request); return (!user.isAdmin()) - ? CompletionsForm.copy(formIn).model(ConfigService.INSTANCE.config(host).getModel()).build() + ? CompletionsForm + .copy(formIn) + .model(ConfigService.INSTANCE.config(host).getModel().getCurrentModel()) + .build() : formIn; } diff --git a/dotCMS/src/main/java/com/dotcms/ai/rest/forms/CompletionsForm.java b/dotCMS/src/main/java/com/dotcms/ai/rest/forms/CompletionsForm.java index cfa0045b4450..a6bbbbeeec81 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/forms/CompletionsForm.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/forms/CompletionsForm.java @@ -117,7 +117,7 @@ private CompletionsForm(final Builder builder) { } else { this.temperature = builder.temperature >= 2 ? 2 : builder.temperature; } - this.model = UtilMethods.isSet(builder.model) ? builder.model : ConfigService.INSTANCE.config().getConfig(AppKeys.MODEL); + this.model = UtilMethods.isSet(builder.model) ? builder.model : ConfigService.INSTANCE.config().getConfig(AppKeys.TEXT_MODEL_NAMES); } private String validateBuilderQuery(final String query) { diff --git a/dotCMS/src/main/java/com/dotcms/ai/rest/forms/EmbeddingsForm.java b/dotCMS/src/main/java/com/dotcms/ai/rest/forms/EmbeddingsForm.java index e70a3814a557..61815b1307eb 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/forms/EmbeddingsForm.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/forms/EmbeddingsForm.java @@ -44,7 +44,7 @@ private EmbeddingsForm(Builder builder) { this.indexName = UtilMethods.isSet(builder.indexName) ? builder.indexName : "default"; this.velocityTemplate = builder.velocityTemplate; this.offset = builder.offset; - this.model = UtilMethods.isSet(builder.model) ? builder.model : ConfigService.INSTANCE.config().getConfig(AppKeys.EMBEDDINGS_MODEL); + this.model = UtilMethods.isSet(builder.model) ? builder.model : ConfigService.INSTANCE.config().getEmbeddingsModel().getCurrentModel(); this.fields = (builder.fields != null) ? AppConfig.SPLITTER.split(builder.fields.toLowerCase()) : new String[0]; this.userId= PortalUtil.getUser() != null ? PortalUtil.getUser().getUserId() : APILocator.systemUser().getUserId(); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java b/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java index 2e219c62dbf7..08edb4d5d691 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java @@ -22,7 +22,7 @@ public OpenAIChatServiceImpl(final AppConfig appConfig) { @Override public JSONObject sendRawRequest(final JSONObject prompt) { - prompt.putIfAbsent(AiKeys.MODEL, config.getModel()); + prompt.putIfAbsent(AiKeys.MODEL, config.getModel().getCurrentModel()); prompt.putIfAbsent(AiKeys.TEMPERATURE, config.getConfigFloat(AppKeys.COMPLETION_TEMPERATURE)); if (UtilMethods.isEmpty(prompt.optString(AiKeys.MESSAGES))) { @@ -36,7 +36,7 @@ public JSONObject sendRawRequest(final JSONObject prompt) { prompt.remove(AiKeys.PROMPT); - return new JSONObject(doRequest(config.getApiUrl(), config.getApiKey(), prompt)); + return new JSONObject(doRequest(config.getApiUrl(), prompt)); } @Override @@ -47,8 +47,8 @@ public JSONObject sendTextPrompt(final String textPrompt) { } @VisibleForTesting - String doRequest(final String urlIn, final String openAiAPIKey, final JSONObject json) { - return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, openAiAPIKey, json); + String doRequest(final String urlIn, final JSONObject json) { + return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, config, json); } } diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java b/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java index c5da0cd6f4b7..57b571dc140b 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java @@ -55,12 +55,12 @@ public JSONObject sendRequest(final JSONObject jsonObject) { } OpenAiRequestUtil.get().handleLargePrompt(jsonObject); - jsonObject.putIfAbsent(AiKeys.MODEL, config.getImageModel()); + jsonObject.putIfAbsent(AiKeys.MODEL, config.getImageModel().getCurrentModel()); jsonObject.putIfAbsent(AiKeys.SIZE, config.getImageSize()); String responseString = ""; try { - responseString = doRequest(config.getApiImageUrl(), config.getApiKey(), jsonObject); + responseString = doRequest(config.getApiImageUrl(), jsonObject); JSONObject returnObject = new JSONObject(responseString); if (returnObject.containsKey(AiKeys.ERROR)) { @@ -87,7 +87,7 @@ public JSONObject sendRawRequest(final String prompt) { @Override public JSONObject sendRequest(final AIImageRequestDTO dto) { JSONObject jsonRequest = new JSONObject(); - jsonRequest.put(AiKeys.MODEL, config.getImageModel()); + jsonRequest.put(AiKeys.MODEL, config.getImageModel().getCurrentModel()); jsonRequest.put(AiKeys.PROMPT, dto.getPrompt()); jsonRequest.put(AiKeys.SIZE, dto.getSize()); return sendRequest(jsonRequest); @@ -173,8 +173,8 @@ private String generateFileName(final String originalPrompt) { } @VisibleForTesting - String doRequest(final String urlIn, final String openAiAPIKey, final JSONObject json) { - return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, openAiAPIKey, json); + String doRequest(final String urlIn, final JSONObject json) { + return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, config, json); } @VisibleForTesting diff --git a/dotCMS/src/main/java/com/dotcms/ai/util/EncodingUtil.java b/dotCMS/src/main/java/com/dotcms/ai/util/EncodingUtil.java index cb5a836c1783..0a4258959cd7 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/util/EncodingUtil.java +++ b/dotCMS/src/main/java/com/dotcms/ai/util/EncodingUtil.java @@ -1,6 +1,5 @@ package com.dotcms.ai.util; -import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.knuddels.jtokkit.Encodings; import com.knuddels.jtokkit.api.Encoding; @@ -11,7 +10,7 @@ public class EncodingUtil { public static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); - public static final String model = ConfigService.INSTANCE.config().getConfig(AppKeys.EMBEDDINGS_MODEL); + public static final String model = ConfigService.INSTANCE.config().getEmbeddingsModel().getCurrentModel(); public static Lazy encoding = Lazy.of(()-> registry.getEncodingForModel(model).get() diff --git a/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIModel.java b/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIModel.java deleted file mode 100644 index 7e368c002ae3..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIModel.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.dotcms.ai.util; - -import com.dotmarketing.exception.DotRuntimeException; - -import java.util.Arrays; -import java.util.stream.Collectors; - -/** - * Enum representing different models of OpenAI. - * Each enum value contains the model name, tokens per minute, API per minute, maximum tokens, and a flag indicating if it's a completion model. - */ -public enum OpenAIModel { - - GPT_3_5_TURBO("gpt-3.5-turbo", 3000, 3500, 4096, true), - GPT_3_5_TURBO_16k("gpt-3.5-turbo-16k", 180000, 3500, 16384, true), - GPT_4("gpt-4", 10000, 200, 8191, true), - GPT_4_TURBO("gpt-4-1106-preview", 10000, 200, 128000, true), - GPT_4_TURBO_PREVIEW("gpt-4-turbo-preview", 10000, 200, 128000, true), - TEXT_EMBEDDING_ADA_002("text-embedding-ada-002", 1000000, 3000, 8191, false), - DALL_E_2("dall-e-2", 0, 50, 0, false), - DALL_E_3("dall-e-3", 0, 50, 0, false); - - public final int tokensPerMinute; - public final int apiPerMinute; - public final int maxTokens; - public final String modelName; - public final boolean completionModel; - - OpenAIModel(final String modelName, - final int tokensPerMinute, - final int apiPerMinute, - final int maxTokens, - final boolean completionModel) { - this.modelName = modelName; - this.tokensPerMinute = tokensPerMinute; - this.apiPerMinute = apiPerMinute; - this.maxTokens = maxTokens; - this.completionModel = completionModel; - } - - /** - * Resolves the model based on the input string. - * - * @param modelIn The input string representing the model. - * @return The corresponding OpenAIModel. - * @throws DotRuntimeException If the input string does not correspond to any OpenAIModel. - */ - public static OpenAIModel resolveModel(final String modelIn) { - final String modelOut = modelIn.replace("-", "_").replace(".", "_").toUpperCase().trim(); - for (final OpenAIModel openAiModel : OpenAIModel.values()) { - if (openAiModel.modelName.equalsIgnoreCase(modelIn) || openAiModel.name().equalsIgnoreCase(modelOut)) { - return openAiModel; - } - } - - throw new DotRuntimeException( - "Unable to parse model:'" + modelIn + "'. Only " + supportedModels() + " are supported "); - } - - /** - * Returns a string representing the supported models. - * - * @return A string representing the supported models. - */ - private static String supportedModels() { - return Arrays.stream(OpenAIModel.values()).map(o -> o.modelName).collect(Collectors.joining(", ")); - } - - /** - * Returns the minimum interval between calls for the model. - * - * @return The minimum interval between calls for the model. - */ - public long minIntervalBetweenCalls() { - return 60000 / apiPerMinute; - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIRequest.java b/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIRequest.java index f319fea6a3cc..e851c9b8f871 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIRequest.java +++ b/dotCMS/src/main/java/com/dotcms/ai/util/OpenAIRequest.java @@ -1,6 +1,8 @@ package com.dotcms.ai.util; import com.dotcms.ai.AiKeys; +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.dotmarketing.exception.DotRuntimeException; @@ -19,6 +21,7 @@ import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.OutputStream; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** @@ -30,62 +33,10 @@ */ public class OpenAIRequest { - private static final ConcurrentHashMap lastRestCall = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap lastRestCall = new ConcurrentHashMap<>(); private OpenAIRequest() {} - /** - * Sends a request to the specified URL with the specified method, OpenAI API key, and JSON payload. - * The response from the request is returned as a string. - * - * @param url the URL to send the request to - * @param method the HTTP method to use for the request - * @param openAiAPIKey the OpenAI API key to use for the request - * @param json the JSON payload to send with the request - * @return the response from the request as a string - */ - public static String doRequest(final String url, - final String method, - final String openAiAPIKey, - final JSONObject json) { - final ByteArrayOutputStream out = new ByteArrayOutputStream(); - doRequest(url, method, openAiAPIKey, json, out); - - return out.toString(); - } - - /** - * Sends a POST request to the specified URL with the specified OpenAI API key and JSON payload. - * The response from the request is written to the provided OutputStream. - * - * @param urlIn the URL to send the request to - * @param openAiAPIKey the OpenAI API key to use for the request - * @param json the JSON payload to send with the request - * @param out the OutputStream to write the response to - */ - public static void doPost(final String urlIn, - final String openAiAPIKey, - final JSONObject json, - final OutputStream out) { - doRequest(urlIn, HttpMethod.POST, openAiAPIKey, json, out); - } - - /** - * Sends a GET request to the specified URL with the specified OpenAI API key and JSON payload. - * The response from the request is written to the provided OutputStream. - * - * @param urlIn the URL to send the request to - * @param openAiAPIKey the OpenAI API key to use for the request - * @param json the JSON payload to send with the request - * @param out the OutputStream to write the response to - */ - public static void doGet(final String urlIn, - final String openAiAPIKey, - final JSONObject json, - final OutputStream out) { - doRequest(urlIn, HttpMethod.GET, openAiAPIKey,json,out); - } - /** * Sends a request to the specified URL with the specified method, OpenAI API key, and JSON payload. * The response from the request is written to the provided OutputStream. @@ -93,21 +44,23 @@ public static void doGet(final String urlIn, * * @param urlIn the URL to send the request to * @param method the HTTP method to use for the request - * @param openAiAPIKey the OpenAI API key to use for the request - * @param json the JSON payload to send with the request + * @param appConfig the AppConfig object containing the OpenAI API key and models + * @param payload the JSON payload to send with the request * @param out the OutputStream to write the response to */ public static void doRequest(final String urlIn, final String method, - final String openAiAPIKey, - final JSONObject json, + final AppConfig appConfig, + final JSONObject payload, final OutputStream out) { - if (ConfigService.INSTANCE.config().getConfigBoolean(AppKeys.DEBUG_LOGGING)) { - Logger.debug(OpenAIRequest.class, "posting:" + json); + final JSONObject json = Optional.ofNullable(payload).orElse(new JSONObject()); + + if (appConfig.getConfigBoolean(AppKeys.DEBUG_LOGGING)) { + Logger.debug(OpenAIRequest.class, "posting: " + json); } - final OpenAIModel model = OpenAIModel.resolveModel(json.optString(AiKeys.MODEL)); + final AIModel model = appConfig.resolveModelOrThrow(json.optString(AiKeys.MODEL)); final long sleep = lastRestCall.computeIfAbsent(model, m -> 0L) + model.minIntervalBetweenCalls() - System.currentTimeMillis(); @@ -115,9 +68,9 @@ public static void doRequest(final String urlIn, Logger.info( OpenAIRequest.class, "Rate limit:" - + model.apiPerMinute + + model.getApiPerMinute() + "/minute, or 1 every " - + (60000 / model.apiPerMinute) + + model.minIntervalBetweenCalls() + "ms. Sleeping:" + sleep); Try.run(() -> Thread.sleep(sleep)); @@ -129,7 +82,7 @@ public static void doRequest(final String urlIn, final StringEntity jsonEntity = new StringEntity(json.toString(), ContentType.APPLICATION_JSON); final HttpUriRequest httpRequest = resolveMethod(method, urlIn); httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); - httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openAiAPIKey); + httpRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + appConfig.getApiKey()); if (!json.getAsMap().isEmpty()) { Try.run(() -> ((HttpEntityEnclosingRequestBase) httpRequest).setEntity(jsonEntity)); @@ -157,6 +110,58 @@ public static void doRequest(final String urlIn, } } + /** + * Sends a request to the specified URL with the specified method, OpenAI API key, and JSON payload. + * The response from the request is returned as a string. + * + * @param url the URL to send the request to + * @param method the HTTP method to use for the request + * @param appConfig the AppConfig object containing the OpenAI API key and models + * @param payload the JSON payload to send with the request + * @return the response from the request as a string + */ + public static String doRequest(final String url, + final String method, + final AppConfig appConfig, + final JSONObject payload) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + doRequest(url, method, appConfig, payload, out); + + return out.toString(); + } + + /** + * Sends a POST request to the specified URL with the specified OpenAI API key and JSON payload. + * The response from the request is written to the provided OutputStream. + * + * @param urlIn the URL to send the request to + * @param appConfig the AppConfig object containing the OpenAI API key and models + * @param payload the JSON payload to send with the request + * @param out the OutputStream to write the response to + */ + public static void doPost(final String urlIn, + final AppConfig appConfig, + final JSONObject payload, + final OutputStream out) { + doRequest(urlIn, HttpMethod.POST, appConfig, payload, out); + } + + /** + * Sends a GET request to the specified URL with the specified OpenAI API key and JSON payload. + * The response from the request is written to the provided OutputStream. + * + * @param urlIn the URL to send the request to + * @param appConfig the AppConfig object containing the OpenAI API key and models + * @param payload the JSON payload to send with the request + * @param out the OutputStream to write the response to + */ + public static void doGet(final String urlIn, + final AppConfig appConfig, + final JSONObject payload, + final OutputStream out) { + doRequest(urlIn, HttpMethod.GET, appConfig, payload, out); + } + private static HttpUriRequest resolveMethod(final String method, final String urlIn) { switch(method) { case HttpMethod.POST: diff --git a/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java b/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java index 25dd1b0c74cd..03f73a37a8ec 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java +++ b/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java @@ -49,8 +49,8 @@ public Map getConfig() { this.config.getConfig(AppKeys.COMPLETION_ROLE_PROMPT), AppKeys.COMPLETION_TEXT_PROMPT.key, this.config.getConfig(AppKeys.COMPLETION_TEXT_PROMPT), - AppKeys.MODEL.key, - this.config.getConfig(AppKeys.MODEL)); + AppKeys.TEXT_MODEL_NAMES.key, + this.config.getConfig(AppKeys.TEXT_MODEL_NAMES)); } /** diff --git a/dotCMS/src/main/java/com/dotcms/ai/viewtool/EmbeddingsTool.java b/dotCMS/src/main/java/com/dotcms/ai/viewtool/EmbeddingsTool.java index 6754aa034204..aa2813d123d0 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/viewtool/EmbeddingsTool.java +++ b/dotCMS/src/main/java/com/dotcms/ai/viewtool/EmbeddingsTool.java @@ -2,10 +2,8 @@ import com.dotcms.ai.api.EmbeddingsAPI; import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.util.EncodingUtil; -import com.dotcms.ai.util.OpenAIModel; import com.dotmarketing.beans.Host; import com.dotmarketing.business.web.WebAPILocator; import com.dotmarketing.util.Logger; @@ -54,7 +52,7 @@ public void init(Object initData) { */ public int countTokens(final String prompt) { return EncodingUtil.registry - .getEncodingForModel(appConfig.getModel()) + .getEncodingForModel(appConfig.getModel().getCurrentModel()) .map(encoding -> encoding.countTokens(prompt)) .orElse(-1); } @@ -69,9 +67,7 @@ public int countTokens(final String prompt) { */ public List generateEmbeddings(final String prompt) { int tokens = countTokens(prompt); - int maxTokens = OpenAIModel - .resolveModel(ConfigService.INSTANCE.config(host).getConfig(AppKeys.EMBEDDINGS_MODEL)) - .maxTokens; + int maxTokens = ConfigService.INSTANCE.config(host).getEmbeddingsModel().getMaxTokens(); if (tokens > maxTokens) { Logger.warn( EmbeddingsTool.class, diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java index f09a172cb937..c05689bccf5f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java @@ -45,7 +45,7 @@ public List getParameters() { limitTagsToHost, new WorkflowActionletParameter(OpenAIParams.RUN_DELAY.key, "Update the content asynchronously, after X seconds. O means run in-process", "5", true), - new WorkflowActionletParameter(OpenAIParams.MODEL.key, "The AI model to use, defaults to " + ConfigService.INSTANCE.config().getConfig(AppKeys.MODEL), ConfigService.INSTANCE.config().getConfig(AppKeys.MODEL), false), + new WorkflowActionletParameter(OpenAIParams.MODEL.key, "The AI model to use, defaults to " + ConfigService.INSTANCE.config().getConfig(AppKeys.TEXT_MODEL_NAMES), ConfigService.INSTANCE.config().getConfig(AppKeys.TEXT_MODEL_NAMES), false), new WorkflowActionletParameter(OpenAIParams.TEMPERATURE.key, "The AI temperature for the response. Between .1 and 2.0.", ".1", false) ); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java index 9c8d403bb430..5e99b809d843 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java @@ -40,7 +40,7 @@ public List getParameters() { overwriteParameter, new WorkflowActionletParameter(OpenAIParams.OPEN_AI_PROMPT.key, "The prompt that will be sent to the AI", "We need an attractive search result in Google. Return a json object that includes the fields \"pageTitle\" for a meta title of less than 55 characters and \"metaDescription\" for the meta description of less than 300 characters using this content:\\n\\n${fieldContent}\\n\\n", true), new WorkflowActionletParameter(OpenAIParams.RUN_DELAY.key, "Update the content asynchronously, after X seconds. O means run in-process", "5", true), - new WorkflowActionletParameter(OpenAIParams.MODEL.key, "The AI model to use, defaults to " + ConfigService.INSTANCE.config().getConfig(AppKeys.MODEL), ConfigService.INSTANCE.config().getConfig(AppKeys.MODEL), false), + new WorkflowActionletParameter(OpenAIParams.MODEL.key, "The AI model to use, defaults to " + ConfigService.INSTANCE.config().getConfig(AppKeys.TEXT_MODEL_NAMES), ConfigService.INSTANCE.config().getConfig(AppKeys.TEXT_MODEL_NAMES), false), new WorkflowActionletParameter(OpenAIParams.TEMPERATURE.key, "The AI temperature for the response. Between .1 and 2.0. Defaults to " + ConfigService.INSTANCE.config().getConfig(AppKeys.COMPLETION_TEMPERATURE), ConfigService.INSTANCE.config().getConfig(AppKeys.COMPLETION_TEMPERATURE), false) ); } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java index 2adcd87badbb..62cbdc3f5f99 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/AnalyticsAPIImpl.java @@ -202,8 +202,9 @@ public void resetAnalyticsKey(final AnalyticsApp analyticsApp, final boolean for Logger.info( this, String.format( - "For clientId %s found this ANALYTICS_KEY response:\n%s", + "For clientId %s found this ANALYTICS_KEY response:%s%s", analyticsApp.getAnalyticsProperties().clientId(), + System.lineSeparator(), DotObjectMapperProvider.getInstance().getDefaultObjectMapper().writeValueAsString(response))); AnalyticsHelper.get().extractAnalyticsKey(response) @@ -278,7 +279,7 @@ private void validateAnalyticsApp(final AnalyticsApp analyticsApp) { * @param analyticsApp analytics app */ private void logTokenResponse(final CircuitBreakerUrl.Response response, AnalyticsApp analyticsApp) { - if (AnalyticsHelper.get().isSuccessResponse(response)) { + if (CircuitBreakerUrl.isSuccessResponse(response)) { return; } @@ -340,7 +341,7 @@ private String prepareRequestData(final AnalyticsApp analyticsApp) { private void logKeyResponse(final CircuitBreakerUrl.Response response, final AnalyticsApp analyticsApp) { - if (AnalyticsHelper.get().isSuccessResponse(response)) { + if (CircuitBreakerUrl.isSuccessResponse(response)) { return; } @@ -382,10 +383,7 @@ private CircuitBreakerUrl.Response requestAnalyticsKey(final Analy * @return map representation of http headers */ private Map analyticsKeyHeaders(final AccessToken accessToken) throws AnalyticsException { - return ImmutableMap.builder() - .put(HttpHeaders.AUTHORIZATION, AnalyticsHelper.get().formatBearer(accessToken)) - .put(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) - .build(); + return CircuitBreakerUrl.authHeaders(AnalyticsHelper.get().formatBearer(accessToken)); } } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/helper/AnalyticsHelper.java b/dotCMS/src/main/java/com/dotcms/analytics/helper/AnalyticsHelper.java index 32a7b1ce4015..c62d3add78e4 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/helper/AnalyticsHelper.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/helper/AnalyticsHelper.java @@ -25,8 +25,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; -import javax.validation.constraints.NotNull; -import javax.ws.rs.core.Response; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; @@ -51,26 +49,6 @@ public static AnalyticsHelper get(){ private AnalyticsHelper() {} - /** - * Evaluates if a given status code instance has a http status within the SUCCESSFUL range. - * - * @param statusCode http status code - * @return true if the response http status is considered tobe successful, otherwise false - */ - public boolean isSuccessResponse(final int statusCode) { - return Response.Status.Family.familyOf(statusCode) == Response.Status.Family.SUCCESSFUL; - } - - /** - * Evaluates if a given status code instance has a http status within the SUCCESSFUL range. - * - * @param response http response representation - * @return true if the response http status is considered tobe successful, otherwise false - */ - public boolean isSuccessResponse(@NotNull final CircuitBreakerUrl.Response response) { - return isSuccessResponse(response.getStatusCode()); - } - /** * Given a {@link CircuitBreakerUrl.Response} instance, extracts JSON representing the token and * deserializes to {@link AccessToken}. @@ -251,7 +229,7 @@ public AnalyticsApp appFromHost(final Host host) { */ public void throwFromResponse(final CircuitBreakerUrl.Response response, final String message) throws AnalyticsException { - if (isSuccessResponse(response)) { + if (CircuitBreakerUrl.isSuccessResponse(response)) { return; } diff --git a/dotCMS/src/main/java/com/dotcms/http/CircuitBreakerUrl.java b/dotCMS/src/main/java/com/dotcms/http/CircuitBreakerUrl.java index 9c6b6ca91338..e074368ef21b 100644 --- a/dotCMS/src/main/java/com/dotcms/http/CircuitBreakerUrl.java +++ b/dotCMS/src/main/java/com/dotcms/http/CircuitBreakerUrl.java @@ -20,7 +20,6 @@ import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; @@ -35,6 +34,9 @@ import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.NotNull; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -65,6 +67,13 @@ */ public class CircuitBreakerUrl { + private static final Lazy circuitBreakerMaxConnTotal = + Lazy.of(() -> Config.getIntProperty("CIRCUIT_BREAKER_MAX_CONN_TOTAL", 100)); + private static final Lazy allowAccessToPrivateSubnets = + Lazy.of(() -> Config.getBooleanProperty("ALLOW_ACCESS_TO_PRIVATE_SUBNETS", false)); + private static final CircuitBreakerConnectionControl circuitBreakerConnectionControl = + new CircuitBreakerConnectionControl(circuitBreakerMaxConnTotal.get()); + private final String proxyUrl; private final long timeoutMs; private final CircuitBreaker circuitBreaker; @@ -76,11 +85,12 @@ public class CircuitBreakerUrl { private final boolean allowRedirects; private final boolean throwWhenNot2xx; - private static final Lazy circuitBreakerMaxConnTotal = Lazy.of(()->Config.getIntProperty("CIRCUIT_BREAKER_MAX_CONN_TOTAL",100)); - private static final Lazy allowAccessToPrivateSubnets = Lazy.of(()->Config.getBooleanProperty("ALLOW_ACCESS_TO_PRIVATE_SUBNETS", false)); - private static final CircuitBreakerConnectionControl circuitBreakerConnectionControl = new CircuitBreakerConnectionControl(circuitBreakerMaxConnTotal.get()); + public static final Response EMPTY_RESPONSE = new Response<>(StringPool.BLANK, 0, new Header[] {}); + + public enum Method { + GET, POST, PUT, DELETE, PATCH + } - public static final Response EMPTY_RESPONSE = new Response<>(StringPool.BLANK, 0, new Header[]{}); /** * * @param proxyUrl @@ -118,13 +128,12 @@ public CircuitBreakerUrl(final String proxyUrl, timeoutMs, circuitBreaker, new HttpGet(proxyUrl), - ImmutableMap.of(), - ImmutableMap.of(), + Map.of(), + Map.of(), verbose, null); } - - + @VisibleForTesting public CircuitBreakerUrl(final String proxyUrl, final long timeoutMs, @@ -135,23 +144,8 @@ public CircuitBreakerUrl(final String proxyUrl, final boolean verbose, final String rawData) { this(proxyUrl, timeoutMs, circuitBreaker, request, params, headers, verbose, rawData, false, true); - } - /** - * Full featured constructor - * - * @param proxyUrl - * @param timeoutMs - * @param circuitBreaker - * @param request - * @param params - * @param headers - * @param verbose - * @param rawData - * @param allowRedirects - * @param throwWhenNot2xx - */ @VisibleForTesting public CircuitBreakerUrl(final String proxyUrl, final long timeoutMs, @@ -207,9 +201,10 @@ public String doString() throws IOException { Logger.warn( this, String.format( - "Invalid response detected when consuming [%s] with http status [%d] and response:\n%s", + "Invalid response detected when consuming [%s] with http status [%d] and response:%s%s", this.proxyUrl, this.response, + System.lineSeparator(), output)); } return output; @@ -234,7 +229,7 @@ public boolean isReady() { @Override public void setWriteListener(WriteListener writeListener) { - + // no-op } }; } @@ -305,42 +300,18 @@ public void doOut(final HttpServletResponse response) throws IOException { } } - public static boolean isWithin2xx(final int response) { - return response >= 200 && response <= 299; - } - - private void copyHeaders(final HttpResponse innerResponse, final HttpServletResponse response) { - final Header contentTypeHeader = innerResponse.getFirstHeader("Content-Type"); - - if (UtilMethods.isSet(contentTypeHeader)) { - response.setHeader(contentTypeHeader.getName(), contentTypeHeader.getValue()); - } - - final Header contentLengthHeader = innerResponse.getFirstHeader("Content-Length"); - - if (UtilMethods.isSet(contentLengthHeader)) { - response.setHeader(contentLengthHeader.getName(), contentLengthHeader.getValue()); - } - } - public int response() { - return this.response; + return this.response; } - public static CircuitBreakerUrlBuilder builder() { - return new CircuitBreakerUrlBuilder(); - } - - - @Override - public String toString() { - return "CircuitBreakerUrl [proxyUrl=" + proxyUrl + ", timeoutMs=" + timeoutMs + ", circuitBreaker=" + circuitBreaker + "]"; + public static boolean isWithin2xx(final int response) { + return response >= 200 && response <= 299; } public T doObject(final Class clazz) { return Try.of(() -> DotObjectMapperProvider.getInstance().getDefaultObjectMapper().readValue(doString(), clazz)) - .onFailure(e -> Logger.warnAndDebug(CircuitBreakerUrl.class, e)) - .getOrElse((T) null); + .onFailure(e -> Logger.warnAndDebug(CircuitBreakerUrl.class, e)) + .getOrElse((T) null); } public Response doResponse(final Class clazz) { @@ -364,9 +335,54 @@ public Header[] getResponseHeaders() { return responseHeaders; } - public enum Method { - GET, POST, PUT, DELETE, PATCH; + @Override + public String toString() { + return "CircuitBreakerUrl [proxyUrl=" + proxyUrl + ", timeoutMs=" + timeoutMs + ", circuitBreaker=" + circuitBreaker + "]"; + } + + private void copyHeaders(final HttpResponse innerResponse, final HttpServletResponse response) { + final Header contentTypeHeader = innerResponse.getFirstHeader("Content-Type"); + + if (UtilMethods.isSet(contentTypeHeader)) { + response.setHeader(contentTypeHeader.getName(), contentTypeHeader.getValue()); + } + final Header contentLengthHeader = innerResponse.getFirstHeader("Content-Length"); + + if (UtilMethods.isSet(contentLengthHeader)) { + response.setHeader(contentLengthHeader.getName(), contentLengthHeader.getValue()); + } + } + + public static Map authHeaders(final String token) { + return ImmutableMap.builder() + .put(HttpHeaders.AUTHORIZATION, token) + .put(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .build(); + } + + /** + * Evaluates if a given status code instance has a http status within the SUCCESSFUL range. + * + * @param statusCode http status code + * @return true if the response http status is considered tobe successful, otherwise false + */ + public static boolean isSuccessResponse(final int statusCode) { + return javax.ws.rs.core.Response.Status.Family.familyOf(statusCode) == javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; + } + + /** + * Evaluates if a given status code instance has a http status within the SUCCESSFUL range. + * + * @param response http response representation + * @return true if the response http status is considered tobe successful, otherwise false + */ + public static boolean isSuccessResponse(@NotNull final CircuitBreakerUrl.Response response) { + return isSuccessResponse(response.getStatusCode()); + } + + public static CircuitBreakerUrlBuilder builder() { + return new CircuitBreakerUrlBuilder(); } public static class CircuitBreakerConnectionControl { @@ -381,7 +397,6 @@ public CircuitBreakerConnectionControl(final int maxConnTotal) { public void check(final String proxyUrl) { if (threadIdConnectionCountSet.size() >= maxConnTotal) { - Logger.info(this, "The maximum number of connections has been reached, size: " + threadIdConnectionCountSet.size() + ", url: " + proxyUrl); throw new RejectedExecutionException("The maximum number of connections has been reached."); @@ -389,14 +404,13 @@ public void check(final String proxyUrl) { } public void start(final long id) { - threadIdConnectionCountSet.add(id); } public void end(final long id) { - threadIdConnectionCountSet.remove(id); } + } public static class Response implements Serializable { @@ -447,6 +461,7 @@ public String toString() { ", statusCode=" + statusCode + '}'; } + } } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationHelper.java index 6f8f83862eae..b35147c981a5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationHelper.java @@ -7,7 +7,6 @@ import com.dotcms.enterprise.cluster.ClusterFactory; import com.dotcms.enterprise.license.LicenseLevel; import com.dotcms.enterprise.license.LicenseManager; -import com.dotcms.util.CollectionsUtils; import com.dotmarketing.business.APILocator; import com.dotmarketing.util.Config; import com.dotmarketing.util.Constants; diff --git a/dotCMS/src/main/java/com/dotcms/system/event/local/business/LocalSystemEventSubscribersInitializer.java b/dotCMS/src/main/java/com/dotcms/system/event/local/business/LocalSystemEventSubscribersInitializer.java index c30029e2f858..335b77c7f29f 100644 --- a/dotCMS/src/main/java/com/dotcms/system/event/local/business/LocalSystemEventSubscribersInitializer.java +++ b/dotCMS/src/main/java/com/dotcms/system/event/local/business/LocalSystemEventSubscribersInitializer.java @@ -1,5 +1,6 @@ package com.dotcms.system.event.local.business; +import com.dotcms.ai.listener.AIAppListener; import com.dotcms.analytics.listener.AnalyticsAppListener; import com.dotcms.config.DotInitializer; import com.dotcms.content.elasticsearch.business.event.ContentletCheckinEvent; @@ -68,6 +69,7 @@ public void notify(final ChangeLoggerLevelEvent event) { APILocator.getLocalSystemEventsAPI().subscribe(APILocator.getContainerAPI()); APILocator.getLocalSystemEventsAPI().subscribe(AppSecretSavedEvent.class, AnalyticsAppListener.Instance.get()); + APILocator.getLocalSystemEventsAPI().subscribe(AppSecretSavedEvent.class, AIAppListener.Instance.get()); this.initDotVelocityMacrosVtlFiles(); } diff --git a/dotCMS/src/main/resources/apps/dotAI.yml b/dotCMS/src/main/resources/apps/dotAI.yml index c48eb25b26e0..e51d99bfd4c5 100644 --- a/dotCMS/src/main/resources/apps/dotAI.yml +++ b/dotCMS/src/main/resources/apps/dotAI.yml @@ -61,19 +61,82 @@ params: value: "1920x1080" - label: "256x256 (Small Square 1:1)" value: "256x256" - model: - value: "gpt-3.5-turbo-16k" + textModelNames: + value: "" + hidden: false + type: "STRING" + label: "Model Names" + hint: "Comma delimited list of models used to generate OpenAI API response." + required: true + textModelTokensPerMinute: + value: "180000" + hidden: false + type: "STRING" + label: "Tokens per Minute" + hint: "Tokens per minute used to generate OpenAI API response." + required: false + textModelApiPerMinute: + value: "3500" + hidden: false + type: "STRING" + label: "API per Minute" + hint: "API per minute used to generate OpenAI API response." + required: false + textModelMaxTokens: + value: "16384" + hidden: false + type: "STRING" + label: "Max Tokens" + hint: "Maximum number of tokens used to generate OpenAI API response." + required: false + textModelCompletion: + value: "false" + hidden: false + type: "BOOL" + label: "Completion model enabled" + hint: "Enable completion model used to generate OpenAI API response." + required: false + imageModelNames: + value: "" hidden: false type: "STRING" - label: "Model" - hint: "Model used to generate ChatGPT API response." + label: "Image Model Names" + hint: "Comma delimited list of image models used to generate OpenAI API response." required: true - imageModel: - value: "dall-e-3" + imageModelTokensPerMinute: + value: "0" + hidden: false + type: "STRING" + label: "Image Tokens per Minute" + hint: "Tokens per minute used to generate OpenAI API response." + required: false + imageModelApiPerMinute: + value: "50" + hidden: false + type: "STRING" + label: "Image API per Minute" + hint: "API per minute used to generate OpenAI API response." + required: false + imageModelMaxTokens: + value: "0" + hidden: false + type: "STRING" + label: "Image Max Tokens" + hint: "Maximum number of tokens used to generate OpenAI API response." + required: false + imageModelCompletion: + value: "false" + hidden: false + type: "BOOL" + label: "Image Completion model enabled" + hint: "Enable completion model used to generate OpenAI API response." + required: false + embeddingsModelNames: + value: "" hidden: false type: "STRING" - label: "Image Model" - hint: "Image Model used to generate AI Images" + label: "Embeddings Model Names" + hint: "Comma delimited list of embeddings models used to generate OpenAI API response." required: true listenerIndexer: value: "" diff --git a/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java b/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java new file mode 100644 index 000000000000..0d7ce095668a --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java @@ -0,0 +1,176 @@ +package com.dotcms.ai.app; + +import com.dotcms.security.apps.Secret; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the \AIAppUtil\ class. This test class verifies the functionality + * of methods in \AIAppUtil\ such as discovering secrets, creating models, and resolving + * environment-specific secrets. It uses mock objects to simulate the \Secret\ dependencies. + * + * @author vico + */ +public class AIAppUtilTest { + + private AIAppUtil aiAppUtil; + private Map secrets; + private Secret secret; + + @Before + public void setUp() { + aiAppUtil = AIAppUtil.get(); + secrets = mock(Map.class); + secret = mock(Secret.class); + } + + /** + * Given a map of secrets containing a key with a secret value + * When the discoverSecret method is called with the key and a default value + * Then the secret value should be returned. + */ + @Test + public void testDiscoverSecretWithDefaultValue() { + when(secrets.get("apiKey")).thenReturn(secret); + when(secret.getString()).thenReturn("secretValue"); + + String result = aiAppUtil.discoverSecret(secrets, AppKeys.API_KEY, "defaultValue"); + assertEquals("secretValue", result); + } + + /** + * Given a map of secrets not containing a key + * When the discoverSecret method is called with the key and a default value + * Then the default value should be returned. + */ + @Test + public void testDiscoverSecretWithDefaultValueNotFound() { + when(secrets.get("key")).thenReturn(null); + + String result = aiAppUtil.discoverSecret(secrets, AppKeys.API_KEY, "defaultValue"); + assertEquals("defaultValue", result); + } + + /** + * Given a map of secrets containing a key with a secret value + * When the discoverSecret method is called with the key + * Then the secret value should be returned. + */ + @Test + public void testDiscoverSecretWithKeyDefaultValue() { + when(secrets.get("apiKey")).thenReturn(secret); + when(secret.getString()).thenReturn("secretValue"); + + String result = aiAppUtil.discoverSecret(secrets, AppKeys.API_KEY); + assertEquals("secretValue", result); + } + + /** + * Given a map of secrets not containing a key + * When the discoverSecret method is called with the key + * Then the default value of the key should be returned. + */ + @Test + public void testDiscoverSecretWithKeyDefaultValueNotFound() { + when(secrets.get("key")).thenReturn(null); + + String result = aiAppUtil.discoverSecret(secrets, AppKeys.API_KEY); + assertEquals(AppKeys.API_KEY.defaultValue, result); + } + + /** + * Given a map of secrets containing a key with an environment secret value + * When the discoverEnvSecret method is called with the key + * Then the environment secret value should be returned. + */ + @Test + public void testDiscoverEnvSecret() { + when(secrets.get("apiKey")).thenReturn(secret); + when(secret.getString()).thenReturn("envSecretValue"); + + String result = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_KEY); + assertEquals("envSecretValue", result); + } + + /** + * Given a map of secrets containing a key with an integer secret value + * When the discoverIntSecret method is called with the key + * Then the integer secret value should be returned. + */ + @Test + public void testDiscoverIntSecret() { + when(secrets.get("apiKey")).thenReturn(secret); + when(secret.getString()).thenReturn("123"); + + int result = aiAppUtil.discoverIntSecret(secrets, AppKeys.API_KEY); + assertEquals(123, result); + } + + /** + * Given a map of secrets containing a key with a boolean secret value + * When the discoverBooleanSecret method is called with the key + * Then the boolean secret value should be returned. + */ + @Test + public void testDiscoverBooleanSecret() { + when(secrets.get("apiKey")).thenReturn(secret); + when(secret.getString()).thenReturn("true"); + + boolean result = aiAppUtil.discoverBooleanSecret(secrets, AppKeys.API_KEY); + assertTrue(result); + } + + /** + * Given a map of secrets containing a key with a text model name + * When the createTextModel method is called + * Then an AIModel instance should be created with the specified type and model name. + */ + @Test + public void testCreateTextModel() { + when(secrets.get(AppKeys.TEXT_MODEL_NAMES.key)).thenReturn(secret); + when(secret.getString()).thenReturn("textModel"); + + AIModel model = aiAppUtil.createTextModel(secrets); + assertNotNull(model); + assertEquals(AIModelType.TEXT, model.getType()); + assertTrue(model.getNames().contains("textModel")); + } + + /** + * Given a map of secrets containing a key with an image model name + * When the createImageModel method is called + * Then an AIModel instance should be created with the specified type and model name. + */ + @Test + public void testCreateImageModel() { + when(secrets.get(AppKeys.IMAGE_MODEL_NAMES.key)).thenReturn(secret); + when(secret.getString()).thenReturn("imageModel"); + + AIModel model = aiAppUtil.createImageModel(secrets); + assertNotNull(model); + assertEquals(AIModelType.IMAGE, model.getType()); + assertTrue(model.getNames().contains("imageModel")); + } + + /** + * Given a map of secrets containing a key with an embeddings model name + * When the createEmbeddingsModel method is called + * Then an AIModel instance should be created with the specified type and model name. + */ + @Test + public void testCreateEmbeddingsModel() { + when(secrets.get(AppKeys.EMBEDDINGS_MODEL_NAMES.key)).thenReturn(secret); + when(secret.getString()).thenReturn("embeddingsModel"); + + AIModel model = aiAppUtil.createEmbeddingsModel(secrets); + assertNotNull(model); + assertEquals(AIModelType.EMBEDDINGS, model.getType()); + assertTrue(model.getNames().contains("embeddingsmodel")); + } + +} \ No newline at end of file diff --git a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java index 996098bc51b4..e110608dbb2c 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java @@ -1,5 +1,7 @@ package com.dotcms.ai.service; +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AIModelType; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotmarketing.util.json.JSONObject; @@ -52,14 +54,15 @@ public void test_sendTextPrompt() { private OpenAIChatService prepareService(final String response) { return new OpenAIChatServiceImpl(config) { @Override - String doRequest(final String urlIn, final String openAiAPIKey, final JSONObject json) { + String doRequest(final String urlIn, final JSONObject json) { return response; } }; } private JSONObject prepareJsonObject(final String prompt) { - when(config.getModel()).thenReturn("some-model"); + when(config.getModel()) + .thenReturn(AIModel.builder().withType(AIModelType.TEXT).withNames("some-model").build()); when(config.getConfigFloat(AppKeys.COMPLETION_TEMPERATURE)).thenReturn(123.321F); when(config.getRolePrompt()).thenReturn("some-role-prompt"); diff --git a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java index 0e7fef5054a2..1338b3110c74 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java @@ -1,5 +1,7 @@ package com.dotcms.ai.service; +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AIModelType; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.model.AIImageRequestDTO; import com.dotcms.ai.util.StopWordsUtil; @@ -199,7 +201,7 @@ private OpenAIImageService prepareService(final String response, final User user) { return new OpenAIImageServiceImpl(config, user, hostApi, tempFileApi) { @Override - String doRequest(final String urlIn, final String openAiAPIKey, final JSONObject json) { + String doRequest(final String urlIn, final JSONObject json) { return response; } @@ -216,7 +218,7 @@ AIImageRequestDTO.Builder getDtoBuilder() { } private JSONObject prepareJsonObject(final String prompt, final boolean tempFileError) throws Exception { - when(config.getImageModel()).thenReturn("some-image-model"); + when(config.getImageModel()).thenReturn(AIModel.builder().withType(AIModelType.IMAGE).withNames("some-image-model").build()); when(config.getImageSize()).thenReturn("some-image-size"); final File file = mock(File.class); when(file.getName()).thenReturn(UUIDGenerator.shorty()); diff --git a/dotCMS/src/test/java/com/dotcms/analytics/helper/AnalyticsHelperTest.java b/dotCMS/src/test/java/com/dotcms/analytics/helper/AnalyticsHelperTest.java index d8593326a51d..2c062cba0365 100644 --- a/dotCMS/src/test/java/com/dotcms/analytics/helper/AnalyticsHelperTest.java +++ b/dotCMS/src/test/java/com/dotcms/analytics/helper/AnalyticsHelperTest.java @@ -47,36 +47,6 @@ public void setup() { when(response.getStatusCode()).thenReturn(HttpStatus.SC_OK); } - /** - * Given an int status code - * Then evaluate it does have a SUCCESS http status - */ - @Test - public void test_isSuccessStatusCode() { - assertTrue(AnalyticsHelper.get().isSuccessResponse(HttpStatus.SC_ACCEPTED)); - assertTrue(AnalyticsHelper.get().isSuccessResponse(HttpStatus.SC_OK)); - assertFalse(AnalyticsHelper.get().isSuccessResponse(HttpStatus.SC_BAD_REQUEST)); - assertFalse(AnalyticsHelper.get().isSuccessResponse(HttpStatus.SC_FORBIDDEN)); - assertFalse(AnalyticsHelper.get().isSuccessResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR)); - } - - /** - * Given a {@link Response} - * Then evaluate it does have a SUCCESS http status - */ - @Test - public void test_isSuccessResponse() { - assertTrue(AnalyticsHelper.get().isSuccessResponse(response)); - when(response.getStatusCode()).thenReturn(HttpStatus.SC_ACCEPTED); - assertTrue(AnalyticsHelper.get().isSuccessResponse(response)); - when(response.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); - assertFalse(AnalyticsHelper.get().isSuccessResponse(response)); - when(response.getStatusCode()).thenReturn(HttpStatus.SC_FORBIDDEN); - assertFalse(AnalyticsHelper.get().isSuccessResponse(response)); - when(response.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - assertFalse(AnalyticsHelper.get().isSuccessResponse(response)); - } - /** * Given an {@link Response} * Then verify that an {@link AccessToken} can be extracted as an entity diff --git a/dotcms-integration/pom.xml b/dotcms-integration/pom.xml index 58dcf3984df9..cf321d39bb74 100644 --- a/dotcms-integration/pom.xml +++ b/dotcms-integration/pom.xml @@ -411,7 +411,9 @@ ${test.webapp.root}/WEB-INF/velocity ${test.webapp.root}/WEB-INF/geoip2/GeoLite2-City.mmdb ${test.webapp.root}/WEB-INF/bin + http://localhost:50505/e http://localhost:50505/e + http://localhost:50505/m ${it.test.fork-folder}${surefire.forkNumber}/${test.temp.folder} diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java index 0097ec91cbf3..86304a1c8d95 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java @@ -1,5 +1,6 @@ package com.dotcms; +import com.dotcms.ai.app.AIModelsTest; import com.dotcms.ai.listener.EmbeddingContentListenerTest; import com.dotcms.ai.viewtool.AIViewToolTest; import com.dotcms.ai.viewtool.CompletionsToolTest; @@ -299,6 +300,7 @@ SearchToolTest.class, EmbeddingsToolTest.class, CompletionsToolTest.class, + AIModelsTest.class, TimeMachineAPITest.class, Task240513UpdateContentTypesSystemFieldTest.class, PruneTimeMachineBackupJobTest.class, diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AiTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java similarity index 82% rename from dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AiTest.java rename to dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java index 32391726afe4..fb529a7dc30f 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AiTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java @@ -1,10 +1,11 @@ -package com.dotcms.ai.viewtool; +package com.dotcms.ai; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.security.apps.Secret; import com.dotcms.security.apps.Type; import com.dotcms.util.WireMockTestHelper; +import com.dotmarketing.beans.Host; import com.github.tomakehurst.wiremock.WireMockServer; import java.util.HashMap; @@ -20,12 +21,12 @@ public interface AiTest { String IMAGE_SIZE = "1024x1024"; int PORT = 50505; - static AppConfig prepareConfig(final WireMockServer wireMockServer) { - return new AppConfig(appConfigMap(wireMockServer)); + static AppConfig prepareConfig(final Host host, final WireMockServer wireMockServer) { + return new AppConfig(host.getHostname(), appConfigMap(wireMockServer)); } - static AppConfig prepareCompletionConfig(final WireMockServer wireMockServer) { - return new AppConfig(completionAppConfigMap(appConfigMap(wireMockServer))); + static AppConfig prepareCompletionConfig(final Host host, final WireMockServer wireMockServer) { + return new AppConfig(host.getHostname(), completionAppConfigMap(appConfigMap(wireMockServer))); } static WireMockServer prepareWireMock() { @@ -49,10 +50,10 @@ private static Map completionAppConfigMap(final Map all = new HashMap<>(configMap); @@ -77,10 +78,10 @@ static Map appConfigMap(final WireMockServer wireMockServer) { AppKeys.API_KEY.key, Secret.builder().withType(Type.STRING).withValue(API_KEY.toCharArray()).build(), - AppKeys.MODEL.key, + AppKeys.TEXT_MODEL_NAMES.key, Secret.builder().withType(Type.STRING).withValue(MODEL.toCharArray()).build(), - AppKeys.IMAGE_MODEL.key, + AppKeys.IMAGE_MODEL_NAMES.key, Secret.builder().withType(Type.STRING).withValue(IMAGE_MODEL.toCharArray()).build(), AppKeys.IMAGE_SIZE.key, diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java new file mode 100644 index 000000000000..deba9567ca88 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java @@ -0,0 +1,203 @@ +package com.dotcms.ai.app; + +import com.dotcms.ai.AiTest; +import com.dotcms.datagen.SiteDataGen; +import com.dotcms.util.IntegrationTestInitService; +import com.dotcms.util.network.IPUtils; +import com.dotmarketing.beans.Host; +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Integration tests for the \AIModels\ class. This test class verifies the functionality + * of methods in \AIModels\ such as loading models, finding models by host and type, and + * retrieving supported models. It uses \WireMockServer\ to simulate external dependencies + * and \IntegrationTestInitService\ for initializing the test environment. + * + * @author vico + */ +public class AIModelsTest { + + private static WireMockServer wireMockServer; + + private final AIModels aiModels = AIModels.get(); + private Host host; + private Host otherHost; + + @BeforeClass + public static void beforeClass() throws Exception { + IntegrationTestInitService.getInstance().init(); + IPUtils.disabledIpPrivateSubnet(true); + wireMockServer = AiTest.prepareWireMock(); + } + + @AfterClass + public static void afterClass() { + wireMockServer.stop(); + IPUtils.disabledIpPrivateSubnet(false); + } + + @Before + public void before() { + host = new SiteDataGen().nextPersisted(); + otherHost = new SiteDataGen().nextPersisted(); + } + + /** + * Given a set of models loaded into the AIModels instance + * When the findModel method is called with various model names and types + * Then the correct models should be found and returned. + */ + @Test + public void test_loadModels_andFindThem() { + loadModels(); + + final String hostId = host.getHostname(); + final Optional notFound = aiModels.findModel(hostId, "some-invalid-model-name"); + assertTrue(notFound.isEmpty()); + + final Optional text1 = aiModels.findModel(hostId, "text-model-1"); + final Optional text2 = aiModels.findModel(hostId, "text-model-2"); + assertModels(text1, text2, AIModelType.TEXT); + + final Optional image1 = aiModels.findModel(hostId, "image-model-3"); + final Optional image2 = aiModels.findModel(hostId, "image-model-4"); + assertModels(image1, image2, AIModelType.IMAGE); + + final Optional embeddings1 = aiModels.findModel(hostId, "embeddings-model-5"); + assertTrue(embeddings1.isPresent()); + final Optional embeddings2 = aiModels.findModel(hostId, "embeddings-model-6"); + assertModels(embeddings1, embeddings2, AIModelType.EMBEDDINGS); + + assertNotSame(text1.get(), image1.get()); + assertNotSame(text1.get(), embeddings1.get()); + assertNotSame(image1.get(), embeddings1.get()); + + final Optional text3 = aiModels.findModel(hostId, AIModelType.TEXT); + assertSameModels(text3, text1, text2); + + final Optional image3 = aiModels.findModel(hostId, AIModelType.IMAGE); + assertSameModels(image3, image1, image2); + + final Optional embeddings3 = aiModels.findModel(hostId, AIModelType.EMBEDDINGS); + assertSameModels(embeddings3, embeddings1, embeddings2); + + final Optional text4 = aiModels.findModel(otherHost.getHostname(), "text-model-1"); + assertTrue(text3.isPresent()); + assertNotSame(text1.get(), text4.get()); + } + + /** + * Given a set of models loaded into the AIModels instance + * When the resetModels method is called with a specific host + * Then the models for that host should be reset and no longer found. + */ + @Test + public void test_resetModels() { + loadModels(); + final Optional aiModel = aiModels.findModel(host.getHostname(), AIModelType.TEXT); + + aiModels.resetModels(host); + + assertNotSame(aiModel.get(), aiModels.findModel(host.getHostname(), AIModelType.TEXT)); + assertTrue(aiModels.findModel(host.getHostname(), "text-model-1").isEmpty()); + } + + /** + * Given a URL for supported models + * When the getOrPullSupportedModules method is called + * Then a list of supported models should be returned. + */ + @Test + public void test_getOrPullSupportedModules() { + final List supported = aiModels.getOrPullSupportedModels(); + assertNotNull(supported); + assertEquals(32, supported.size()); + } + + /** + * Given an invalid URL for supported models + * When the getOrPullSupportedModules method is called + * Then an empty list of supported models should be returned. + */ + @Test + public void test_getOrPullSupportedModules_invalidEndpoint() { + IPUtils.disabledIpPrivateSubnet(false); + + final List supported = aiModels.getOrPullSupportedModels(); + assertNotNull(supported); + assertTrue(supported.isEmpty()); + + IPUtils.disabledIpPrivateSubnet(true); + } + + private void loadModels() { + aiModels.loadModels( + host.getHostname(), + List.of( + AIModel.builder() + .withType(AIModelType.TEXT) + .withNames("text-model-1", "text-model-2") + .withTokensPerMinute(123) + .withApiPerMinute(321) + .withMaxTokens(555) + .withIsCompletion(true) + .build(), + AIModel.builder() + .withType(AIModelType.IMAGE) + .withNames("image-model-3", "image-model-4") + .withTokensPerMinute(111) + .withApiPerMinute(222) + .withMaxTokens(333) + .withIsCompletion(false) + .build(), + AIModel.builder() + .withType(AIModelType.EMBEDDINGS) + .withNames("embeddings-model-5", "embeddings-model-6") + .withTokensPerMinute(999) + .withApiPerMinute(888) + .withMaxTokens(777) + .withIsCompletion(false) + .build())); + aiModels.loadModels( + otherHost.getHostname(), + List.of( + AIModel.builder() + .withType(AIModelType.TEXT) + .withNames("text-model-1") + .withTokensPerMinute(123) + .withApiPerMinute(321) + .withMaxTokens(555) + .withIsCompletion(true) + .build())); + } + + private static void assertSameModels(Optional text3, Optional text1, Optional text2) { + assertTrue(text3.isPresent()); + assertSame(text1.get(), text3.get()); + assertSame(text2.get(), text3.get()); + } + + private static void assertModels(final Optional model1, + final Optional model2, + final AIModelType type) { + assertTrue(model1.isPresent()); + assertTrue(model2.isPresent()); + assertSame(model1.get(), model2.get()); + assertSame(type, model1.get().getType()); + assertSame(type, model2.get().getType()); + } + +} diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java index e4c9567aa348..9f76e9da6f33 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java @@ -2,7 +2,7 @@ import com.dotcms.ai.api.EmbeddingsAPI; import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.viewtool.AiTest; +import com.dotcms.ai.AiTest; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.datagen.TestDataUtils; diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java index 29341e19ddfa..0071d755f616 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java @@ -1,9 +1,11 @@ package com.dotcms.ai.viewtool; +import com.dotcms.ai.AiTest; import com.dotcms.ai.app.AppConfig; import com.dotcms.datagen.UserDataGen; import com.dotcms.util.IntegrationTestInitService; import com.dotcms.util.network.IPUtils; +import com.dotmarketing.business.APILocator; import com.dotmarketing.util.json.JSONObject; import com.github.tomakehurst.wiremock.WireMockServer; import com.liferay.portal.model.User; @@ -43,7 +45,7 @@ public static void beforeClass() throws Exception { IntegrationTestInitService.getInstance().init(); IPUtils.disabledIpPrivateSubnet(true); wireMockServer = AiTest.prepareWireMock(); - config = AiTest.prepareConfig(wireMockServer); + config = AiTest.prepareConfig(APILocator.systemHost(), wireMockServer); } @AfterClass diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java index c99cca9a6a4d..a8769c973d5d 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java @@ -1,5 +1,6 @@ package com.dotcms.ai.viewtool; +import com.dotcms.ai.AiTest; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.datagen.EmbeddingsDTODataGen; @@ -39,16 +40,17 @@ public class CompletionsToolTest { private static AppConfig config; private static WireMockServer wireMockServer; + private static Host host; - private Host host; private CompletionsTool completionsTool; @BeforeClass public static void beforeClass() throws Exception { IntegrationTestInitService.getInstance().init(); IPUtils.disabledIpPrivateSubnet(true); + host = new SiteDataGen().nextPersisted(); wireMockServer = AiTest.prepareWireMock(); - config = AiTest.prepareCompletionConfig(wireMockServer); + config = AiTest.prepareCompletionConfig(host, wireMockServer); } @AfterClass @@ -61,7 +63,7 @@ public static void afterClass() { public void before() { final ViewContext viewContext = mock(ViewContext.class); when(viewContext.getRequest()).thenReturn(mock(HttpServletRequest.class)); - host = new SiteDataGen().nextPersisted(); + completionsTool = prepareCompletionsTool(viewContext); } @@ -80,7 +82,7 @@ public void test_getConfig() { assertNotNull(config); assertEquals(AppKeys.COMPLETION_ROLE_PROMPT.defaultValue, config.get(AppKeys.COMPLETION_ROLE_PROMPT.key)); assertEquals(AppKeys.COMPLETION_TEXT_PROMPT.defaultValue, config.get(AppKeys.COMPLETION_TEXT_PROMPT.key)); - assertEquals(AppKeys.MODEL.defaultValue, config.get(AppKeys.MODEL.key)); + assertEquals(AppKeys.TEXT_MODEL_NAMES.defaultValue, config.get(AppKeys.TEXT_MODEL_NAMES.key)); } /** diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java index 3e7d009c132f..22e352b60a38 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java @@ -1,5 +1,7 @@ package com.dotcms.ai.viewtool; +import com.dotcms.ai.app.AIModel; +import com.dotcms.ai.app.AIModelType; import com.dotcms.ai.app.AppConfig; import com.dotcms.datagen.EmbeddingsDTODataGen; import com.dotcms.datagen.SiteDataGen; @@ -21,6 +23,14 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +/** + * Integration tests for the \EmbeddingsTool\ class. This test class verifies the functionality + * of methods in \EmbeddingsTool\ such as counting tokens, generating embeddings, and + * retrieving index counts. It uses mock objects to simulate the \ViewContext\ and + * \AppConfig\ dependencies. + * + * @author vico + */ public class EmbeddingsToolTest { private Host host; @@ -107,9 +117,10 @@ AppConfig appConfig() { } private AppConfig prepareAppConfig() { - final AppConfig appConfig = mock(AppConfig.class); - when(appConfig.getModel()).thenReturn("gpt-3.5-turbo-16k"); - return appConfig; + final AppConfig config = mock(AppConfig.class); + final AIModel aiModel = AIModel.builder().withType(AIModelType.TEXT).withNames("gpt-3.5-turbo-16k").build(); + when(config.getModel()).thenReturn(aiModel); + return config; } } diff --git a/dotcms-integration/src/test/java/com/dotcms/http/CircuitBreakerUrlTest.java b/dotcms-integration/src/test/java/com/dotcms/http/CircuitBreakerUrlTest.java index 2877cd65b4c5..6fcbacc07602 100644 --- a/dotcms-integration/src/test/java/com/dotcms/http/CircuitBreakerUrlTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/http/CircuitBreakerUrlTest.java @@ -17,7 +17,9 @@ import com.dotmarketing.util.DateUtil; import org.apache.commons.io.output.NullOutputStream; +import org.apache.http.HttpStatus; import org.junit.Assert; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -29,6 +31,13 @@ import net.jodah.failsafe.CircuitBreaker; import net.jodah.failsafe.CircuitBreakerOpenException; +import javax.ws.rs.core.Response; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class CircuitBreakerUrlTest { @@ -43,6 +52,14 @@ public class CircuitBreakerUrlTest { final static String PARAM="X-MY-PARAM"; final static String PARAM_VALUE="PARAM SEEMS TO BE WORKING"; + private CircuitBreakerUrl.Response response; + + @Before + public void setup() { + response = mock(CircuitBreakerUrl.Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.SC_OK); + } + @Test() public void test_circuitBreakerConnectionControl() { @@ -423,4 +440,34 @@ public void testBadRequest_dontThrow() throws Exception { assert (cburl.response() >= 400 && cburl.response() <= 499); } + /** + * Given an int status code + * Then evaluate it does have a SUCCESS http status + */ + @Test + public void test_isSuccessStatusCode() { + assertTrue(CircuitBreakerUrl.isSuccessResponse(HttpStatus.SC_ACCEPTED)); + assertTrue(CircuitBreakerUrl.isSuccessResponse(HttpStatus.SC_OK)); + assertFalse(CircuitBreakerUrl.isSuccessResponse(HttpStatus.SC_BAD_REQUEST)); + assertFalse(CircuitBreakerUrl.isSuccessResponse(HttpStatus.SC_FORBIDDEN)); + assertFalse(CircuitBreakerUrl.isSuccessResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR)); + } + + /** + * Given a {@link Response} + * Then evaluate it does have a SUCCESS http status + */ + @Test + public void test_isSuccessResponse() { + assertTrue(CircuitBreakerUrl.isSuccessResponse(response)); + when(response.getStatusCode()).thenReturn(HttpStatus.SC_ACCEPTED); + assertTrue(CircuitBreakerUrl.isSuccessResponse(response)); + when(response.getStatusCode()).thenReturn(HttpStatus.SC_BAD_REQUEST); + assertFalse(CircuitBreakerUrl.isSuccessResponse(response)); + when(response.getStatusCode()).thenReturn(HttpStatus.SC_FORBIDDEN); + assertFalse(CircuitBreakerUrl.isSuccessResponse(response)); + when(response.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); + assertFalse(CircuitBreakerUrl.isSuccessResponse(response)); + } + } diff --git a/dotcms-integration/src/test/resources/mappings/openai-models.json b/dotcms-integration/src/test/resources/mappings/openai-models.json new file mode 100644 index 000000000000..9bf3d1ca8a0f --- /dev/null +++ b/dotcms-integration/src/test/resources/mappings/openai-models.json @@ -0,0 +1,206 @@ +{ + "request": { + "method": "GET", + "url": "/m" + }, + "response": { + "status": 200, + "jsonBody": { + "object": "list", + "data": [ + { + "id": "dall-e-3", + "object": "model", + "created": 1698785189, + "owned_by": "system" + }, + { + "id": "gpt-4-1106-preview", + "object": "model", + "created": 1698957206, + "owned_by": "system" + }, + { + "id": "dall-e-2", + "object": "model", + "created": 1698798177, + "owned_by": "system" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1715367049, + "owned_by": "system" + }, + { + "id": "tts-1-hd-1106", + "object": "model", + "created": 1699053533, + "owned_by": "system" + }, + { + "id": "tts-1-hd", + "object": "model", + "created": 1699046015, + "owned_by": "system" + }, + { + "id": "gpt-4-0125-preview", + "object": "model", + "created": 1706037612, + "owned_by": "system" + }, + { + "id": "babbage-002", + "object": "model", + "created": 1692634615, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo-preview", + "object": "model", + "created": 1706037777, + "owned_by": "system" + }, + { + "id": "text-embedding-3-small", + "object": "model", + "created": 1705948997, + "owned_by": "system" + }, + { + "id": "text-embedding-3-large", + "object": "model", + "created": 1705953180, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-0613", + "object": "model", + "created": 1686587434, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo-instruct", + "object": "model", + "created": 1692901427, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-instruct-0914", + "object": "model", + "created": 1694122472, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1721172741, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-2024-07-18", + "object": "model", + "created": 1721172717, + "owned_by": "system" + }, + { + "id": "whisper-1", + "object": "model", + "created": 1677532384, + "owned_by": "openai-internal" + }, + { + "id": "gpt-4o-2024-05-13", + "object": "model", + "created": 1715368132, + "owned_by": "system" + }, + { + "id": "text-embedding-ada-002", + "object": "model", + "created": 1671217299, + "owned_by": "openai-internal" + }, + { + "id": "gpt-3.5-turbo-16k", + "object": "model", + "created": 1683758102, + "owned_by": "openai-internal" + }, + { + "id": "davinci-002", + "object": "model", + "created": 1692634301, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-16k-0613", + "object": "model", + "created": 1685474247, + "owned_by": "openai" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712601677, + "owned_by": "system" + }, + { + "id": "tts-1-1106", + "object": "model", + "created": 1699053241, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-0125", + "object": "model", + "created": 1706048358, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo", + "object": "model", + "created": 1712361441, + "owned_by": "system" + }, + { + "id": "tts-1", + "object": "model", + "created": 1681940951, + "owned_by": "openai-internal" + }, + { + "id": "gpt-3.5-turbo-1106", + "object": "model", + "created": 1698959748, + "owned_by": "system" + }, + { + "id": "gpt-4-0613", + "object": "model", + "created": 1686588896, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo-0301", + "object": "model", + "created": 1677649963, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687882411, + "owned_by": "openai" + } + ] + } + } +} \ No newline at end of file diff --git a/dotcms-postman/pom.xml b/dotcms-postman/pom.xml index 8c4487005f73..2d1962d864d9 100644 --- a/dotcms-postman/pom.xml +++ b/dotcms-postman/pom.xml @@ -137,7 +137,9 @@ http://wm:8080/c http://wm:8080/i ${wiremock.api.key} + http://wm:8080/e http://wm:8080/e + http://wm:8080/m true diff --git a/dotcms-postman/src/main/resources/postman/AI.postman_collection.json b/dotcms-postman/src/main/resources/postman/AI.postman_collection.json index b80c4f376bc1..5843dece1d37 100644 --- a/dotcms-postman/src/main/resources/postman/AI.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/AI.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "0d99325f-2e9a-49d7-a096-6770084ffa49", + "_postman_id": "7e9f91c0-35bf-4908-9f25-ba12d1dbf773", "name": "AI", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "11174695" @@ -1010,13 +1010,14 @@ "pm.test('Emebeddings are created', function () {", " pm.expect(jsonData.indexName, 'Index name should be \"default\"').equals('default');", " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - " if (currentSeo.embedded) {", - " pm.expect(jsonData.totalToEmbed, 'Total to embed is greater than zero').greaterThan(0);", - " } else {", - " pm.expect(jsonData.totalToEmbed, 'Total to embed is greater than zero').equals(0);", - " }", "});", "", + "if (currentSeo.embedded) {", + " pm.expect(jsonData.totalToEmbed, 'Total to embed is greater than zero').greaterThan(0);", + "} else {", + " pm.expect(jsonData.totalToEmbed, 'Total to embed is zero').equals(0);", + "}", + "", "seoIndex++;", "pm.collectionVariables.set('seoIndex', seoIndex);", "console.log('New seoIndex', seoIndex);", @@ -1082,7 +1083,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"fields\": \"seo\"\n}", + "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"fields\": \"seo\",\n \"model\": \"text-embedding-ada-002\"\n}", "options": { "raw": { "language": "json" @@ -1245,7 +1246,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\"\n}", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"model\": \"text-embedding-ada-002\"\n}", "options": { "raw": { "language": "json" @@ -2724,7 +2725,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"fields\": \"seo\"\n}", + "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"model\": \"text-embedding-ada-002\",\n \"fields\": \"seo\"\n}", "options": { "raw": { "language": "json" @@ -2959,7 +2960,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1\n}", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"model\": \"text-embedding-ada-002\",\n \"responseLengthTokens\": 1\n}", "options": { "raw": { "language": "json" @@ -3035,7 +3036,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"stream\": true\n}", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"model\": \"text-embedding-ada-002\",\n \"stream\": true\n}", "options": { "raw": { "language": "json" @@ -3269,14 +3270,11 @@ " pm.expect(jsonData).to.have.property(\"apiUrl\");", " pm.expect(jsonData).to.have.property(\"availableModels\");", " pm.expect(jsonData.availableModels).to.be.an(\"array\");", - " const expectedModels = [\"gpt-3.5-turbo\", \"gpt-3.5-turbo-16k\", \"gpt-4\", \"gpt-4-1106-preview\", \"gpt-4-turbo-preview\"];", - " expectedModels.forEach(function(model) {", - " pm.expect(jsonData.availableModels).to.include(model);", - " });", + " pm.expect(jsonData.availableModels.length).is.greaterThan(0);", " pm.expect(jsonData[\"com.dotcms.ai.completion.default.temperature\"]).to.equal(\"1\");", " pm.expect(jsonData[\"com.dotcms.ai.debug.logging\"]).to.equal(\"false\");", - " pm.expect(jsonData[\"com.dotcms.ai.embeddings.model\"]).to.equal(\"text-embedding-ada-002\");", - " pm.expect(jsonData.imageModel).to.equal(\"dall-e-3\");", + " pm.expect(jsonData.embeddingsModelNames).to.equal(\"text-embedding-ada-002\");", + " pm.expect(jsonData.imageModelNames).to.equal(\"dall-e-3\");", " pm.expect(jsonData.textPrompt).to.include(\"Descriptive writing style\");", " pm.expect(jsonData.rolePrompt).to.include(\"dotCMSbot\");", " pm.expect(jsonData.apiImageUrl).to.match(/^https?:\\/\\/.+/);", diff --git a/dotcms-postman/src/test/resources/mappings/openai-models.json b/dotcms-postman/src/test/resources/mappings/openai-models.json new file mode 100644 index 000000000000..9bf3d1ca8a0f --- /dev/null +++ b/dotcms-postman/src/test/resources/mappings/openai-models.json @@ -0,0 +1,206 @@ +{ + "request": { + "method": "GET", + "url": "/m" + }, + "response": { + "status": 200, + "jsonBody": { + "object": "list", + "data": [ + { + "id": "dall-e-3", + "object": "model", + "created": 1698785189, + "owned_by": "system" + }, + { + "id": "gpt-4-1106-preview", + "object": "model", + "created": 1698957206, + "owned_by": "system" + }, + { + "id": "dall-e-2", + "object": "model", + "created": 1698798177, + "owned_by": "system" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1715367049, + "owned_by": "system" + }, + { + "id": "tts-1-hd-1106", + "object": "model", + "created": 1699053533, + "owned_by": "system" + }, + { + "id": "tts-1-hd", + "object": "model", + "created": 1699046015, + "owned_by": "system" + }, + { + "id": "gpt-4-0125-preview", + "object": "model", + "created": 1706037612, + "owned_by": "system" + }, + { + "id": "babbage-002", + "object": "model", + "created": 1692634615, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo-preview", + "object": "model", + "created": 1706037777, + "owned_by": "system" + }, + { + "id": "text-embedding-3-small", + "object": "model", + "created": 1705948997, + "owned_by": "system" + }, + { + "id": "text-embedding-3-large", + "object": "model", + "created": 1705953180, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-0613", + "object": "model", + "created": 1686587434, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo-instruct", + "object": "model", + "created": 1692901427, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-instruct-0914", + "object": "model", + "created": 1694122472, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1721172741, + "owned_by": "system" + }, + { + "id": "gpt-4o-mini-2024-07-18", + "object": "model", + "created": 1721172717, + "owned_by": "system" + }, + { + "id": "whisper-1", + "object": "model", + "created": 1677532384, + "owned_by": "openai-internal" + }, + { + "id": "gpt-4o-2024-05-13", + "object": "model", + "created": 1715368132, + "owned_by": "system" + }, + { + "id": "text-embedding-ada-002", + "object": "model", + "created": 1671217299, + "owned_by": "openai-internal" + }, + { + "id": "gpt-3.5-turbo-16k", + "object": "model", + "created": 1683758102, + "owned_by": "openai-internal" + }, + { + "id": "davinci-002", + "object": "model", + "created": 1692634301, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-16k-0613", + "object": "model", + "created": 1685474247, + "owned_by": "openai" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712601677, + "owned_by": "system" + }, + { + "id": "tts-1-1106", + "object": "model", + "created": 1699053241, + "owned_by": "system" + }, + { + "id": "gpt-3.5-turbo-0125", + "object": "model", + "created": 1706048358, + "owned_by": "system" + }, + { + "id": "gpt-4-turbo", + "object": "model", + "created": 1712361441, + "owned_by": "system" + }, + { + "id": "tts-1", + "object": "model", + "created": 1681940951, + "owned_by": "openai-internal" + }, + { + "id": "gpt-3.5-turbo-1106", + "object": "model", + "created": 1698959748, + "owned_by": "system" + }, + { + "id": "gpt-4-0613", + "object": "model", + "created": 1686588896, + "owned_by": "openai" + }, + { + "id": "gpt-3.5-turbo-0301", + "object": "model", + "created": 1677649963, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687882411, + "owned_by": "openai" + } + ] + } + } +} \ No newline at end of file