diff --git a/.gitignore b/.gitignore index cf56f6bd31..8cae5022ff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ bld/ # Visual Studio cache/options directory .vs/ .vscode/ +*.lscache # Uncomment if you have tasks that create the project's static files in wwwroot **/wwwroot/lib/ diff --git a/applications/Unity.AutoUI/.gitignore b/applications/Unity.AutoUI/.gitignore index 2a269a8917..2b2e1fdfca 100644 --- a/applications/Unity.AutoUI/.gitignore +++ b/applications/Unity.AutoUI/.gitignore @@ -10,6 +10,9 @@ cypress/videos cypress/screenshots cypress/report +cypress/scripts/last-submission-id.json +cypress/scripts/bulk-submission-ids.json + # Optional npm cache directory .npm @@ -33,38 +36,3 @@ pids *.seed *.pid.lock -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -.vs/ -.vscode/ - -# User-specific files -*.suo -*.user -*.rsuser -*.userosscache -*.sln.docstates - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - diff --git a/applications/Unity.AutoUI/package-lock.json b/applications/Unity.AutoUI/package-lock.json index badb1ccea8..2aa71448bb 100644 --- a/applications/Unity.AutoUI/package-lock.json +++ b/applications/Unity.AutoUI/package-lock.json @@ -4,18 +4,20 @@ "requires": true, "packages": { "": { + "name": "Unity.AutoUI", "dependencies": { "form-data": "^4.0.5", "typescript": "^5.8.3" }, "devDependencies": { - "cypress": "^15.8.1" + "@types/node": "^25.4.0", + "cypress": "^15.17.0" } }, "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-4.0.1.tgz", + "integrity": "sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -32,14 +34,13 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.14.1", + "qs": "^6.15.2", "safe-buffer": "^5.1.2", "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" + "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">= 6" + "node": ">= 14.17.0" } }, "node_modules/@cypress/xvfb": { @@ -64,14 +65,13 @@ } }, "node_modules/@types/node": { - "version": "25.0.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", - "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -95,65 +95,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" + "environment": "^1.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -226,16 +178,6 @@ "node": ">=0.8" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -339,16 +281,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -442,27 +374,20 @@ "node": ">=8" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-table3": { @@ -482,22 +407,68 @@ } }, "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -591,14 +562,14 @@ } }, "node_modules/cypress": { - "version": "15.9.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.9.0.tgz", - "integrity": "sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==", + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.17.0.tgz", + "integrity": "sha512-WL5Gcqi1GaDWozBwXmkSAtOPafTsVSRS764iX6xvuz3DPzvBAxbkRyEi4BreVdVWxLDpiYRgZCyJUafBw44njw==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.10", + "@cypress/request": "^4.0.0", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -607,25 +578,21 @@ "blob-util": "^2.0.2", "bluebird": "^3.7.2", "buffer": "^5.7.1", - "cachedir": "^2.3.0", + "cachedir": "^2.4.0", "chalk": "^4.1.0", "ci-info": "^4.1.0", - "cli-cursor": "^3.1.0", "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", "debug": "^4.3.4", - "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", "fs-extra": "^9.1.0", "hasha": "5.2.2", "is-installed-globally": "~0.4.0", - "listr2": "^3.8.3", + "listr2": "^9.0.5", "lodash": "^4.17.23", "log-symbols": "^4.0.0", "minimist": "^1.2.8", @@ -635,11 +602,12 @@ "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "supports-color": "^8.1.1", - "systeminformation": "^5.27.14", + "systeminformation": "^5.31.1", "tmp": "~0.2.4", "tree-kill": "1.2.2", + "tslib": "1.14.1", "untildify": "^4.0.0", - "yauzl": "^2.10.0" + "yauzl": "^3.3.1" }, "bin": { "cypress": "bin/cypress" @@ -648,6 +616,13 @@ "node": "^20.1.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/cypress/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -737,19 +712,17 @@ "once": "^1.4.0" } }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/es-define-property": { @@ -797,16 +770,6 @@ "node": ">= 0.4" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -814,6 +777,13 @@ "dev": true, "license": "MIT" }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -858,27 +828,6 @@ "dev": true, "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -889,32 +838,6 @@ ], "license": "MIT" }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -926,16 +849,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -966,6 +889,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1119,9 +1055,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1176,16 +1112,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ini": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", @@ -1331,37 +1257,27 @@ } }, "node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } + "node": ">=20.0.0" } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -1390,55 +1306,98 @@ } }, "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/math-intrinsics": { @@ -1488,6 +1447,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1564,22 +1536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -1656,9 +1612,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1682,17 +1638,49 @@ } }, "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rfdc": { @@ -1702,16 +1690,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1764,15 +1742,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -1784,14 +1762,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1847,18 +1825,49 @@ "license": "ISC" }, "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/sshpk": { @@ -1942,9 +1951,9 @@ } }, "node_modules/systeminformation": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.5.tgz", - "integrity": "sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A==", + "version": "5.31.7", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.7.tgz", + "integrity": "sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==", "dev": true, "license": "MIT", "os": [ @@ -1978,13 +1987,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -2006,9 +2008,9 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -2038,13 +2040,6 @@ "tree-kill": "cli.js" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2089,12 +2084,11 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", @@ -2116,16 +2110,6 @@ "node": ">=8" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -2158,23 +2142,90 @@ } }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2183,14 +2234,16 @@ "license": "ISC" }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" } } } diff --git a/applications/Unity.AutoUI/package.json b/applications/Unity.AutoUI/package.json index d275c5ea12..cbdade5aeb 100644 --- a/applications/Unity.AutoUI/package.json +++ b/applications/Unity.AutoUI/package.json @@ -15,6 +15,6 @@ }, "devDependencies": { "@types/node": "^25.4.0", - "cypress": "15.12.0" + "cypress": "^15.17.0" } -} \ No newline at end of file +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/AIApplicationPromptDataDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/AIApplicationPromptDataDto.cs new file mode 100644 index 0000000000..d54d2b7897 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/AIApplicationPromptDataDto.cs @@ -0,0 +1,32 @@ +using System; + +namespace Unity.AI.Operations; + +public class AIApplicationPromptDataDto +{ + public Guid ApplicationId { get; set; } + + public Guid ApplicationFormId { get; set; } + + public string ProjectName { get; set; } = string.Empty; + + public string ReferenceNo { get; set; } = string.Empty; + + public decimal RequestedAmount { get; set; } + + public decimal TotalProjectBudget { get; set; } + + public string? EconomicRegion { get; set; } + + public string? City { get; set; } + + public DateTime SubmissionDate { get; set; } + + public string? ProjectSummary { get; set; } + + public DateTime? ProjectStartDate { get; set; } + + public DateTime? ProjectEndDate { get; set; } + + public string? Community { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs new file mode 100644 index 0000000000..17deda9348 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Unity.AI.Models; + +namespace Unity.AI.Operations +{ + public class ApplicationAnalysisOperationInputDto + { + [JsonPropertyName("applicationId")] + public Guid ApplicationId { get; set; } + + [JsonPropertyName("schema")] + public JsonElement Schema { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } = new(); + + [JsonPropertyName("promptVersion")] + public string? PromptVersion { get; set; } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs new file mode 100644 index 0000000000..d2c5946dcf --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Unity.AI.Models; + +namespace Unity.AI.Operations +{ + public class ApplicationScoringOperationInputDto + { + [JsonPropertyName("applicationId")] + public Guid ApplicationId { get; set; } + + [JsonPropertyName("data")] + public JsonElement Data { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } = new(); + + [JsonPropertyName("sections")] + public List Sections { get; set; } = new(); + + [JsonPropertyName("promptVersion")] + public string? PromptVersion { get; set; } + } + + public class ApplicationScoringSectionOperationInputDto + { + [JsonPropertyName("sectionName")] + public string SectionName { get; set; } = string.Empty; + + [JsonPropertyName("sectionSchema")] + public JsonElement SectionSchema { get; set; } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputBuilder.cs new file mode 100644 index 0000000000..6236bffbdd --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputBuilder.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Operations; + +public interface IAIApplicationInputBuilder +{ + Task BuildApplicationAnalysisInputAsync(AIApplicationPromptDataDto application, string? promptVersion); + Task BuildApplicationScoringInputAsync(AIApplicationPromptDataDto application, string? promptVersion); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs new file mode 100644 index 0000000000..67df324548 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Operations; + +public interface IAIGenerationPrerequisiteValidator +{ + Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId); + + Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId); + + Task EnsureApplicationScoringAvailableAsync(Guid applicationId); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs deleted file mode 100644 index e82a6f7669..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.Attachments; - -public interface IAttachmentSummaryAppService : IApplicationService -{ - Task> GenerateAttachmentSummariesForPipelineAsync(List attachmentIds, string? promptVersion = null); -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/GenerateAttachmentSummariesInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/GenerateAttachmentSummariesInputDto.cs new file mode 100644 index 0000000000..82956d14de --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/GenerateAttachmentSummariesInputDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Unity.AI.Generation; + +public class GenerateAttachmentSummariesInputDto +{ + public Guid ApplicationId { get; set; } + + public List AttachmentIds { get; set; } = []; + + public string? PromptVersion { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs new file mode 100644 index 0000000000..580d7dc09a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Unity.GrantManager.Attachments; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Generation; + +public interface IAIGenerationAppService : IApplicationService +{ + Task> GenerateAttachmentSummariesAsync(GenerateAttachmentSummariesInputDto input); + + Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + + Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); + + Task GenerateContentAsync(Guid applicationId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs deleted file mode 100644 index 9796792385..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications -{ - public interface IApplicationAnalysisAppService : IApplicationService - { - Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); - Task GenerateApplicationAnalysisForPipelineAsync(Guid applicationId, string? promptVersion = null); - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs deleted file mode 100644 index 1ce45e8a99..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationContentAppService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications; - -public interface IApplicationContentAppService : IApplicationService -{ - Task GenerateContentAsync(Guid applicationId, string? promptVersion = null); -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs deleted file mode 100644 index 158f04b607..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications -{ - public interface IApplicationScoringAppService : IApplicationService - { - Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); - Task GenerateApplicationScoringForPipelineAsync(Guid applicationId, string? promptVersion = null); - } -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs index 6c0a363c86..e555345b1c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissionDefinitionProvider.cs @@ -16,10 +16,15 @@ public override void Define(IPermissionDefinitionContext context) L("Permission:AI")); - aiPermissionsGroup.AddPermission( + var aiReporting = aiPermissionsGroup.AddPermission( AIPermissions.Reporting.ReportingDefault, L("Permission:AI.Reporting")) - .RequireFeatures("Unity.AIReporting"); + .RequireFeatures("Unity.AIReporting"); + + aiReporting.AddChild( + AIPermissions.Reporting.CreateEditDataModel, + L("Permission:AI.Reporting.CreateEditDataModel")) + .RequireFeatures("Unity.AIReporting"); var viewApplicationAnalysis = aiPermissionsGroup.AddPermission( AIPermissions.Analysis.ViewApplicationAnalysis, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs index b9d561deca..b9ea59607f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Permissions/AIPermissions.cs @@ -9,6 +9,7 @@ public static class AIPermissions public static class Reporting { public const string ReportingDefault = GroupName + ".Reporting"; + public const string CreateEditDataModel = GroupName + ".Reporting.CreateEditDataModel"; } public static class Analysis diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs new file mode 100644 index 0000000000..b37897ef6b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs @@ -0,0 +1,161 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Unity.AI.Models; +using Unity.AI.Prompts; +using Unity.Flex.Domain.Scoresheets; +using Unity.GrantManager.Applications; +using Volo.Abp; +using Volo.Abp.DependencyInjection; + +namespace Unity.AI.Operations; + +public class AIApplicationInputBuilder( + IApplicationFormRepository applicationFormRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationFormVersionRepository applicationFormVersionRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + IScoresheetRepository scoresheetRepository, + ILogger logger) : IAIApplicationInputBuilder, ITransientDependency +{ + public async Task BuildApplicationAnalysisInputAsync(AIApplicationPromptDataDto application, string? promptVersion) + { + var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(application.ApplicationId); + var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == application.ApplicationId); + var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); + + return new ApplicationAnalysisOperationInputDto + { + ApplicationId = application.ApplicationId, + Schema = JsonSerializer.SerializeToElement(PromptDataPayloadBuilder.BuildFormFieldConfiguration(formSchema, logger)), + Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission?.Submission, formSchema, logger), + Attachments = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments), + PromptVersion = promptVersion + }; + } + + public async Task BuildApplicationScoringInputAsync(AIApplicationPromptDataDto application, string? promptVersion) + { + var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); + if (applicationForm.ScoresheetId == null) + { + throw new UserFriendlyException("Scoring requires a configured scoresheet."); + } + + var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); + if (scoresheet == null || !scoresheet.Sections.Any() || !scoresheet.Sections.SelectMany(s => s.Fields).Any()) + { + throw new UserFriendlyException("Scoring requires a scoresheet with fields."); + } + + var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == application.ApplicationId); + var attachmentSummaries = PromptDataPayloadBuilder.BuildAttachmentSummaries(attachments); + + var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(application.ApplicationId); + var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); + var promptData = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission?.Submission, formSchema, logger); + + var sections = scoresheet.Sections + .OrderBy(s => s.Order) + .Select(section => new ApplicationScoringSectionOperationInputDto + { + SectionName = section.Name, + SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section)) + }) + .ToList(); + + return new ApplicationScoringOperationInputDto + { + ApplicationId = application.ApplicationId, + Data = promptData, + Attachments = attachmentSummaries, + Sections = sections, + PromptVersion = promptVersion + }; + } + + private async Task GetFormSchemaAsync(Guid? formVersionId) + { + if (formVersionId == null) + { + return null; + } + + try + { + var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); + return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Unable to load form schema for AI input generation for form version {FormVersionId}.", formVersionId); + return null; + } + } + + private static List BuildSectionQuestionsData(ScoresheetSection section) + { + var sectionQuestionsData = new List(); + foreach (var field in section.Fields.OrderBy(f => f.Order)) + { + var options = ExtractSelectListOptions(field); + sectionQuestionsData.Add(new + { + id = field.Id.ToString(), + section = section.Name, + question = field.Label, + description = field.Description, + type = field.Type.ToString(), + options, + allowed_answers = ExtractSelectListOptionNumbers(options) + }); + } + + return sectionQuestionsData; + } + + private static object[]? ExtractSelectListOptions(Question field) + { + if (field.Type != Unity.Flex.Scoresheets.Enums.QuestionType.SelectList || string.IsNullOrEmpty(field.Definition)) + { + return null; + } + + try + { + var definition = JsonSerializer.Deserialize(field.Definition); + if (definition?.Options != null && definition.Options.Count > 0) + { + return definition.Options + .Select((option, index) => + (object)new + { + number = index + 1, + value = option.Value + }) + .ToArray(); + } + } + catch (JsonException) + { + // Ignore malformed definition and return null options. + } + + return null; + } + + private static string[]? ExtractSelectListOptionNumbers(object[]? options) + { + if (options == null || options.Length == 0) + { + return null; + } + + return options + .Select((_, index) => (index + 1).ToString()) + .ToArray(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs new file mode 100644 index 0000000000..50b4217195 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Linq; +using System.Threading.Tasks; +using Unity.Flex.Domain.Scoresheets; +using Unity.AI.Localization; +using Unity.GrantManager.Applications; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Linq; + +namespace Unity.AI.Operations; + +public class AIGenerationPrerequisiteValidator( + IApplicationRepository applicationRepository, + IApplicationFormRepository applicationFormRepository, + IApplicationFormSubmissionRepository applicationFormSubmissionRepository, + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + IScoresheetRepository scoresheetRepository, + IAsyncQueryableExecuter asyncExecuter, + IStringLocalizer localizer) : IAIGenerationPrerequisiteValidator, ITransientDependency +{ + public async Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId) + { + var attachmentQuery = await applicationChefsFileAttachmentRepository.GetQueryableAsync(); + var hasAttachments = await asyncExecuter.AnyAsync(attachmentQuery.Where(a => a.ApplicationId == applicationId)); + if (!hasAttachments) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.NoAttachmentsAvailable]); + } + } + + public async Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId) + { + var submission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); + if (submission == null || string.IsNullOrWhiteSpace(submission.Submission)) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ApplicationAnalysisRequiresSubmission]); + } + } + + public async Task EnsureApplicationScoringAvailableAsync(Guid applicationId) + { + var application = await applicationRepository.GetAsync(applicationId); + var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); + if (applicationForm.ScoresheetId == null) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheet]); + } + + var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); + if (scoresheet == null || !scoresheet.Sections.Any() || !scoresheet.Sections.SelectMany(s => s.Fields).Any()) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.ScoringRequiresScoresheetFields]); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs index 12b9b24342..b2ce118946 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs @@ -1,198 +1,33 @@ -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Unity.AI; using Unity.AI.Models; -using Unity.AI.Prompts; using Unity.AI.Requests; using Unity.AI.Runtime; -using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations { public class ApplicationAnalysisService( - IApplicationRepository applicationRepository, - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IApplicationFormVersionRepository applicationFormVersionRepository, - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IAIService aiService, - ILogger logger) : IApplicationAnalysisService, ITransientDependency + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator) : IApplicationAnalysisService, ITransientDependency { - private const string ComponentsKey = "components"; - private static readonly HashSet ExcludedSchemaKeys = new(StringComparer.OrdinalIgnoreCase) + public async Task RegenerateAsync(ApplicationAnalysisOperationInputDto input, CancellationToken cancellationToken = default) { - "applicantAgent" - }; - - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) - { - var application = await applicationRepository.GetAsync(applicationId); - var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); - var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); - var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); - - var attachmentSummaries = attachments - .Where(a => !string.IsNullOrWhiteSpace(a.AISummary)) - .Select(a => new AIAttachmentItem - { - Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), - Summary = a.AISummary!.Trim() - }) - .ToList(); - - object formFieldConfiguration = new { message = "Form configuration not available." }; - if (formSubmission?.ApplicationFormVersionId != null) - { - formFieldConfiguration = await ExtractFormFieldConfigurationAsync(formSubmission.ApplicationFormVersionId.Value); - } + await aiGenerationPrerequisiteValidator.EnsureApplicationAnalysisAvailableAsync(input.ApplicationId); var analysis = await aiService.GenerateApplicationAnalysisAsync(new ApplicationAnalysisRequest { - Schema = JsonSerializer.SerializeToElement(formFieldConfiguration), - Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), - Attachments = attachmentSummaries, - PromptVersion = promptVersion, + Schema = input.Schema, + Data = input.Data, + Attachments = input.Attachments, + PromptVersion = input.PromptVersion, }, cancellationToken); var analysisJson = JsonSerializer.Serialize(analysis, AIJsonDefaults.Indented); - application.AIAnalysis = analysisJson; - await applicationRepository.UpdateAsync(application); return analysisJson; } - private async Task GetFormSchemaAsync(Guid? formVersionId) - { - if (formVersionId == null) - { - return null; - } - - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); - return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Unable to load form schema for prompt data generation for form version {FormVersionId}.", formVersionId); - return null; - } - } - - private async Task ExtractFormFieldConfigurationAsync(Guid formVersionId) - { - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId); - if (formVersion == null || string.IsNullOrEmpty(formVersion.FormSchema)) - { - return new { message = "Form configuration not available." }; - } - - var schema = JObject.Parse(formVersion.FormSchema); - var components = schema[ComponentsKey] as JArray; - if (components == null || components.Count == 0) - { - return new { message = "No form fields configured." }; - } - - var requiredFields = new List(); - var optionalFields = new List(); - ExtractFieldRequirements(components, requiredFields, optionalFields, string.Empty); - - return new - { - required_fields = requiredFields, - optional_fields = optionalFields - }; - } - catch (Exception ex) - { - logger.LogError(ex, "Error extracting form field configuration for form version {FormVersionId}", formVersionId); - return new { message = "Form configuration could not be extracted." }; - } - } - - private static void ExtractFieldRequirements(JArray components, List requiredFields, List optionalFields, string currentPath) - { - foreach (var component in components.OfType()) - { - var key = component["key"]?.ToString(); - var label = component["label"]?.ToString(); - var type = component["type"]?.ToString(); - var skipTypes = new HashSet { "button", "simplebuttonadvanced", "html", "htmlelement", "content", "simpleseparator" }; - - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(type) || skipTypes.Contains(type) || ExcludedSchemaKeys.Contains(key)) - { - ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, currentPath); - continue; - } - - var displayName = !string.IsNullOrEmpty(label) ? $"{label} ({key})" : key; - var fullPath = string.IsNullOrEmpty(currentPath) ? displayName : $"{currentPath} > {displayName}"; - var validate = component["validate"] as JObject; - var isRequired = validate?["required"]?.Value() ?? false; - - if (component["input"]?.Value() == true) - { - if (isRequired) requiredFields.Add(fullPath); - else optionalFields.Add(fullPath); - } - - ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, fullPath); - } - } - - private static void ProcessNestedFieldRequirements(JObject component, string? type, List requiredFields, List optionalFields, string currentPath) - { - switch (type) - { - case "panel": - case "simplepanel": - case "fieldset": - case "well": - case "container": - case "datagrid": - case "table": - if (component[ComponentsKey] is JArray nestedComponents) - { - ExtractFieldRequirements(nestedComponents, requiredFields, optionalFields, currentPath); - } - break; - case "columns": - case "simplecols2": - case "simplecols3": - case "simplecols4": - if (component["columns"] is JArray columns) - { - foreach (var column in columns.OfType()) - { - if (column[ComponentsKey] is JArray columnComponents) - { - ExtractFieldRequirements(columnComponents, requiredFields, optionalFields, currentPath); - } - } - } - break; - case "tabs": - case "simpletabs": - if (component[ComponentsKey] is JArray tabs) - { - foreach (var tab in tabs.OfType()) - { - if (tab[ComponentsKey] is JArray tabComponents) - { - ExtractFieldRequirements(tabComponents, requiredFields, optionalFields, currentPath); - } - } - } - break; - } - } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs index f6188f689b..ec6bb55e8d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs @@ -5,64 +5,29 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Unity.Flex.Domain.Scoresheets; +using Unity.AI; using Unity.AI.Models; -using Unity.AI.Prompts; using Unity.AI.Requests; using Unity.AI.Runtime; -using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations { public class ApplicationScoringService( - IApplicationRepository applicationRepository, - IApplicationFormRepository applicationFormRepository, - IApplicationFormSubmissionRepository applicationFormSubmissionRepository, - IApplicationFormVersionRepository applicationFormVersionRepository, - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, - IScoresheetRepository scoresheetRepository, IAIService aiService, AIExecutionModeResolver executionModeResolver, ILogger logger) : IApplicationScoringService, ITransientDependency { - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default) + public async Task RegenerateAsync(ApplicationScoringOperationInputDto input, CancellationToken cancellationToken = default) { - var application = await applicationRepository.GetAsync(applicationId); - var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); - if (applicationForm.ScoresheetId == null) - { - return "{}"; - } - - var scoresheet = await scoresheetRepository.GetWithChildrenAsync(applicationForm.ScoresheetId.Value); - if (scoresheet == null) - { - return "{}"; - } - - var attachments = await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId); - var attachmentSummaries = attachments - .Where(a => !string.IsNullOrEmpty(a.AISummary)) - .Select(a => new AIAttachmentItem - { - Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), - Summary = a.AISummary!.Trim() - }) - .ToList(); - - var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); - var formSchema = await GetFormSchemaAsync(formSubmission?.ApplicationFormVersionId); - var promptData = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger); - - var sections = scoresheet.Sections.OrderBy(s => s.Order).ToList(); + var sections = input.Sections; var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.ApplicationScoringOperation); var perSectionResults = await AIExecutionStrategy.RunAsync( sections, mode, - section => ProcessSectionAsync(applicationId, section, promptData, attachmentSummaries, promptVersion, cancellationToken), - batch => ProcessSectionsAsync(applicationId, batch, promptData, attachmentSummaries, promptVersion, cancellationToken)); + section => ProcessSectionAsync(input.ApplicationId, section, input.Data, input.Attachments, input.PromptVersion, cancellationToken), + batch => ProcessSectionsAsync(input.ApplicationId, batch, input.Data, input.Attachments, input.PromptVersion, cancellationToken)); var allSectionResults = new Dictionary(); foreach (var sectionResult in perSectionResults) @@ -75,14 +40,12 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro var combinedResults = JsonSerializer.Serialize(allSectionResults, AIJsonDefaults.Indented); var validatedJson = ValidateApplicationScoringJson(combinedResults); - application.AIScoresheetAnswers = validatedJson; - await applicationRepository.UpdateAsync(application); return validatedJson; } private async Task> ProcessSectionAsync( Guid applicationId, - ScoresheetSection section, + ApplicationScoringSectionOperationInputDto section, JsonElement promptData, List attachmentSummaries, string? promptVersion, @@ -95,8 +58,8 @@ private async Task> ProcessSectionAsync( { Data = promptData, Attachments = attachmentSummaries, - SectionName = section.Name, - SectionSchema = JsonSerializer.SerializeToElement(BuildSectionQuestionsData(section), AIJsonDefaults.IndentedCamelCase), + SectionName = section.SectionName, + SectionSchema = section.SectionSchema, PromptVersion = promptVersion, }; var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); @@ -112,14 +75,15 @@ private async Task> ProcessSectionAsync( } catch (Exception ex) { - logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.Name, applicationId); + logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.SectionName, applicationId); } + return sectionResults; } private async Task>> ProcessSectionsAsync( Guid applicationId, - IReadOnlyCollection sections, + IReadOnlyCollection sections, JsonElement promptData, List attachmentSummaries, string? promptVersion, @@ -128,17 +92,14 @@ private async Task>> ProcessSectionsAsync( var sectionResults = new Dictionary(); try { - var questions = sections - .OrderBy(s => s.Order) - .SelectMany(BuildSectionQuestionsData) - .ToList(); + var questions = BuildBatchSectionSchema(sections); var applicationScoringRequest = new ApplicationScoringRequest { Data = promptData, Attachments = attachmentSummaries, SectionName = "All Sections", - SectionSchema = JsonSerializer.SerializeToElement(questions, AIJsonDefaults.IndentedCamelCase), + SectionSchema = questions, PromptVersion = promptVersion, }; var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest, cancellationToken); @@ -160,25 +121,24 @@ private async Task>> ProcessSectionsAsync( return [sectionResults]; } - private static List BuildSectionQuestionsData(ScoresheetSection section) + private static JsonElement BuildBatchSectionSchema(IReadOnlyCollection sections) { - var sectionQuestionsData = new List(); - foreach (var field in section.Fields.OrderBy(f => f.Order)) + var questions = new List(); + foreach (var section in sections) { - var options = ExtractSelectListOptions(field); - sectionQuestionsData.Add(new + if (section.SectionSchema.ValueKind != JsonValueKind.Array) { - id = field.Id.ToString(), - section = section.Name, - question = field.Label, - description = field.Description, - type = field.Type.ToString(), - options, - allowed_answers = ExtractSelectListOptionNumbers(options) - }); + throw new InvalidOperationException( + $"Section schema for '{section.SectionName}' must be a JSON array."); + } + + foreach (var question in section.SectionSchema.EnumerateArray()) + { + questions.Add(question.Clone()); + } } - return sectionQuestionsData; + return JsonSerializer.SerializeToElement(questions, AIJsonDefaults.IndentedCamelCase); } private void CopyAnswers(Dictionary answers, Dictionary results) @@ -191,25 +151,6 @@ private void CopyAnswers(Dictionary answers, D } } - private async Task GetFormSchemaAsync(Guid? formVersionId) - { - if (formVersionId == null) - { - return null; - } - - try - { - var formVersion = await applicationFormVersionRepository.GetAsync(formVersionId.Value); - return string.IsNullOrWhiteSpace(formVersion?.FormSchema) ? null : formVersion.FormSchema; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Unable to load form schema for application scoring prompt data generation for form version {FormVersionId}.", formVersionId); - return null; - } - } - private static string ValidateApplicationScoringJson(string scoresheetAnswers) { try @@ -227,45 +168,5 @@ private static string ValidateApplicationScoringJson(string scoresheetAnswers) return "{}"; } - - private static object[]? ExtractSelectListOptions(Question field) - { - if (field.Type != Unity.Flex.Scoresheets.Enums.QuestionType.SelectList || string.IsNullOrEmpty(field.Definition)) - return null; - - try - { - var definition = JsonSerializer.Deserialize(field.Definition); - if (definition?.Options != null && definition.Options.Count > 0) - { - return definition.Options - .Select((option, index) => - (object)new - { - number = index + 1, - value = option.Value - }) - .ToArray(); - } - } - catch (JsonException) - { - // Ignore malformed definition and return null options. - } - - return null; - } - - private static string[]? ExtractSelectListOptionNumbers(object[]? options) - { - if (options == null || options.Length == 0) - { - return null; - } - - return options - .Select((_, index) => (index + 1).ToString()) - .ToArray(); - } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs index 78762cea48..7b3a2e562f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs @@ -1,14 +1,19 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Unity.AI.Extraction; +using Unity.AI.Localization; using Unity.AI.Requests; using Unity.GrantManager.Applications; using Unity.GrantManager.Intakes; +using Volo.Abp; using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; namespace Unity.AI.Operations; @@ -17,18 +22,28 @@ public class AttachmentSummaryService( IChefsFileAttachmentStreamProvider chefsFileAttachmentStreamProvider, ITextExtractionService textExtractionService, IAIService aiService, + IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, AIExecutionModeResolver executionModeResolver, - ILogger logger) : IAttachmentSummaryService, ITransientDependency + IUnitOfWorkManager unitOfWorkManager, + ILogger logger, + IStringLocalizer localizer) : IAttachmentSummaryService, ITransientDependency { private const string SummaryGenerationFailedMessage = "AI summary generation failed."; + private const string TextExtractionFailedSummary = "Attachment text could not be extracted for AI summary generation."; public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null, CancellationToken cancellationToken = default) { - var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var attachment = await LoadAttachmentAsync(attachmentId); var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; await using var attachmentStream = await OpenAttachmentStreamAsync(attachment, fileName, cancellationToken); var extractedText = await textExtractionService.ExtractTextAsync(fileName, attachmentStream.Content, attachmentStream.ContentType, cancellationToken); + if (ShouldStopOnEmptyExtraction(fileName, extractedText)) + { + LogEmptyExtraction(attachmentId, fileName, attachmentStream); + await SaveSummaryAsync(attachmentId, TextExtractionFailedSummary); + return TextExtractionFailedSummary; + } var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest { @@ -38,8 +53,7 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt PromptVersion = promptVersion, }, cancellationToken); - attachment.AISummary = summaryResponse.Summary; - await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + await SaveSummaryAsync(attachmentId, summaryResponse.Summary); return summaryResponse.Summary; } @@ -47,6 +61,11 @@ public async Task GenerateAndSaveAsync(Guid attachmentId, string? prompt public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null, CancellationToken cancellationToken = default) { var ids = attachmentIds as IReadOnlyCollection ?? attachmentIds.ToList(); + if (ids.Count == 0) + { + throw new UserFriendlyException(localizer[AILocalizationKeys.SelectAttachmentForSummaries]); + } + var mode = executionModeResolver.ResolveMode(AIExecutionModeResolver.AttachmentSummaryOperation); if (mode != AIExecutionMode.Sequential) { @@ -103,9 +122,9 @@ public async Task> GenerateForApplicationAsync( IReadOnlyCollection? attachmentIds = null, CancellationToken cancellationToken = default) { - var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) - .Select(a => a.Id) - .ToList(); + await WithUnitOfWorkAsync(() => aiGenerationPrerequisiteValidator.EnsureAttachmentSummaryAvailableAsync(applicationId)); + + var applicationAttachmentIds = await LoadApplicationAttachmentIdsAsync(applicationId); if (attachmentIds is not { Count: > 0 }) { @@ -123,8 +142,47 @@ public async Task> GenerateForApplicationAsync( return await GenerateAndSaveAsync(selectedIds, promptVersion, cancellationToken); } + private async Task LoadAttachmentAsync(Guid attachmentId) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var source = new AttachmentSummarySource( + attachment.Id, + attachment.FileName, + attachment.ChefsSubmissionId, + attachment.ChefsFileId); + await uow.CompleteAsync(); + return source; + } + + private async Task SaveSummaryAsync(Guid attachmentId, string summary) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true); + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + attachment.AISummary = summary; + await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + await uow.CompleteAsync(); + } + + private async Task> LoadApplicationAttachmentIdsAsync(Guid applicationId) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + var ids = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) + .Select(a => a.Id) + .ToList(); + await uow.CompleteAsync(); + return ids; + } + + private async Task WithUnitOfWorkAsync(Func operation) + { + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + await operation(); + await uow.CompleteAsync(); + } + private async Task OpenAttachmentStreamAsync( - ApplicationChefsFileAttachment attachment, + AttachmentSummarySource attachment, string fileName, CancellationToken cancellationToken) { @@ -156,4 +214,52 @@ private async Task OpenAttachmentStreamAsync( return ChefsFileAttachmentStream.Empty; } } + + private static bool ShouldStopOnEmptyExtraction(string fileName, string extractedText) + { + return string.IsNullOrWhiteSpace(extractedText) && IsSupportedOfficeOrPdf(fileName); + } + + private static bool IsSupportedOfficeOrPdf(string fileName) + { + var extension = Path.GetExtension(fileName)?.ToLowerInvariant(); + return extension is ".pdf" or ".docx" or ".xlsx" or ".xls" or ".pptx"; + } + + private void LogEmptyExtraction( + Guid attachmentId, + string fileName, + ChefsFileAttachmentStream attachmentStream) + { + logger.LogWarning( + "No text extracted for supported attachment {AttachmentId} ({FileName}). Skipping AI summary generation. ContentType: {ContentType}; StreamCanSeek: {StreamCanSeek}; StreamLength: {StreamLength}.", + attachmentId, + fileName, + attachmentStream.ContentType, + attachmentStream.Content.CanSeek, + TryGetStreamLength(attachmentStream.Content)); + } + + private static long? TryGetStreamLength(Stream stream) + { + if (!stream.CanSeek) + { + return null; + } + + try + { + return stream.Length; + } + catch + { + return null; + } + } + + private sealed record AttachmentSummarySource( + Guid Id, + string? FileName, + string? ChefsSubmissionId, + string? ChefsFileId); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs index ac02842761..ddff668f03 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs @@ -6,6 +6,6 @@ namespace Unity.AI.Operations { public interface IApplicationAnalysisService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); + Task RegenerateAsync(ApplicationAnalysisOperationInputDto input, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs index 2b2bc1796f..a7890b76a3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs @@ -6,6 +6,6 @@ namespace Unity.AI.Operations { public interface IApplicationScoringService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default); + Task RegenerateAsync(ApplicationScoringOperationInputDto input, CancellationToken cancellationToken = default); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs index 315a26fc92..fbcd721bf5 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Unity.AI.Operations; +using Unity.AI.Models; using Unity.GrantManager.Applications; namespace Unity.AI.Prompts @@ -33,14 +35,31 @@ internal static class PromptDataPayloadBuilder "simpleseparator" }; + private static readonly HashSet NonFieldRequirementComponentTypes = new(StringComparer.OrdinalIgnoreCase) + { + "button", + "simplebuttonadvanced", + "html", + "htmlelement", + "content", + "simpleseparator" + }; + + private const string ComponentsKey = "components"; + + private static readonly HashSet ExcludedSchemaKeys = new(StringComparer.OrdinalIgnoreCase) + { + "applicantAgent" + }; + public static JsonElement BuildPromptDataPayload( - Application application, - ApplicationFormSubmission? formSubmission, + AIApplicationPromptDataDto application, + string? submissionJson, string? formSchema, ILogger logger) { var fallbackPayload = BuildFallbackPromptDataPayload(application); - if (TryBuildPromptDataValues(formSubmission?.Submission, formSchema, out var values, out var exception)) + if (TryBuildPromptDataValues(submissionJson, formSchema, out var values, out var exception)) { return JsonSerializer.SerializeToElement(values); } @@ -50,13 +69,64 @@ public static JsonElement BuildPromptDataPayload( logger.LogWarning( exception, "Failed to parse form submission JSON for prompt payload generation for application {ApplicationId}.", - application.Id); + application.ApplicationId); } return JsonSerializer.SerializeToElement(fallbackPayload); } - private static object BuildFallbackPromptDataPayload(Application application) + public static List BuildAttachmentSummaries( + IEnumerable attachments, + bool excludeWhitespaceOnlySummaries = true) + { + return attachments + .Where(a => excludeWhitespaceOnlySummaries + ? !string.IsNullOrWhiteSpace(a.AISummary) + : !string.IsNullOrEmpty(a.AISummary)) + .Select(a => new AIAttachmentItem + { + Name = string.IsNullOrWhiteSpace(a.FileName) ? "attachment" : a.FileName.Trim(), + Summary = a.AISummary!.Trim() + }) + .ToList(); + } + + public static object BuildFormFieldConfiguration( + string? formSchema, + ILogger logger) + { + if (string.IsNullOrWhiteSpace(formSchema)) + { + return new { message = "Form configuration not available." }; + } + + try + { + var schema = JObject.Parse(formSchema); + var components = schema[ComponentsKey] as JArray; + if (components == null || components.Count == 0) + { + return new { message = "No form fields configured." }; + } + + var requiredFields = new List(); + var optionalFields = new List(); + ExtractFieldRequirements(components, requiredFields, optionalFields, string.Empty); + + return new + { + required_fields = requiredFields, + optional_fields = optionalFields + }; + } + catch (Exception ex) + { + logger.LogError(ex, "Error extracting form field configuration from form schema."); + return new { message = "Form configuration could not be extracted." }; + } + } + + private static object BuildFallbackPromptDataPayload(AIApplicationPromptDataDto application) { var notSpecified = "Not specified"; return new @@ -164,7 +234,7 @@ private static HashSet ExtractAllowedSchemaKeys(string? formSchema) try { var schema = JObject.Parse(formSchema); - if (schema["components"] is not JArray components) + if (schema[ComponentsKey] is not JArray components) { return new HashSet(StringComparer.OrdinalIgnoreCase); } @@ -217,5 +287,81 @@ private static void ProcessNestedSchemaComponents(JObject component, HashSet requiredFields, List optionalFields, string currentPath) + { + foreach (var component in components.OfType()) + { + var key = component["key"]?.ToString(); + var label = component["label"]?.ToString(); + var type = component["type"]?.ToString(); + + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(type) || NonFieldRequirementComponentTypes.Contains(type) || ExcludedSchemaKeys.Contains(key)) + { + ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, currentPath); + continue; + } + + var displayName = !string.IsNullOrEmpty(label) ? $"{label} ({key})" : key; + var fullPath = string.IsNullOrEmpty(currentPath) ? displayName : $"{currentPath} > {displayName}"; + var validate = component["validate"] as JObject; + var isRequired = validate?["required"]?.Value() ?? false; + + if (component["input"]?.Value() == true) + { + if (isRequired) requiredFields.Add(fullPath); + else optionalFields.Add(fullPath); + } + + ProcessNestedFieldRequirements(component, type, requiredFields, optionalFields, fullPath); + } + } + + private static void ProcessNestedFieldRequirements(JObject component, string? type, List requiredFields, List optionalFields, string currentPath) + { + switch (type) + { + case "panel": + case "simplepanel": + case "fieldset": + case "well": + case "container": + case "datagrid": + case "table": + if (component[ComponentsKey] is JArray nestedComponents) + { + ExtractFieldRequirements(nestedComponents, requiredFields, optionalFields, currentPath); + } + break; + case "columns": + case "simplecols2": + case "simplecols3": + case "simplecols4": + if (component["columns"] is JArray columns) + { + foreach (var column in columns.OfType()) + { + if (column[ComponentsKey] is JArray columnComponents) + { + ExtractFieldRequirements(columnComponents, requiredFields, optionalFields, currentPath); + } + } + } + break; + case "tabs": + case "simpletabs": + if (component[ComponentsKey] is JArray tabs) + { + foreach (var tab in tabs.OfType()) + { + if (tab[ComponentsKey] is JArray tabComponents) + { + ExtractFieldRequirements(tabComponents, requiredFields, optionalFields, currentPath); + } + } + } + break; + } + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md index 21255c7dff..0f6146d2ab 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/README.md @@ -1,11 +1,9 @@ # Runtime Prompt Templates -These files are the source of truth for runtime prompts. -`OpenAIRuntimeService` resolves templates from: +Runtime prompts are now resolved from the database-backed `AIPrompts` and `AIPromptVersions` records seeded by the AI module. +These files are retained as prompt asset references and seed inputs, not as the runtime source of truth. -- `AI/Prompts/Versions//