diff --git a/AIStatsMeasurement.Web/package-lock.json b/AIStatsMeasurement.Web/package-lock.json index c0ef8ac..9d3b3c8 100644 --- a/AIStatsMeasurement.Web/package-lock.json +++ b/AIStatsMeasurement.Web/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "lucide-react": "^0.577.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "recharts": "^3.8.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1010,6 +1012,42 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -1367,6 +1405,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1412,6 +1462,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1440,7 +1553,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1456,6 +1569,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", @@ -1948,6 +2067,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1982,6 +2110,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2001,9 +2142,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2022,6 +2284,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2036,6 +2304,16 @@ "dev": true, "license": "ISC" }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2285,6 +2563,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2463,6 +2747,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2490,6 +2784,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2879,6 +3182,36 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2889,6 +3222,95 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2960,6 +3382,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3019,6 +3447,12 @@ "node": ">=8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3148,6 +3582,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/AIStatsMeasurement.Web/package.json b/AIStatsMeasurement.Web/package.json index 5a0f457..f2ba603 100644 --- a/AIStatsMeasurement.Web/package.json +++ b/AIStatsMeasurement.Web/package.json @@ -12,7 +12,9 @@ "dependencies": { "lucide-react": "^0.577.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.1", + "recharts": "^3.8.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/AIStatsMeasurement.Web/src/Analytics.tsx b/AIStatsMeasurement.Web/src/Analytics.tsx new file mode 100644 index 0000000..49c68ac --- /dev/null +++ b/AIStatsMeasurement.Web/src/Analytics.tsx @@ -0,0 +1,558 @@ +import { useState } from 'react' +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + LineChart, + Line, + CartesianGrid, + RadarChart, + PolarGrid, + PolarAngleAxis, + Radar +} from 'recharts' + +const nsiOptions = ['CBS', 'OECD', 'STATBANK DENMARK'] + +const llmOptions = [ + 'gemini-2.5-flash-lite-preview-09-2025', + 'gpt-4o-mini', + 'grok-4-1-fast-non-reasoning', + 'gemini-3.1-pro-preview', + 'gpt-5.4', + 'grok-4.20-reasoning' +] + +const themeOptions = [ + 'Arbeid en sociale zekerheid', + 'Bedrijven', + 'Bevolking', + 'Bouwen en wonen', + 'Caribisch Nederland', + 'Energie', + 'Financiële en zakelijke diensten', + 'Gezondheid en welzijn', + 'Handel en horeca', + 'Industrie', + 'Inkomen en bestedingen', + 'Internationale handel', + 'Landbouw', + 'Macro-economie', + 'Natuur en milieu', + 'Nederland regionaal', + 'Onderwijs', + 'Overheid', + 'Prijzen', + 'Veiligheid en recht', + 'Verkeer en vervoer', + 'Vrije tijd en cultuur', + 'Agriculture and fisheries', + 'Development', + 'Economy', + 'Education and skills', + 'Environment and climate change', + 'Finance and investment', + 'Public governance', + 'Health', + 'Industry, business and entrepreneurship', + 'Science, technology and innovation', + 'Employment', + 'Society', + 'Regional, rural and urban development', + 'Trade', + 'Transport', + 'Taxation' +] + +const pageTheme = { + background: '#f8fafc', + cardBackground: '#ffffff', + text: '#0f172a', + mutedText: '#475569', + border: '#e2e8f0', + grid: '#cbd5e1', + primary: '#2563eb', + secondary: '#10b981', + accent: '#f59e0b', + radarFill: '#2563eb', + radarStroke: '#1d4ed8', + tooltipBackground: '#ffffff' +} + +type SummaryDto = { + accuracy: number + findability: number + consistency: number + totalmeasurements: number +} + +type BarItemDto = { + name: string + score: number +} + +type TimelineItemDto = { + run: string + accuracy: number + findability: number +} + +type RadarItemDto = { + metric: string + value: number +} + +type AnalyticsResponse = { + accuracyScore: number + findabilityScore: number + consistencyScore: number + totalMeasurements: number + barData?: BarItemDto[] + timelineData?: TimelineItemDto[] + radarData?: RadarItemDto[] +} + +function ScoreCard({ title, value }: { title: string; value: number }) { + return ( +
+
+ {title} +
+ +
+ {value.toFixed(1)} +
+
+ ) +} + +function Analytics() { + const [selectedNsi, setSelectedNsi] = useState('') + const [selectedLlm, setSelectedLlm] = useState('') + const [selectedTheme, setSelectedTheme] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + + const [summary, setSummary] = useState({ + accuracy: 0, + findability: 0, + consistency: 0, + totalmeasurements: 0 + }) + + const [barData, setBarData] = useState([]) + const [timelineData, setTimelineData] = useState([]) + const [radarData, setRadarData] = useState([]) + + const handleSend = async () => { + setIsLoading(true) + setError('') + + try { + const response = await fetch('http://localhost:5201/api/metrics', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nsi: selectedNsi, + llm: selectedLlm, + theme: selectedTheme + }) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Failed to load analytics') + } + + const data: AnalyticsResponse = await response.json() + + setSummary({ + accuracy: data.accuracyScore ?? 0, + findability: data.findabilityScore ?? 0, + consistency: data.consistencyScore ?? 0, + totalmeasurements: data.totalMeasurements ?? 0 + }) + + setBarData( + data.barData ?? [ + { name: selectedLlm || 'Selected LLM', score: data.accuracyScore ?? 0 } + ] + ) + + setTimelineData( + data.timelineData ?? [ + { + run: 'Current', + accuracy: data.accuracyScore ?? 0, + findability: data.findabilityScore ?? 0 + } + ] + ) + + setRadarData( + data.radarData ?? [ + { metric: 'Accuracy score', value: data.accuracyScore ?? 0 }, + { metric: 'Source score', value: data.findabilityScore ?? 0 }, + { metric: 'Consistency score', value: data.consistencyScore ?? 0 } + ] + ) +} catch (err) { + if (err instanceof Error) { + setError(err.message) + } else { + setError('Something went wrong') + } + } finally { + setIsLoading(false) + } + } + + return ( +
+
+

Analytics

+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ +
+ +
+ +
+

Selected filters

+

+ NSI: {selectedNsi || 'None'} +

+

+ LLM: {selectedLlm || 'None'} +

+

+ Theme: {selectedTheme || 'None'} +

+ + {error && ( +

+ Error: {error} +

+ )} +
+ +
+ + + + +
+ +
+
+

+ Score per LLM +

+ + + + + + + + +
+ +
+

+ Timeline +

+ + + + + + + + + +
+ +
+

+ Metrics overview +

+ + + + + + + +
+
+
+
+ ) +} + +export default Analytics \ No newline at end of file diff --git a/AIStatsMeasurement.Web/src/App.css b/AIStatsMeasurement.Web/src/App.css deleted file mode 100644 index 517c62a..0000000 --- a/AIStatsMeasurement.Web/src/App.css +++ /dev/null @@ -1,76 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} - -.app-container { - max-width: 1100px; - margin: 0 auto; - padding: 32px; -} - -.prompt-select { - margin-top: 20px; -} - -.select-input { - width: 100%; - padding: 10px; - margin-top: 8px; - border-radius: 6px; - border: 1px solid #ccc; -} - -.question-preview { - margin-top: 12px; - padding: 10px; - background: #f3f4f6; - border-radius: 6px; -} - -.run-button { - margin-top: 20px; - padding: 10px 16px; - background: #2563eb; - color: white; - border: none; - border-radius: 6px; - cursor: pointer; -} - -.results-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 20px; - margin-top: 20px; -} - -.result-card { - padding: 18px; - border-radius: 10px; - text-align: left; - display: flex; - flex-direction: column; - justify-content: space-between; - min-height: 420px; - overflow-wrap: break-word; - word-break: break-word; -} - -.raw-text { - font-size: 14px; - margin-top: 6px; - line-height: 1.5; - overflow-wrap: break-word; - word-break: break-word; -} diff --git a/AIStatsMeasurement.Web/src/App.tsx b/AIStatsMeasurement.Web/src/App.tsx index 1671960..532c161 100644 --- a/AIStatsMeasurement.Web/src/App.tsx +++ b/AIStatsMeasurement.Web/src/App.tsx @@ -1,250 +1,18 @@ -import { type FormEvent, useState, useEffect } from 'react' -import './App.css' - -type Prompt = { - id: number - theme: string - subject: string - question: string -} - -type ExportRow = { - id: number - theme: string - question: string - expectedAnswer: number - expectedSource: string - actualAnswer: number - actualSource: string[] - provider: string - rawText: string | null - exception: string | null - squareMeanRootError: number - relativeError: number - answerIsCorrect: boolean - sourceIsCorrect: boolean - averageRelativeError: number - averageAnswer: number - averageAnswerCorrectness: number - averageSourceCorrectness: number - createdUtc: string -} +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Navbar from "./Navbar.tsx"; +import Analytics from "./Analytics.tsx"; +import RunSinglePrompt from "./RunSinglePrompt.tsx"; function App() { - const [prompts, setPrompts] = useState([]) - const [selectedPromptId, setSelectedPromptId] = useState(null) - const [submittedQuestion, setSubmittedQuestion] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [results, setResults] = useState([]) - const [error, setError] = useState('') - - useEffect(() => { - fetch('http://localhost:5201/api/prompts') - .then(res => res.json()) - .then(data => setPrompts(data)) - .catch(() => console.log('Failed loading prompts')) - }, []) - - const handlePromptSelect = (id: number) => { - setSelectedPromptId(id) - - const prompt = prompts.find(p => p.id === id) - if (prompt) { - setSubmittedQuestion(prompt.question) - } - } - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - - if (!selectedPromptId || isLoading) return - - setIsLoading(true) - setError('') - setResults([]) - - try { - const response = await fetch('http://localhost:5201/api/llm/run', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify([selectedPromptId]), - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(errorText || 'Request failed') - } - - const data: ExportRow[] = await response.json() - setResults(data) - } catch (err) { - if (err instanceof Error) { - setError(err.message) - } else { - setError('Er ging iets mis.') - } - } finally { - setIsLoading(false) - } - } - return ( -
-

LLM Statistics Monitoring Tool

- -
- - - -
- - {submittedQuestion && ( -
- Question: {submittedQuestion} -
- )} - -
- -
- - {error &&
{error}
} - - {results.length > 0 && ( -
-

Responses

- -
- {results.map((result, index) => { - const color = - result.provider.includes('gpt') - ? '#2563eb' - : result.provider.includes('gemini') - ? '#16a34a' - : '#9333ea' - - const background = - result.provider.includes('gpt') - ? '#eff6ff' - : result.provider.includes('gemini') - ? '#f0fdf4' - : '#faf5ff' - - return ( -
-

{result.provider}

- -

- Actual answer: {result.actualAnswer} -

- -

- Expected answer: {result.expectedAnswer} -

- -
- Actual source: - {result.actualSource && result.actualSource.length > 0 ? ( -
    - {result.actualSource.map((src, i) => ( -
  • - - {src} - -
  • - ))} -
- ) : ( -

no reference found

- )} -
- -

- Expected source:{' '} - - {result.expectedSource} - -

- -

- Relative error: {(result.relativeError * 100).toFixed(1) + "%"} -

- -

- Answer correct: {result.answerIsCorrect ? "yes" : "no"} -

- -

- Source correct: {result.sourceIsCorrect ? "yes" : "no"} -

- -

- Average answer: {result.averageAnswer} -

- -

- Average relative error: {result.averageRelativeError} -

- -

- Average answer correctness: {(result.averageAnswerCorrectness * 100).toFixed(1) + "%"} -

- -

- Average source correctness: {(result.averageSourceCorrectness * 100).toFixed(1) + "%"} -

- -
- Raw text: -

{result.rawText}

-
- - {result.exception && ( -

- Exception: {result.exception} -

- )} -
- ) - })} -
-
- )} -
- ) + + + + } /> + } /> + + + ); } -export default App +export default App; \ No newline at end of file diff --git a/AIStatsMeasurement.Web/src/Navbar.css b/AIStatsMeasurement.Web/src/Navbar.css new file mode 100644 index 0000000..b240101 --- /dev/null +++ b/AIStatsMeasurement.Web/src/Navbar.css @@ -0,0 +1,24 @@ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + background: #1e293b; + padding: 12px 24px; + color: white; +} + +.logo { + margin: 0; +} + +.nav-links a { + margin-left: 16px; + text-decoration: none; + color: #cbd5f5; + font-weight: 500; +} + +.nav-links a.active { + color: white; + border-bottom: 2px solid white; +} \ No newline at end of file diff --git a/AIStatsMeasurement.Web/src/Navbar.tsx b/AIStatsMeasurement.Web/src/Navbar.tsx new file mode 100644 index 0000000..3b5a7d3 --- /dev/null +++ b/AIStatsMeasurement.Web/src/Navbar.tsx @@ -0,0 +1,30 @@ +import { Link, useLocation } from "react-router-dom"; +import "./Navbar.css"; + +function Navbar() { + const location = useLocation(); + + return ( + + ); +} + +export default Navbar; \ No newline at end of file diff --git a/AIStatsMeasurement.Web/src/RunSinglePrompt.css b/AIStatsMeasurement.Web/src/RunSinglePrompt.css new file mode 100644 index 0000000..58b3b6d --- /dev/null +++ b/AIStatsMeasurement.Web/src/RunSinglePrompt.css @@ -0,0 +1,229 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Inter, system-ui, Arial, sans-serif; + background: #f8fafc; + color: #0f172a; +} + +.app-container { + max-width: 1200px; + margin: 0 auto; + padding: 32px 20px 60px; +} + +h1 { + margin-bottom: 24px; + font-size: 2rem; +} + +h2 { + margin-bottom: 18px; +} + +.prompt-select, +.question-preview, +.error-message { + background: white; + border-radius: 18px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + border: 1px solid #e2e8f0; +} + +.select-input { + width: 100%; + margin-top: 8px; + padding: 12px 14px; + border-radius: 12px; + border: 1px solid #cbd5e1; + background: #fff; + font-size: 1rem; +} + +.run-button { + margin: 12px 0 20px; + border: none; + border-radius: 14px; + padding: 12px 20px; + background: #0f172a; + color: white; + font-weight: 700; + cursor: pointer; + transition: transform 0.15s ease, opacity 0.15s ease; +} + +.run-button:hover { + transform: translateY(-1px); +} + +.run-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.error-message { + color: #b91c1c; + background: #fef2f2; + border-color: #fecaca; +} + +.results-section { + margin-top: 24px; +} + +.results-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 20px; +} + +.result-card { + border-radius: 24px; + padding: 20px; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); +} + +.result-card h3 { + margin-top: 0; + margin-bottom: 16px; + font-size: 1.2rem; + text-transform: capitalize; +} + +.result-card p { + line-height: 1.5; +} + +.sources-section { + margin-top: 16px; + margin-bottom: 16px; +} + +.source-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px; + margin-top: 10px; +} + +.expected-grid { + grid-template-columns: 1fr; +} + +.source-card { + background: rgba(255, 255, 255, 0.88); + border: 1px solid #e2e8f0; + border-radius: 18px; + padding: 14px; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; + backdrop-filter: blur(8px); +} + +.source-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.1); + border-color: #cbd5e1; +} + +.source-card-header { + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.source-type-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 999px; + background: #e0e7ff; + color: #4338ca; + font-size: 12px; + font-weight: 700; + text-transform: capitalize; +} + +.expected-badge { + background: #dcfce7; + color: #166534; +} + +.source-card-body { + display: flex; + flex-direction: column; + gap: 10px; +} + +.source-name { + margin: 0; + font-weight: 700; + color: #111827; + word-break: break-word; +} + +.source-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: fit-content; + padding: 9px 12px; + border-radius: 12px; + background: #f8fafc; + color: #2563eb; + text-decoration: none; + font-weight: 600; + border: 1px solid #dbeafe; + transition: background 0.18s ease, border-color 0.18s ease; +} + +.source-link:hover { + background: #eff6ff; + border-color: #bfdbfe; +} + +.source-no-link, +.no-source-text { + margin: 0; + color: #64748b; +} + +.raw-text-block { + margin-top: 16px; + background: rgba(255, 255, 255, 0.65); + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 14px; +} + +.source-url { + margin: 0; + font-size: 0.85rem; + color: #475569; + word-break: break-all; + line-height: 1.4; + background: #f1f5f9; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid #e2e8f0; +} + +.raw-text { + margin-top: 8px; + white-space: pre-wrap; + word-break: break-word; + color: #334155; +} + +.exception-text { + margin-top: 14px; + color: #b91c1c; + background: #fff1f2; + border: 1px solid #fecdd3; + padding: 12px; + border-radius: 14px; +} diff --git a/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx b/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx new file mode 100644 index 0000000..1e52dd0 --- /dev/null +++ b/AIStatsMeasurement.Web/src/RunSinglePrompt.tsx @@ -0,0 +1,320 @@ +import { type FormEvent, useState, useEffect } from 'react' + +import './RunSinglePrompt.css' + +type Prompt = { + id: number + theme: string + subject: string + question: string +} + +type ExportRow = { + id: number + theme: string + question: string + expectedAnswer: number + expectedSource: string + actualAnswer: number + actualSource: number[] + provider: string + rawText: string | null + exception: string | null + squareMeanRootError: number + relativeError: number + answerIsCorrect: boolean + sourceIsCorrect: boolean + createdUtc: string +} + +type SourceDto = { + id: number + name: string | null + url: string | null + type: string | null +} + +type ResultWithSources = ExportRow & { + actualSourceDetails: SourceDto[] +} + +const fetchSourcesByIds = async (ids: number[]): Promise => { + if (!ids.length) return [] + + const response = await fetch('http://localhost:5201/api/sources/getByIds', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(ids) + }) + + if (!response.ok) { + throw new Error('Failed to fetch sources') + } + + return response.json() +} + +function RunSinglePrompt() { + const [prompts, setPrompts] = useState([]) + const [selectedPromptId, setSelectedPromptId] = useState(null) + const [submittedQuestion, setSubmittedQuestion] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [results, setResults] = useState([]) + const [error, setError] = useState('') + + useEffect(() => { + fetch('http://localhost:5201/api/prompts') + .then((res) => res.json()) + .then((data) => setPrompts(data)) + .catch(() => console.log('Failed loading prompts')) + }, []) + + const handlePromptSelect = (id: number) => { + setSelectedPromptId(id) + + const prompt = prompts.find((p) => p.id === id) + if (prompt) { + setSubmittedQuestion(prompt.question) + } else { + setSubmittedQuestion('') + } + } + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + if (!selectedPromptId || isLoading) return + + setIsLoading(true) + setError('') + setResults([]) + + try { + const response = await fetch('http://localhost:5201/api/llm/run', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify([selectedPromptId]) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Request failed') + } + + const data: ExportRow[] = await response.json() + + const allSourceIds = [...new Set(data.flatMap((r) => r.actualSource ?? []))] + + const sourceDtos = await fetchSourcesByIds(allSourceIds) + + const sourceMap = new Map( + sourceDtos.map((source) => [source.id, source]) + ) + + const enrichedResults: ResultWithSources[] = data.map((result) => ({ + ...result, + actualSourceDetails: (result.actualSource ?? []) + .map((id) => sourceMap.get(id)) + .filter((source): source is SourceDto => Boolean(source)) + })) + + setResults(enrichedResults) + } catch (err) { + if (err instanceof Error) { + setError(err.message) + } else { + setError('Something went wrong.') + } + } finally { + setIsLoading(false) + } + } + + return ( +
+

LLM Statistics Monitoring Tool

+ +
+ + + +
+ + {submittedQuestion && ( +
+ Question: {submittedQuestion} +
+ )} + +
+ +
+ + {error &&
{error}
} + + {results.length > 0 && ( +
+

Responses

+ +
+ {results.map((result, index) => { + const color = result.provider.includes('gpt') + ? '#2563eb' + : result.provider.includes('gemini') + ? '#16a34a' + : '#9333ea' + + const background = result.provider.includes('gpt') + ? '#eff6ff' + : result.provider.includes('gemini') + ? '#f0fdf4' + : '#faf5ff' + + return ( +
+

{result.provider}

+ +

+ Expected answer: {result.expectedAnswer} +

+ +

+ Actual answer: {result.actualAnswer} +

+ +
+ Expected source: + +
+
+
+ + NSI Database + +
+ +
+

{result.expectedSource}

+ + Open expected source + +
+
+
+
+ +
+ Actual sources: + + {result.actualSourceDetails.length > 0 ? ( +
+ {result.actualSourceDetails.map((src) => ( +
+
+ + {src.type || 'unknown'} + +
+ +
+

+ {src.name || `Source #${src.id}`} +

+ + {src.url ? ( + <> +

{src.url}

+ + + Open source + + + ) : ( +

No URL available

+ )} +
+
+ ))} +
+ ) : ( +

No reference found

+ )} +
+ +

+ Relative error:{' '} + {(result.relativeError * 100).toFixed(1) + '%'} +

+ +

+ Answer correct:{' '} + {result.answerIsCorrect ? 'yes' : 'no'} +

+ +

+ Source correct:{' '} + {result.sourceIsCorrect ? 'yes' : 'no'} +

+ +
+ Raw text: +

{result.rawText ?? 'No raw text available'}

+
+ + {result.exception && ( +

+ Exception: {result.exception} +

+ )} +
+ ) + })} +
+
+ )} +
+ ) +} + +export default RunSinglePrompt diff --git a/Backend/AI_stats_measurement.Backend.csproj b/Backend/AI_stats_measurement.Backend.csproj index 11242a7..2ce2fb7 100644 --- a/Backend/AI_stats_measurement.Backend.csproj +++ b/Backend/AI_stats_measurement.Backend.csproj @@ -18,7 +18,7 @@ - + diff --git a/Backend/Clients/ChatGPTWebSearchQuerier.cs b/Backend/Clients/ChatGPTWebSearchQuerier.cs new file mode 100644 index 0000000..8ac2315 --- /dev/null +++ b/Backend/Clients/ChatGPTWebSearchQuerier.cs @@ -0,0 +1,37 @@ +#pragma warning disable OPENAI001 +using AI_stats_measurement.Interface; +using OpenAI.Responses; + +namespace AI_stats_measurement.Backend.Clients +{ + public class ChatGPTWebSearchQuerier : ILlmQuerier + { + private readonly ResponsesClient _client; + + public string Name => "gpt-5.4"; + + public ChatGPTWebSearchQuerier(IConfiguration config) + { + _client = new ResponsesClient(config["LlmKeys:OpenAI"]); + } + + public async Task AskAsync(Prompt prompt, CancellationToken ct = default) + { + var options = new CreateResponseOptions + { + Model = Name + }; + + options.Tools.Add(ResponseTool.CreateWebSearchTool()); + + options.InputItems.Add( + ResponseItem.CreateUserMessageItem( + $"{prompt.Instruction}\n\n{prompt.Question}") + ); + + var response = await _client.CreateResponseAsync(options, ct); + + return response.Value.GetOutputText(); + } + } +} diff --git a/Backend/Clients/GeminiWebSearchQuerier.cs b/Backend/Clients/GeminiWebSearchQuerier.cs new file mode 100644 index 0000000..312ef9f --- /dev/null +++ b/Backend/Clients/GeminiWebSearchQuerier.cs @@ -0,0 +1,41 @@ +using AI_stats_measurement.Interface; +using Google.GenAI; +using Google.GenAI.Types; + +namespace AI_stats_measurement.Backend.Clients +{ + public class GeminiWebSearchQuerier : ILlmQuerier + { + private readonly Client _client; + + public string Name => "gemini-3.1-pro-preview"; + + public GeminiWebSearchQuerier(IConfiguration config) + { + _client = new Client(apiKey: config["LlmKeys:Gemini"]); + } + + public async Task AskAsync(Prompt prompt, CancellationToken ct = default) + { + var config = new GenerateContentConfig + { + Tools = new List + { + new Tool + { + GoogleSearch = new GoogleSearch() + } + } + }; + + var response = await _client.Models.GenerateContentAsync( + model: Name, + contents: $"{prompt.Instruction}\n\n{prompt.Question}", + config: config, + cancellationToken: ct + ); + + return response.Candidates[0].Content.Parts[0].Text ?? ""; + } + } +} diff --git a/Backend/Clients/GrokQuerier.cs b/Backend/Clients/GrokQuerier.cs index 4a5e16f..1563282 100644 --- a/Backend/Clients/GrokQuerier.cs +++ b/Backend/Clients/GrokQuerier.cs @@ -1,6 +1,4 @@ using AI_stats_measurement.Interface; -using OpenAI; -using OpenAI.Chat; using System.Net.Http.Headers; using System.Text; using System.Text.Json; diff --git a/Backend/Clients/GrokWebSearchQuerier.cs b/Backend/Clients/GrokWebSearchQuerier.cs new file mode 100644 index 0000000..e50c59e --- /dev/null +++ b/Backend/Clients/GrokWebSearchQuerier.cs @@ -0,0 +1,91 @@ +using AI_stats_measurement.Interface; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; + +namespace AI_stats_measurement.Backend.Clients +{ + public class GrokWebSearchQuerier : ILlmQuerier + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + + public string Name => "grok-4.20-reasoning"; + + public GrokWebSearchQuerier(IConfiguration config) + { + _apiKey = config["LlmKeys:Grok"] ?? throw new InvalidOperationException("Missing Grok API key."); + _httpClient = new HttpClient + { + BaseAddress = new Uri("https://api.x.ai/") + }; + } + + public async Task AskAsync(Prompt prompt, CancellationToken ct = default) + { + var request = new HttpRequestMessage(HttpMethod.Post, "v1/responses"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); + + var body = new + { + model = Name, + input = new object[] + { + new + { + role = "system", + content = prompt.Instruction + }, + new + { + role = "user", + content = prompt.Question + } + }, + tools = new object[] + { + new + { + type = "web_search" + } + } + }; + + request.Content = new StringContent( + JsonSerializer.Serialize(body), + Encoding.UTF8, + "application/json"); + + var response = await _httpClient.SendAsync(request, ct); + var jsonText = await response.Content.ReadAsStringAsync(ct); + + response.EnsureSuccessStatusCode(); + + using var doc = JsonDocument.Parse(jsonText); + var root = doc.RootElement; + + if (root.TryGetProperty("output", out var output)) + { + foreach (var item in output.EnumerateArray()) + { + if (item.TryGetProperty("type", out var itemType) && + itemType.GetString() == "message" && + item.TryGetProperty("content", out var content)) + { + foreach (var part in content.EnumerateArray()) + { + if (part.TryGetProperty("type", out var partType) && + partType.GetString() == "output_text" && + part.TryGetProperty("text", out var text)) + { + return text.GetString() ?? ""; + } + } + } + } + } + + return ""; + } + } +} diff --git a/Backend/Controllers/MetricsController.cs b/Backend/Controllers/MetricsController.cs new file mode 100644 index 0000000..44df2e2 --- /dev/null +++ b/Backend/Controllers/MetricsController.cs @@ -0,0 +1,63 @@ +using AI_stats_measurement.Backend.Dto; +using AI_stats_measurement.Backend.Services; +using AI_stats_measurement.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace AI_stats_measurement.Backend.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class MetricsController : ControllerBase + { + private readonly AIMeasureDbContext _context; + private readonly AnalyticsService _analyticsService; + + public MetricsController( + AIMeasureDbContext context, + AnalyticsService analyticsService) + { + _context = context; + _analyticsService = analyticsService; + } + + [HttpPost] + public ActionResult GetMetrics([FromBody] MetricsFilterDto filter) + { + var factsQuery = _context.FactCheckResults + .Include(f => f.ParsedModelResponse) + .ThenInclude(p => p.ModelResponse) + .ThenInclude(m => m.Prompt) + .AsQueryable(); + + if (!string.IsNullOrWhiteSpace(filter.Theme)) + { + factsQuery = factsQuery.Where(f => + f.ParsedModelResponse.ModelResponse.Prompt.Theme == filter.Theme); + } + + if (!string.IsNullOrWhiteSpace(filter.Llm)) + { + factsQuery = factsQuery.Where(f => + f.ParsedModelResponse.ModelResponse.Provider == filter.Llm); + } + + if (!string.IsNullOrWhiteSpace(filter.Nsi)) + { + factsQuery = factsQuery.Where(f => + f.ParsedModelResponse.ModelResponse.Prompt.Provider == filter.Nsi); + } + + var facts = factsQuery.ToList(); + + var metrics = _analyticsService.GetMetrics( + facts, + filter.Nsi, + filter.Llm, + filter.Theme + ); + + return Ok(metrics); + } + } +} diff --git a/Backend/Controllers/PromptsController.cs b/Backend/Controllers/PromptsController.cs index 156b541..6cae18a 100644 --- a/Backend/Controllers/PromptsController.cs +++ b/Backend/Controllers/PromptsController.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using AI_stats_measurement.Data; using AI_stats_measurement.Backend.Dto; +using AI_stats_measurement.Backend.Models; namespace AI_stats_measurement.Backend.Controllers { @@ -74,7 +75,7 @@ public async Task PutPrompt(int id, Prompt prompt) // POST: api/Prompts [HttpPost] - public async Task>> PostPrompts([FromBody] Listdtos) + public async Task>> PostPrompts([FromBody] List dtos) { if (dtos == null || dtos.Count == 0) return BadRequest("No prompts provided."); @@ -83,14 +84,35 @@ public async Task>> PostPrompts([FromBody] List + s.Name == sourceName && + s.Url == sourceUrl); + + if (source == null) + { + source = new Source + { + Name = sourceName, + Type = sourceType, + Url = sourceUrl + }; + + _context.Sources.Add(source); + } + var prompt = new Prompt( + dto.Provider, dto.Instruction, dto.Theme, dto.Periode, dto.Subject, dto.Question, dto.Answer, - dto.Source, + source, dto.AnswerLocation ); diff --git a/Backend/Controllers/SourcesController.cs b/Backend/Controllers/SourcesController.cs new file mode 100644 index 0000000..d0b52d8 --- /dev/null +++ b/Backend/Controllers/SourcesController.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using AI_stats_measurement.Backend.Models; +using AI_stats_measurement.Data; + +namespace AI_stats_measurement.Backend.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class SourcesController : ControllerBase + { + private readonly AIMeasureDbContext _context; + + public SourcesController(AIMeasureDbContext context) + { + _context = context; + } + + // GET: api/Sources + [HttpGet] + public async Task>> GetSources() + { + return await _context.Sources.ToListAsync(); + } + + [HttpPost("getByIds")] + public async Task>> GetSources([FromBody] List ids) + { + if (ids == null || !ids.Any()) + return BadRequest("No ids provided"); + + var sources = await _context.Sources + .Where(s => ids.Contains(s.Id)) + .ToListAsync(); + + if (!sources.Any()) + return NotFound(); + + return Ok(sources); + } + + // PUT: api/Sources/5 + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPut("{id}")] + public async Task PutSource(int id, Source source) + { + if (id != source.Id) + { + return BadRequest(); + } + + _context.Entry(source).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!SourceExists(id)) + { + return NotFound(); + } + else + { + throw; + } + } + + return NoContent(); + } + + // POST: api/Sources + // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [HttpPost] + public async Task> PostSource(Source source) + { + _context.Sources.Add(source); + await _context.SaveChangesAsync(); + + return CreatedAtAction("GetSource", new { id = source.Id }, source); + } + + // DELETE: api/Sources/5 + [HttpDelete("{id}")] + public async Task DeleteSource(int id) + { + var source = await _context.Sources.FindAsync(id); + if (source == null) + { + return NotFound(); + } + + _context.Sources.Remove(source); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + private bool SourceExists(int id) + { + return _context.Sources.Any(e => e.Id == id); + } + } +} diff --git a/Backend/Data/AIMeasureDbContext.cs b/Backend/Data/AIMeasureDbContext.cs index 4b164e3..c124656 100644 --- a/Backend/Data/AIMeasureDbContext.cs +++ b/Backend/Data/AIMeasureDbContext.cs @@ -1,8 +1,6 @@ using AI_stats_measurement.Backend.Models; -using AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models; using AI_stats_measurement.Models; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; namespace AI_stats_measurement.Data; @@ -18,6 +16,8 @@ public AIMeasureDbContext(DbContextOptions options) public DbSet ModelResponses => Set(); public DbSet ParsedModelResponses => Set(); public DbSet FactCheckResults => Set(); + public DbSet Sources => Set(); + public DbSet ParsedModelResponseSources => Set(); public DbSet ExportRows => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -32,6 +32,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(d => d.PromptId) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasOne(p => p.Source) + .WithMany(s => s.Prompts) + .HasForeignKey(p => p.SourceId) + .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() .HasOne(r => r.Prompt) .WithMany(p => p.ModelResponses) @@ -50,6 +56,33 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(f => f.ParsedModelResponseId) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .HasIndex(s => new { s.Name, s.Url }) + .IsUnique(); + + modelBuilder.Entity() + .Property(x => x.Url) + .HasMaxLength(2048); + + modelBuilder.Entity() + .Property(x => x.Name) + .HasMaxLength(512); + + modelBuilder.Entity() + .HasKey(ps => new { ps.ParsedModelResponseId, ps.SourceId }); + + modelBuilder.Entity() + .HasOne(ps => ps.ParsedModelResponse) + .WithMany(p => p.ParsedModelResponseSources) + .HasForeignKey(ps => ps.ParsedModelResponseId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(ps => ps.Source) + .WithMany(s => s.ParsedModelResponseSources) + .HasForeignKey(x => x.SourceId) + .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity(); } } diff --git a/Backend/Dto/ChartPointDto.cs b/Backend/Dto/ChartPointDto.cs new file mode 100644 index 0000000..ee84a72 --- /dev/null +++ b/Backend/Dto/ChartPointDto.cs @@ -0,0 +1,8 @@ +namespace AI_stats_measurement.Backend.Dto +{ + public class ChartPointDto + { + public string Label { get; set; } = string.Empty; + public double Value { get; set; } + } +} diff --git a/Backend/Dto/DashboardMetricsDto.cs b/Backend/Dto/DashboardMetricsDto.cs new file mode 100644 index 0000000..2826d3a --- /dev/null +++ b/Backend/Dto/DashboardMetricsDto.cs @@ -0,0 +1,10 @@ +namespace AI_stats_measurement.Backend.Dto +{ + public class DashboardMetricsDto + { + public double AccuracyScore { get; set; } + public double ConsistencyScore { get; set; } + public double FindabilityScore { get; set; } + public int TotalMeasurements { get; set; } + } +} diff --git a/Backend/Dto/MetricsFilterDto.cs b/Backend/Dto/MetricsFilterDto.cs new file mode 100644 index 0000000..3e0c6bf --- /dev/null +++ b/Backend/Dto/MetricsFilterDto.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace AI_stats_measurement.Backend.Dto +{ + public class MetricsFilterDto + { + public string? Nsi { get; set; } + public string? Llm { get; set; } + public string? Theme { get; set; } + } +} diff --git a/Backend/Dto/PromptDto.cs b/Backend/Dto/PromptDto.cs index 203cb47..523565d 100644 --- a/Backend/Dto/PromptDto.cs +++ b/Backend/Dto/PromptDto.cs @@ -2,13 +2,16 @@ { public class PromptDto { + public string Provider { get; set; } = null!; public string Instruction { get; set; } = null!; public string Theme { get; set; } = null!; public DateTime Periode { get; set; } public string Subject { get; set; } = null!; public string Question { get; set; } = null!; public decimal Answer { get; set; } - public string Source { get; set; } = null!; + public string SourceName { get; set; } = null!; + public string SourceType { get; set; } = null!; + public string SourceUrl { get; set; } = null!; public string AnswerLocation { get; set; } = null!; public Dictionary Dimensions { get; set; } = new(); } diff --git a/Backend/Migrations/20260312141833_InitialCreate1.cs b/Backend/Migrations/20260312141833_InitialCreate1.cs deleted file mode 100644 index 78dfa4b..0000000 --- a/Backend/Migrations/20260312141833_InitialCreate1.cs +++ /dev/null @@ -1,148 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace AI_stats_measurement.Backend.Migrations -{ - /// - public partial class InitialCreate1 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "IsCorrect", - table: "FactCheckResults", - newName: "SourceIsCorrect"); - - migrationBuilder.RenameColumn( - name: "IsCorrect", - table: "ExportRows", - newName: "SourceIsCorrect"); - - migrationBuilder.AddColumn( - name: "AnswerIsCorrect", - table: "FactCheckResults", - type: "bit", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "AverageAnswer", - table: "FactCheckResults", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageAnswerCorrectness", - table: "FactCheckResults", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageRelativeError", - table: "FactCheckResults", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageSourceCorrectness", - table: "FactCheckResults", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AnswerIsCorrect", - table: "ExportRows", - type: "bit", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "AverageAnswer", - table: "ExportRows", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageAnswerCorrectness", - table: "ExportRows", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageRelativeError", - table: "ExportRows", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - - migrationBuilder.AddColumn( - name: "AverageSourceCorrectness", - table: "ExportRows", - type: "decimal(18,2)", - nullable: false, - defaultValue: 0m); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "AnswerIsCorrect", - table: "FactCheckResults"); - - migrationBuilder.DropColumn( - name: "AverageAnswer", - table: "FactCheckResults"); - - migrationBuilder.DropColumn( - name: "AverageAnswerCorrectness", - table: "FactCheckResults"); - - migrationBuilder.DropColumn( - name: "AverageRelativeError", - table: "FactCheckResults"); - - migrationBuilder.DropColumn( - name: "AverageSourceCorrectness", - table: "FactCheckResults"); - - migrationBuilder.DropColumn( - name: "AnswerIsCorrect", - table: "ExportRows"); - - migrationBuilder.DropColumn( - name: "AverageAnswer", - table: "ExportRows"); - - migrationBuilder.DropColumn( - name: "AverageAnswerCorrectness", - table: "ExportRows"); - - migrationBuilder.DropColumn( - name: "AverageRelativeError", - table: "ExportRows"); - - migrationBuilder.DropColumn( - name: "AverageSourceCorrectness", - table: "ExportRows"); - - migrationBuilder.RenameColumn( - name: "SourceIsCorrect", - table: "FactCheckResults", - newName: "IsCorrect"); - - migrationBuilder.RenameColumn( - name: "SourceIsCorrect", - table: "ExportRows", - newName: "IsCorrect"); - } - } -} diff --git a/Backend/Migrations/20260317110026_InitialCreate.Designer.cs b/Backend/Migrations/20260317110026_InitialCreate.Designer.cs new file mode 100644 index 0000000..ff3aadd --- /dev/null +++ b/Backend/Migrations/20260317110026_InitialCreate.Designer.cs @@ -0,0 +1,399 @@ +// +using System; +using AI_stats_measurement.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + [DbContext(typeof(AIMeasureDbContext))] + [Migration("20260317110026_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualAnswer") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedAnswer") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpectedSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.Property("SquareMeanRootError") + .HasColumnType("decimal(18,2)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ExportRows"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.Property("ExportRowId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("ExportRowId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "Name") + .IsUnique(); + + b.ToTable("PromptDimensions"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId"); + + b.ToTable("ModelResponses"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("ModelResponseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ModelResponseId") + .IsUnique(); + + b.ToTable("ParsedModelResponses"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.Property("SquareMeanRootError") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("ParsedModelResponseId") + .IsUnique(); + + b.ToTable("FactCheckResults"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerLocation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Periode") + .HasColumnType("datetime2"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.ToTable("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", null) + .WithMany("ActualSource") + .HasForeignKey("ExportRowId"); + + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("Dimensions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("ModelResponses") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.HasOne("AI_stats_measurement.Models.ModelResponse", "ModelResponse") + .WithOne("ParsedResponse") + .HasForeignKey("AI_stats_measurement.Models.ParsedModelResponse", "ModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelResponse"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithOne("FactCheckResult") + .HasForeignKey("FactCheckResult", "ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", b => + { + b.Navigation("ActualSource"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Navigation("ParsedResponse"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Navigation("Dimensions"); + + b.Navigation("ModelResponses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260312104828_InitialCreate.cs b/Backend/Migrations/20260317110026_InitialCreate.cs similarity index 68% rename from Backend/Migrations/20260312104828_InitialCreate.cs rename to Backend/Migrations/20260317110026_InitialCreate.cs index 23caae6..b1a8e4d 100644 --- a/Backend/Migrations/20260312104828_InitialCreate.cs +++ b/Backend/Migrations/20260317110026_InitialCreate.cs @@ -22,13 +22,13 @@ protected override void Up(MigrationBuilder migrationBuilder) ExpectedAnswer = table.Column(type: "decimal(18,2)", nullable: false), ExpectedSource = table.Column(type: "nvarchar(max)", nullable: false), ActualAnswer = table.Column(type: "decimal(18,2)", nullable: false), - ActualSource = table.Column(type: "nvarchar(max)", nullable: false), Provider = table.Column(type: "nvarchar(max)", nullable: false), RawText = table.Column(type: "nvarchar(max)", nullable: true), Exception = table.Column(type: "nvarchar(max)", nullable: true), SquareMeanRootError = table.Column(type: "decimal(18,2)", nullable: false), RelativeError = table.Column(type: "decimal(18,2)", nullable: false), - IsCorrect = table.Column(type: "bit", nullable: false), + AnswerIsCorrect = table.Column(type: "bit", nullable: false), + SourceIsCorrect = table.Column(type: "bit", nullable: false), CreatedUtc = table.Column(type: "datetime2", nullable: false) }, constraints: table => @@ -36,6 +36,21 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_ExportRows", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Sources", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(450)", nullable: true), + Url = table.Column(type: "nvarchar(450)", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sources", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Prompts", columns: table => new @@ -48,13 +63,19 @@ protected override void Up(MigrationBuilder migrationBuilder) Subject = table.Column(type: "nvarchar(max)", nullable: false), Question = table.Column(type: "nvarchar(max)", nullable: false), Answer = table.Column(type: "decimal(18,2)", nullable: false), - Source = table.Column(type: "nvarchar(max)", nullable: false), + SourceId = table.Column(type: "int", nullable: false), AnswerLocation = table.Column(type: "nvarchar(max)", nullable: false), CreatedUtc = table.Column(type: "datetime2", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Prompts", x => x.Id); + table.ForeignKey( + name: "FK_Prompts_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); }); migrationBuilder.CreateTable( @@ -108,8 +129,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), ModelResponseId = table.Column(type: "int", nullable: false), - Answer = table.Column(type: "decimal(18,2)", nullable: false), - Sources = table.Column(type: "nvarchar(max)", nullable: false) + Answer = table.Column(type: "decimal(18,2)", nullable: false) }, constraints: table => { @@ -131,7 +151,8 @@ protected override void Up(MigrationBuilder migrationBuilder) ParsedModelResponseId = table.Column(type: "int", nullable: false), SquareMeanRootError = table.Column(type: "decimal(18,2)", nullable: false), RelativeError = table.Column(type: "decimal(18,2)", nullable: false), - IsCorrect = table.Column(type: "bit", nullable: false) + AnswerIsCorrect = table.Column(type: "bit", nullable: false), + SourceIsCorrect = table.Column(type: "bit", nullable: false) }, constraints: table => { @@ -144,6 +165,36 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "ParsedModelResponseSources", + columns: table => new + { + ParsedModelResponseId = table.Column(type: "int", nullable: false), + SourceId = table.Column(type: "int", nullable: false), + ExportRowId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ParsedModelResponseSources", x => new { x.ParsedModelResponseId, x.SourceId }); + table.ForeignKey( + name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId", + column: x => x.ExportRowId, + principalTable: "ExportRows", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_ParsedModelResponseSources_ParsedModelResponses_ParsedModelResponseId", + column: x => x.ParsedModelResponseId, + principalTable: "ParsedModelResponses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ParsedModelResponseSources_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + migrationBuilder.CreateIndex( name: "IX_FactCheckResults_ParsedModelResponseId", table: "FactCheckResults", @@ -161,25 +212,50 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "ModelResponseId", unique: true); + migrationBuilder.CreateIndex( + name: "IX_ParsedModelResponseSources_ExportRowId", + table: "ParsedModelResponseSources", + column: "ExportRowId"); + + migrationBuilder.CreateIndex( + name: "IX_ParsedModelResponseSources_SourceId", + table: "ParsedModelResponseSources", + column: "SourceId"); + migrationBuilder.CreateIndex( name: "IX_PromptDimensions_PromptId_Name", table: "PromptDimensions", columns: new[] { "PromptId", "Name" }, unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Prompts_SourceId", + table: "Prompts", + column: "SourceId"); + + migrationBuilder.CreateIndex( + name: "IX_Sources_Name_Url", + table: "Sources", + columns: new[] { "Name", "Url" }, + unique: true, + filter: "[Name] IS NOT NULL AND [Url] IS NOT NULL"); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "ExportRows"); + name: "FactCheckResults"); migrationBuilder.DropTable( - name: "FactCheckResults"); + name: "ParsedModelResponseSources"); migrationBuilder.DropTable( name: "PromptDimensions"); + migrationBuilder.DropTable( + name: "ExportRows"); + migrationBuilder.DropTable( name: "ParsedModelResponses"); @@ -188,6 +264,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "Prompts"); + + migrationBuilder.DropTable( + name: "Sources"); } } } diff --git a/Backend/Migrations/20260312141833_InitialCreate1.Designer.cs b/Backend/Migrations/20260317124535_InitialCreate1.Designer.cs similarity index 77% rename from Backend/Migrations/20260312141833_InitialCreate1.Designer.cs rename to Backend/Migrations/20260317124535_InitialCreate1.Designer.cs index 49475d2..2504697 100644 --- a/Backend/Migrations/20260312141833_InitialCreate1.Designer.cs +++ b/Backend/Migrations/20260317124535_InitialCreate1.Designer.cs @@ -12,7 +12,7 @@ namespace AI_stats_measurement.Backend.Migrations { [DbContext(typeof(AIMeasureDbContext))] - [Migration("20260312141833_InitialCreate1")] + [Migration("20260317124535_InitialCreate1")] partial class InitialCreate1 { /// @@ -43,18 +43,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("AnswerIsCorrect") .HasColumnType("bit"); - b.Property("AverageAnswer") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageAnswerCorrectness") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageRelativeError") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageSourceCorrectness") - .HasColumnType("decimal(18,2)"); - b.Property("CreatedUtc") .HasColumnType("datetime2"); @@ -97,6 +85,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("ExportRows"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.Property("Id") @@ -124,6 +127,32 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("PromptDimensions"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Property("Id") @@ -169,10 +198,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ModelResponseId") .HasColumnType("int"); - b.PrimitiveCollection("Sources") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); b.HasIndex("ModelResponseId") @@ -192,18 +217,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("AnswerIsCorrect") .HasColumnType("bit"); - b.Property("AverageAnswer") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageAnswerCorrectness") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageRelativeError") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageSourceCorrectness") - .HasColumnType("decimal(18,2)"); - b.Property("ParsedModelResponseId") .HasColumnType("int"); @@ -253,9 +266,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Source") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("SourceId") + .HasColumnType("int"); b.Property("Subject") .IsRequired() @@ -267,9 +279,30 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("SourceId"); + b.ToTable("Prompts"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.HasOne("Prompt", "Prompt") @@ -314,6 +347,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("ParsedModelResponse"); }); + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Navigation("ParsedResponse"); @@ -322,6 +373,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => { b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); }); modelBuilder.Entity("Prompt", b => diff --git a/Backend/Migrations/20260317124535_InitialCreate1.cs b/Backend/Migrations/20260317124535_InitialCreate1.cs new file mode 100644 index 0000000..10f8bd4 --- /dev/null +++ b/Backend/Migrations/20260317124535_InitialCreate1.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + /// + public partial class InitialCreate1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId", + table: "ParsedModelResponseSources"); + + migrationBuilder.DropIndex( + name: "IX_ParsedModelResponseSources_ExportRowId", + table: "ParsedModelResponseSources"); + + migrationBuilder.DropColumn( + name: "ExportRowId", + table: "ParsedModelResponseSources"); + + migrationBuilder.AddColumn( + name: "ActualSource", + table: "ExportRows", + type: "nvarchar(max)", + nullable: false, + defaultValue: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ActualSource", + table: "ExportRows"); + + migrationBuilder.AddColumn( + name: "ExportRowId", + table: "ParsedModelResponseSources", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ParsedModelResponseSources_ExportRowId", + table: "ParsedModelResponseSources", + column: "ExportRowId"); + + migrationBuilder.AddForeignKey( + name: "FK_ParsedModelResponseSources_ExportRows_ExportRowId", + table: "ParsedModelResponseSources", + column: "ExportRowId", + principalTable: "ExportRows", + principalColumn: "Id"); + } + } +} diff --git a/Backend/Migrations/20260312104828_InitialCreate.Designer.cs b/Backend/Migrations/20260319103433_InitialCreate2.Designer.cs similarity index 74% rename from Backend/Migrations/20260312104828_InitialCreate.Designer.cs rename to Backend/Migrations/20260319103433_InitialCreate2.Designer.cs index a100c64..5952da3 100644 --- a/Backend/Migrations/20260312104828_InitialCreate.Designer.cs +++ b/Backend/Migrations/20260319103433_InitialCreate2.Designer.cs @@ -12,8 +12,8 @@ namespace AI_stats_measurement.Backend.Migrations { [DbContext(typeof(AIMeasureDbContext))] - [Migration("20260312104828_InitialCreate")] - partial class InitialCreate + [Migration("20260319103433_InitialCreate2")] + partial class InitialCreate2 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -40,6 +40,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + b.Property("CreatedUtc") .HasColumnType("datetime2"); @@ -53,9 +56,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("IsCorrect") - .HasColumnType("bit"); - b.Property("Provider") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -70,6 +70,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RelativeError") .HasColumnType("decimal(18,2)"); + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + b.Property("SquareMeanRootError") .HasColumnType("decimal(18,2)"); @@ -82,6 +85,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("ExportRows"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.Property("Id") @@ -109,6 +127,32 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("PromptDimensions"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Property("Id") @@ -154,10 +198,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("ModelResponseId") .HasColumnType("int"); - b.PrimitiveCollection("Sources") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); b.HasIndex("ModelResponseId") @@ -174,7 +214,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("IsCorrect") + b.Property("AbsoluteError") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerIsCorrect") .HasColumnType("bit"); b.Property("ParsedModelResponseId") @@ -183,8 +226,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RelativeError") .HasColumnType("decimal(18,2)"); - b.Property("SquareMeanRootError") - .HasColumnType("decimal(18,2)"); + b.Property("SourceIsCorrect") + .HasColumnType("bit"); b.HasKey("Id"); @@ -219,14 +262,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Periode") .HasColumnType("datetime2"); - b.Property("Question") + b.Property("Provider") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Source") + b.Property("Question") .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("SourceId") + .HasColumnType("int"); + b.Property("Subject") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -237,9 +283,30 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("SourceId"); + b.ToTable("Prompts"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.HasOne("Prompt", "Prompt") @@ -284,6 +351,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("ParsedModelResponse"); }); + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Navigation("ParsedResponse"); @@ -292,6 +377,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => { b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); }); modelBuilder.Entity("Prompt", b => diff --git a/Backend/Migrations/20260319103433_InitialCreate2.cs b/Backend/Migrations/20260319103433_InitialCreate2.cs new file mode 100644 index 0000000..fcf530c --- /dev/null +++ b/Backend/Migrations/20260319103433_InitialCreate2.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + /// + public partial class InitialCreate2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "SquareMeanRootError", + table: "FactCheckResults", + newName: "AbsoluteError"); + + migrationBuilder.AddColumn( + name: "Provider", + table: "Prompts", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Provider", + table: "Prompts"); + + migrationBuilder.RenameColumn( + name: "AbsoluteError", + table: "FactCheckResults", + newName: "SquareMeanRootError"); + } + } +} diff --git a/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs b/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs new file mode 100644 index 0000000..200326e --- /dev/null +++ b/Backend/Migrations/20260319135710_InitialCreate3.Designer.cs @@ -0,0 +1,393 @@ +// +using System; +using AI_stats_measurement.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + [DbContext(typeof(AIMeasureDbContext))] + [Migration("20260319135710_InitialCreate3")] + partial class InitialCreate3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ExportRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualAnswer") + .HasColumnType("decimal(18,2)"); + + b.PrimitiveCollection("ActualSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedAnswer") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpectedSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.Property("SquareMeanRootError") + .HasColumnType("decimal(18,2)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ExportRows"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "Name") + .IsUnique(); + + b.ToTable("PromptDimensions"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId"); + + b.ToTable("ModelResponses"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("ModelResponseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ModelResponseId") + .IsUnique(); + + b.ToTable("ParsedModelResponses"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsoluteError") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ParsedModelResponseId") + .IsUnique(); + + b.ToTable("FactCheckResults"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerLocation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Periode") + .HasColumnType("datetime2"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.ToTable("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("Dimensions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("ModelResponses") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.HasOne("AI_stats_measurement.Models.ModelResponse", "ModelResponse") + .WithOne("ParsedResponse") + .HasForeignKey("AI_stats_measurement.Models.ParsedModelResponse", "ModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelResponse"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithOne("FactCheckResult") + .HasForeignKey("FactCheckResult", "ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Navigation("ParsedResponse"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Navigation("Dimensions"); + + b.Navigation("ModelResponses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260319135710_InitialCreate3.cs b/Backend/Migrations/20260319135710_InitialCreate3.cs new file mode 100644 index 0000000..01dc56e --- /dev/null +++ b/Backend/Migrations/20260319135710_InitialCreate3.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + /// + public partial class InitialCreate3 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Backend/Migrations/20260323152744_InitialCreate4.Designer.cs b/Backend/Migrations/20260323152744_InitialCreate4.Designer.cs new file mode 100644 index 0000000..2074e67 --- /dev/null +++ b/Backend/Migrations/20260323152744_InitialCreate4.Designer.cs @@ -0,0 +1,394 @@ +// +using System; +using AI_stats_measurement.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + [DbContext(typeof(AIMeasureDbContext))] + [Migration("20260323152744_InitialCreate4")] + partial class InitialCreate4 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ExportRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualAnswer") + .HasColumnType("decimal(18,2)"); + + b.PrimitiveCollection("ActualSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedAnswer") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpectedSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.Property("SquareMeanRootError") + .HasColumnType("decimal(18,2)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ExportRows"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "Name") + .IsUnique(); + + b.ToTable("PromptDimensions"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId"); + + b.ToTable("ModelResponses"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("ModelResponseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ModelResponseId") + .IsUnique(); + + b.ToTable("ParsedModelResponses"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsoluteError") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ParsedModelResponseId") + .IsUnique(); + + b.ToTable("FactCheckResults"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerLocation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Periode") + .HasColumnType("datetime2"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.ToTable("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("Dimensions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("ModelResponses") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.HasOne("AI_stats_measurement.Models.ModelResponse", "ModelResponse") + .WithOne("ParsedResponse") + .HasForeignKey("AI_stats_measurement.Models.ParsedModelResponse", "ModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelResponse"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithOne("FactCheckResult") + .HasForeignKey("FactCheckResult", "ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Navigation("ParsedResponse"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Navigation("Dimensions"); + + b.Navigation("ModelResponses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260323152744_InitialCreate4.cs b/Backend/Migrations/20260323152744_InitialCreate4.cs new file mode 100644 index 0000000..eca9908 --- /dev/null +++ b/Backend/Migrations/20260323152744_InitialCreate4.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + /// + public partial class InitialCreate4 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Url", + table: "Sources", + type: "nvarchar(2048)", + maxLength: 2048, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Url", + table: "Sources", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(2048)", + oldMaxLength: 2048, + oldNullable: true); + } + } +} diff --git a/Backend/Migrations/20260323153545_InitialCreate5.Designer.cs b/Backend/Migrations/20260323153545_InitialCreate5.Designer.cs new file mode 100644 index 0000000..e576045 --- /dev/null +++ b/Backend/Migrations/20260323153545_InitialCreate5.Designer.cs @@ -0,0 +1,394 @@ +// +using System; +using AI_stats_measurement.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + [DbContext(typeof(AIMeasureDbContext))] + [Migration("20260323153545_InitialCreate5")] + partial class InitialCreate5 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ExportRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ActualAnswer") + .HasColumnType("decimal(18,2)"); + + b.PrimitiveCollection("ActualSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("ExpectedAnswer") + .HasColumnType("decimal(18,2)"); + + b.Property("ExpectedSource") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.Property("SquareMeanRootError") + .HasColumnType("decimal(18,2)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ExportRows"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId", "Name") + .IsUnique(); + + b.ToTable("PromptDimensions"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Exception") + .HasColumnType("nvarchar(max)"); + + b.Property("PromptId") + .HasColumnType("int"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RawText") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PromptId"); + + b.ToTable("ModelResponses"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("ModelResponseId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ModelResponseId") + .IsUnique(); + + b.ToTable("ParsedModelResponses"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsoluteError") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); + + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("RelativeError") + .HasColumnType("decimal(18,2)"); + + b.Property("SourceIsCorrect") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ParsedModelResponseId") + .IsUnique(); + + b.ToTable("FactCheckResults"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasColumnType("decimal(18,2)"); + + b.Property("AnswerLocation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedUtc") + .HasColumnType("datetime2"); + + b.Property("Instruction") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Periode") + .HasColumnType("datetime2"); + + b.Property("Provider") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Question") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Theme") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SourceId"); + + b.ToTable("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("Dimensions") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.HasOne("Prompt", "Prompt") + .WithMany("ModelResponses") + .HasForeignKey("PromptId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prompt"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.HasOne("AI_stats_measurement.Models.ModelResponse", "ModelResponse") + .WithOne("ParsedResponse") + .HasForeignKey("AI_stats_measurement.Models.ParsedModelResponse", "ModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelResponse"); + }); + + modelBuilder.Entity("FactCheckResult", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithOne("FactCheckResult") + .HasForeignKey("FactCheckResult", "ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => + { + b.Navigation("ParsedResponse"); + }); + + modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => + { + b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); + }); + + modelBuilder.Entity("Prompt", b => + { + b.Navigation("Dimensions"); + + b.Navigation("ModelResponses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/Migrations/20260323153545_InitialCreate5.cs b/Backend/Migrations/20260323153545_InitialCreate5.cs new file mode 100644 index 0000000..43e7d62 --- /dev/null +++ b/Backend/Migrations/20260323153545_InitialCreate5.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AI_stats_measurement.Backend.Migrations +{ + /// + public partial class InitialCreate5 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Backend/Migrations/AIMeasureDbContextModelSnapshot.cs b/Backend/Migrations/AIMeasureDbContextModelSnapshot.cs index 368ca6e..8beebac 100644 --- a/Backend/Migrations/AIMeasureDbContextModelSnapshot.cs +++ b/Backend/Migrations/AIMeasureDbContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models.ExportRow", b => + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ExportRow", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -40,18 +40,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AnswerIsCorrect") .HasColumnType("bit"); - b.Property("AverageAnswer") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageAnswerCorrectness") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageRelativeError") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageSourceCorrectness") - .HasColumnType("decimal(18,2)"); - b.Property("CreatedUtc") .HasColumnType("datetime2"); @@ -94,6 +82,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ExportRows"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.Property("ParsedModelResponseId") + .HasColumnType("int"); + + b.Property("SourceId") + .HasColumnType("int"); + + b.HasKey("ParsedModelResponseId", "SourceId"); + + b.HasIndex("SourceId"); + + b.ToTable("ParsedModelResponseSources"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.Property("Id") @@ -121,6 +124,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("PromptDimensions"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("Name", "Url") + .IsUnique() + .HasFilter("[Name] IS NOT NULL AND [Url] IS NOT NULL"); + + b.ToTable("Sources"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Property("Id") @@ -166,10 +196,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ModelResponseId") .HasColumnType("int"); - b.PrimitiveCollection("Sources") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.HasKey("Id"); b.HasIndex("ModelResponseId") @@ -186,20 +212,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("AnswerIsCorrect") - .HasColumnType("bit"); - - b.Property("AverageAnswer") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageAnswerCorrectness") - .HasColumnType("decimal(18,2)"); - - b.Property("AverageRelativeError") + b.Property("AbsoluteError") .HasColumnType("decimal(18,2)"); - b.Property("AverageSourceCorrectness") - .HasColumnType("decimal(18,2)"); + b.Property("AnswerIsCorrect") + .HasColumnType("bit"); b.Property("ParsedModelResponseId") .HasColumnType("int"); @@ -210,9 +227,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SourceIsCorrect") .HasColumnType("bit"); - b.Property("SquareMeanRootError") - .HasColumnType("decimal(18,2)"); - b.HasKey("Id"); b.HasIndex("ParsedModelResponseId") @@ -246,14 +260,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Periode") .HasColumnType("datetime2"); - b.Property("Question") + b.Property("Provider") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Source") + b.Property("Question") .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("SourceId") + .HasColumnType("int"); + b.Property("Subject") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -264,9 +281,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("SourceId"); + b.ToTable("Prompts"); }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.ParsedModelResponseSource", b => + { + b.HasOne("AI_stats_measurement.Models.ParsedModelResponse", "ParsedModelResponse") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("ParsedModelResponseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("ParsedModelResponseSources") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParsedModelResponse"); + + b.Navigation("Source"); + }); + modelBuilder.Entity("AI_stats_measurement.Backend.Models.PromptDimension", b => { b.HasOne("Prompt", "Prompt") @@ -311,6 +349,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ParsedModelResponse"); }); + modelBuilder.Entity("Prompt", b => + { + b.HasOne("AI_stats_measurement.Backend.Models.Source", "Source") + .WithMany("Prompts") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("AI_stats_measurement.Backend.Models.Source", b => + { + b.Navigation("ParsedModelResponseSources"); + + b.Navigation("Prompts"); + }); + modelBuilder.Entity("AI_stats_measurement.Models.ModelResponse", b => { b.Navigation("ParsedResponse"); @@ -319,6 +375,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("AI_stats_measurement.Models.ParsedModelResponse", b => { b.Navigation("FactCheckResult"); + + b.Navigation("ParsedModelResponseSources"); }); modelBuilder.Entity("Prompt", b => diff --git a/Backend/Models/ExportRow.cs b/Backend/Models/ExportRow.cs index fb8ca71..216717c 100644 --- a/Backend/Models/ExportRow.cs +++ b/Backend/Models/ExportRow.cs @@ -1,72 +1,60 @@ -namespace AI_stats_measurement.Backend.Models -{ - using System; +using AI_stats_measurement.Backend.Dto; +using global::AI_stats_measurement.Backend.Dto; +using System; +using System.ComponentModel.DataAnnotations.Schema; - namespace AI_stats_measurement.Backend.Models +namespace AI_stats_measurement.Backend.Models +{ + public class ExportRow { - public class ExportRow - { - public int Id { get; private set; } - public string Theme { get; private set; } = null!; - public string Question { get; private set; } = null!; - public decimal ExpectedAnswer { get; private set; } - public string ExpectedSource { get; private set; } = null!; - public decimal ActualAnswer { get; private set; } - public List ActualSource { get; private set; } = null!; - public string Provider { get; private set; } = null!; - public string? RawText { get; private set; } - public string? Exception { get; private set; } - public decimal SquareMeanRootError { get; private set; } - public decimal RelativeError { get; private set; } - public bool AnswerIsCorrect { get; private set; } - public bool SourceIsCorrect { get; private set; } - public decimal AverageRelativeError { get; private set; } - public decimal AverageAnswer { get; private set; } - public decimal AverageAnswerCorrectness { get; private set; } - public decimal AverageSourceCorrectness { get; private set; } - public DateTime CreatedUtc { get; private set; } + public int Id { get; private set; } + public string Theme { get; private set; } = null!; + public string Question { get; private set; } = null!; + public decimal ExpectedAnswer { get; private set; } + public string ExpectedSource { get; private set; } = null!; + public decimal ActualAnswer { get; private set; } + public List ActualSource { get; private set; } = null!; + public string Provider { get; private set; } = null!; + public string? RawText { get; private set; } + public string? Exception { get; private set; } + public decimal SquareMeanRootError { get; private set; } + public decimal RelativeError { get; private set; } + public bool AnswerIsCorrect { get; private set; } + public bool SourceIsCorrect { get; private set; } + public DateTime CreatedUtc { get; private set; } - private ExportRow() { } + private ExportRow() { } - public ExportRow( - string theme, - string question, - decimal expectedAnswer, - string expectedSource, - decimal actualAnswer, - List actualSource, - string provider, - string? rawText, - string? exception, - decimal squareMeanRootError, - decimal relativeError, - bool answerIsCorrect, - bool sourceIsCorrect, - decimal averageRelativeError, - decimal averageAnswer , - decimal averageAnswerCorrectness , - decimal averageSourceCorrectness , - DateTime createdUtc) - { - Theme = theme; - Question = question; - ExpectedAnswer = expectedAnswer; - ExpectedSource = expectedSource; - ActualAnswer = actualAnswer; - ActualSource = actualSource; - Provider = provider; - RawText = rawText; - Exception = exception; - SquareMeanRootError = squareMeanRootError; - RelativeError = relativeError; - AnswerIsCorrect = answerIsCorrect; - SourceIsCorrect = sourceIsCorrect; - AverageRelativeError = averageRelativeError; - AverageAnswer = averageAnswer; - AverageAnswerCorrectness = averageAnswerCorrectness; - AverageSourceCorrectness = averageSourceCorrectness; - CreatedUtc = createdUtc; - } + public ExportRow( + string theme, + string question, + decimal expectedAnswer, + string expectedSource, + decimal actualAnswer, + List actualSource, + string provider, + string? rawText, + string? exception, + decimal squareMeanRootError, + decimal relativeError, + bool answerIsCorrect, + bool sourceIsCorrect, + DateTime createdUtc) + { + Theme = theme; + Question = question; + ExpectedAnswer = expectedAnswer; + ExpectedSource = expectedSource; + ActualAnswer = actualAnswer; + ActualSource = actualSource; + Provider = provider; + RawText = rawText; + Exception = exception; + SquareMeanRootError = squareMeanRootError; + RelativeError = relativeError; + AnswerIsCorrect = answerIsCorrect; + SourceIsCorrect = sourceIsCorrect; + CreatedUtc = createdUtc; } } } diff --git a/Backend/Models/FactCheckResult.cs b/Backend/Models/FactCheckResult.cs index c10b113..73101fe 100644 --- a/Backend/Models/FactCheckResult.cs +++ b/Backend/Models/FactCheckResult.cs @@ -5,38 +5,25 @@ public class FactCheckResult { public int Id { get; init; } public int ParsedModelResponseId { get; private set; } - public decimal SquareMeanRootError { get; private set; } + public decimal AbsoluteError { get; private set; } public decimal RelativeError { get; private set; } public bool AnswerIsCorrect { get; private set; } public bool SourceIsCorrect { get; private set; } - public decimal AverageRelativeError { get; private set; } - public decimal AverageAnswer { get; private set; } - public decimal AverageAnswerCorrectness { get; private set; } - public decimal AverageSourceCorrectness { get; private set; } - public ParsedModelResponse ParsedModelResponse { get; set; } = null!; private FactCheckResult() { } public FactCheckResult( int parsedModelResponseId, - decimal squareMeanRootError, + decimal absoluteError, decimal relativeError, bool answerIsCorrect, - bool sourceIsCorrect, - decimal averageRelativeError, - decimal averageAnswer, - decimal averageAnswerCorrectness, - decimal averageSourceCorrectness) + bool sourceIsCorrect) { ParsedModelResponseId = parsedModelResponseId; - SquareMeanRootError = squareMeanRootError; + AbsoluteError = absoluteError; RelativeError = relativeError; AnswerIsCorrect = answerIsCorrect; SourceIsCorrect = sourceIsCorrect; - AverageRelativeError = averageRelativeError; - AverageAnswer = averageAnswer; - AverageAnswerCorrectness = averageAnswerCorrectness; - AverageSourceCorrectness = averageSourceCorrectness; } } \ No newline at end of file diff --git a/Backend/Models/ParsedModelResponse.cs b/Backend/Models/ParsedModelResponse.cs index 7d731e2..e7593b8 100644 --- a/Backend/Models/ParsedModelResponse.cs +++ b/Backend/Models/ParsedModelResponse.cs @@ -1,22 +1,36 @@ -namespace AI_stats_measurement.Models +using AI_stats_measurement.Backend.Models; +using System.ComponentModel.DataAnnotations.Schema; + +namespace AI_stats_measurement.Models { public class ParsedModelResponse { - public int Id { get; init; } - public int ModelResponseId { get; private set; } - public decimal Answer { get; private set; } - public List Sources { get; private set; } = new(); + public int Id { get; set; } + public int ModelResponseId { get; set; } + public decimal Answer { get; set; } public ModelResponse ModelResponse { get; set; } = null!; public FactCheckResult? FactCheckResult { get; set; } + public List ParsedModelResponseSources { get; set; } = new(); + + [NotMapped] + public List ExtractedSources { get; set; } = new(); + private ParsedModelResponse() { } - public ParsedModelResponse(int modelResponseId, decimal answer, List sources) + public ParsedModelResponse(int modelResponseId, decimal answer, List extractedSources) { ModelResponseId = modelResponseId; Answer = answer; - Sources = sources; + ExtractedSources = extractedSources; } } + + public class ExtractedSource + { + public string Name { get; set; } = null!; + public string Url { get; set; } = null!; + public string Type { get; set; } = null!; + } } diff --git a/Backend/Models/ParsedModelResponseSource.cs b/Backend/Models/ParsedModelResponseSource.cs new file mode 100644 index 0000000..e4cdb27 --- /dev/null +++ b/Backend/Models/ParsedModelResponseSource.cs @@ -0,0 +1,13 @@ +using AI_stats_measurement.Models; + +namespace AI_stats_measurement.Backend.Models +{ + public class ParsedModelResponseSource + { + public int ParsedModelResponseId { get; set; } + public ParsedModelResponse ParsedModelResponse { get; set; } = null!; + + public int SourceId { get; set; } + public Source Source { get; set; } = null!; + } +} diff --git a/Backend/Models/Prompt.cs b/Backend/Models/Prompt.cs index c186357..dac9e8e 100644 --- a/Backend/Models/Prompt.cs +++ b/Backend/Models/Prompt.cs @@ -6,13 +6,15 @@ public class Prompt { public int Id { get; private set; } + public string Provider { get; private set; } public string Instruction { get; private set; } = null!; public string Theme { get; private set; } = null!; public DateTime Periode { get; private set; } public string Subject { get; private set; } = null!; public string Question { get; private set; } = null!; public decimal Answer { get; private set; } - public string Source { get; private set; } = null!; + public int SourceId { get; set; } + public Source Source { get; private set; } = null!; public string AnswerLocation { get; private set; } = null!; public DateTime CreatedUtc { get; private set; } = DateTime.UtcNow; @@ -21,9 +23,10 @@ public class Prompt private Prompt() { } - public Prompt(string instruction, string theme, DateTime periode, string subject, string question, decimal answer, string source, string answerLocation) + public Prompt(string provider, string instruction, string theme, DateTime periode, string subject, string question, decimal answer, Source source, string answerLocation) { - Theme = theme; + Provider = provider; + Theme = theme; Periode = periode; Subject = subject; Instruction = instruction; diff --git a/Backend/Models/Source.cs b/Backend/Models/Source.cs new file mode 100644 index 0000000..a0915ae --- /dev/null +++ b/Backend/Models/Source.cs @@ -0,0 +1,13 @@ +namespace AI_stats_measurement.Backend.Models +{ + public class Source + { + public int Id { get; set; } + public string? Name { get; set; } + public string? Url { get; set; } + public string? Type { get; set; } + + public List Prompts { get; set; } = new(); + public List ParsedModelResponseSources { get; set; } = new(); + } +} diff --git a/Backend/Program.cs b/Backend/Program.cs index 1aa153e..6720c5c 100644 --- a/Backend/Program.cs +++ b/Backend/Program.cs @@ -1,3 +1,4 @@ +using AI_stats_measurement.Backend.Clients; using AI_stats_measurement.Backend.Services; using AI_stats_measurement.Clients; using AI_stats_measurement.Data; @@ -26,8 +27,11 @@ builder.Services.AddControllers(); builder.Services.AddScoped(); +//builder.Services.AddScoped(); builder.Services.AddScoped(); +//builder.Services.AddScoped(); builder.Services.AddScoped(); +//builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -35,6 +39,10 @@ new FactChecker(0.05m, "CBS") ); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + builder.Services.AddScoped(); var app = builder.Build(); diff --git a/Backend/Services/AnalyticsService.cs b/Backend/Services/AnalyticsService.cs new file mode 100644 index 0000000..a7930c9 --- /dev/null +++ b/Backend/Services/AnalyticsService.cs @@ -0,0 +1,120 @@ +using AI_stats_measurement.Backend.Dto; +using AI_stats_measurement.Models; + +namespace AI_stats_measurement.Backend.Services +{ + public class AnalyticsService + { + public AnalyticsService() { } + + public DashboardMetricsDto GetMetrics( + List results, + string? nsi, + string? llm, + string? theme) + { + var filtered = ApplyFilters(results, nsi, llm, theme); + + return new DashboardMetricsDto + { + AccuracyScore = ComputeAccuracyScore(filtered), + ConsistencyScore = ComputeConsistencyScore(filtered), + FindabilityScore = ComputeFindabilityScore(filtered), + TotalMeasurements = filtered.Count + }; + } + + private double ComputeFindabilityScore(List filtered) + { + if (filtered.Count == 0) return 0; + + int correctAnswers = filtered.Count(r => r.SourceIsCorrect); + + double ratio = (double)correctAnswers / filtered.Count; + + return ratio * 10; + } + + private double ComputeConsistencyScore(List filtered) + { + if (filtered.Count == 0) return 0; + + var values = filtered + .Select(r => (double)r.ParsedModelResponse.Answer) + .ToList(); + + double mean = values.Average(); + + if (mean == 0) return 0; // avoid division by zero + + double variance = values.Average(v => Math.Pow(v - mean, 2)); + double stdDev = Math.Sqrt(variance); + + double relative = stdDev / mean; + + // Convert to score (inverse relationship) + double score = (1 - relative) * 10; + + // Clamp between 0 and 10 + return Math.Max(0, Math.Min(10, score)); + } + + public double ComputeAccuracyScore(List factCheckResults) + { + if (!factCheckResults.Any()) + return 0.0; + + double correctAnswersScore = ComputeCorrectAnswerScore(factCheckResults); + double rmseScore = ComputeRelativeRmseScore(factCheckResults); + + return Math.Round(correctAnswersScore + rmseScore, 2); + } + + private List ApplyFilters( + List factCheckResults, + string? filterByNSI, + string? filterByLLM, + string? filterByTheme) + { + return factCheckResults.Where(r => + (string.IsNullOrWhiteSpace(filterByNSI) || + r.ParsedModelResponse.ModelResponse.Prompt.Provider == filterByNSI) && + (string.IsNullOrWhiteSpace(filterByLLM) || + r.ParsedModelResponse.ModelResponse.Provider == filterByLLM) && + (string.IsNullOrWhiteSpace(filterByTheme) || + r.ParsedModelResponse.ModelResponse.Prompt.Theme == filterByTheme) + ).ToList(); + } + + private double ComputeCorrectAnswerScore(List results) + { + double total = results.Count; + double correctCount = results.Count(r => r.AnswerIsCorrect); + return (correctCount / total) * 6.0; + } + + private double ComputeRelativeRmseScore(List results) + { + double rmse = Math.Sqrt( + results.Average(r => + { + double expected = (double)r.ParsedModelResponse.ModelResponse.Prompt.Answer; + double actual = (double)r.ParsedModelResponse.Answer; + double error = expected - actual; + return error * error; + }) + ); + + double meanExpected = results.Average(r => + (double)r.ParsedModelResponse.ModelResponse.Prompt.Answer); + + double rrmse = rmse / meanExpected; + + if (rrmse <= 0.05) return 4.0; + if (rrmse <= 0.10) return 3.0; + if (rrmse <= 0.20) return 2.0; + if (rrmse <= 0.30) return 1.0; + return 0.0; + } + } +} diff --git a/Backend/Services/EvaluationPipeline.cs b/Backend/Services/EvaluationPipeline.cs index 5018823..2d83950 100644 --- a/Backend/Services/EvaluationPipeline.cs +++ b/Backend/Services/EvaluationPipeline.cs @@ -1,14 +1,16 @@ using AI_stats_measurement.Backend.Models; -using AI_stats_measurement.Backend.Models.AI_stats_measurement.Backend.Models; using AI_stats_measurement.Data; +using AI_stats_measurement.Models; using AI_stats_measurement.Services; using Azure; using Elfie.Serialization; using Google.GenAI.Types; +using Humanizer; using Microsoft.DotNet.Scaffolding.Shared; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Client; using System; +using System.Text.RegularExpressions; namespace AI_stats_measurement.Backend.Services { @@ -16,19 +18,24 @@ public class EvaluationPipeline { private readonly LlmAggregator _llmAggregator; private readonly FactChecker _checker; + private readonly SourceNormalizer _sourceNormalizer; private readonly AIMeasureDbContext _context; + private readonly AnalyticsService _analyticsService; - public EvaluationPipeline(LlmAggregator llmAggregator, FactChecker checker, AIMeasureDbContext context) + public EvaluationPipeline(LlmAggregator llmAggregator, FactChecker checker, AIMeasureDbContext context, SourceNormalizer sourceNormalizer, AnalyticsService analyticsService) { _llmAggregator = llmAggregator; _checker = checker; _context = context; + _sourceNormalizer = sourceNormalizer; + _analyticsService = analyticsService; } public async Task> RunAsync(List promptIds, CancellationToken ct) { // Step 1: Retrieve prompts from the database var prompts = await _context.Prompts + .Include(s => s.Source) .Where(p => promptIds.Contains(p.Id)) .ToListAsync(ct); @@ -49,47 +56,52 @@ public async Task> RunAsync(List promptIds, CancellationTok continue; } + ParsedModelResponse parsed = new ParsedModelResponse(0, 0, []); + // Step 3: Parse the model response - var parsed = ModelResponseParser.Parse(response.Id, response.RawText); + if (response.Prompt.Provider == "CBS") + { + parsed = ModelResponseParser.ParseDutch(response.Id, response.RawText); + } + else if (response.Prompt.Provider == "OECD") + { + parsed = ModelResponseParser.ParseEnglish(response.Id, response.RawText); + } + else if (response.Prompt.Provider == "StatBank Denmark") + { + parsed = ModelResponseParser.ParseEnglish(response.Id, response.RawText); + } + + await _sourceNormalizer.AttachNormalizedSourcesAsync(parsed, ct); _context.ParsedModelResponses.Add(parsed); await _context.SaveChangesAsync(ct); - // Retrieve the last 3 parsed responses for the same prompt to use as context for cosistency - var previousParsed = _context.ParsedModelResponses - .Include(r => r.ModelResponse) - .Where(r => r.ModelResponse.PromptId == parsed.ModelResponse.PromptId - && r.ModelResponse.Provider == parsed.ModelResponse.Provider - ) - .OrderByDescending(r => r.Id) - .Take(3) - .ToList(); - // Step 4: Fact-check the parsed response - var fact = _checker.Check(previousParsed, parsed, prompt.Answer, prompt.Source); + var fact = _checker.Check(parsed, prompt.Answer, prompt.Provider); _context.FactCheckResults.Add(fact); await _context.SaveChangesAsync(ct); + + var actualSources = parsed.ParsedModelResponseSources + .Select(p => p.SourceId) + .ToList(); // Step 5: Create export rows rows.Add(new ExportRow( theme: prompt.Theme, question: prompt.Question, expectedAnswer: prompt.Answer, - expectedSource: prompt.Source, + expectedSource: prompt.Source.Url, actualAnswer: parsed.Answer, - actualSource: parsed.Sources, + actualSource: actualSources, provider: response.Provider, rawText: response.RawText, exception: response.Exception, - squareMeanRootError: fact.SquareMeanRootError, + squareMeanRootError: 0, relativeError: fact.RelativeError, answerIsCorrect: fact.AnswerIsCorrect, sourceIsCorrect: fact.SourceIsCorrect, - averageRelativeError: fact.AverageRelativeError, - averageAnswer: fact.AverageAnswer, - averageAnswerCorrectness: fact.AverageAnswerCorrectness, - averageSourceCorrectness: fact.AverageSourceCorrectness, createdUtc: response.CreatedUtc )); } @@ -98,6 +110,8 @@ public async Task> RunAsync(List promptIds, CancellationTok await _context.SaveChangesAsync(ct); return rows; - } + } + + } } \ No newline at end of file diff --git a/Backend/Services/FactChecker.cs b/Backend/Services/FactChecker.cs index 1295c08..27424ac 100644 --- a/Backend/Services/FactChecker.cs +++ b/Backend/Services/FactChecker.cs @@ -1,5 +1,4 @@ -using AI_stats_measurement.Data; -using AI_stats_measurement.Models; +using AI_stats_measurement.Backend.Models; using AI_stats_measurement.Models; namespace AI_stats_measurement.Services @@ -15,45 +14,26 @@ public FactChecker(decimal relativeTolerance, string provider) Provider = provider; } - public FactCheckResult Check(List previousParsed, ParsedModelResponse parsed, decimal expectedAnswer, string expectedSource) + public FactCheckResult Check(ParsedModelResponse parsed, decimal expectedAnswer, string expectedSource) { if (parsed is null) throw new ArgumentNullException(nameof(parsed)); - previousParsed ??= new List(); - decimal actualAnswer = parsed.Answer; - decimal rmse = ComputeRmse(expectedAnswer, actualAnswer); - decimal relativeError = ComputeRelativeError(expectedAnswer, actualAnswer); + decimal absoluteError = AbsoluteError(expectedAnswer,actualAnswer); + decimal relativeError = ComputeRelativeError(actualAnswer, expectedAnswer); bool answerIsCorrect = relativeError <= RelativeTolerance; - bool sourceIsCorrect = ComputeSourceCorrectness(parsed.Sources, expectedSource); - - decimal averageRelativeError = ComputeAverageRelativeError(previousParsed, expectedAnswer); - decimal averageAnswer = ComputeConsistencyAnswer(previousParsed.Select(p => p.Answer).ToList()); - decimal averageAnswerCorrectness = ComputeAverageAnswerCorrectness(previousParsed, expectedAnswer); - decimal averageSourceCorrectness = ComputeAverageSourceCorrectness(previousParsed, expectedSource); + bool sourceIsCorrect = ComputeSourceCorrectness(expectedSource, parsed.ExtractedSources); - var result = new FactCheckResult( + return new FactCheckResult( parsed.Id, - rmse, + absoluteError, relativeError, answerIsCorrect, - sourceIsCorrect, - averageRelativeError, - averageAnswer, - averageAnswerCorrectness, - averageSourceCorrectness + sourceIsCorrect ); - - return result; - } - - private static decimal ComputeRmse(decimal actual, decimal predicted) - { - decimal diff = predicted - actual; - return Math.Abs(diff); } private static decimal ComputeRelativeError(decimal actual, decimal predicted) @@ -64,58 +44,26 @@ private static decimal ComputeRelativeError(decimal actual, decimal predicted) return Math.Abs(predicted - actual) / Math.Abs(actual); } - public static decimal ComputeConsistencyAnswer(List parsedModelResponses) - { - if (parsedModelResponses is null || parsedModelResponses.Count == 0) - return 0m; - - return parsedModelResponses.Average(); - } - - private decimal ComputeAverageRelativeError(List previousParsed, decimal actual) - { - if (previousParsed is null || previousParsed.Count == 0) - return 0m; - - return previousParsed - .Select(p => ComputeRelativeError(actual, p.Answer)) - .Average(); - } - - private decimal ComputeAverageAnswerCorrectness(List previousParsed, decimal actual) - { - if (previousParsed is null || previousParsed.Count == 0) - return 0m; - - return previousParsed - .Average(p => ComputeRelativeError(actual, p.Answer) <= RelativeTolerance ? 1m : 0m); - } - - private decimal ComputeAverageSourceCorrectness(List previousParsed, string expectedSource) - { - if (previousParsed is null || previousParsed.Count == 0) - return 0m; - - return previousParsed - .Average(p => ComputeSourceCorrectness(p.Sources, expectedSource) ? 1m : 0m); - } - - private static bool ComputeSourceCorrectness(List? actualSources, string expectedSource) + private static bool ComputeSourceCorrectness(string expectedSource, List? actualSources) { if (actualSources is null || actualSources.Count == 0) return false; return actualSources.Any(s => - !string.IsNullOrWhiteSpace(s) && - s.Contains("cbs", StringComparison.OrdinalIgnoreCase)); + s != null && + ( + (!string.IsNullOrWhiteSpace(s.Name) && + s.Name.Contains(expectedSource, StringComparison.OrdinalIgnoreCase)) + || + (!string.IsNullOrWhiteSpace(s.Url) && + s.Url.Contains(expectedSource, StringComparison.OrdinalIgnoreCase)) + ) + ); } - private static string NormalizeSource(string source) + private static decimal AbsoluteError(decimal actual, decimal predicted) { - return source - .Trim() - .TrimEnd('/') - .ToLowerInvariant(); + return Math.Abs(predicted - actual); } } } diff --git a/Backend/Services/ModelResponseParser.cs b/Backend/Services/ModelResponseParser.cs index 9296a1c..85cf422 100644 --- a/Backend/Services/ModelResponseParser.cs +++ b/Backend/Services/ModelResponseParser.cs @@ -1,7 +1,9 @@ -using AI_stats_measurement.Data; +using AI_stats_measurement.Backend.Models; +using AI_stats_measurement.Data; using AI_stats_measurement.Models; using Microsoft.Recognizers.Text; using Microsoft.Recognizers.Text.Number; +using Newtonsoft.Json.Linq; using System; using System.Globalization; using System.Security.Policy; @@ -11,116 +13,403 @@ namespace AI_stats_measurement.Services; public class ModelResponseParser { - private static AIMeasureDbContext _context; - - ModelResponseParser(AIMeasureDbContext context) + ModelResponseParser() { - _context = context; + } - // This regex matches URLs starting with http:// or https:// and continues until a whitespace, closing parenthesis, or closing square bracket is encountered. + private static readonly Regex MarkdownLinkRegex = + new(@"\[([^\]]+)\]\((https?:\/\/[^\s\)]+)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex UrlRegex = - new Regex(@"https?://[^\s\)\]]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); + new(@"https?:\/\/[^\s\)\]]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); + // Parses the raw text response from the model to extract a numeric answer and sources. - public static ParsedModelResponse Parse(int responseId, string? rawText) + public static ParsedModelResponse ParseDutch(int responseId, string? rawText) { if (string.IsNullOrWhiteSpace(rawText)) - return new ParsedModelResponse(responseId,0 , new List()); + return new ParsedModelResponse(responseId, 0, new List()); var text = rawText.Trim(); - List sources = ExtractSource(text); + var sources = ExtractDutchSources(text); - // Dutch culture - var results = NumberRecognizer.RecognizeNumber(text, Culture.Dutch); + var answer = ExtractDutchNumber(text); - decimal? best = null; + var response = new ParsedModelResponse(responseId, answer ?? 0, sources); - foreach (var result in results) + return response; + } + + // Parses the raw text response from the model to extract a numeric answer and sources. + public static ParsedModelResponse ParseEnglish(int responseId, string? rawText) + { + if (string.IsNullOrWhiteSpace(rawText)) + return new ParsedModelResponse(responseId, 0, new List()); + + var text = rawText.Trim(); + + var sources = ExtractEnglishSources(text); + + var answer = ExtractEnglishNumber(text); + + var response = new ParsedModelResponse(responseId, answer ?? 0, sources); + return response; + } + + private static List ExtractDutchSources(string text) + { + var sources = new List(); + + if (string.IsNullOrWhiteSpace(text)) + return sources; + + // 1. Markdown: [naam](url) + foreach (Match match in MarkdownLinkRegex.Matches(text)) { - // We are only interested in results that have a resolution with a "value" key that can be parsed as a decimal number. - if (result.Resolution == null) continue; + var name = match.Groups[1].Value.Trim(); + var url = match.Groups[2].Value.Trim().TrimEnd('.', ',', ';'); - if (!result.Resolution.TryGetValue("value", out var valueObj)) continue; + // If the name looks like a url, extract a cleaner name from the url + if (name.Contains("https")) + name = GetSourceName(name); - var valueText = valueObj?.ToString(); - if (string.IsNullOrWhiteSpace(valueText)) continue; - - // Try to parse the value as a decimal number using invariant culture to ensure consistent parsing regardless of locale. - if (!decimal.TryParse( - valueText, - NumberStyles.Number | NumberStyles.AllowLeadingSign, - CultureInfo.InvariantCulture, - out var value)) + sources.Add(new ExtractedSource { - continue; - } + Name = name, + Url = url, + Type = SourceTypeHelper.GetSourceType(url) + }); + } - // skip likely years - if (IsLikelyYear(value)) + // Remove all markdown links from the text + text = MarkdownLinkRegex.Replace(text, ""); + + // 2. urls + foreach (Match match in UrlRegex.Matches(text)) + { + var url = match.Value.Trim().TrimEnd('.', ',', ';'); + + // Check if this url was already added via markdown links + if (sources.Any(s => string.Equals(s.Url, url, StringComparison.OrdinalIgnoreCase))) continue; - best = value; - break; + var name = GetSourceName(url); + + sources.Add(new ExtractedSource + { + Name = name, + Url = url, + Type = SourceTypeHelper.GetSourceType(url) + }); } - var response = new ParsedModelResponse(responseId, best ?? 0, sources); + // 3. Sources mentioned in plain text, e.g. "Bron: CBS" or "Source: Rijksoverheid" + var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries); - return response; + foreach (var rawLine in lines) + { + var line = rawLine.Trim().Replace("**", ""); + + var bronIndex = line.IndexOf("bron:", StringComparison.OrdinalIgnoreCase); + if (bronIndex < 0) + continue; + + var sourceText = line[(bronIndex + "bron:".Length)..].Trim(); + sourceText = sourceText.TrimEnd('.', ',', ';'); + + if (string.IsNullOrWhiteSpace(sourceText)) + continue; + + // skip if already found from markdown/url + if (sources.Any(s => string.Equals(s.Name?.Trim(), sourceText, StringComparison.OrdinalIgnoreCase))) + continue; + + sources.Add(new ExtractedSource + { + Name = sourceText, + Url = null, + Type = null + }); + } + + return sources; } -private static List ExtractSource(string text) + private static List ExtractEnglishSources(string text) { - var sources = new List(); + var sources = new List(); + + if (string.IsNullOrWhiteSpace(text)) + return sources; + + // 1. Markdown: [naam](url) + foreach (Match match in MarkdownLinkRegex.Matches(text)) + { + var name = match.Groups[1].Value.Trim(); + var url = match.Groups[2].Value.Trim().TrimEnd('.', ',', ';'); + + // If the name looks like a url, extract a cleaner name from the url + if (name.Contains("https")) + name = GetSourceName(name); + sources.Add(new ExtractedSource + { + Name = name, + Url = url, + Type = SourceTypeHelper.GetSourceType(url) + }); + } + + // Remove all markdown links from the text + text = MarkdownLinkRegex.Replace(text, ""); + + // 2. urls foreach (Match match in UrlRegex.Matches(text)) { - var url = match.Value.Trim(); + var url = match.Value.Trim().TrimEnd('.', ',', ';'); - // Do not add duplicate URLs - if (!sources.Contains(url)) - sources.Add(url); + // Check if this url was already added via markdown links + if (sources.Any(s => string.Equals(s.Url, url, StringComparison.OrdinalIgnoreCase))) + continue; + + var name = GetSourceName(url); + + sources.Add(new ExtractedSource + { + Name = name, + Url = url, + Type = SourceTypeHelper.GetSourceType(url) + }); } + // 3. Sources mentioned in plain text, e.g. "Bron: CBS" or "Source: Rijksoverheid" var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) + foreach (var rawLine in lines) { - var trimmed = line.Trim().Replace("**", ""); + var line = rawLine.Trim().Replace("**", ""); - // Look for lines that contain "bron:" or "source:" (case-insensitive) and extract the text following the colon as a potential source. - if (trimmed.Contains("bron:", StringComparison.OrdinalIgnoreCase) || - trimmed.Contains("source:", StringComparison.OrdinalIgnoreCase)) + var bronIndex = line.IndexOf("source:", StringComparison.OrdinalIgnoreCase); + if (bronIndex < 0) + continue; + + var sourceText = line[(bronIndex + "source:".Length)..].Trim(); + sourceText = sourceText.TrimEnd('.', ',', ';'); + + if (string.IsNullOrWhiteSpace(sourceText)) + continue; + + // skip if already found from markdown/url + if (sources.Any(s => string.Equals(s.Name?.Trim(), sourceText, StringComparison.OrdinalIgnoreCase))) + continue; + + sources.Add(new ExtractedSource { - var idx = trimmed.IndexOf(':'); + Name = sourceText, + Url = null, + Type = null + }); + } - // If there is no colon or if the colon is at the end of the line, skip this line as it does not contain a valid source. - if (idx < 0 || idx >= trimmed.Length - 1) - continue; + return sources; + } - var source = trimmed[(idx + 1)..].Trim(); + // Extract numbers in Dutch format. + private static decimal? ExtractDutchNumber(string text) + { + text = CleanText(text); - // If the source contains a comma or a dash, take only the part before the comma or dash as the source, as these characters often indicate additional information that is not part of the source name. - var commaIndex = source.IndexOf(','); - if (commaIndex >= 0) - source = source[..commaIndex]; + var matches = Regex.Matches( + text, + @"(\d{1,3}(?:[.\s]\d{3})*(?:,\d+)?)\s*(miljoen|duizend|miljard)?", + RegexOptions.IgnoreCase); + + decimal? best = null; - var dashIndex = source.IndexOf('-'); - if (dashIndex >= 0) - source = source[..dashIndex]; + foreach (Match match in matches) + { + var numberText = match.Groups[1].Value; + + // normalize Dutch format + numberText = numberText.Replace(".", "").Replace(",", "."); + + if (!decimal.TryParse(numberText, NumberStyles.Number, + CultureInfo.InvariantCulture, out var value)) + continue; - // remove trailing dots and trim whitespace - sources.Add(source.Trim().TrimEnd('.')); + var magnitude = match.Groups[2].Value.ToLowerInvariant(); + + switch (magnitude) + { + case "duizend": + value *= 1_000m; + break; + case "ton": + value *= 1_000m; + break; + case "miljoen": + value *= 1_000_000m; + break; + case "miljard": + value *= 1_000_000_000m; + break; } + + if (best == null || value > best) + best = value; } - return sources; + return best; + } + + // Extract numbers in English format using Microsoft Recognizers Text library. + private static decimal? ExtractEnglishNumber(string text) + { + text = CleanText(text); + + var results = NumberRecognizer.RecognizeNumber(text, Culture.English); + + decimal? best = null; + + foreach (var result in results) + { + if (result.Resolution == null) + continue; + + if (!result.Resolution.TryGetValue("value", out var valueObj)) + continue; + + var valueText = valueObj?.ToString(); + if (string.IsNullOrWhiteSpace(valueText)) + continue; + + if (!decimal.TryParse(valueText, NumberStyles.Number, CultureInfo.InvariantCulture, out var value)) + continue; + + if (best == null || value > best) + best = value; + } + + return best; } - // This method checks if a given decimal value is likely to be a year, based on a reasonable range of years. - private static bool IsLikelyYear(decimal value) + + private static string CleanText(string text) + { + text = Regex.Replace(text, @"https?:\/\/\S+", ""); // remove urls + text = Regex.Replace(text, @"\*\*", ""); // markdown bold + text = Regex.Replace(text, @"\bin\s+(19|20)\d{2}\b", "", RegexOptions.IgnoreCase); // remove years "in 2021" + text = Regex.Replace(text, @"\b(19|20)\d{2}-\w+", "", RegexOptions.IgnoreCase); // remove patterns like "2020/2021" + text = Regex.Replace(text, @"\b(19|20)\d{2}\b", ""); // remove "2021-cijfers" + text = Regex.Replace(text, @"^\d{4}\s*/\s*\d{4}$", ""); // remove standalone year ranges like "1990/2000" + + return text; + } + + private static string GetSourceName(string url) { - return value >= 1990 && value <= 2030; + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return url; + + var parts = uri.Host.Split('.'); + + if (parts.Length == 0) + return uri.Host; + + if (parts[0] == "www" && parts.Length > 1) + return parts[1]; + + return parts[0]; + } + + public static class SourceTypeHelper + { + public static string GetSourceType(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + url = url.Trim(); + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + return null; + + var host = uri.Host.ToLowerInvariant(); + var path = uri.AbsolutePath.ToLowerInvariant(); + var fullUrl = url.ToLowerInvariant(); + + // CBS / NSI database + if (host.Contains("opendata.cbs.nl") || + host.Contains("ec.europa.eu") || + host.Contains("stats.oecd.org") || + host.Contains("data-explorer.oecd.org") + ) + return "NSI database"; + + // CBS / NSI webartikel + if (host.EndsWith("cbs.nl") || host.EndsWith("longreads.cbs.nl")) + { + // check for typical news/article paths + if (path.Contains("/nieuws/") || + path.Contains("/news/") || + path.Contains("/cijfers/detail/") || + path.Contains("/visualisaties/") || + path.Contains("/figures/") + ) + { + return "NSI website"; + } + + return "NSI not specific"; + } + + // OECD + if (host.EndsWith("oecd.org")) + { + // check for typical news/article paths + if (path.Contains("/publications/") || + path.Contains("/topics/") + ) + { + return "NSI website"; + } + + return "NSI not specific"; + } + + // StatBank Denmark + if (host.EndsWith("oecd.org")) + { + // check for typical news/article paths + if (path.Contains("/nieuws/") || + path.Contains("/news/") || + path.Contains("/cijfers/detail/") || + path.Contains("/visualisaties/") + ) + { + return "NSI website"; + } + + return "NSI not specific"; + } + + // INsee / NSI webartikel + if (host.EndsWith("insee.fr")) + { + // check for typical news/article paths + if (path.Contains("/statistiques/") + ) + { + return "NSI website"; + } + + return "NSI not specific"; + } + + // Rijksoverheid / Eurostat / Worldbank / etc. webartikel + return "External publication"; + } } } diff --git a/Backend/Services/SourceNormalizer.cs b/Backend/Services/SourceNormalizer.cs new file mode 100644 index 0000000..312fe88 --- /dev/null +++ b/Backend/Services/SourceNormalizer.cs @@ -0,0 +1,73 @@ +using AI_stats_measurement.Backend.Models; +using AI_stats_measurement.Data; +using AI_stats_measurement.Models; +using Microsoft.EntityFrameworkCore; + +namespace AI_stats_measurement.Backend.Services +{ + public class SourceNormalizer + { + private readonly AIMeasureDbContext _context; + + public SourceNormalizer(AIMeasureDbContext context) + { + _context = context; + } + + public async Task AttachNormalizedSourcesAsync(ParsedModelResponse parsed, CancellationToken ct) + { + var result = new List(); + + var seen = new HashSet<(string?, string?)>(); + + foreach (var extracted in parsed.ExtractedSources) + { + var name = NormalizeSource(extracted.Name); + var url = NormalizeUrl(extracted.Url); + var type = extracted.Type; + + // skip duplicates early + if (!seen.Add((name, url))) + continue; + + var source = await _context.Sources + .FirstOrDefaultAsync(s => s.Name == name && s.Url == url, ct); + + if (source == null) + { + source = new Source + { + Name = name, + Type = type, + Url = url, + }; + + _context.Sources.Add(source); + } + + result.Add(new ParsedModelResponseSource + { + Source = source + }); + } + + parsed.ParsedModelResponseSources = result; + } + + private static string? NormalizeSource(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + return value.Trim(); + } + + private static string? NormalizeUrl(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + return url.Trim().TrimEnd('/'); + } + } +} diff --git a/Tests/AnalyticsServiceTest.cs b/Tests/AnalyticsServiceTest.cs new file mode 100644 index 0000000..f629303 --- /dev/null +++ b/Tests/AnalyticsServiceTest.cs @@ -0,0 +1,22 @@ +using AI_stats_measurement.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AI_stats_measurement.Tests +{ + public class AnalyticsServiceTest + { + [Fact] + public void ComputeAccuracyScore_() + { + var text = "In 2020 was de gemiddelde verkoopprijs ongeveer € 348.000."; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Equal(348_000m, parsed.Answer); + } + } +} diff --git a/Tests/FactCheckerTests.cs b/Tests/FactCheckerTests.cs index fcbc89d..f98a259 100644 --- a/Tests/FactCheckerTests.cs +++ b/Tests/FactCheckerTests.cs @@ -7,185 +7,5 @@ namespace AI_stats_measurement.Tests { public class FactCheckerTests { - - - [Fact] - public void ComputeConsistencyAnswer_ReturnsAverage() - { - var answers = new List { 400000m, 500000m, 600000m }; - - var result = FactChecker.ComputeConsistencyAnswer(answers); - - Assert.Equal(500000m, result); - } - - [Fact] - public void ComputeConsistencyAnswer_ReturnsZero_WhenEmpty() - { - var answers = new List(); - - var result = FactChecker.ComputeConsistencyAnswer(answers); - - Assert.Equal(0m, result); - } - - [Fact] - public void Check_ComputesRelativeError_AndAnswerCorrectness() - { - var factChecker = new FactChecker(0.05m, "CBS"); - - var previousParsed = new List(); - - var parsed = new ParsedModelResponse( - 1, - 105m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.Equal(5m, result.SquareMeanRootError); - Assert.Equal(0.05m, result.RelativeError); - Assert.True(result.AnswerIsCorrect); - } - - [Fact] - public void Check_ComputesSourceCorrectness_True_WhenExpectedSourceMatches() - { - var factChecker = new FactChecker(0.05m, "cbs"); - - var previousParsed = new List(); - - var parsed = new ParsedModelResponse( - 1, - 100m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.True(result.SourceIsCorrect); - } - - [Fact] - public void Check_ComputesAverageRelativeError_FromPreviousParsed() - { - var factChecker = new FactChecker(0.05m, "cbs"); - - var previousParsed = new List - { - new ParsedModelResponse(1, 90m, new List { "https://www.cbs.nl" }), - new ParsedModelResponse(2, 110m, new List { "https://www.cbs.nl" }) - }; - - var parsed = new ParsedModelResponse( - 3, - 100m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.Equal(0.10m, result.AverageRelativeError); - } - - [Fact] - public void Check_ComputesAverageAnswer_FromPreviousParsed() - { - var factChecker = new FactChecker(0.05m, "cbs"); - - var previousParsed = new List - { - new ParsedModelResponse(1, 80m, new List { "https://www.cbs.nl" }), - new ParsedModelResponse(2, 100m, new List { "https://www.cbs.nl" }), - new ParsedModelResponse(3, 120m, new List { "https://www.cbs.nl" }) - }; - - var parsed = new ParsedModelResponse( - 4, - 100m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.Equal(100m, result.AverageAnswer); - } - - [Fact] - public void Check_ComputesAverageAnswerCorrectness() - { - var factChecker = new FactChecker(0.05m, "cbs"); - - var previousParsed = new List - { - new ParsedModelResponse(1, 100m, new List { "https://www.cbs.nl" }), // correct - new ParsedModelResponse(2, 104m, new List { "https://www.cbs.nl" }), // correct - new ParsedModelResponse(3, 130m, new List { "https://www.cbs.nl" }) // incorrect - }; - - var parsed = new ParsedModelResponse( - 4, - 100m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.Equal(2m / 3m, result.AverageAnswerCorrectness); - } - - [Fact] - public void Check_ComputesAverageSourceCorrectness() - { - var factChecker = new FactChecker(0.05m, "cbs"); - - var previousParsed = new List - { - new ParsedModelResponse(1, 100m, new List { "https://www.cbs.nl" }), - new ParsedModelResponse(2, 100m, new List { "https://www.cbs.nl" }), - new ParsedModelResponse(3, 100m, new List { "https://www.example.com" }) - }; - - var parsed = new ParsedModelResponse( - 4, - 100m, - new List { "https://www.cbs.nl" } - ); - - var result = factChecker.Check( - previousParsed, - parsed, - 100m, - "https://www.cbs.nl" - ); - - Assert.Equal(2m / 3m, result.AverageSourceCorrectness); - } } -} +} \ No newline at end of file diff --git a/Tests/ModelResponseParserTests.cs b/Tests/ModelResponseParserTests.cs index e5cb537..674d4e1 100644 --- a/Tests/ModelResponseParserTests.cs +++ b/Tests/ModelResponseParserTests.cs @@ -1,5 +1,11 @@ -using AI_stats_measurement.Services; +using AI_stats_measurement.Backend.Models; +using AI_stats_measurement.Services; +using Microsoft.CodeAnalysis.Elfie.Serialization; +using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.Blazor; +using System.Diagnostics.Metrics; +using System.Runtime.ConstrainedExecution; using Xunit; +using Xunit.Abstractions; using static System.Net.WebRequestMethods; namespace AI_stats_measurement.Tests; @@ -7,69 +13,184 @@ namespace AI_stats_measurement.Tests; public class ModelResponseParserTests { [Fact] - public void Parse_ParsesDutchThousandsCorrectlyOpenAI() + public void Parse_Extracts_DutchThousandNumber() { - var text = "In 2020 was de gemiddelde verkoopprijs van bestaande koopwoningen in Nederland ongeveer € 348.000. Deze prijs is een indicatie en kan variëren afhankelijk van de regio en het type woning. Voor de meest actuele informatie en gedetailleerde cijfers kun je de website van het Kadaster raadplegen. \n\nBron: Kadaster.nl."; - List predictedSources = ["Kadaster.nl"]; + var text = "In 2020 was de gemiddelde verkoopprijs ongeveer € 348.000."; - var parsed = ModelResponseParser.Parse(0,text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(348_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Equal(348_000m, parsed.Answer); } + + [Fact] + public void Parse_Extracts_MillionNumber() + { + var text = "In 2020 waren er ongeveer 1,1 miljoen uitkeringen."; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Equal(1_100_000m, parsed.Answer); + } + + [Fact] + public void Parse_Ignores_Year_As_Answer() + { + var text = "In 2020 waren er 68.500 studenten."; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Equal(68_500m, parsed.Answer); + } + + [Fact] + public void Parse_Extracts_Plain_Url_Source() + { + var text = "Meer informatie vind je hier: https://www.cbs.nl"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Url == "https://www.cbs.nl"); + } + + [Fact] + public void Parse_Extracts_Plain_Url_Name() + { + var text = "Meer informatie vind je hier: https://www.cbs.nl"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Name == "cbs"); + } + + [Fact] + public void Parse_Extracts_Markdown_Link_Url() + { + var text = "Bron: [CBS](https://www.cbs.nl)"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Url == "https://www.cbs.nl"); + } + + [Fact] + public void Parse_Extracts_Markdown_Link_Name() + { + var text = "Bron: [CBS](https://www.cbs.nl)"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Name == "CBS"); + } + + [Fact] + public void Parse_Extracts_Bron_Line_Text() + { + var text = "Bron: Kadaster.nl."; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Name == "Kadaster.nl"); + } + + [Fact] + public void Parse_Extracts_Markdown_Over_Url() + { + var text = "[CBS](https://www.cbs.nl) Bron: CBS"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Name == "CBS"); + } + [Fact] - public void Parse_ParsesDutchThousandsCorrectly1Gemini() + public void Parse_Extracts_Markdown_Over_SourceLine() { - var text = "Het gemiddelde verkoopprijs van bestaande koopwoningen in Nederland was in 2020 **€ 321.000**.\\n\\nBron: [CBS: Huizenprijzen 2020](https://www.cbs.nl/nl-nl/visualisaties/dashboard-huizenprijzen)"; - List predictedSources = ["https://www.cbs.nl/nl-nl/visualisaties/dashboard-huizenprijzen", "[CBS: Huizenprijzen 2020](https://www.cbs.nl/nl"]; + var text = "[CBS](https://www.cbs.nl) https://www.cbs.nl"; - var parsed = ModelResponseParser.Parse(0, text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(321_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Contains(parsed.ExtractedSources, s => s.Name == "CBS"); } + + [Fact] + public void Parse_Extracts_Markdown_AND_Url() + { + var text = "[CBS](https://www.cbs.nl) https://www.uwv.nl"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Name == "CBS"); + Assert.Contains(parsed.ExtractedSources, s => s.Name == "uwv"); + } + [Fact] - public void Parse_ParsesDutchThousandsCorrectlyGrok() + public void Parse_Extracts_Sourceline_Markdown_Name_Same_As_Url() { - var text = "De gemiddelde verkoopprijs van bestaande koopwoningen in Nederland in 2020 was **€ 299.000**.\n\n**Bron:** Kadaster (Nederlands Kadaster), jaarrapportage woningmarkt 2020. Zie [kadaster.nl](https://www.kadaster.nl/documenten/rapportages/woningmarktcijfers/2020)."; - List predictedSources = ["https://www.kadaster.nl/documenten/rapportages/woningmarktcijfers/2020", "Kadaster (Nederlands Kadaster)"]; + var text = "**Bron:** CBS, Tabel: Handel en diensten; omzet en productie. [https://www.cbs.nl](https://www.cbs.nl)"; - var parsed = ModelResponseParser.Parse(0, text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(299_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Contains(parsed.ExtractedSources, s => s.Name == "cbs"); } + + [Fact] - public void Parse_ParsesDutchThousandsCorrectlyOPenAi1() + public void Parse_Extracts__SourceType_NSI_Database() { - var text = "In 2020 waren er in Nederland ongeveer 1,1 miljoen lopende arbeidsongeschiktheidsuitkeringen op grond van de Wet langdurige zorg (Wlz) en andere arbeidsongeschiktheidswetten. Dit cijfer kan variëren afhankelijk van de bron en de gebruikte definities.\n\nVoor de meest actuele en gedetailleerde informatie raad ik aan om de website van het Centraal Bureau voor de Statistiek (CBS) te raadplegen. Daar vind je uitgebreide statistieken over arbeidsongeschiktheid in Nederland. \n\nMeer informatie vind je hier: [CBS](https://www.cbs.nl)"; - List predictedSources = ["https://www.cbs.nl"]; + var text = "https://www.cbs.nl/nl-nl/cijfers/detail/83648NED"; - var parsed = ModelResponseParser.Parse(0, text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(1_100_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Contains(parsed.ExtractedSources, s => s.Type == "NSI database"); } + [Fact] - public void Parse_ParsesDutchThousandsCorrectlyGemini1() + public void Parse_Extracts__SourceType_NSI_Not_Specific() { - var text = "In 2020 waren er in Nederland **732.000** lopende uitkeringen op grond van de Arbeidsongeschiktheidswetten (WAO/WIA).\n\nBron: CBS (Centraal Bureau voor de Statistiek) - [Arbeidsongeschiktheid; uitkeringen en arbeidsongeschikten](https://www.cbs.nl/nl-nl/cijfers/detail/arbeidsongeschiktheid-uitkeringen-en-arbeidsongeschikten) (Onder de tabel 'Uitkeringen op grond van de Arbeidsongeschiktheidswetten')"; - List predictedSources = ["https://www.cbs.nl/nl-nl/cijfers/detail/arbeidsongeschiktheid-uitkeringen-en-arbeidsongeschikten", "CBS (Centraal Bureau voor de Statistiek)",]; + var text = "https://www.cbs.nl"; - var parsed = ModelResponseParser.Parse(0, text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(732_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Contains(parsed.ExtractedSources, s => s.Type == "NSI not specific"); } + + [Fact] + public void Parse_Extracts__SourceType_NSI_Webarticle() + { + var text = "https://www.cbs.nl/nl-nl/nieuws/2026/09/eind-2025-1-1-procent-meer-mensen-met-bijstand"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Type == "NSI webarticle"); + } + + [Fact] + public void Parse_Extracts__SourceType_External_Publication() + { + var text = "https://www.uwv.nl"; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Contains(parsed.ExtractedSources, s => s.Type == "External publication"); + } + + [Fact] + public void Parse_Extracts_Miljoen_As_Full_Number() + { + var text = "De uitvoer bedroeg € 1.972 miljoen."; + + var parsed = ModelResponseParser.ParseDutch(0, text); + + Assert.Equal(1_972_000_000m, parsed.Answer); + } + [Fact] - public void Parse_ParsesDutchThousandsGrok1() + public void Parse_Returns_Zero_When_No_Answer_Is_Found() { - var text = "In 2020 waren er **ongeveer 400.000 lopende Arbeidsongeschiktheidsuitkeringen (AOW-uitkeringen)** in Nederland op grond van de Wet arbeidsongeschiktheidsverzekering zelfstandigen (Waarz) en de Wet op de arbeidsongeschiktheidsverzekering (WAO), plus circa **800.000 uitkeringen onder de Wet werk en inkomen naar arbeidsvermogen (WIA)**. Dit zijn de hoofdwetgevingen voor arbeidsongeschiktheid.\n\n**Bron:** CBS StatLine (Centraal Bureau voor de Statistiek), dataset \"Uitkeringsontvangers per uitkeringssoort\" (gepubliceerd 2021), raadpleegbaar via [https://www.cbs.nl/nl-nl/cijfers/detail/83648NED](https://www.cbs.nl/nl-nl/cijfers/detail/83648NED). Exacte aantallen kunnen licht variëren per peildatum (bijv. eind 2020: WIA ~805.000, WAO/Waarz ~398.000)."; - List predictedSources = ["https://www.cbs.nl/nl-nl/cijfers/detail/83648NED", "CBS StatLine (Centraal Bureau voor de Statistiek)"]; + var text = "Ik heb geen specifieke data gevonden."; - var parsed = ModelResponseParser.Parse(0, text); + var parsed = ModelResponseParser.ParseDutch(0, text); - Assert.Equal(400_000L, parsed.Answer); - Assert.Equal(predictedSources, parsed.Sources); + Assert.Equal(0m, parsed.Answer); } }