From 363179495603f8f043914302dc5ec8a6eb8fc054 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Tue, 14 Apr 2026 22:07:51 -0700
Subject: [PATCH 01/70] Add UI test
---
graphrag-ui/package-lock.json | 10906 ++++++++++++++++++++++++++++++++
graphrag-ui/package.json | 6 +-
2 files changed, 10909 insertions(+), 3 deletions(-)
create mode 100644 graphrag-ui/package-lock.json
diff --git a/graphrag-ui/package-lock.json b/graphrag-ui/package-lock.json
new file mode 100644
index 0000000..7def4e4
--- /dev/null
+++ b/graphrag-ui/package-lock.json
@@ -0,0 +1,10906 @@
+{
+ "name": "tg-cbot-v5",
+ "version": "0.0.5",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tg-cbot-v5",
+ "version": "0.0.5",
+ "dependencies": {
+ "@hookform/resolvers": "^3.6.0",
+ "@radix-ui/react-dialog": "^1.1.1",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.1.1",
+ "@radix-ui/react-radio-group": "^1.2.0",
+ "@radix-ui/react-select": "^2.1.1",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@react-three/drei": "9.56.1",
+ "@react-three/fiber": "8.13.3",
+ "@tailwindcss/typography": "^0.5.18",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "i18next": "^23.11.5",
+ "install": "^0.13.0",
+ "lucide-react": "^0.390.0",
+ "npm": "^10.8.1",
+ "react": "^18.3.1",
+ "react-chatbot-kit": "^2.2.2",
+ "react-dom": "^18.3.1",
+ "react-hook-form": "^7.51.5",
+ "react-i18next": "^14.1.2",
+ "react-icons": "^5.2.1",
+ "react-markdown": "^9.0.1",
+ "react-router-dom": "^6.23.1",
+ "react-use-websocket": "^4.8.1",
+ "reagraph": "4.15.19",
+ "remark-gfm": "^4.0.0",
+ "tailwind-merge": "^2.3.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.59.1",
+ "@types/node": "^25.6.0",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@typescript-eslint/eslint-plugin": "^7.5.0",
+ "@typescript-eslint/parser": "^7.5.0",
+ "@vitejs/plugin-react-swc": "^3.6.0",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^6.1.1",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.18",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
+ "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
+ "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
+ "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/animated": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
+ "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/core": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
+ "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/rafz": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
+ "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/shared": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
+ "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/three": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
+ "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/core": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/@react-spring/types": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
+ "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "9.56.1",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.56.1.tgz",
+ "integrity": "sha512-xHQHMqqn4ww62YVDoXLazFhhrM5pkzoaA/2v5ytjbKjU9hP2iHos3odxGxQEKUS0WXwduziP6ScRkdSevpDFsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "@react-spring/three": "^9.3.1",
+ "@use-gesture/react": "^10.2.0",
+ "camera-controls": "^1.38.0",
+ "detect-gpu": "^5.0.8",
+ "glsl-noise": "^0.0.0",
+ "lodash.clamp": "^4.0.3",
+ "lodash.omit": "^4.5.0",
+ "lodash.pick": "^4.4.0",
+ "maath": "^0.5.2",
+ "meshline": "^3.1.6",
+ "react-composer": "^5.0.3",
+ "react-merge-refs": "^1.1.0",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.0.8",
+ "three-mesh-bvh": "^0.5.22",
+ "three-stdlib": "^2.21.6",
+ "troika-three-text": "^0.47.1",
+ "utility-types": "^3.10.0",
+ "zustand": "^3.5.13"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=8.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "8.13.3",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.3.tgz",
+ "integrity": "sha512-mCdTUB8D1kwlsOSxGhUg5nuGHt3HN3aNFc0s9I/N7ayk+nzT2ttLdn49c56nrHu+YK+SU1xnrxe6LqftZgIRmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.1",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-gl": ">=11.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz",
+ "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.26"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.15.24",
+ "@swc/core-darwin-x64": "1.15.24",
+ "@swc/core-linux-arm-gnueabihf": "1.15.24",
+ "@swc/core-linux-arm64-gnu": "1.15.24",
+ "@swc/core-linux-arm64-musl": "1.15.24",
+ "@swc/core-linux-ppc64-gnu": "1.15.24",
+ "@swc/core-linux-s390x-gnu": "1.15.24",
+ "@swc/core-linux-x64-gnu": "1.15.24",
+ "@swc/core-linux-x64-musl": "1.15.24",
+ "@swc/core-win32-arm64-msvc": "1.15.24",
+ "@swc/core-win32-ia32-msvc": "1.15.24",
+ "@swc/core-win32-x64-msvc": "1.15.24"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz",
+ "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz",
+ "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz",
+ "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz",
+ "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz",
+ "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-ppc64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz",
+ "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-s390x-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz",
+ "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz",
+ "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz",
+ "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz",
+ "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz",
+ "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz",
+ "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.26",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
+ "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
+ "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+ "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.183.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
+ "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.0.1"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/type-utils": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+ "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+ "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
+ "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@swc/core": "^1.12.11"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.69",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
+ "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@yomguithereal/helpers": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@yomguithereal/helpers/-/helpers-1.1.1.tgz",
+ "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==",
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.17",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz",
+ "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/camera-controls": {
+ "version": "1.38.2",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.38.2.tgz",
+ "integrity": "sha512-EfzbovxLssyWpJVG9uKcazSDDIEcd1hUsPhPF/OWWnICsKY9WbLY/2S4UPW73HHbvnVeR/Z9wsWaQKtANy/2Yg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
+ "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",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/ctrl-keys": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/ctrl-keys/-/ctrl-keys-1.0.6.tgz",
+ "integrity": "sha512-fENSKrbIfvX83uHxruP3S/9GizirvgT66vHhgKHOCTVHK+22Xpud/vttg5c5IifRl+6Gom/GjE+ZSXJKf0DMTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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-binarytree": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+ "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+ "license": "MIT"
+ },
+ "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-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force-3d": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+ "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-binarytree": "1",
+ "d3-dispatch": "1 - 3",
+ "d3-octree": "1",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "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-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "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-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "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-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",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.334",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
+ "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ellipsize": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.5.1.tgz",
+ "integrity": "sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz",
+ "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "zod": "^3.22.4 || ^4.0.0",
+ "zod-validation-error": "^3.0.3 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "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",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glodrei": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/glodrei/-/glodrei-0.0.1.tgz",
+ "integrity": "sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "@mediapipe/tasks-vision": "0.10.8",
+ "@react-spring/three": "~9.6.1",
+ "@use-gesture/react": "^10.2.24",
+ "camera-controls": "^2.4.2",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.28",
+ "glsl-noise": "^0.0.0",
+ "maath": "^0.10.7",
+ "meshline": "^3.1.6",
+ "react-composer": "^5.0.3",
+ "react-merge-refs": "^1.1.0",
+ "stats-gl": "^2.0.0",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.7.0",
+ "three-stdlib": "^2.29.4",
+ "troika-three-text": "^0.47.2",
+ "tunnel-rat": "^0.1.2",
+ "utility-types": "^3.10.0",
+ "uuid": "^9.0.1",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=8.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/animated": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
+ "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/core": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
+ "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/rafz": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
+ "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
+ "license": "MIT"
+ },
+ "node_modules/glodrei/node_modules/@react-spring/shared": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
+ "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/three": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
+ "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/core": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/types": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
+ "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/glodrei/node_modules/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/glodrei/node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/three-mesh-bvh": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz",
+ "integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.151.0"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/graphology": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz",
+ "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0",
+ "obliterator": "^2.0.2"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.24.0"
+ }
+ },
+ "node_modules/graphology-indices": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz",
+ "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.4.2",
+ "mnemonist": "^0.39.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-layout": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz",
+ "integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.3.0",
+ "pandemonium": "^2.4.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-layout-forceatlas2": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
+ "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.1.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-layout-noverlap": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz",
+ "integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.3.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-metrics": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/graphology-metrics/-/graphology-metrics-2.4.0.tgz",
+ "integrity": "sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-indices": "^0.17.0",
+ "graphology-shortest-path": "^2.0.0",
+ "graphology-utils": "^2.4.4",
+ "mnemonist": "^0.39.0",
+ "pandemonium": "2.4.1"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-shortest-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz",
+ "integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@yomguithereal/helpers": "^1.1.1",
+ "graphology-indices": "^0.17.0",
+ "graphology-utils": "^2.4.3",
+ "mnemonist": "^0.39.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-types": {
+ "version": "0.24.8",
+ "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
+ "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/graphology-utils": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
+ "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphology-types": ">=0.23.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hold-event": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/hold-event/-/hold-event-0.2.0.tgz",
+ "integrity": "sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "23.16.8",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
+ "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/install": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
+ "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "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-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
+ "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.clamp": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
+ "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.omit": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.18.0.tgz",
+ "integrity": "sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==",
+ "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.",
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.390.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.390.0.tgz",
+ "integrity": "sha512-APqbfEcVuHnZbiy3E97gYWLeBdkE4e6NbY6AuVETZDZVn/bQCHYUoHyxcUHyvRopfPOHhFUEvDyyQzHwM+S9/w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.5.3.tgz",
+ "integrity": "sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.144.0",
+ "three": ">=0.144.0"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
+ "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
+ "license": "MIT"
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mnemonist": {
+ "version": "0.39.8",
+ "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
+ "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "obliterator": "^2.0.1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm": {
+ "version": "10.9.8",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.8.tgz",
+ "integrity": "sha512-fYwb6ODSmHkqrJQQaCxY3M2lPf/mpgC7ik0HSzzIwG5CGtabRp4bNqikatvCoT42b5INQSqudVH0R7yVmC9hVg==",
+ "bundleDependencies": [
+ "@isaacs/string-locale-compare",
+ "@npmcli/arborist",
+ "@npmcli/config",
+ "@npmcli/fs",
+ "@npmcli/map-workspaces",
+ "@npmcli/package-json",
+ "@npmcli/promise-spawn",
+ "@npmcli/redact",
+ "@npmcli/run-script",
+ "@sigstore/tuf",
+ "abbrev",
+ "archy",
+ "cacache",
+ "chalk",
+ "ci-info",
+ "cli-columns",
+ "fastest-levenshtein",
+ "fs-minipass",
+ "glob",
+ "graceful-fs",
+ "hosted-git-info",
+ "ini",
+ "init-package-json",
+ "is-cidr",
+ "json-parse-even-better-errors",
+ "libnpmaccess",
+ "libnpmdiff",
+ "libnpmexec",
+ "libnpmfund",
+ "libnpmhook",
+ "libnpmorg",
+ "libnpmpack",
+ "libnpmpublish",
+ "libnpmsearch",
+ "libnpmteam",
+ "libnpmversion",
+ "make-fetch-happen",
+ "minimatch",
+ "minipass",
+ "minipass-pipeline",
+ "ms",
+ "node-gyp",
+ "nopt",
+ "normalize-package-data",
+ "npm-audit-report",
+ "npm-install-checks",
+ "npm-package-arg",
+ "npm-pick-manifest",
+ "npm-profile",
+ "npm-registry-fetch",
+ "npm-user-validate",
+ "p-map",
+ "pacote",
+ "parse-conflict-json",
+ "proc-log",
+ "qrcode-terminal",
+ "read",
+ "semver",
+ "spdx-expression-parse",
+ "ssri",
+ "supports-color",
+ "tar",
+ "text-table",
+ "tiny-relative-date",
+ "treeverse",
+ "validate-npm-package-name",
+ "which",
+ "write-file-atomic"
+ ],
+ "license": "Artistic-2.0",
+ "workspaces": [
+ "docs",
+ "smoke-tests",
+ "mock-globals",
+ "mock-registry",
+ "workspaces/*"
+ ],
+ "dependencies": {
+ "@isaacs/string-locale-compare": "^1.1.0",
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/config": "^9.0.0",
+ "@npmcli/fs": "^4.0.0",
+ "@npmcli/map-workspaces": "^4.0.2",
+ "@npmcli/package-json": "^6.2.0",
+ "@npmcli/promise-spawn": "^8.0.3",
+ "@npmcli/redact": "^3.2.2",
+ "@npmcli/run-script": "^9.1.0",
+ "@sigstore/tuf": "^3.1.1",
+ "abbrev": "^3.0.1",
+ "archy": "~1.0.0",
+ "cacache": "^19.0.1",
+ "chalk": "^5.6.2",
+ "ci-info": "^4.4.0",
+ "cli-columns": "^4.0.0",
+ "fastest-levenshtein": "^1.0.16",
+ "fs-minipass": "^3.0.3",
+ "glob": "^10.5.0",
+ "graceful-fs": "^4.2.11",
+ "hosted-git-info": "^8.1.0",
+ "ini": "^5.0.0",
+ "init-package-json": "^7.0.2",
+ "is-cidr": "^5.1.1",
+ "json-parse-even-better-errors": "^4.0.0",
+ "libnpmaccess": "^9.0.0",
+ "libnpmdiff": "^7.0.5",
+ "libnpmexec": "^9.0.5",
+ "libnpmfund": "^6.0.5",
+ "libnpmhook": "^11.0.0",
+ "libnpmorg": "^7.0.0",
+ "libnpmpack": "^8.0.5",
+ "libnpmpublish": "^10.0.2",
+ "libnpmsearch": "^8.0.0",
+ "libnpmteam": "^7.0.0",
+ "libnpmversion": "^7.0.0",
+ "make-fetch-happen": "^14.0.3",
+ "minimatch": "^9.0.9",
+ "minipass": "^7.1.3",
+ "minipass-pipeline": "^1.2.4",
+ "ms": "^2.1.2",
+ "node-gyp": "^11.5.0",
+ "nopt": "^8.1.0",
+ "normalize-package-data": "^7.0.1",
+ "npm-audit-report": "^6.0.0",
+ "npm-install-checks": "^7.1.2",
+ "npm-package-arg": "^12.0.2",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-profile": "^11.0.1",
+ "npm-registry-fetch": "^18.0.2",
+ "npm-user-validate": "^3.0.0",
+ "p-map": "^7.0.4",
+ "pacote": "^19.0.1",
+ "parse-conflict-json": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "qrcode-terminal": "^0.12.0",
+ "read": "^4.1.0",
+ "semver": "^7.7.4",
+ "spdx-expression-parse": "^4.0.0",
+ "ssri": "^12.0.0",
+ "supports-color": "^9.4.0",
+ "tar": "^7.5.11",
+ "text-table": "~0.2.0",
+ "tiny-relative-date": "^1.3.0",
+ "treeverse": "^3.0.0",
+ "validate-npm-package-name": "^6.0.2",
+ "which": "^5.0.0",
+ "write-file-atomic": "^6.0.0"
+ },
+ "bin": {
+ "npm": "bin/npm-cli.js",
+ "npx": "bin/npx-cli.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/string-locale-compare": {
+ "version": "1.1.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/@npmcli/agent": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^10.0.1",
+ "socks-proxy-agent": "^8.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/arborist": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/string-locale-compare": "^1.1.0",
+ "@npmcli/fs": "^4.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/map-workspaces": "^4.0.1",
+ "@npmcli/metavuln-calculator": "^8.0.0",
+ "@npmcli/name-from-folder": "^3.0.0",
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.1",
+ "@npmcli/query": "^4.0.0",
+ "@npmcli/redact": "^3.0.0",
+ "@npmcli/run-script": "^9.0.1",
+ "bin-links": "^5.0.0",
+ "cacache": "^19.0.1",
+ "common-ancestor-path": "^1.0.1",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "json-stringify-nice": "^1.1.4",
+ "lru-cache": "^10.2.2",
+ "minimatch": "^9.0.4",
+ "nopt": "^8.0.0",
+ "npm-install-checks": "^7.1.0",
+ "npm-package-arg": "^12.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.1",
+ "pacote": "^19.0.0",
+ "parse-conflict-json": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "proggy": "^3.0.0",
+ "promise-all-reject-late": "^1.0.0",
+ "promise-call-limit": "^3.0.1",
+ "promise-retry": "^2.0.1",
+ "read-package-json-fast": "^4.0.0",
+ "semver": "^7.3.7",
+ "ssri": "^12.0.0",
+ "treeverse": "^3.0.0",
+ "walk-up-path": "^3.0.1"
+ },
+ "bin": {
+ "arborist": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/config": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/map-workspaces": "^4.0.1",
+ "@npmcli/package-json": "^6.0.1",
+ "ci-info": "^4.0.0",
+ "ini": "^5.0.0",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "walk-up-path": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/fs": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/git": {
+ "version": "6.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^8.0.0",
+ "ini": "^5.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^10.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/installed-package-contents": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-bundled": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/map-workspaces": {
+ "version": "4.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/name-from-folder": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "glob": "^10.2.2",
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/metavuln-calculator": {
+ "version": "8.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cacache": "^19.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "pacote": "^20.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": {
+ "version": "20.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^7.5.10"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/name-from-folder": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/node-gyp": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/package-json": {
+ "version": "6.2.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.5.3",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/promise-spawn": {
+ "version": "8.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/query": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/redact": {
+ "version": "3.2.2",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/run-script": {
+ "version": "9.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "node-gyp": "^11.0.0",
+ "proc-log": "^5.0.0",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/bundle": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/core": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/protobuf-specs": {
+ "version": "0.4.3",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/sign": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "make-fetch-happen": "^14.0.2",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/tuf": {
+ "version": "3.1.1",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.1",
+ "tuf-js": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/verify": {
+ "version": "2.1.1",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/abbrev": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/agent-base": {
+ "version": "7.1.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/aproba": {
+ "version": "2.1.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/archy": {
+ "version": "1.0.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/bin-links": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cmd-shim": "^7.0.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "read-cmd-shim": "^5.0.0",
+ "write-file-atomic": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/cacache": {
+ "version": "19.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^4.0.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^10.0.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^7.0.2",
+ "ssri": "^12.0.0",
+ "tar": "^7.4.3",
+ "unique-filename": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/chalk": {
+ "version": "5.6.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/chownr": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/ci-info": {
+ "version": "4.4.0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/cidr-regex": {
+ "version": "4.1.3",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "ip-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/cli-columns": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/npm/node_modules/cmd-shim": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/color-convert": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/color-name": {
+ "version": "1.1.4",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/common-ancestor-path": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/cssesc": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm/node_modules/debug": {
+ "version": "4.4.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/npm/node_modules/diff": {
+ "version": "5.2.2",
+ "inBundle": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/npm/node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/encoding": {
+ "version": "0.1.13",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/npm/node_modules/env-paths": {
+ "version": "2.2.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npm/node_modules/err-code": {
+ "version": "2.0.3",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "inBundle": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/npm/node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/npm/node_modules/fdir": {
+ "version": "6.5.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/npm/node_modules/foreground-child": {
+ "version": "3.3.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/glob": {
+ "version": "10.5.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/hosted-git-info": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "inBundle": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/npm/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm/node_modules/ignore-walk": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/npm/node_modules/ini": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/init-package-json": {
+ "version": "7.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/package-json": "^6.0.0",
+ "npm-package-arg": "^12.0.0",
+ "promzard": "^2.0.0",
+ "read": "^4.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/ip-address": {
+ "version": "10.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/npm/node_modules/ip-regex": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/is-cidr": {
+ "version": "5.1.1",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "cidr-regex": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/isexe": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/npm/node_modules/json-parse-even-better-errors": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/json-stringify-nice": {
+ "version": "1.1.4",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/jsonparse": {
+ "version": "1.3.1",
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/just-diff": {
+ "version": "6.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/just-diff-apply": {
+ "version": "5.5.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/libnpmaccess": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-package-arg": "^12.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmdiff": {
+ "version": "7.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "binary-extensions": "^2.3.0",
+ "diff": "^5.1.0",
+ "minimatch": "^9.0.4",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0",
+ "tar": "^7.5.11"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmexec": {
+ "version": "9.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/run-script": "^9.0.1",
+ "ci-info": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0",
+ "proc-log": "^5.0.0",
+ "read": "^4.0.0",
+ "read-package-json-fast": "^4.0.0",
+ "semver": "^7.3.7",
+ "walk-up-path": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmfund": {
+ "version": "6.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmhook": {
+ "version": "11.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmorg": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmpack": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/run-script": "^9.0.1",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmpublish": {
+ "version": "10.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "ci-info": "^4.0.0",
+ "normalize-package-data": "^7.0.0",
+ "npm-package-arg": "^12.0.0",
+ "npm-registry-fetch": "^18.0.1",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.7",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmsearch": {
+ "version": "8.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmteam": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmversion": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.1",
+ "@npmcli/run-script": "^9.0.1",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/make-fetch-happen": {
+ "version": "14.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/agent": "^3.0.0",
+ "cacache": "^19.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^1.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/minimatch": {
+ "version": "9.0.9",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/minipass": {
+ "version": "7.1.3",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-fetch": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minizlib": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/npm/node_modules/ms": {
+ "version": "2.1.3",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/mute-stream": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/negotiator": {
+ "version": "1.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/npm/node_modules/node-gyp": {
+ "version": "11.5.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^14.0.3",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.4.3",
+ "tinyglobby": "^0.2.12",
+ "which": "^5.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/nopt": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^3.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/normalize-package-data": {
+ "version": "7.0.1",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-audit-report": {
+ "version": "6.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-bundled": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-install-checks": {
+ "version": "7.1.2",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-normalize-package-bin": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-package-arg": {
+ "version": "12.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-packlist": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "ignore-walk": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-pick-manifest": {
+ "version": "10.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^7.1.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-profile": {
+ "version": "11.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-registry-fetch": {
+ "version": "18.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/redact": "^3.0.0",
+ "jsonparse": "^1.3.1",
+ "make-fetch-happen": "^14.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minizlib": "^3.0.1",
+ "npm-package-arg": "^12.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-user-validate": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/p-map": {
+ "version": "7.0.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/npm/node_modules/pacote": {
+ "version": "19.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^7.5.10"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/parse-conflict-json": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^4.0.0",
+ "just-diff": "^6.0.0",
+ "just-diff-apply": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/path-key": {
+ "version": "3.1.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/picomatch": {
+ "version": "4.0.3",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/npm/node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm/node_modules/proc-log": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/proggy": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/promise-all-reject-late": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/promise-call-limit": {
+ "version": "3.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/promise-retry": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm/node_modules/promzard": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "read": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/qrcode-terminal": {
+ "version": "0.12.0",
+ "inBundle": true,
+ "bin": {
+ "qrcode-terminal": "bin/qrcode-terminal.js"
+ }
+ },
+ "node_modules/npm/node_modules/read": {
+ "version": "4.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "mute-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/read-cmd-shim": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/read-package-json-fast": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/retry": {
+ "version": "0.12.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm/node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/npm/node_modules/semver": {
+ "version": "7.7.4",
+ "inBundle": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/sigstore": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "@sigstore/sign": "^3.1.0",
+ "@sigstore/tuf": "^3.1.0",
+ "@sigstore/verify": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/socks": {
+ "version": "2.8.7",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "inBundle": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/npm/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "inBundle": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/npm/node_modules/ssri": {
+ "version": "12.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/string-width": {
+ "version": "4.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/supports-color": {
+ "version": "9.4.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/tar": {
+ "version": "7.5.11",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/text-table": {
+ "version": "0.2.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/tiny-relative-date": {
+ "version": "1.3.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/npm/node_modules/treeverse": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/tuf-js": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/models": "3.0.1",
+ "debug": "^4.4.1",
+ "make-fetch-happen": "^14.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^9.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/unique-filename": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/unique-slug": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/validate-npm-package-name": {
+ "version": "6.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/walk-up-path": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/which": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/which/node_modules/isexe": {
+ "version": "3.1.5",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "5.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/write-file-atomic": {
+ "version": "6.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/yallist": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/obliterator": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
+ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pandemonium": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz",
+ "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==",
+ "license": "MIT",
+ "dependencies": {
+ "mnemonist": "^0.39.2"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-chatbot-kit": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-chatbot-kit/-/react-chatbot-kit-2.2.2.tgz",
+ "integrity": "sha512-8p/i0KkzkhoyG2XsL6Pb6f72k9j7GYNAc5SOa4f9OZwbCD3Q34uEruNPc06qa1wZHKfT6aFna19PA2plFuO2NA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-conditionally-render": "^1.0.2"
+ }
+ },
+ "node_modules/react-composer": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
+ "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-conditionally-render": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/react-conditionally-render/-/react-conditionally-render-1.0.2.tgz",
+ "integrity": "sha512-CtjIgaLHVDSgHis3gv/PT/8EnD6GPUL8PrhUjh7DP6S5Y3p56dGu7y2nVg6pYv1kv+fGznRhRmX3assr/vRw3A==",
+ "license": "ISC"
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.72.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz",
+ "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-i18next": {
+ "version": "14.1.3",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz",
+ "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 23.2.3",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-merge-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
+ "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+ "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.21.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-gesture": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz",
+ "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==",
+ "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-websocket": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz",
+ "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==",
+ "license": "MIT"
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reagraph": {
+ "version": "4.15.19",
+ "resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.15.19.tgz",
+ "integrity": "sha512-acM2agUYyNKyKLzKhnEoMNbBc58KxpBQ5wzIqYvsoVa3Se2weuB8npVfdjJZV9AxW9BaSaeu90NwCrcO3XATTg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-spring/three": "9.6.1",
+ "@react-three/fiber": "8.13.5",
+ "camera-controls": "^2.8.3",
+ "classnames": "^2.5.1",
+ "d3-array": "^3.2.4",
+ "d3-force-3d": "^3.0.3",
+ "d3-hierarchy": "^3.1.2",
+ "d3-scale": "^4.0.2",
+ "ellipsize": "^0.5.1",
+ "glodrei": "^0.0.1",
+ "graphology": "^0.25.4",
+ "graphology-layout": "^0.6.1",
+ "graphology-layout-forceatlas2": "^0.10.1",
+ "graphology-layout-noverlap": "^0.4.2",
+ "graphology-metrics": "^2.1.0",
+ "graphology-shortest-path": "^2.0.2",
+ "hold-event": "^0.2.0",
+ "react-use-gesture": "^9.1.3",
+ "reakeys": "^2.0.0",
+ "three": "^0.154.0",
+ "three-stdlib": "^2.23.13",
+ "zustand": "4.3.9"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/animated": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
+ "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/core": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
+ "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/rafz": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
+ "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/@react-spring/shared": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
+ "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/three": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
+ "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/core": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/types": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
+ "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/@react-three/fiber": {
+ "version": "8.13.5",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.5.tgz",
+ "integrity": "sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.1",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-gl": ">=11.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-three/fiber/node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reagraph/node_modules/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/reagraph/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/three": {
+ "version": "0.154.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
+ "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/zustand": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz",
+ "integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reakeys": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/reakeys/-/reakeys-2.0.6.tgz",
+ "integrity": "sha512-dmZPhOwU3NuLjy61CLqf3dGEhhetx4Du7m/DlX1eqZrBKcKrDqpR0O1tHyYMB95KVdhVRjrfcuFFawI7EqGyxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ctrl-keys": "^1.0.6"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+ "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/suspend-react": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.0.8.tgz",
+ "integrity": "sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
+ "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.183.2",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
+ "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.5.24.tgz",
+ "integrity": "sha512-VTIgfjz8aFoPKTQoMIQQv9jJD4ybFRZuKKE1/kqy78FQcuHQ0+iIWv7C5cSb2inlvs7bNMVY3yRx3RXGZfrvzQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.123.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/troika-three-text": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz",
+ "integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.47.2",
+ "troika-worker-utils": "^0.47.2",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz",
+ "integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz",
+ "integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==",
+ "license": "MIT"
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/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/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/graphrag-ui/package.json b/graphrag-ui/package.json
index 4ef113e..12cca39 100755
--- a/graphrag-ui/package.json
+++ b/graphrag-ui/package.json
@@ -19,6 +19,9 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.0",
+ "@react-three/drei": "9.56.1",
+ "@react-three/fiber": "8.13.3",
+ "@tailwindcss/typography": "^0.5.18",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"i18next": "^23.11.5",
@@ -35,12 +38,9 @@
"react-router-dom": "^6.23.1",
"react-use-websocket": "^4.8.1",
"reagraph": "4.15.19",
- "@react-three/fiber": "8.13.3",
- "@react-three/drei": "9.56.1",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
- "@tailwindcss/typography": "^0.5.18",
"zod": "^3.23.8"
},
"devDependencies": {
From d22fca4984f46686078deb4d1937c67c2db9196a Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Tue, 21 Apr 2026 22:21:21 -0700
Subject: [PATCH 02/70] Update version number
---
VERSION | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/VERSION b/VERSION
index 3a3cd8c..88c5fb8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.3.1
+1.4.0
From 51aeeb0c2760668e5e5012bec988aed6c75a96cb Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 16:09:25 +0530
Subject: [PATCH 03/70] feat: Add Trace Logs UI for agent execution
observability
---
graphrag-ui/src/actions/ActionProvider.tsx | 8 +-
.../src/components/CustomChatMessage.tsx | 14 +
graphrag-ui/src/main.tsx | 5 +
graphrag-ui/src/pages/TraceLogs.tsx | 697 ++++++++++++++++++
graphrag/app/agent/agent.py | 22 +-
5 files changed, 742 insertions(+), 4 deletions(-)
create mode 100644 graphrag-ui/src/pages/TraceLogs.tsx
diff --git a/graphrag-ui/src/actions/ActionProvider.tsx b/graphrag-ui/src/actions/ActionProvider.tsx
index 58fc7aa..4bda231 100644
--- a/graphrag-ui/src/actions/ActionProvider.tsx
+++ b/graphrag-ui/src/actions/ActionProvider.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useCallback, useEffect, useContext} from 'react';
+import React, {useState, useRef, useCallback, useEffect, useContext} from 'react';
import {createClientMessage} from 'react-chatbot-kit';
import useWebSocket, {ReadyState} from 'react-use-websocket';
import Loader from '../components/Loader';
@@ -81,6 +81,7 @@ const ActionProvider: React.FC = ({
}) => {
const selectedGraph = useContext(SelectedGraphContext);
const selectedRagPattern = useContext(RagPatternContext);
+ const lastUserQueryRef = useRef("");
const WS_URL = "/ui/" + selectedGraph + "/chat" + "?rag_pattern=" + selectedRagPattern;
const [messageHistory, setMessageHistory] = useState[]>(
[],
@@ -205,6 +206,7 @@ const ActionProvider: React.FC = ({
};
const defaultQuestions = (msg: string) => {
+ lastUserQueryRef.current = msg;
const clientMessage = createClientMessage(msg, {
delay: 300,
});
@@ -213,6 +215,7 @@ const ActionProvider: React.FC = ({
};
const queryGraphragWs = (msg) => {
+ lastUserQueryRef.current = msg;
const queryGraphragWsTest = (msg: string) => {
sendMessage(msg);
};
@@ -269,6 +272,9 @@ const ActionProvider: React.FC = ({
return; // Don't create a bot message for conversation ID
}
+ // Attach the user query so the trace page can display it
+ messageData.userQuery = lastUserQueryRef.current;
+
// Handle regular bot messages
const botMessage = createChatBotMessage(messageData);
setState((prev) => {
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 0aef2ea..285ac8f 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,6 +10,8 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
+import { LuActivity } from "react-icons/lu";
+import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
@@ -127,6 +129,7 @@ const AuthenticatedImage: FC<{ src: string; alt: string }> = ({ src, alt }) => {
export const CustomChatMessage: FC = ({
message,
}) => {
+ const navigate = useNavigate();
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
@@ -191,6 +194,17 @@ export const CustomChatMessage: FC = ({
showTable={handleShowTable}
showGraph={handleShowGraph}
/>
+ {(message.response_type !== "progress" && (message.query_sources?.result || message.query_sources?.reasoning)) && (
+ {
+ navigate("/trace", { state: { message, userQuery: message.userQuery || "" } });
+ }}
+ >
+
+ View Trace
+
+ )}
{showGraphVis ? (
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 70a14d3..2f6c599 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -4,6 +4,7 @@ import "./index.css";
import { Outlet, RouterProvider, createBrowserRouter, Navigate } from "react-router-dom";
import Chat from "./pages/Chat";
import ChatDialog from "./pages/ChatDialog.tsx";
+import TraceLogs from "./pages/TraceLogs.tsx";
import SetupLayout from "./pages/setup/SetupLayout.tsx";
import KGAdmin from "./pages/setup/KGAdmin.tsx";
import IngestGraph from "./pages/setup/IngestGraph.tsx";
@@ -56,6 +57,10 @@ const router = createBrowserRouter([
path: "/preferences",
element: ,
},
+ {
+ path: "/trace",
+ element: ,
+ },
{
path: "/setup",
element: ,
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
new file mode 100644
index 0000000..033a726
--- /dev/null
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -0,0 +1,697 @@
+import { FC, useState, useMemo } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import {
+ LuArrowLeft,
+ LuChevronDown,
+ LuChevronUp,
+ LuCopy,
+ LuDownload,
+ LuClock,
+ LuWrench,
+ LuBookOpen,
+ LuActivity,
+} from "react-icons/lu";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+interface TraceLogEntry {
+ id: number;
+ type: "tool_call" | "tool_result" | "citation";
+ timestamp: string;
+ label: string;
+ detail?: string;
+ durationMs?: number;
+ content?: string;
+ step?: number;
+}
+
+interface ToolCallEntry {
+ id: number;
+ name: string;
+ timestamp: string;
+ durationMs: number;
+ input?: string;
+ output?: string;
+}
+
+interface CitationEntry {
+ id: number;
+ source: string;
+ cited: boolean;
+ text: string;
+}
+
+interface TimelineStep {
+ step: number;
+ name: string;
+ durationMs: number;
+}
+
+interface TraceData {
+ originalQuery: string;
+ conversationContext: string[];
+ status: "completed" | "in_progress" | "failed";
+ sessionId: string;
+ timing: {
+ totalDuration: number;
+ toolExecution: number;
+ llmThinking: number;
+ startTime: string;
+ endTime: string;
+ };
+ logs: TraceLogEntry[];
+ toolCalls: ToolCallEntry[];
+ citations: CitationEntry[];
+ timeline: TimelineStep[];
+ finalResponse: string;
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function formatDuration(seconds: number): string {
+ if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`;
+ return `${seconds.toFixed(2)}s`;
+}
+
+function safeJson(obj: any, maxLen = 2000): string {
+ if (obj == null) return "N/A";
+ if (typeof obj === "string") {
+ try {
+ const parsed = JSON.parse(obj);
+ const pretty = JSON.stringify(parsed, null, 2);
+ return pretty.length > maxLen ? pretty.slice(0, maxLen) + "\n…truncated" : pretty;
+ } catch {
+ return obj.length > maxLen ? obj.slice(0, maxLen) + "\n…truncated" : obj;
+ }
+ }
+ try {
+ const s = JSON.stringify(obj, null, 2);
+ return s.length > maxLen ? s.slice(0, maxLen) + "\n…truncated" : s;
+ } catch {
+ return String(obj);
+ }
+}
+
+function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
+ const now = new Date();
+ const sessionTs = now.toISOString().replace(/[-:T]/g, "").slice(0, 15);
+ const sessionId = `chat_${sessionTs}`;
+
+ const query = userQuery || message?.originalQuery || message?.query || "N/A";
+ const qs = message?.query_sources || {};
+ const responseType = message?.response_type || "";
+ const totalResponseTime = message?.response_time || 0;
+ const ts = now.toLocaleTimeString();
+
+ // ── Tool Calls ──────────────────────────────────────────────────────────
+ // query_sources.agent_steps = array of { node, duration_s } from the
+ // actual LangGraph agent execution (collected in agent.py)
+ const toolCalls: ToolCallEntry[] = [];
+ const agentSteps: { node: string; duration_s: number }[] =
+ qs.agent_steps || [];
+
+ if (agentSteps.length > 0) {
+ agentSteps.forEach((step: { node: string; duration_s: number }, i: number) => {
+ toolCalls.push({
+ id: i + 1,
+ name: step.node,
+ timestamp: ts,
+ durationMs: Math.round(step.duration_s * 1000),
+ input: "",
+ output: "",
+ });
+ });
+ }
+
+ // ── Citations ───────────────────────────────────────────────────────────
+ const rawReasoning = qs.reasoning;
+ const finalRetrieval =
+ typeof qs.result === "object" && qs.result?.final_retrieval
+ ? qs.result.final_retrieval
+ : null;
+ const citations: CitationEntry[] = [];
+
+ if (rawReasoning && Array.isArray(rawReasoning)) {
+ rawReasoning.forEach((src: any, i: number) => {
+ if (src == null) return;
+ const raw = typeof src === "string" ? src : String(src);
+ const cited = raw.startsWith("* ");
+ const chunkName = raw.replace(/^\*\s*/, "");
+
+ let chunkText = "";
+ if (finalRetrieval && finalRetrieval[chunkName]) {
+ const val = finalRetrieval[chunkName];
+ chunkText = Array.isArray(val) ? val.join("\n\n") : String(val);
+ }
+
+ citations.push({
+ id: i + 1,
+ source: chunkName,
+ cited,
+ text: chunkText,
+ });
+ });
+ }
+
+ // ── Logs ────────────────────────────────────────────────────────────────
+ const logs: TraceLogEntry[] = [];
+ let logId = 0;
+ toolCalls.forEach((tc) => {
+ logs.push({
+ id: logId++,
+ type: "tool_call",
+ timestamp: tc.timestamp,
+ label: `${tc.name} - Input`,
+ content: tc.input,
+ });
+ logs.push({
+ id: logId++,
+ type: "tool_result",
+ timestamp: tc.timestamp,
+ label: `${tc.name} - Result`,
+ content: tc.output,
+ });
+ });
+
+ // ── Timeline ────────────────────────────────────────────────────────────
+ const timeline: TimelineStep[] = toolCalls.map((tc, i) => ({
+ step: i + 1,
+ name: tc.name,
+ durationMs: tc.durationMs,
+ }));
+
+ const totalToolSec = agentSteps.reduce(
+ (sum: number, s: { duration_s: number }) => sum + s.duration_s,
+ 0
+ );
+ const llmThinking = Math.max(0, totalResponseTime - totalToolSec);
+ const endTime = new Date(now.getTime() + totalResponseTime * 1000);
+
+ return {
+ originalQuery: query,
+ conversationContext: [`user: ${query}`],
+ status: "completed",
+ sessionId,
+ timing: {
+ totalDuration: totalResponseTime,
+ toolExecution: totalToolSec,
+ llmThinking,
+ startTime: now.toLocaleTimeString(),
+ endTime: endTime.toLocaleTimeString(),
+ },
+ logs,
+ toolCalls,
+ citations,
+ timeline,
+ finalResponse: message?.content || "",
+ };
+}
+
+// ─── Sub-components ───────────────────────────────────────────────────────────
+
+const StatusBadge: FC<{ status: string }> = ({ status }) => {
+ const color =
+ status === "completed"
+ ? "bg-emerald-500"
+ : status === "in_progress"
+ ? "bg-blue-500"
+ : "bg-red-500";
+ return (
+
+ {status}
+
+ );
+};
+
+const TimingRow: FC<{
+ items: { value: string; label: string; color: string }[];
+}> = ({ items }) => (
+
+ {items.map((item, i) => (
+
+
+ {item.value}
+
+
+ {item.label}
+
+
+ ))}
+
+);
+
+const ExpandableRow: FC<{
+ children: React.ReactNode;
+ content?: string;
+ defaultOpen?: boolean;
+}> = ({ children, content, defaultOpen = false }) => {
+ const [open, setOpen] = useState(defaultOpen);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+ {children}
+
+
+ {content && (
+ {
+ e.stopPropagation();
+ navigator.clipboard.writeText(content);
+ }}
+ title="Copy"
+ >
+
+
+ )}
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && content && (
+
+ )}
+
+ );
+};
+
+// ─── Tab Panels ───────────────────────────────────────────────────────────────
+
+const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
+ const [collapsed, setCollapsed] = useState(false);
+ const toolCount = trace.logs.filter(
+ (l) => l.type === "tool_call" || l.type === "tool_result"
+ ).length;
+ const citationCount = trace.citations.length;
+ const totalEvents = trace.logs.length;
+
+ return (
+
+
+
+
+ {trace.logs.length} log entries ({totalEvents} events)
+
+
+ Tools ({toolCount})
+
+
+ Citations ({citationCount})
+
+
+
setCollapsed((p) => !p)}
+ >
+ {collapsed ? "Expand All" : "Collapse All"}
+
+
+
+
+ {trace.logs.map((log) => {
+ const isToolCall = log.type === "tool_call";
+ const isToolResult = log.type === "tool_result";
+
+ const dotColor = isToolResult
+ ? "bg-emerald-500"
+ : "bg-blue-500";
+ const iconBg = isToolResult
+ ? "text-emerald-600 dark:text-emerald-400"
+ : "text-blue-600 dark:text-blue-400";
+ const typeLabel = isToolCall
+ ? "Tool Call"
+ : "Tool Result";
+
+ return (
+
+
+
+
+
+ {isToolCall && }
+ {isToolResult && "✓ "}
+ {typeLabel}
+
+
+ {log.timestamp}
+
+
+ {log.label}
+
+ {log.durationMs && (
+
+ ({log.durationMs}ms)
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+
+ {tc.id}
+
+ {tc.name}
+ {tc.timestamp}
+
+
+ {tc.durationMs > 0 && (
+
+ {tc.durationMs}ms
+
+ )}
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && (
+
+
+
+ Input
+
+
+ {tc.input || "N/A"}
+
+
+
+
+ Result
+
+
+ {tc.output || "N/A"}
+
+
+
+ )}
+
+ );
+};
+
+const ToolCallsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.toolCalls.map((tc) => (
+
+ ))}
+
+);
+
+
+const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+
+
+ [{c.source}]
+
+ {c.cited && (
+
+ Cited
+
+ )}
+
+
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && (
+
+ {c.text || "No content retrieved for this chunk."}
+
+ )}
+
+ );
+};
+
+const CitationsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.citations.length === 0 ? (
+
+ No citations available for this trace.
+
+ ) : (
+ trace.citations.map((c) =>
)
+ )}
+
+);
+
+const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.timeline.map((item, i) => (
+
+
+ Step {item.step}
+
+
+
+ {item.durationMs}ms
+
+ {item.name}
+
+
+ ))}
+
+);
+
+// ─── Main Page ────────────────────────────────────────────────────────────────
+
+const TraceLogs: FC = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const message = location.state?.message;
+ const userQuery = location.state?.userQuery;
+
+ const trace = useMemo(
+ () => buildTraceFromMessage(message, userQuery),
+ [message, userQuery]
+ );
+
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleDownload = () => {
+ const blob = new Blob([JSON.stringify(trace, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `trace_${trace.sessionId}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Back to Chat
+
+
Trace Logs
+
+
+
+
+ Session: {trace.sessionId}
+
+
+
+ Download
+
+
+
+
+
+
+ {/* Original Query */}
+
+
Original Query
+
+ {trace.originalQuery}
+
+
+
+ {/* Conversation Context */}
+
+
Conversation Context
+
+ {trace.conversationContext.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+
+ {/* Timing Overview */}
+
+
Timing Overview
+
+ {/* Timeline bar */}
+
+
+
+ Start
+ {trace.timing.startTime}
+ {trace.timing.endTime}
+
+
+
+
+ {/* Tabs */}
+
+
+
+
+ Logs
+
+
+ Tool Calls
+
+ {trace.toolCalls.length}
+
+
+
+ Citations
+
+ {trace.citations.length}
+
+
+
+ Timeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Final Response */}
+ {trace.finalResponse && (
+
+
Final Response
+
+
+ {trace.finalResponse}
+
+
+
+ )}
+
+
+ );
+};
+
+export default TraceLogs;
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 49b8552..735565c 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,14 +129,30 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
+ agent_steps = []
+ step_start = time.time()
+
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
- # logger.info(f"testing steps {key}: {value}")
- LogWriter.info(f"request_id={req_id_cv.get()} executed node {key}")
+ step_end = time.time()
+ step_duration = round(step_end - step_start, 3)
+ agent_steps.append({
+ "node": key,
+ "duration_s": step_duration,
+ })
+ LogWriter.info(
+ f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
+ )
+ step_start = step_end
+
+ answer = value["answer"]
+ if answer.query_sources is None:
+ answer.query_sources = {}
+ answer.query_sources["agent_steps"] = agent_steps
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
- return value["answer"]
+ return answer
except Exception as e:
metrics.llm_query_error_total.labels(self.model_name).inc()
LogWriter.error(f"request_id={req_id_cv.get()} FAILURE question_for_agent")
From d5b1ed2b8c1c5a32b7d4ca552a386e62f7b2f210 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 17:02:19 +0530
Subject: [PATCH 04/70] fix: avoid intermediate variable in agent.py return
---
graphrag/app/agent/agent.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 735565c..ddb6a2e 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -146,13 +146,12 @@ def question_for_agent(
)
step_start = step_end
- answer = value["answer"]
- if answer.query_sources is None:
- answer.query_sources = {}
- answer.query_sources["agent_steps"] = agent_steps
+ if value["answer"].query_sources is None:
+ value["answer"].query_sources = {}
+ value["answer"].query_sources["agent_steps"] = agent_steps
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
- return answer
+ return value["answer"]
except Exception as e:
metrics.llm_query_error_total.labels(self.model_name).inc()
LogWriter.error(f"request_id={req_id_cv.get()} FAILURE question_for_agent")
From 025f0b9aa9ea2d47df002f9cf205add3c484a484 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 18:23:08 +0530
Subject: [PATCH 05/70] feat: capture node input/output in agent_steps and
display in Trace Logs
---
graphrag-ui/src/pages/TraceLogs.tsx | 11 ++++-------
graphrag/app/agent/agent.py | 12 ++++++++++++
2 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 033a726..bc4a597 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -102,26 +102,23 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const query = userQuery || message?.originalQuery || message?.query || "N/A";
const qs = message?.query_sources || {};
- const responseType = message?.response_type || "";
const totalResponseTime = message?.response_time || 0;
const ts = now.toLocaleTimeString();
// ── Tool Calls ──────────────────────────────────────────────────────────
- // query_sources.agent_steps = array of { node, duration_s } from the
- // actual LangGraph agent execution (collected in agent.py)
const toolCalls: ToolCallEntry[] = [];
- const agentSteps: { node: string; duration_s: number }[] =
+ const agentSteps: { node: string; duration_s: number; input?: string; output?: string }[] =
qs.agent_steps || [];
if (agentSteps.length > 0) {
- agentSteps.forEach((step: { node: string; duration_s: number }, i: number) => {
+ agentSteps.forEach((step, i: number) => {
toolCalls.push({
id: i + 1,
name: step.node,
timestamp: ts,
durationMs: Math.round(step.duration_s * 1000),
- input: "",
- output: "",
+ input: safeJson(step.input),
+ output: safeJson(step.output),
});
});
}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index ddb6a2e..313861a 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -131,16 +131,28 @@ def question_for_agent(
agent_steps = []
step_start = time.time()
+ prev_output = input_data["input"]
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
+
+ def _safe_serialize(obj, max_len=3000):
+ try:
+ s = json.dumps(obj, default=str)
+ except Exception:
+ s = str(obj)
+ return s[:max_len] if len(s) > max_len else s
+
agent_steps.append({
"node": key,
"duration_s": step_duration,
+ "input": _safe_serialize(prev_output),
+ "output": _safe_serialize(value),
})
+ prev_output = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
)
From c741963020e62f04202469f4bbd1357fa4e61873 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 19:08:50 +0530
Subject: [PATCH 06/70] fix: move _safe_serialize above loop, remove unused
LuClock import
---
graphrag-ui/src/pages/TraceLogs.tsx | 1 -
graphrag/app/agent/agent.py | 14 +++++++-------
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index bc4a597..a3ffe34 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -7,7 +7,6 @@ import {
LuChevronUp,
LuCopy,
LuDownload,
- LuClock,
LuWrench,
LuBookOpen,
LuActivity,
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 313861a..c8a67a1 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,6 +129,13 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
+ def _safe_serialize(obj, max_len=3000):
+ try:
+ s = json.dumps(obj, default=str)
+ except Exception:
+ s = str(obj)
+ return s[:max_len] if len(s) > max_len else s
+
agent_steps = []
step_start = time.time()
prev_output = input_data["input"]
@@ -139,13 +146,6 @@ def question_for_agent(
step_end = time.time()
step_duration = round(step_end - step_start, 3)
- def _safe_serialize(obj, max_len=3000):
- try:
- s = json.dumps(obj, default=str)
- except Exception:
- s = str(obj)
- return s[:max_len] if len(s) > max_len else s
-
agent_steps.append({
"node": key,
"duration_s": step_duration,
From 0d1cef7787cf17239129dd62030856b1067f875e Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 11:39:36 +0530
Subject: [PATCH 07/70] feat: role-gated View Trace, remove truncation, fix
routing labels
---
.../src/components/CustomChatMessage.tsx | 17 +-
graphrag-ui/src/components/Interact.tsx | 31 +++-
graphrag-ui/src/pages/TraceLogs.tsx | 169 ++++++++++--------
graphrag/app/agent/agent.py | 64 ++++++-
4 files changed, 180 insertions(+), 101 deletions(-)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 285ac8f..1830018 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
-import { LuActivity } from "react-icons/lu";
import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
@@ -193,18 +192,12 @@ export const CustomChatMessage: FC = ({
showExplain={handleShowExplain}
showTable={handleShowTable}
showGraph={handleShowGraph}
+ onViewTrace={() => {
+ navigate(`/trace/${message.messageId || message.message_id || ""}`, {
+ state: { message, userQuery: message.userQuery || "" },
+ });
+ }}
/>
- {(message.response_type !== "progress" && (message.query_sources?.result || message.query_sources?.reasoning)) && (
- {
- navigate("/trace", { state: { message, userQuery: message.userQuery || "" } });
- }}
- >
-
- View Trace
-
- )}
{showGraphVis ? (
diff --git a/graphrag-ui/src/components/Interact.tsx b/graphrag-ui/src/components/Interact.tsx
index e1426f0..9f259f4 100644
--- a/graphrag-ui/src/components/Interact.tsx
+++ b/graphrag-ui/src/components/Interact.tsx
@@ -10,7 +10,8 @@ import { PiArrowsCounterClockwiseFill } from "react-icons/pi";
import { Feedback, Message } from "@/actions/ActionProvider";
import { PiGraph } from "react-icons/pi";
import { FaTable } from "react-icons/fa";
-import { LuInfo } from "react-icons/lu";
+import { LuInfo, LuActivity } from "react-icons/lu";
+import { useRoles } from "@/hooks/useRoles";
const GRAPHRAG_URL = "";
interface Interactions {
@@ -18,6 +19,7 @@ interface Interactions {
showExplain: () => boolean;
showTable: () => boolean;
showGraph: () => boolean;
+ onViewTrace?: () => void;
}
export const Interactions: FC = ({
@@ -25,8 +27,11 @@ export const Interactions: FC = ({
showExplain,
showTable,
showGraph,
+ onViewTrace,
}: Interactions) => {
const [feedback, setFeedback] = useState(Feedback.NoFeedback);
+ const { isSuperuser, isGlobalDesigner, isGraphAdmin } = useRoles();
+ const canViewTrace = isSuperuser || isGlobalDesigner || isGraphAdmin;
const sendFeedback = async (action: Feedback, message: Message) => {
const creds = sessionStorage.getItem("creds");
@@ -90,13 +95,23 @@ export const Interactions: FC = ({
*/}
- showExplain()}
- >
-
- Explain
-
+ {canViewTrace ? (
+ onViewTrace?.()}
+ >
+
+ View Trace
+
+ ) : (
+ showExplain()}
+ >
+
+ Explain
+
+ )}
maxLen ? pretty.slice(0, maxLen) + "\n…truncated" : pretty;
+ return JSON.stringify(JSON.parse(obj), null, 2);
} catch {
- return obj.length > maxLen ? obj.slice(0, maxLen) + "\n…truncated" : obj;
+ return obj;
}
}
try {
- const s = JSON.stringify(obj, null, 2);
- return s.length > maxLen ? s.slice(0, maxLen) + "\n…truncated" : s;
+ return JSON.stringify(obj, null, 2);
} catch {
return String(obj);
}
}
+const NODE_LABELS: Record = {
+ entry: "Entry",
+ supportai: "SupportAI",
+ map_question_to_schema: "Map Question to Schema",
+ generate_function: "Generate Function",
+ generate_cypher: "Generate Cypher",
+ generate_answer: "Generate Answer",
+ lookup_history: "Lookup History",
+ merge_history_context: "Merge History Context",
+ rewrite_question: "Rewrite Question",
+ apologize: "Apologize",
+ greet: "Greet",
+};
+
function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const now = new Date();
const sessionTs = now.toISOString().replace(/[-:T]/g, "").slice(0, 15);
@@ -113,7 +124,7 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
agentSteps.forEach((step, i: number) => {
toolCalls.push({
id: i + 1,
- name: step.node,
+ name: NODE_LABELS[step.node] || step.node,
timestamp: ts,
durationMs: Math.round(step.duration_s * 1000),
input: safeJson(step.input),
@@ -160,14 +171,15 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
id: logId++,
type: "tool_call",
timestamp: tc.timestamp,
- label: `${tc.name} - Input`,
+ label: `${tc.name} — Input`,
content: tc.input,
+ durationMs: tc.durationMs,
});
logs.push({
id: logId++,
- type: "tool_result",
+ type: "citation",
timestamp: tc.timestamp,
- label: `${tc.name} - Result`,
+ label: `${tc.name} — Output`,
content: tc.output,
});
});
@@ -283,7 +295,7 @@ const ExpandableRow: FC<{
{open && content && (
)}
@@ -294,24 +306,19 @@ const ExpandableRow: FC<{
const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
const [collapsed, setCollapsed] = useState(false);
- const toolCount = trace.logs.filter(
- (l) => l.type === "tool_call" || l.type === "tool_result"
- ).length;
- const citationCount = trace.citations.length;
- const totalEvents = trace.logs.length;
return (
- {trace.logs.length} log entries ({totalEvents} events)
+ {trace.logs.length} agent steps
- Tools ({toolCount})
+ Nodes ({trace.toolCalls.length})
- Citations ({citationCount})
+ Citations ({trace.citations.length})
= ({ trace }) => {
- {trace.logs.map((log) => {
- const isToolCall = log.type === "tool_call";
- const isToolResult = log.type === "tool_result";
-
- const dotColor = isToolResult
- ? "bg-emerald-500"
- : "bg-blue-500";
- const iconBg = isToolResult
- ? "text-emerald-600 dark:text-emerald-400"
- : "text-blue-600 dark:text-blue-400";
- const typeLabel = isToolCall
- ? "Tool Call"
- : "Tool Result";
-
- return (
-
-
-
-
-
- {isToolCall && }
- {isToolResult && "✓ "}
- {typeLabel}
-
-
- {log.timestamp}
-
-
- {log.label}
+ {trace.logs.map((log) => (
+
+
+
+
+
+
+ Node
+
+
+ {log.timestamp}
+
+
+ {log.label}
+
+ {log.durationMs != null && log.durationMs > 0 && (
+
+ ({log.durationMs}ms)
- {log.durationMs && (
-
- ({log.durationMs}ms)
-
- )}
-
-
+ )}
+
- );
- })}
+
+ ))}
);
@@ -408,15 +399,15 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
Input
-
+
{tc.input || "N/A"}
- Result
+ Output
-
+
{tc.output || "N/A"}
@@ -512,8 +503,34 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
const TraceLogs: FC = () => {
const location = useLocation();
const navigate = useNavigate();
- const message = location.state?.message;
- const userQuery = location.state?.userQuery;
+ const { messageId } = useParams<{ messageId: string }>();
+
+ const stateMessage = location.state?.message;
+ const stateUserQuery = location.state?.userQuery;
+
+ const [apiData, setApiData] = useState(null);
+ const [loading, setLoading] = useState(!stateMessage);
+
+ useEffect(() => {
+ if (stateMessage || !messageId) return;
+ setLoading(true);
+ fetch(`/ui/trace/${messageId}`)
+ .then((res) => {
+ if (!res.ok) throw new Error("Not found");
+ return res.json();
+ })
+ .then((data) => setApiData(data))
+ .catch(() => setApiData(null))
+ .finally(() => setLoading(false));
+ }, [messageId, stateMessage]);
+
+ const message = stateMessage || (apiData ? {
+ content: apiData.natural_language_response,
+ response_time: apiData.response_time,
+ response_type: apiData.response_type,
+ query_sources: apiData.query_sources,
+ } : null);
+ const userQuery = stateUserQuery || apiData?.user_query;
const trace = useMemo(
() => buildTraceFromMessage(message, userQuery),
@@ -536,6 +553,14 @@ const TraceLogs: FC = () => {
URL.revokeObjectURL(url);
};
+ if (loading) {
+ return (
+
+
Loading trace data...
+
+ );
+ }
+
return (
{/* Header */}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index c8a67a1..e64b389 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,35 +129,81 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
- def _safe_serialize(obj, max_len=3000):
+ def _safe(obj):
try:
- s = json.dumps(obj, default=str)
+ return json.dumps(obj, default=str)
except Exception:
- s = str(obj)
- return s[:max_len] if len(s) > max_len else s
+ return str(obj)
+
+ def _node_output(node, state):
+ """Extract the meaningful output that this node produced."""
+ _LOOKUP_LABELS = {"inquiryai": "db_search", "supportai": "vector_search"}
+ lookup = state.get("lookup_source", "")
+ lookup = _LOOKUP_LABELS.get(lookup, lookup)
+
+ if node == "entry":
+ return ""
+ elif node == "map_question_to_schema":
+ return _safe({"schema_mapping": str(state.get("schema_mapping", ""))})
+ elif node == "generate_function":
+ ctx = state.get("context", {})
+ return _safe({
+ "context": ctx if isinstance(ctx, dict) else str(ctx),
+ "lookup_source": lookup,
+ })
+ elif node == "generate_cypher":
+ ctx = state.get("context", {})
+ return _safe({
+ "cypher": ctx.get("cypher", "") if isinstance(ctx, dict) else "",
+ "reasoning": ctx.get("reasoning", "") if isinstance(ctx, dict) else "",
+ "result": ctx.get("result", "") if isinstance(ctx, dict) else "",
+ "lookup_source": lookup,
+ })
+ elif node == "supportai":
+ ctx = state.get("context", {})
+ return _safe({
+ "context": ctx if isinstance(ctx, dict) else str(ctx),
+ "lookup_source": lookup,
+ })
+ elif node == "generate_answer":
+ ans = state.get("answer")
+ return _safe({
+ "natural_language_response": getattr(ans, "natural_language_response", "") if ans else "",
+ "answered_question": getattr(ans, "answered_question", False) if ans else False,
+ "response_type": getattr(ans, "response_type", "") if ans else "",
+ })
+ elif node in ("greet", "apologize"):
+ ans = state.get("answer")
+ return getattr(ans, "natural_language_response", "") if ans else ""
+ return _safe(state)
agent_steps = []
step_start = time.time()
- prev_output = input_data["input"]
+ prev_state = {"question": input_data["input"], "conversation": input_data["conversation"]}
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
-
agent_steps.append({
"node": key,
"duration_s": step_duration,
- "input": _safe_serialize(prev_output),
- "output": _safe_serialize(value),
+ "input": _safe(prev_state),
+ "output": _node_output(key, value),
})
- prev_output = value
+ prev_state = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
)
step_start = step_end
+ # Backfill entry with routing decision
+ if len(agent_steps) >= 2 and agent_steps[0]["node"] == "entry":
+ next_node = agent_steps[1]["node"]
+ _ROUTE_LABELS = {"supportai": "vector_search", "map_question_to_schema": "db_search", "lookup_history": "history_lookup"}
+ agent_steps[0]["output"] = _safe({"routing_decision": _ROUTE_LABELS.get(next_node, next_node)})
+
if value["answer"].query_sources is None:
value["answer"].query_sources = {}
value["answer"].query_sources["agent_steps"] = agent_steps
From 043591ded48ee23ae1ca27a268d171936f8d24fe Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 14:51:52 +0530
Subject: [PATCH 08/70] feat: add trace_logs volume mount in docker-compose
---
docker-compose.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/docker-compose.yml b/docker-compose.yml
index 97a0952..449939b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,7 @@ services:
USE_CYPHER: "true"
volumes:
- ./configs/:/code/configs
+ - ./trace_logs/:/code/trace_logs
graphrag-ecc:
image: tigergraph/graphrag-ecc:latest
From de18fbb1bc4420a8bf1821d9970cc7b9a67bfa88 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 23:44:48 +0530
Subject: [PATCH 09/70] feat: add trace log save/fetch endpoints in ui.py and
nginx /trace route
---
docs/tutorials/configs/nginx.conf | 8 +++++
graphrag/app/routers/ui.py | 58 +++++++++++++++++++++++++++++++
2 files changed, 66 insertions(+)
diff --git a/docs/tutorials/configs/nginx.conf b/docs/tutorials/configs/nginx.conf
index 975d8a0..121922c 100644
--- a/docs/tutorials/configs/nginx.conf
+++ b/docs/tutorials/configs/nginx.conf
@@ -29,6 +29,14 @@ server {
proxy_pass http://graphrag-ui:3000/;
}
+ location /trace {
+ proxy_pass http://graphrag-ui:3000;
+ }
+
+ location /trace/ {
+ proxy_pass http://graphrag-ui:3000;
+ }
+
location ~^/ui/.*/chat$ {
proxy_pass http://graphrag:8000;
proxy_http_version 1.1;
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 400435d..432eb3a 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -70,6 +70,28 @@
logger = logging.getLogger(__name__)
+TRACE_LOGS_DIR = os.environ.get("TRACE_LOGS_DIR", "/code/trace_logs")
+
+def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
+ try:
+ os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+ trace_data = {
+ "message_id": message_id,
+ "conversation_id": conversation_id,
+ "user_query": user_query,
+ "response_time": elapsed,
+ "response_type": resp.response_type,
+ "answered_question": resp.answered_question,
+ "query_sources": resp.query_sources,
+ "natural_language_response": resp.natural_language_response,
+ "timestamp": time.time(),
+ }
+ filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
+ with open(filepath, "w") as f:
+ json.dump(trace_data, f, default=str)
+ except Exception:
+ logger.warning(f"Failed to save trace log for message {message_id}", exc_info=True)
+
# Validated graph name path parameter — rejects path traversal characters
ValidGraphName = Annotated[str, Path(pattern=r"^[A-Za-z_][A-Za-z0-9_]*$")]
@@ -338,6 +360,40 @@ def add_feedback(
return {"message": "feedback saved", "message_id": message.message_id}
+@router.get(route_prefix + "/trace/{message_id}")
+def get_trace_log(message_id: str):
+ filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
+ if not os.path.exists(filepath):
+ raise HTTPException(status_code=404, detail="Trace log not found")
+ with open(filepath, "r") as f:
+ return json.load(f)
+
+
+@router.get(route_prefix + "/traces/{conversation_id}")
+def list_trace_logs(conversation_id: str):
+ if not os.path.isdir(TRACE_LOGS_DIR):
+ return []
+ traces = []
+ for filename in os.listdir(TRACE_LOGS_DIR):
+ if not filename.endswith(".json"):
+ continue
+ filepath = os.path.join(TRACE_LOGS_DIR, filename)
+ try:
+ with open(filepath, "r") as f:
+ data = json.load(f)
+ if data.get("conversation_id") == conversation_id:
+ traces.append({
+ "message_id": data.get("message_id"),
+ "user_query": data.get("user_query"),
+ "response_time": data.get("response_time"),
+ "timestamp": data.get("timestamp"),
+ })
+ except Exception:
+ continue
+ traces.sort(key=lambda t: t.get("timestamp", 0))
+ return traces
+
+
@router.post(route_prefix + "/{graphname}/create_graph")
def create_graph(
graphname: ValidGraphName,
@@ -1074,6 +1130,7 @@ async def graph_query(
query_sources=resp.query_sources,
)
await write_message_to_history(message, auth)
+ _save_trace_log(message.message_id, convo_id, data, resp, elapsed)
prev_id = message.message_id
# reply
@@ -1200,6 +1257,7 @@ async def chat(
query_sources=resp.query_sources,
)
await write_message_to_history(message, usr_auth)
+ _save_trace_log(message.message_id, convo_id, data, resp, elapsed)
prev_id = message.message_id
# reply
From 7d48f0eb8fdbf2c4d6c389d411e13504063aff8d Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Sat, 18 Apr 2026 00:23:50 +0530
Subject: [PATCH 10/70] fix: update trace route to /trace/:messageId in
main.tsx
---
graphrag-ui/src/main.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 2f6c599..79def9b 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -58,7 +58,7 @@ const router = createBrowserRouter([
element: ,
},
{
- path: "/trace",
+ path: "/trace/:messageId",
element: ,
},
{
From 71bb9240c62b285daac4e349a4cf0f0388267df9 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 21 Apr 2026 15:59:05 +0530
Subject: [PATCH 11/70] fix: show all durations in seconds in TraceLogs
---
graphrag-ui/src/pages/TraceLogs.tsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 2dfc851..4b43936 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -71,7 +71,6 @@ interface TraceData {
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDuration(seconds: number): string {
- if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`;
return `${seconds.toFixed(2)}s`;
}
@@ -353,7 +352,7 @@ const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
{log.durationMs != null && log.durationMs > 0 && (
- ({log.durationMs}ms)
+ ({formatDuration(log.durationMs / 1000)})
)}
@@ -383,7 +382,7 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{tc.durationMs > 0 && (
- {tc.durationMs}ms
+ {formatDuration(tc.durationMs / 1000)}
)}
{open ? (
@@ -489,7 +488,7 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
- {item.durationMs}ms
+ {formatDuration(item.durationMs / 1000)}
{item.name}
From 96f5770e0915225bb3995f7a7989a7ca27daa8ad Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Mon, 27 Apr 2026 20:06:48 +0530
Subject: [PATCH 12/70] feat: add LLM token usage tracking per node and Token
Overview tab
---
common/llm_services/base_llm.py | 28 ++
.../src/components/CustomChatMessage.tsx | 8 +-
graphrag-ui/src/main.tsx | 1 +
graphrag-ui/src/pages/TraceLogs.tsx | 253 +++++++++++++++++-
graphrag/app/agent/agent.py | 41 ++-
5 files changed, 321 insertions(+), 10 deletions(-)
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index ba1c770..005fab1 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -23,6 +23,32 @@
logger = logging.getLogger(__name__)
+# Per-request collector for LLM usage so callers (e.g. agent trace logs) can
+# aggregate token usage without breaking the existing return signatures.
+# It's a context-local list the agent resets before each node executes.
+import contextvars as _contextvars
+
+_usage_collector: _contextvars.ContextVar = _contextvars.ContextVar(
+ "llm_usage_collector", default=None
+)
+
+
+def start_usage_collection():
+ """Begin collecting LLM usage for the current context (per node)."""
+ _usage_collector.set([])
+
+
+def get_collected_usage():
+ """Return the usage entries collected since the last start (or None)."""
+ return _usage_collector.get()
+
+
+def _record_usage(caller_name: str, usage_data: dict):
+ bucket = _usage_collector.get()
+ if bucket is not None:
+ bucket.append({"caller_name": caller_name, **usage_data})
+
+
class LLM_Model:
"""Base LLM_Model Class
@@ -95,6 +121,7 @@ def invoke_with_parser(
usage_data["total_tokens"] = cb.total_tokens
usage_data["cost"] = cb.total_cost
logger.info(f"{caller_name} usage: {usage_data}")
+ _record_usage(caller_name, usage_data)
raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output)
@@ -131,6 +158,7 @@ async def ainvoke_with_parser(
usage_data["total_tokens"] = cb.total_tokens
usage_data["cost"] = cb.total_cost
logger.info(f"{caller_name} usage: {usage_data}")
+ _record_usage(caller_name, usage_data)
raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 1830018..14ccf87 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
-import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
@@ -128,7 +127,6 @@ const AuthenticatedImage: FC<{ src: string; alt: string }> = ({ src, alt }) => {
export const CustomChatMessage: FC = ({
message,
}) => {
- const navigate = useNavigate();
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
@@ -193,9 +191,9 @@ export const CustomChatMessage: FC = ({
showTable={handleShowTable}
showGraph={handleShowGraph}
onViewTrace={() => {
- navigate(`/trace/${message.messageId || message.message_id || ""}`, {
- state: { message, userQuery: message.userQuery || "" },
- });
+ const messageId = message.messageId || message.message_id || "";
+ // No noopener — browser copies sessionStorage to the new tab automatically.
+ window.open(`/trace/${messageId}`, "_blank");
}}
/>
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 79def9b..1788c05 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -26,6 +26,7 @@ const RequireAuth = ({ children }: { children: any }) => {
return children;
};
+
const Layout = () => {
useIdleTimeout();
return (
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 4b43936..566cc4a 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -10,6 +10,8 @@ import {
LuWrench,
LuBookOpen,
LuActivity,
+ LuCoins,
+ LuInfo,
} from "react-icons/lu";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -27,6 +29,17 @@ interface TraceLogEntry {
step?: number;
}
+interface TokenUsage {
+ input_tokens: number;
+ output_tokens: number;
+ total_tokens: number;
+ cost: number;
+}
+
+interface LlmCall extends TokenUsage {
+ caller_name: string;
+}
+
interface ToolCallEntry {
id: number;
name: string;
@@ -34,6 +47,7 @@ interface ToolCallEntry {
durationMs: number;
input?: string;
output?: string;
+ usage?: TokenUsage & { calls?: LlmCall[] };
}
interface CitationEntry {
@@ -65,12 +79,14 @@ interface TraceData {
toolCalls: ToolCallEntry[];
citations: CitationEntry[];
timeline: TimelineStep[];
+ tokenUsage: TokenUsage;
finalResponse: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDuration(seconds: number): string {
+ if (seconds < 0.01) return `${Math.round(seconds * 1000)}ms`;
return `${seconds.toFixed(2)}s`;
}
@@ -116,8 +132,13 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
// ── Tool Calls ──────────────────────────────────────────────────────────
const toolCalls: ToolCallEntry[] = [];
- const agentSteps: { node: string; duration_s: number; input?: string; output?: string }[] =
- qs.agent_steps || [];
+ const agentSteps: {
+ node: string;
+ duration_s: number;
+ input?: string;
+ output?: string;
+ usage?: TokenUsage & { calls?: LlmCall[] };
+ }[] = qs.agent_steps || [];
if (agentSteps.length > 0) {
agentSteps.forEach((step, i: number) => {
@@ -128,6 +149,7 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
durationMs: Math.round(step.duration_s * 1000),
input: safeJson(step.input),
output: safeJson(step.output),
+ usage: step.usage,
});
});
}
@@ -197,6 +219,22 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const llmThinking = Math.max(0, totalResponseTime - totalToolSec);
const endTime = new Date(now.getTime() + totalResponseTime * 1000);
+ // ── Token usage totals ─────────────────────────────────────────────────
+ const serverTotal = qs.token_usage as TokenUsage | undefined;
+ const tokenUsage: TokenUsage = serverTotal || agentSteps.reduce(
+ (acc, s) => {
+ const u = s.usage;
+ if (!u) return acc;
+ return {
+ input_tokens: acc.input_tokens + (u.input_tokens || 0),
+ output_tokens: acc.output_tokens + (u.output_tokens || 0),
+ total_tokens: acc.total_tokens + (u.total_tokens || 0),
+ cost: acc.cost + (u.cost || 0),
+ };
+ },
+ { input_tokens: 0, output_tokens: 0, total_tokens: 0, cost: 0 } as TokenUsage
+ );
+
return {
originalQuery: query,
conversationContext: [`user: ${query}`],
@@ -213,10 +251,21 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
toolCalls,
citations,
timeline,
+ tokenUsage,
finalResponse: message?.content || "",
};
}
+function formatCost(cost: number): string {
+ if (!cost) return "$0.00";
+ if (cost < 0.01) return `$${cost.toFixed(6)}`;
+ return `$${cost.toFixed(4)}`;
+}
+
+function formatNumber(n: number): string {
+ return (n || 0).toLocaleString();
+}
+
// ─── Sub-components ───────────────────────────────────────────────────────────
const StatusBadge: FC<{ status: string }> = ({ status }) => {
@@ -380,6 +429,14 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{tc.timestamp}
+ {tc.usage && tc.usage.total_tokens > 0 && (
+
+ {formatNumber(tc.usage.total_tokens)} tokens
+
+ )}
{tc.durationMs > 0 && (
{formatDuration(tc.durationMs / 1000)}
@@ -394,6 +451,37 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{open && (
+ {tc.usage && tc.usage.total_tokens > 0 && (
+
+
+ LLM Usage
+
+
+
+
Input
+
{formatNumber(tc.usage.input_tokens)}
+
+
+
Output
+
{formatNumber(tc.usage.output_tokens)}
+
+
+
Total
+
{formatNumber(tc.usage.total_tokens)}
+
+
+
Cost
+
{formatCost(tc.usage.cost)}
+
+
+ {tc.usage.calls && tc.usage.calls.length > 0 && (
+
+ {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
+ {tc.usage.calls.map((c) => c.caller_name).join(", ")}
+
+ )}
+
+ )}
Input
@@ -497,6 +585,142 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
);
+const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => {
+ const usage = trace.tokenUsage;
+ const nodesWithUsage = trace.toolCalls.filter(
+ (tc) => tc.usage && tc.usage.total_tokens > 0
+ );
+
+ return (
+
+ {/* Totals */}
+
+
+
+ Input Tokens
+
+
+ {formatNumber(usage.input_tokens)}
+
+
+
+
+ Output Tokens
+
+
+ {formatNumber(usage.output_tokens)}
+
+
+
+
+ Total Tokens
+
+
+ {formatNumber(usage.total_tokens)}
+
+
+
+
+ Est. Cost
+
+
+
+ Cost is estimated based on the model's published per-token pricing. Actual billing may differ.
+
+
+
+
+
+ {formatCost(usage.cost)}
+
+
estimated
+
+
+
+ {/* Per-node breakdown */}
+
+
+
Usage by Node
+
+ {nodesWithUsage.length === 0 ? (
+
+ No LLM usage recorded for this trace.
+
+ ) : (
+
+
+
+
+ Node
+ Input
+ Output
+ Total
+
+
+ Est. Cost
+
+
+
+ Cost is estimated based on the model's published per-token pricing. Actual billing may differ.
+
+
+
+
+ LLM Calls
+
+
+
+ {nodesWithUsage.map((tc) => (
+
+ {tc.name}
+
+ {formatNumber(tc.usage!.input_tokens)}
+
+
+ {formatNumber(tc.usage!.output_tokens)}
+
+
+ {formatNumber(tc.usage!.total_tokens)}
+
+
+ {formatCost(tc.usage!.cost)}
+
+
+ {tc.usage!.calls && tc.usage!.calls.length > 0
+ ? tc.usage!.calls.map((c) => c.caller_name).join(", ")
+ : "—"}
+
+
+ ))}
+
+
+
+ Total
+
+ {formatNumber(usage.input_tokens)}
+
+
+ {formatNumber(usage.output_tokens)}
+
+
+ {formatNumber(usage.total_tokens)}
+
+
+
+ {formatCost(usage.cost)}
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
// ─── Main Page ────────────────────────────────────────────────────────────────
const TraceLogs: FC = () => {
@@ -537,7 +761,13 @@ const TraceLogs: FC = () => {
);
const handleBack = () => {
- navigate(-1);
+ // Trace opens in a new tab — closing it returns the user to the chat tab.
+ // If the tab cannot be closed (e.g. opened via direct link), fall back to navigate.
+ if (window.opener || window.history.length <= 1) {
+ window.close();
+ } else {
+ navigate(-1);
+ }
};
const handleDownload = () => {
@@ -571,7 +801,7 @@ const TraceLogs: FC = () => {
className="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-1"
>
- Back to Chat
+ Close & Back to Chat
Trace Logs
@@ -682,6 +912,18 @@ const TraceLogs: FC = () => {
>
Timeline
+
+
+ Token Overview
+ {trace.tokenUsage.total_tokens > 0 && (
+
+ {formatNumber(trace.tokenUsage.total_tokens)}
+
+ )}
+
@@ -696,6 +938,9 @@ const TraceLogs: FC = () => {
+
+
+
{/* Final Response */}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index e64b389..611f03c 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -11,7 +11,7 @@
from common.config import embedding_service, embedding_store, llm_config, get_completion_config, get_chat_config, get_llm_service
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.embedding_services import EmbeddingModel
-from common.llm_services.base_llm import LLM_Model
+from common.llm_services.base_llm import LLM_Model, start_usage_collection, get_collected_usage
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.metrics.prometheus_metrics import metrics
@@ -181,17 +181,47 @@ def _node_output(node, state):
step_start = time.time()
prev_state = {"question": input_data["input"], "conversation": input_data["conversation"]}
+ # Start collecting LLM usage so we can attribute tokens/cost per node.
+ start_usage_collection()
+
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
+
+ # Grab usage accumulated during this node and reset for next node.
+ node_usage = get_collected_usage() or []
+ input_tokens = sum(int(u.get("input_tokens", 0) or 0) for u in node_usage)
+ output_tokens = sum(int(u.get("output_tokens", 0) or 0) for u in node_usage)
+ total_tokens = sum(int(u.get("total_tokens", 0) or 0) for u in node_usage)
+ cost = sum(float(u.get("cost", 0) or 0) for u in node_usage)
+
agent_steps.append({
"node": key,
"duration_s": step_duration,
"input": _safe(prev_state),
"output": _node_output(key, value),
+ "usage": {
+ "input_tokens": input_tokens,
+ "output_tokens": output_tokens,
+ "total_tokens": total_tokens,
+ "cost": cost,
+ "calls": [
+ {
+ "caller_name": u.get("caller_name"),
+ "input_tokens": u.get("input_tokens", 0),
+ "output_tokens": u.get("output_tokens", 0),
+ "total_tokens": u.get("total_tokens", 0),
+ "cost": u.get("cost", 0),
+ }
+ for u in node_usage
+ ],
+ },
})
+ # Reset the collector for the next node.
+ start_usage_collection()
+
prev_state = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
@@ -208,6 +238,15 @@ def _node_output(node, state):
value["answer"].query_sources = {}
value["answer"].query_sources["agent_steps"] = agent_steps
+ # Aggregate total LLM usage across all nodes for the Token Overview UI.
+ total_usage = {
+ "input_tokens": sum(int(s.get("usage", {}).get("input_tokens", 0) or 0) for s in agent_steps),
+ "output_tokens": sum(int(s.get("usage", {}).get("output_tokens", 0) or 0) for s in agent_steps),
+ "total_tokens": sum(int(s.get("usage", {}).get("total_tokens", 0) or 0) for s in agent_steps),
+ "cost": sum(float(s.get("usage", {}).get("cost", 0) or 0) for s in agent_steps),
+ }
+ value["answer"].query_sources["token_usage"] = total_usage
+
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
return value["answer"]
except Exception as e:
From 41abc99cc5af3a54a172350eb00a91428de9ce73 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Mon, 27 Apr 2026 23:13:24 +0530
Subject: [PATCH 13/70] fix: deduplicate LLM caller names in Token Overview
table
---
graphrag-ui/src/pages/TraceLogs.tsx | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 566cc4a..cd82095 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -266,6 +266,17 @@ function formatNumber(n: number): string {
return (n || 0).toLocaleString();
}
+function formatCallerNames(calls: { caller_name: string }[]): string {
+ if (!calls || calls.length === 0) return "—";
+ const counts: Record = {};
+ calls.forEach((c) => {
+ counts[c.caller_name] = (counts[c.caller_name] || 0) + 1;
+ });
+ return Object.entries(counts)
+ .map(([name, count]) => (count > 1 ? `${name} ×${count}` : name))
+ .join(", ");
+}
+
// ─── Sub-components ───────────────────────────────────────────────────────────
const StatusBadge: FC<{ status: string }> = ({ status }) => {
@@ -474,12 +485,12 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{formatCost(tc.usage.cost)}
- {tc.usage.calls && tc.usage.calls.length > 0 && (
-
- {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
- {tc.usage.calls.map((c) => c.caller_name).join(", ")}
-
- )}
+ {tc.usage.calls && tc.usage.calls.length > 0 && (
+
+ {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
+ {formatCallerNames(tc.usage.calls)}
+
+ )}
)}
@@ -687,7 +698,7 @@ const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => {
{tc.usage!.calls && tc.usage!.calls.length > 0
- ? tc.usage!.calls.map((c) => c.caller_name).join(", ")
+ ? formatCallerNames(tc.usage!.calls)
: "—"}
From b2210ff7bde030daed68671b2c70142f586a74e1 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Wed, 29 Apr 2026 18:55:04 +0530
Subject: [PATCH 14/70] fix: add auth to trace endpoint, fix browser auth
dialog, async file IO, remove unused endpoint
---
.../src/components/CustomChatMessage.tsx | 4 ++-
graphrag-ui/src/pages/TraceLogs.tsx | 26 +++++++++------
graphrag/app/routers/ui.py | 33 ++++---------------
3 files changed, 25 insertions(+), 38 deletions(-)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 14ccf87..07e0d5e 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -192,7 +192,9 @@ export const CustomChatMessage: FC = ({
showGraph={handleShowGraph}
onViewTrace={() => {
const messageId = message.messageId || message.message_id || "";
- // No noopener — browser copies sessionStorage to the new tab automatically.
+ // Store message in sessionStorage so the new tab reads it directly
+ // without needing an authenticated API fetch (which triggers browser auth dialog).
+ sessionStorage.setItem(`trace_msg_${messageId}`, JSON.stringify(message));
window.open(`/trace/${messageId}`, "_blank");
}}
/>
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index cd82095..07b25ab 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -742,13 +742,23 @@ const TraceLogs: FC = () => {
const stateMessage = location.state?.message;
const stateUserQuery = location.state?.userQuery;
+ // Check sessionStorage for message stored by the opener tab before API fetch.
+ const sessionKey = messageId ? `trace_msg_${messageId}` : null;
+ const sessionRaw = sessionKey ? sessionStorage.getItem(sessionKey) : null;
+ const sessionMessage = sessionRaw ? JSON.parse(sessionRaw) : null;
+
+ const resolvedMessage = stateMessage || sessionMessage;
+
const [apiData, setApiData] = useState(null);
- const [loading, setLoading] = useState(!stateMessage);
+ const [loading, setLoading] = useState(!resolvedMessage);
useEffect(() => {
- if (stateMessage || !messageId) return;
+ if (resolvedMessage || !messageId) return;
setLoading(true);
- fetch(`/ui/trace/${messageId}`)
+ const creds = sessionStorage.getItem("creds");
+ fetch(`/ui/trace/${messageId}`, {
+ headers: { Authorization: `Basic ${creds}` },
+ })
.then((res) => {
if (!res.ok) throw new Error("Not found");
return res.json();
@@ -756,15 +766,15 @@ const TraceLogs: FC = () => {
.then((data) => setApiData(data))
.catch(() => setApiData(null))
.finally(() => setLoading(false));
- }, [messageId, stateMessage]);
+ }, [messageId, resolvedMessage]);
- const message = stateMessage || (apiData ? {
+ const message = resolvedMessage || (apiData ? {
content: apiData.natural_language_response,
response_time: apiData.response_time,
response_type: apiData.response_type,
query_sources: apiData.query_sources,
} : null);
- const userQuery = stateUserQuery || apiData?.user_query;
+ const userQuery = stateUserQuery || sessionMessage?.userQuery || apiData?.user_query;
const trace = useMemo(
() => buildTraceFromMessage(message, userQuery),
@@ -817,10 +827,6 @@ const TraceLogs: FC = () => {
Trace Logs
-
-
- Session: {trace.sessionId}
-
Date: Thu, 30 Apr 2026 16:11:52 +0530
Subject: [PATCH 15/70] feat(GML-2086): add Excel and CSV extraction support
with UI warning
---
common/requirements.txt | 2 ++
common/utils/text_extractors.py | 22 +++++++++++++++++----
graphrag-ui/src/pages/setup/IngestGraph.tsx | 10 ++++++++++
3 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/common/requirements.txt b/common/requirements.txt
index 0a7c34f..f4d5ac6 100644
--- a/common/requirements.txt
+++ b/common/requirements.txt
@@ -105,6 +105,8 @@ nest-asyncio==1.6.0
nltk==3.9.1
numpy>=1, <2
openai==1.92.2
+openpyxl>=3.1.0
+xlrd>=2.0.1
ordered-set==4.1.0
orjson==3.10.18
packaging==24.2
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 449ace5..4ba4c0f 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -137,8 +137,6 @@ def __init__(self):
'.xml': 'application/xml',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
- '.png': 'image/png',
- '.gif': 'image/gif',
'.jsonl': 'application/x-jsonlines'
}
@@ -290,7 +288,7 @@ async def process_with_semaphore(file_path):
'error': result.get('error', 'Unknown error')
})
- logger.info(f"Prepared {len(processed_files_info)} files ({len(jsonl_files_copied)} JSONL copied, {len(files_to_process)} converted), {total_docs} total documents")
+ logger.info(f"Processed {len(processed_files_info)} files, extracted {total_docs} total documents")
logger.info(f"Created {len([f for f in processed_files_info if f.get('status') == 'success'])} JSONL files in {temp_folder}")
return {
@@ -624,6 +622,22 @@ def extract_text_from_file(file_path, graphname=None):
import docx
doc = docx.Document(file_path)
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
+ elif extension in ['.xlsx', '.xls']:
+ import pandas as pd
+ engine = 'openpyxl' if extension == '.xlsx' else 'xlrd'
+ try:
+ xl = pd.ExcelFile(file_path, engine=engine)
+ except Exception:
+ xl = pd.ExcelFile(file_path)
+ sheet_texts = []
+ for sheet_name in xl.sheet_names:
+ df = xl.parse(sheet_name)
+ if df.empty:
+ continue
+ df = df.fillna('')
+ sheet_md = df.to_markdown(index=False)
+ sheet_texts.append(f"## Sheet: {sheet_name}\n\n{sheet_md}")
+ return "\n\n".join(sheet_texts) if sheet_texts else "[Excel file is empty or contains no data]"
elif extension == '.xml':
import xml.etree.ElementTree as ET
tree = ET.parse(file_path)
@@ -663,7 +677,7 @@ def get_doc_type_from_extension(extension):
def get_supported_extensions():
"""Get list of supported file extensions."""
- return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif'}
+ return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls'}
def is_supported_file(file_path):
"""Check if a file is supported for text extraction."""
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index db9677a..d920b87 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -1006,6 +1006,16 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
? `Upload destination: uploads/${ingestGraphName}/`
: ""}
+ {selectedFiles && Array.from(selectedFiles).some((f) =>
+ [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase())
+ ) && (
+
+
ℹ️
+
+ CSV and Excel files will be treated as unstructured text documents.
+
+
+ )}
From 352d58a187b3990c715786742d219020ca31c751 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:21:14 +0530
Subject: [PATCH 16/70] fix(GML-2086): preserve all rows for headerless Excel
sheets
---
common/utils/text_extractors.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 4ba4c0f..891acdf 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -631,10 +631,20 @@ def extract_text_from_file(file_path, graphname=None):
xl = pd.ExcelFile(file_path)
sheet_texts = []
for sheet_name in xl.sheet_names:
- df = xl.parse(sheet_name)
+ # Always read with header=None so no data row is silently
+ # consumed as column names for headerless spreadsheets.
+ df = xl.parse(sheet_name, header=None)
if df.empty:
continue
df = df.fillna('')
+ # Detect header row: first row is all non-empty strings with
+ # no purely numeric values → treat as column names.
+ first_row = df.iloc[0]
+ if all(isinstance(v, str) and v.strip() for v in first_row):
+ df.columns = first_row.tolist()
+ df = df.iloc[1:].reset_index(drop=True)
+ else:
+ df.columns = [f"Column {i + 1}" for i in range(len(df.columns))]
sheet_md = df.to_markdown(index=False)
sheet_texts.append(f"## Sheet: {sheet_name}\n\n{sheet_md}")
return "\n\n".join(sheet_texts) if sheet_texts else "[Excel file is empty or contains no data]"
From c8230805d4faba90294b6ee625f8916448502b7e Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:33:39 +0530
Subject: [PATCH 17/70] fix(GML-2086): align supported_extensions dict with
get_supported_extensions
---
common/utils/text_extractors.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 891acdf..957387d 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -137,6 +137,8 @@ def __init__(self):
'.xml': 'application/xml',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
+ '.png': 'image/png',
+ '.gif': 'image/gif',
'.jsonl': 'application/x-jsonlines'
}
@@ -687,7 +689,7 @@ def get_doc_type_from_extension(extension):
def get_supported_extensions():
"""Get list of supported file extensions."""
- return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls'}
+ return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.doc', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls', '.jsonl'}
def is_supported_file(file_path):
"""Check if a file is supported for text extraction."""
From f75a9d2ef2547f0773ce32a04d09c4222f90ac0a Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:37:26 +0530
Subject: [PATCH 18/70] fix(GML-2086): handle non-UTF-8 encodings in CSV
extraction
---
common/utils/text_extractors.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 957387d..82442ba 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -613,9 +613,22 @@ def extract_text_from_file(file_path, graphname=None):
if extension in ['.txt', '.md']:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
- elif extension in ['.html', '.htm', '.csv']:
+ elif extension in ['.html', '.htm']:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
+ elif extension == '.csv':
+ raw = file_path.read_bytes()
+ # utf-8-sig handles UTF-8 with BOM (common Excel CSV export)
+ try:
+ return raw.decode('utf-8-sig').strip()
+ except UnicodeDecodeError:
+ pass
+ # Fall back to chardet detection
+ import chardet
+ detected = chardet.detect(raw)
+ encoding = detected.get('encoding') if detected.get('confidence', 0) >= 0.5 else None
+ # latin-1 as final fallback — never raises DecodeError
+ return raw.decode(encoding or 'latin-1').strip()
elif extension == '.json':
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
From 58b6d3281baa7c9f63a29585cf0cf39a932c3959 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Mon, 4 May 2026 19:23:59 +0530
Subject: [PATCH 19/70] fix: remove chunk text from citations in trace logs UI
and backend JSON
---
graphrag-ui/src/pages/TraceLogs.tsx | 68 ++++++++---------------------
graphrag/app/routers/ui.py | 11 ++++-
2 files changed, 28 insertions(+), 51 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 07b25ab..bddf861 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -156,10 +156,6 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
// ── Citations ───────────────────────────────────────────────────────────
const rawReasoning = qs.reasoning;
- const finalRetrieval =
- typeof qs.result === "object" && qs.result?.final_retrieval
- ? qs.result.final_retrieval
- : null;
const citations: CitationEntry[] = [];
if (rawReasoning && Array.isArray(rawReasoning)) {
@@ -169,17 +165,11 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const cited = raw.startsWith("* ");
const chunkName = raw.replace(/^\*\s*/, "");
- let chunkText = "";
- if (finalRetrieval && finalRetrieval[chunkName]) {
- const val = finalRetrieval[chunkName];
- chunkText = Array.isArray(val) ? val.join("\n\n") : String(val);
- }
-
citations.push({
id: i + 1,
source: chunkName,
cited,
- text: chunkText,
+ text: "",
});
});
}
@@ -524,47 +514,25 @@ const ToolCallsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
);
-const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => {
- const [open, setOpen] = useState(false);
- return (
-
-
setOpen((p) => !p)}
- >
-
-
-
- [{c.source}]
-
- {c.cited && (
-
- Cited
-
- )}
-
-
- {open ? (
-
- ) : (
-
- )}
-
-
- {open && (
-
- {c.text || "No content retrieved for this chunk."}
-
+const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => (
+
+
+
+ [{c.source}]
+ {c.cited && (
+
+ Cited
+
)}
- );
-};
+
+);
const CitationsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 5ddfb17..79dff2a 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -75,6 +75,15 @@
def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
try:
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+
+ # Strip chunk text from query_sources to keep trace files small.
+ # final_retrieval contains the full text of every retrieved chunk.
+ query_sources = dict(resp.query_sources) if resp.query_sources else {}
+ result = query_sources.get("result")
+ if isinstance(result, dict) and "final_retrieval" in result:
+ result = {k: v for k, v in result.items() if k != "final_retrieval"}
+ query_sources = {**query_sources, "result": result}
+
trace_data = {
"message_id": message_id,
"conversation_id": conversation_id,
@@ -82,7 +91,7 @@ def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp
"response_time": elapsed,
"response_type": resp.response_type,
"answered_question": resp.answered_question,
- "query_sources": resp.query_sources,
+ "query_sources": query_sources,
"natural_language_response": resp.natural_language_response,
"timestamp": time.time(),
}
From ae872a590b2bec2ecc4504568dbac0d66aeac4a3 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Sat, 2 May 2026 12:22:44 -0700
Subject: [PATCH 20/70] v1.4.0: schema-aware ingest, prompt centralization,
faster startup
Schema-aware ingest
- Add Initialize Knowledge Graph flow with optional schema generation
from samples or pasted GSQL, reviewable in a form-mode editor
- Populate declared domain vertex / edge instances during extraction;
opt-in strict mode drops non-schema extractions
- Add typed-relationship metadata layer (EntityType / RelationshipType)
carrying per-type definitions used by retrievers and the chat agent;
auto-fills from extracted types when no domain schema is declared
- Surface domain vertex type labels in retrieval LLM context
- Thread schema definitions into LLM prompts for query generation and
extraction; permissive parser accepts UNDIRECTED edges
- Add E2E test covering the schema-aware initialization and ingest path
Prompt management
- Centralize all customizable prompts as in-code defaults
- Make schema-extraction prompt customizable via the /prompts API
- Lift prompt_path to global llm_config scope
- Validate and escape user-customized prompts server-side
- Add E2E test covering the customizable-prompt round-trip
Startup / runtime
- Async embedding-store initialization
- Parallel image description with concurrency knob
- Skip redundant GDS install when already present
- Auto-default Bedrock max_tokens per model family so large prompts
do not truncate
- Surface TigerGraph version mismatch as a clear startup error
- Embedding-store status check returns HTTP 503 on failure
Reliability
- Detect real GSQL failures in schema-change apply
---
.github/workflows/cloud-build-deploy-ci.yaml | 9 +
.github/workflows/cloud-build-nightly.yaml | 9 +
.github/workflows/onprem-build-nightly.yaml | 9 +
.github/workflows/onprem-build.yaml | 9 +
CHANGELOG.md | 33 +
README.md | 120 +-
common/config.py | 121 +-
common/db/schema_extraction.py | 115 ++
common/db/schema_utils.py | 1259 +++++++++++++++++
.../embeddings/tigergraph_embedding_store.py | 53 +-
.../LLMEntityRelationshipExtractor.py | 103 ++
common/gsql/supportai/SupportAI_Schema.gsql | 5 +-
.../create_entity_type_relationships.gsql | 20 -
.../retrievers/Content_Similarity_Search.gsql | 6 +
.../Content_Similarity_Vector_Search.gsql | 6 +
.../retrievers/GraphRAG_Hybrid_Search.gsql | 12 +-
.../GraphRAG_Hybrid_Search_Display.gsql | 8 +-
.../GraphRAG_Hybrid_Vector_Search.gsql | 8 +-
common/llm_services/aws_bedrock_service.py | 80 +-
common/llm_services/base_llm.py | 442 ++++--
.../chatbot_response.txt | 17 -
.../community_summarization.txt | 11 -
.../entity_relationship_extraction.txt | 24 -
.../generate_cypher.txt | 85 --
.../generate_function.txt | 27 -
.../graphrag_scoring.txt | 7 -
.../map_question_to_schema.txt | 14 -
.../aws_bedrock_titan/generate_function.txt | 14 -
.../map_question_to_schema.txt | 19 -
.../entity_relationship_extraction.txt | 24 -
.../generate_function.txt | 29 -
.../map_question_to_schema.txt | 17 -
.../prompts/custom/aml/chatbot_response.txt | 29 -
.../custom/aml/community_summarization.txt | 11 -
.../aml/entity_relationship_extraction.txt | 24 -
common/prompts/custom/aml/generate_cypher.txt | 85 --
.../prompts/custom/aml/generate_function.txt | 27 -
.../prompts/custom/aml/graphrag_scoring.txt | 7 -
.../custom/aml/map_question_to_schema.txt | 14 -
.../community_summarization.txt | 11 -
.../entity_relationship_extraction.txt | 24 -
.../gcp_vertexai_palm/generate_function.txt | 33 -
.../map_question_to_schema.txt | 14 -
.../google_gemini/chatbot_response.txt | 17 -
.../google_gemini/community_summarization.txt | 11 -
.../entity_relationship_extraction.txt | 24 -
.../prompts/google_gemini/generate_cypher.txt | 84 --
.../google_gemini/generate_function.txt | 24 -
.../google_gemini/graphrag_scoring.txt | 7 -
.../google_gemini/map_question_to_schema.txt | 15 -
.../google_gemini/question_expansion.txt | 6 -
.../prompts/llama_70b/generate_function.txt | 24 -
.../llama_70b/map_question_to_schema.txt | 15 -
.../prompts/openai_gpt4/chatbot_response.txt | 17 -
.../openai_gpt4/community_summarization.txt | 11 -
.../entity_relationship_extraction.txt | 24 -
.../prompts/openai_gpt4/generate_cypher.txt | 85 --
.../prompts/openai_gpt4/generate_function.txt | 27 -
.../prompts/openai_gpt4/graphrag_scoring.txt | 7 -
.../openai_gpt4/map_question_to_schema.txt | 15 -
.../openai_gpt4/question_expansion.txt | 6 -
common/utils/image_data_extractor.py | 3 +
common/utils/prompt_validation.py | 135 ++
common/utils/text_extractors.py | 155 +-
ecc/app/ecc_util.py | 4 +-
ecc/app/eventual_consistency_checker.py | 22 +-
ecc/app/graphrag/community_summarizer.py | 2 +-
ecc/app/graphrag/graph_rag.py | 13 +-
ecc/app/graphrag/util.py | 129 +-
ecc/app/graphrag/workers.py | 365 ++++-
ecc/app/main.py | 17 +-
ecc/app/supportai/supportai_init.py | 4 +-
ecc/app/supportai/util.py | 4 +-
ecc/app/supportai/workers.py | 32 +-
graphrag-ui/src/components/SideMenu.tsx | 7 +-
graphrag-ui/src/pages/Setup.tsx | 11 +-
.../src/pages/setup/GraphRAGConfig.tsx | 58 +
graphrag-ui/src/pages/setup/KGAdmin.tsx | 1085 +++++++++++++-
graphrag-ui/src/utils/safeJson.ts | 18 +
graphrag/app/agent/agent.py | 6 +-
graphrag/app/routers/inquiryai.py | 53 +-
graphrag/app/routers/supportai.py | 37 +-
graphrag/app/routers/ui.py | 502 +++++--
graphrag/app/supportai/supportai_ingest.py | 63 +-
graphrag/app/tools/generate_cypher.py | 22 +-
graphrag/app/tools/generate_gsql.py | 22 +-
graphrag/tests/conftest.py | 24 +-
.../tests/test_e2e_prompt_customization.py | 300 ++++
.../tests/test_e2e_schema_aware_ingest.py | 692 +++++++++
.../test_llm_entity_relationship_extractor.py | 143 ++
graphrag/tests/test_prompt_validation.py | 184 +++
graphrag/tests/test_schema_extraction.py | 200 +++
graphrag/tests/test_schema_utils.py | 1133 +++++++++++++++
93 files changed, 7304 insertions(+), 1554 deletions(-)
create mode 100644 common/db/schema_extraction.py
create mode 100644 common/db/schema_utils.py
delete mode 100644 common/gsql/supportai/create_entity_type_relationships.gsql
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/community_summarization.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/generate_function.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt
delete mode 100644 common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt
delete mode 100644 common/prompts/aws_bedrock_titan/generate_function.txt
delete mode 100644 common/prompts/aws_bedrock_titan/map_question_to_schema.txt
delete mode 100644 common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt
delete mode 100644 common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt
delete mode 100644 common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt
delete mode 100644 common/prompts/custom/aml/chatbot_response.txt
delete mode 100644 common/prompts/custom/aml/community_summarization.txt
delete mode 100644 common/prompts/custom/aml/entity_relationship_extraction.txt
delete mode 100644 common/prompts/custom/aml/generate_cypher.txt
delete mode 100644 common/prompts/custom/aml/generate_function.txt
delete mode 100644 common/prompts/custom/aml/graphrag_scoring.txt
delete mode 100644 common/prompts/custom/aml/map_question_to_schema.txt
delete mode 100644 common/prompts/gcp_vertexai_palm/community_summarization.txt
delete mode 100644 common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt
delete mode 100644 common/prompts/gcp_vertexai_palm/generate_function.txt
delete mode 100644 common/prompts/gcp_vertexai_palm/map_question_to_schema.txt
delete mode 100644 common/prompts/google_gemini/chatbot_response.txt
delete mode 100644 common/prompts/google_gemini/community_summarization.txt
delete mode 100644 common/prompts/google_gemini/entity_relationship_extraction.txt
delete mode 100644 common/prompts/google_gemini/generate_cypher.txt
delete mode 100644 common/prompts/google_gemini/generate_function.txt
delete mode 100644 common/prompts/google_gemini/graphrag_scoring.txt
delete mode 100644 common/prompts/google_gemini/map_question_to_schema.txt
delete mode 100644 common/prompts/google_gemini/question_expansion.txt
delete mode 100644 common/prompts/llama_70b/generate_function.txt
delete mode 100644 common/prompts/llama_70b/map_question_to_schema.txt
delete mode 100644 common/prompts/openai_gpt4/chatbot_response.txt
delete mode 100644 common/prompts/openai_gpt4/community_summarization.txt
delete mode 100644 common/prompts/openai_gpt4/entity_relationship_extraction.txt
delete mode 100644 common/prompts/openai_gpt4/generate_cypher.txt
delete mode 100644 common/prompts/openai_gpt4/generate_function.txt
delete mode 100644 common/prompts/openai_gpt4/graphrag_scoring.txt
delete mode 100644 common/prompts/openai_gpt4/map_question_to_schema.txt
delete mode 100644 common/prompts/openai_gpt4/question_expansion.txt
create mode 100644 common/utils/prompt_validation.py
create mode 100644 graphrag-ui/src/utils/safeJson.ts
create mode 100644 graphrag/tests/test_e2e_prompt_customization.py
create mode 100644 graphrag/tests/test_e2e_schema_aware_ingest.py
create mode 100644 graphrag/tests/test_llm_entity_relationship_extractor.py
create mode 100644 graphrag/tests/test_prompt_validation.py
create mode 100644 graphrag/tests/test_schema_extraction.py
create mode 100644 graphrag/tests/test_schema_utils.py
diff --git a/.github/workflows/cloud-build-deploy-ci.yaml b/.github/workflows/cloud-build-deploy-ci.yaml
index 30bc588..9c5e9b0 100644
--- a/.github/workflows/cloud-build-deploy-ci.yaml
+++ b/.github/workflows/cloud-build-deploy-ci.yaml
@@ -3,6 +3,15 @@ name: GraphRAG Build Cloud
on:
push:
branches: [ "cloud-main" ]
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - 'LICENSE'
+ - '.gitignore'
+ - '.gitattributes'
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.github/PULL_REQUEST_TEMPLATE*'
+ - '.github/CODEOWNERS'
workflow_dispatch:
env:
diff --git a/.github/workflows/cloud-build-nightly.yaml b/.github/workflows/cloud-build-nightly.yaml
index e7f5356..86f89a8 100644
--- a/.github/workflows/cloud-build-nightly.yaml
+++ b/.github/workflows/cloud-build-nightly.yaml
@@ -3,6 +3,15 @@ name: GraphRAG Build Cloud Nightly
on:
push:
branches: [ "cloud-dev" ]
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - 'LICENSE'
+ - '.gitignore'
+ - '.gitattributes'
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.github/PULL_REQUEST_TEMPLATE*'
+ - '.github/CODEOWNERS'
workflow_dispatch:
env:
diff --git a/.github/workflows/onprem-build-nightly.yaml b/.github/workflows/onprem-build-nightly.yaml
index 29ef4c8..92b7c3c 100644
--- a/.github/workflows/onprem-build-nightly.yaml
+++ b/.github/workflows/onprem-build-nightly.yaml
@@ -3,6 +3,15 @@ name: GraphRAG Build Nightly
on:
push:
branches: [ "dev" ]
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - 'LICENSE'
+ - '.gitignore'
+ - '.gitattributes'
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.github/PULL_REQUEST_TEMPLATE*'
+ - '.github/CODEOWNERS'
workflow_dispatch:
env:
diff --git a/.github/workflows/onprem-build.yaml b/.github/workflows/onprem-build.yaml
index 04b7456..9fb26a4 100644
--- a/.github/workflows/onprem-build.yaml
+++ b/.github/workflows/onprem-build.yaml
@@ -3,6 +3,15 @@ name: GraphRAG Build On-Prem
on:
push:
branches: [ "main" ]
+ paths-ignore:
+ - '**.md'
+ - 'docs/**'
+ - 'LICENSE'
+ - '.gitignore'
+ - '.gitattributes'
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.github/PULL_REQUEST_TEMPLATE*'
+ - '.github/CODEOWNERS'
workflow_dispatch:
env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b34570..5d89e53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
# Changelog
+## [1.4.0]
+
+### Added
+- **Schema-aware initialization** at *Initialize Knowledge Graph* time, with three modes: skip schema, generate a draft from sample documents, or paste a GSQL schema. Drafts are reviewed in a form-mode editor before being applied as a single atomic schema-change job that never drops existing types.
+- **Schema-aware extraction**: when an extracted entity or relationship matches a declared domain type or pair, ECC populates the domain vertex / edge directly. A configurable strict mode drops non-schema extractions instead of falling back to the raw `Entity` layer.
+- **Typed-relationship metadata layer** (`EntityType` / `RelationshipType` vertices linked by `IS_HEAD_OF` / `HAS_TAIL`) carrying type names and human-readable definitions; available to retrievers and to the chat agent. The layer auto-fills from extracted free-text types when no domain types are declared (with case / suffix / plural deduplication), and is restricted to declared types only when a domain schema exists.
+- **Customizable schema-extraction prompt** in the `/prompts` API alongside the existing chatbot, entity-relationship, community-summarization, and query-generation prompts. Per-graph overrides supported.
+- **Schema definitions threaded into LLM prompts** for query generation (Cypher / GSQL) and entity-relationship extraction, so the model sees per-type descriptions alongside the schema rep.
+- **JSONL caching shared between schema extraction and ingest** — files uploaded for schema extraction are reused by the ingest flow without re-conversion.
+- **Parallel image description** during PDF processing (default 8 workers, env-overridable).
+- **Async embedding-store initialization** — service startup no longer blocks on the TigerGraph connection; status surfaces as `initializing` / `ok` / `error`.
+
+### Changed
+- **All customizable prompts now ship as in-code defaults**, packaged inside the LLM service. Provider prompt directories are kept (empty) for backward compatibility; per-graph and global overrides still win when present.
+- **`prompt_path` is a top-level `llm_config` field**, applied across LLM-prompted services automatically. Per-service `prompt_path` entries are still honored on disk but no longer needed.
+- **Permissive schema parser** accepts both `DIRECTED` and `UNDIRECTED` edges and rejects names that collide with GraphRAG structural types or GSQL keywords.
+- **Server-side prompt validation**: `/prompts` POST rejects edits missing required placeholders and auto-escapes stray `{token}` occurrences in user content.
+- **`apply_proposal` reports a real failure** when the GSQL server returns a known error marker, instead of falsely reporting success.
+- **TigerGraph embedding store skips redundant GDS install** when the `gds.vector` package is already installed, eliminating multi-minute catalog-lock stalls on container restart.
+- **TigerGraph version mismatch** raises a clear `ValueError` at ECC startup instead of leaving the embedding store undefined.
+- **`check_embedding_store_status()`** in the inquiryai / supportai routers raises HTTP 503 instead of swallowing the exception.
+- **Bedrock `max_tokens` is auto-defaulted** per model family (Claude 3.x = 4096, Sonnet 3.5+ / 4.x = 8192, Titan / Cohere / Llama at their published caps), so schema extraction and other large-output prompts no longer truncate at the langchain-aws built-in 1024 default. Explicit `model_kwargs.max_tokens` and the existing `token_limit` config field both override the auto-default.
+- **Hybrid / similarity retrievers surface domain vertex types** in the LLM context with a `: ` label, so type-aware questions (e.g. "which companies …") receive properly grounded answers.
+
+### Removed
+- **`RELATIONSHIP_TYPE` edge** between `EntityType` vertices — superseded by `IS_HEAD_OF` + `HAS_TAIL` through `RelationshipType`.
+
+### Configuration
+- New `graphrag_config` keys: `schema_max_sample_files` (default 5), `schema_max_total_mb` (default 50), `strict_mode` (default false).
+- New env var: `PDF_IMAGE_CONCURRENCY` (default 8).
+
+> Implementation-level details for v1.4.0 (parser internals, endpoint contracts, dialog state machine, prompt-resolution chain, schema-aware ECC worker logic, etc.) live in `dev/plans/graphrag/v1.4.0_implementation_notes.md`.
+
## [1.3.1]
### Changed
diff --git a/README.md b/README.md
index e49317b..94d4cbd 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,14 @@
- [Ollama](#ollama)
- [Hugging Face](#hugging-face)
- [Groq](#groq)
+- [Tuning Guideline](#tuning-guideline)
+ - [Tune in the right order](#1-tune-in-the-right-order)
+ - [Chunking](#2-chunking--get-the-granularity-right)
+ - [Extraction](#3-extraction--make-the-graph-clean-before-tuning-retrieval)
+ - [Retrieval](#4-retrieval--match-context-size-to-the-question)
+ - [Prompts](#5-prompts--last-resort-biggest-leverage-when-the-rest-is-right)
+ - [Performance / cost knobs](#6-performance--cost-knobs)
+ - [A working tuning loop](#7-a-working-tuning-loop)
- [Customization and Extensibility](#customization-and-extensibility)
- [Test Your Code Changes](#test-your-code-changes)
- [Testing with Pytest](#testing-with-pytest)
@@ -55,8 +63,10 @@
---
## Releases
-* **2/28/2026**: GraphRAG v1.2.0 released. Added Admin UI for graph initialization, document ingestion, and knowledge graph rebuild, along with many other improvements and bug fixes. See [release notes](https://github.com/tigergraph/graphrag/releases/tag/v1.2.0) for details.
-* **9/22/2025**: GraphRAG is available now officially v1.1 (v1.1.0). AWS Bedrock support is completed with BDA integration for multimodal document ingestion. See [release notes](https://github.com/tigergraph/graphrag/releases/tag/v1.1.0) for details.
+* **GraphRAG v1.4.0** (in progress): Schema-aware initialization. The *Initialize Knowledge Graph* dialog accepts an optional pasted GSQL schema; the backend parses, diffs, and applies domain types as a single atomic schema-change job. Type definitions captured at init time flow through to the entity-extraction prompt and the query-routing schema rep. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.4.0) when published.
+* **4/10/2026**: GraphRAG v1.3.0 released. Added an admin configuration UI with role-based access and per-graph chatbot LLM override, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.3.0) for details.
+* **2/28/2026**: GraphRAG v1.2.0 released. Added Admin UI for graph initialization, document ingestion, and knowledge graph rebuild, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.2.0) for details.
+* **9/22/2025**: GraphRAG is available now officially v1.1 (v1.1.0). AWS Bedrock support is completed with BDA integration for multimodal document ingestion. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.1.0) for details.
* **6/18/2025**: GraphRAG is available now officially v1.0 (v1.0.0). TigerGraph database is the only graph and vector storagge supported.
Please see [Release Notes](https://docs.tigergraph.com/tg-graphrag/current/release-notes/) for details.
@@ -878,6 +888,112 @@ Example configuration for a model on Hugging Face with a serverless endpoint is
---
+## Tuning Guideline
+
+GraphRAG answer quality, latency, and LLM cost are sensitive to a small set of parameters and prompts. This section is a high-level strategy — adjust *one knob at a time*, run the same set of evaluation questions before and after each change, and keep what helps. Detailed parameter descriptions live in [GraphRAG configuration](#graphrag-configuration).
+
+### 1. Tune in the right order
+
+A common mistake is tuning retrieval and prompts before the underlying graph is good. Work bottom-up:
+
+1. **Chunking** — fix how the source documents are split.
+2. **Extraction** — fix what entities / relationships are pulled from each chunk.
+3. **Retrieval** — pick the right context for each question.
+4. **Response prompts** — shape the final answer.
+
+A bad answer at step 4 is rarely fixed by editing the response prompt; usually it's caused by step 1, 2, or 3.
+
+### 2. Chunking — get the granularity right
+
+| Symptom | Likely cause | Tweak |
+| --- | --- | --- |
+| Answers cite irrelevant facts from elsewhere in the same chunk | chunks too large | drop `chunk_size` (`character` / `markdown` / `html` / `recursive` chunkers); raise `threshold` (`semantic`) so it splits more aggressively |
+| Answers miss context that's clearly in the source | chunks too small or no overlap | raise `chunk_size`; bump `overlap_size` (default 1/8 of `chunk_size`); lower `threshold` (`semantic`) |
+| Tables / figures get fragmented | wrong chunker for the source | use `markdown` for markdown / docs converted to markdown; use `html` for HTML pages with structure; use `regex` with a custom `pattern` for structured logs |
+| Cross-section reasoning fails | no overlap | increase `overlap_size` to ~25% of `chunk_size` |
+
+Default starting point for prose: `chunker: "semantic"`, `threshold: 0.95`, `chunker_config.method: "percentile"`. Move to `markdown` chunker with `chunk_size: 2048` and `overlap_size: 256` if your source is markdown-heavy and table integrity matters.
+
+### 3. Extraction — make the graph clean before tuning retrieval
+
+The extraction prompt drives what becomes a vertex / edge. Two failure modes show up:
+
+- **Document-structure noise** — the graph fills up with layout artifacts (page numbers, section headers, table captions, chart labels) instead of domain entities. This crushes downstream retrieval because the LLM has to wade through structural junk.
+- **Generic abstractions** — over-merged or under-specified buckets (e.g. an "entity" or "record" type that swallows everything) instead of the concrete domain types you actually care about. For example, in a financial corpus you want `Company`, `Fund`, `Account`, `Person`, `Filing`, `Risk` — not a single `record` bin.
+
+Today's primary lever is the **entity-extraction prompt**:
+
+- **Customize the prompt for your domain** via Settings → *Customize Prompts* → entity extraction. Tell the LLM explicitly what counts and what doesn't. For a financial domain: *"Extract concrete real-world entities (companies, people, funds, accounts, filings, transactions, risks). Ignore document layout (page numbers, headers/footers, tables, captions, figures, navigation menus)."*
+- **Add 1–2 short domain examples** in the prompt. Even one well-chosen exemplar (an extracted entity with type and definition) dramatically improves consistency across chunks.
+- **List the canonical edge verbs you want.** Encourage `PUBLISHES`, `OWNS`, `ISSUES`, `MANAGES`, `REPORTS_ON` in the relationship-extraction prompt rather than letting the LLM emit ad-hoc nominal phrases.
+
+If extraction quality is still poor after iterating on the prompt, the next-best option today is to clear the graph's domain types and re-ingest with the improved prompt — schema growth is currently driven entirely by what extraction produces. (A schema-aware initialization flow that lets you supply a curated schema up front is on the roadmap.)
+
+### 4. Retrieval — match context size to the question
+
+Three knobs interact: `top_k`, `num_hops`, `num_seen_min`. Also `chunk_only` / `doc_only` and (for community search) `community_level` / `with_chunk`.
+
+| Question style | Recommended start | Reasoning |
+| --- | --- | --- |
+| *"What is X?"* (specific lookup) | `top_k=3`, `num_hops=1`, `num_seen_min=1` | Tight neighborhood, few seeds. |
+| *"How are X and Y related?"* (relational) | `top_k=5`, `num_hops=2`, `num_seen_min=1` | Need to traverse between concepts. |
+| *"Summarize the report"* (broad) | `top_k=8`, `num_hops=2`, `num_seen_min=2` | More seeds, filter loose connections. |
+| *"Compare A across multiple sections"* (multi-hop reasoning) | `top_k=8`, `num_hops=3`, `num_seen_min=2` | Wide traversal, but tighten the filter. |
+| *"List all X"* (aggregation) | use *Community Search* with `community_level: 1–2` | Broader summaries, not chunk-level retrieval. |
+
+Heuristics:
+
+- If the answer is **vague or hallucinated**, you don't have enough context: raise `top_k` first, then `num_hops`.
+- If the answer is **drowning in irrelevant detail**, you have too much: drop `top_k`, raise `num_seen_min`, or set `chunk_only: true`.
+- If the answer **misses things across sections**, raise `num_hops` (1 → 2 → 3). Each extra hop multiplies result size, so don't go past 3 without strong evidence.
+- If the answer **cites whole documents but loses chunk-level detail**, set `doc_only: false` and `chunk_only: true`.
+- For broad-survey questions, prefer `community_search` over hybrid; tune `community_level` (lower = more granular communities, higher = broader summaries).
+
+Each tweak should be made **alone** — moving `top_k` and `num_hops` together makes it impossible to tell which one helped.
+
+### 5. Prompts — last resort, biggest leverage when the rest is right
+
+Customize prompts via the UI: *Settings → Customize Prompts*. The four customizable prompt groups (UI labels and underlying ids):
+
+- **Entity Relationships** (`entity_relationship`) — combined entity- and relationship-extraction prompt; controls what becomes a vertex / edge. Tune for noise suppression, domain specificity, and verb-form edge names (e.g. `PUBLISHES`, `OWNS`, `MANAGES` instead of nominal phrases). See §3.
+- **Schema Instructions** (`query_generation`) — instructions used when generating GSQL / Cypher and when filtering the schema for a structured query. Tune if your domain has unusual type names that aren't matching user phrasing, or if generated queries miss obvious joins.
+- **Community Summarization** (`community_summarization`) — how community summaries are produced during knowledge-graph build. Tune for length / tone and to bias summaries toward domain-specific framing.
+- **Chatbot Responses** (`chatbot_response`) — the final answer template. Keep it short; the LLM responds best to clear constraints (*"answer in ≤3 sentences, cite the doc id"*).
+
+When customizing:
+
+- **Always start from the system default** (don't write from scratch).
+- **Keep examples short and domain-relevant.**
+- **Test with the same evaluation set** before and after — a prompt change that fixes one question often regresses another.
+- **Know where overrides live.** The runtime resolves prompt files in this order:
+ 1. Graph-scoped: `configs/graph_configs//prompts/.txt` — created when you edit prompts with a specific graph selected. Highest priority.
+ 2. Global override: `configs/prompts/.txt` — created when you edit prompts globally and the bundled provider default path is read-only.
+ 3. Provider default (bundled): `./common/prompts//.txt` — selected by the `prompt_path` field in the LLM config. Shipped with the deployment.
+- **Version-control the override directories** so they survive container rebuilds and travel with the deployment.
+- **Delete custom prompt overrides** if you suspect they're stale; the system falls back to the next layer cleanly.
+
+### 6. Performance / cost knobs
+
+- **`default_concurrency`** drives all internal semaphores. ECC uses 2× this value for ingest workers; the chatbot uses 1×. Raise it to speed up ingestion of large corpora; lower it if you're hitting LLM rate limits or seeing socket exhaustion.
+- **`reuse_embedding: true`** skips re-embedding identical text — major saving on re-ingest of unchanged documents.
+- **Choose `llm_model` thoughtfully** — entity / relationship extraction tolerates cheaper / faster models (Haiku, Nova-lite, Flash); response synthesis benefits from stronger ones (Sonnet, GPT-4-class). The `multimodal_service` is independent — set it to a vision-capable model only when you actually ingest images.
+- **`load_batch_size`** and **`upsert_delay`** control ingestion pressure on TigerGraph. Defaults are fine for most loads; lower the batch size if you see write timeouts.
+
+### 7. A working tuning loop
+
+1. Define **5–10 representative evaluation questions** with expected answers (or at least the docs that should ground them).
+2. Establish a **baseline** — run all questions, save answers + retrieved chunks.
+3. **Change one parameter** (or one prompt). Re-run.
+4. Diff the answers. Keep the change only if it improves more questions than it regresses.
+5. **Iterate in order** — chunking → extraction → retrieval → prompts. Don't skip ahead.
+6. **Save the winning config** to `configs/server_config.json` and document the rationale in your team's runbook.
+
+The chatbot UI's *Explain* panel (which lists the chunks fed into the answer) is the fastest debugging tool — most quality issues become obvious by reading the chunks the system actually retrieved.
+
+[Go back to top](#top)
+
+---
+
## Customization and Extensibility
TigerGraph GraphRAG is designed to be easily extensible. The service can be configured to use different LLM providers, different graph schemas, and different LangChain tools. The service can also be extended to use different embedding services, different LLM generation services, and different LangChain tools. For more information on how to extend the service, see the [Developer Guide](./docs/DeveloperGuide.md).
diff --git a/common/config.py b/common/config.py
index 3dc3be1..3216c60 100644
--- a/common/config.py
+++ b/common/config.py
@@ -166,6 +166,17 @@ def resolve_llm_services(llm_cfg: dict) -> dict:
if svc_key in cfg and "region_name" not in cfg[svc_key]:
cfg[svc_key]["region_name"] = top_region
+ # Inject top-level prompt_path into LLM-prompted service configs
+ # if missing. The UI never lets users set per-service prompt_paths;
+ # in practice they are always identical to completion's.
+ # ``embedding_service`` is excluded — embedding models never load
+ # prompt files (their class hierarchy has no prompt-property machinery).
+ top_prompt_path = cfg.get("prompt_path")
+ if top_prompt_path:
+ for svc_key in ["completion_service", "multimodal_service", "chat_service"]:
+ if svc_key in cfg and "prompt_path" not in cfg[svc_key]:
+ cfg[svc_key]["prompt_path"] = top_prompt_path
+
completion = cfg.get("completion_service", {})
# Resolve embedding: inherit provider-level config from completion
@@ -366,6 +377,15 @@ def get_graphrag_config(graphname=None):
if svc_key in llm_config and "region_name" not in llm_config[svc_key]:
llm_config[svc_key]["region_name"] = llm_config["region_name"]
+# Inject top-level prompt_path into LLM-prompted service configs if
+# missing. Embedding service is excluded — embedding models never load
+# prompt files. Per-service entries on disk are accepted for backward
+# compat but never written by the UI.
+if "prompt_path" in llm_config:
+ for svc_key in ["completion_service", "multimodal_service", "chat_service"]:
+ if svc_key in llm_config and "prompt_path" not in llm_config[svc_key]:
+ llm_config[svc_key]["prompt_path"] = llm_config["prompt_path"]
+
_comp = llm_config.get("completion_service")
if _comp is None:
raise Exception("completion_service is not found in llm_config")
@@ -441,6 +461,15 @@ def get_graphrag_config(graphname=None):
else:
raise Exception("Embedding service not implemented")
+def get_embedding_service():
+ """Return the current embedding service instance.
+
+ Use this instead of importing ``embedding_service`` directly so
+ consumers always read the latest instance after a config reload.
+ """
+ return embedding_service
+
+
def get_llm_service(service_config: dict) -> LLM_Model:
"""
Instantiate an LLM provider from a flat service config dict.
@@ -474,25 +503,73 @@ def get_llm_service(service_config: dict) -> LLM_Model:
raise Exception(f"LLM service '{service_name}' not supported")
-if os.getenv("INIT_EMBED_STORE", "true") == "true":
- conn = TigerGraphConnection(
- host=db_config.get("hostname", "http://tigergraph"),
- username=db_config.get("username", "tigergraph"),
- password=db_config.get("password", "tigergraph"),
- gsPort=db_config.get("gsPort", "14240"),
- restppPort=db_config.get("restppPort", "9000"),
- graphname=db_config.get("graphname", ""),
- apiToken=db_config.get("apiToken", ""),
- )
- if not db_config.get("apiToken") and db_config.get("getToken"):
- conn.getToken()
+# Module-level ``embedding_store`` is kept for back-compat with direct
+# importers (`from common.config import embedding_store`); it's
+# populated by the background-init thread below. New callers should
+# prefer the ``get_embedding_store(timeout)`` getter so they fail
+# fast (or wait briefly) instead of seeing a ``None`` mid-startup.
+embedding_store = None
+_embedding_store_ready = threading.Event()
+service_status["embedding_store"] = {
+ "status": "initializing",
+ "error": "Embedding store is still initializing",
+}
- embedding_store = TigerGraphEmbeddingStore(
- conn,
- embedding_service,
- support_ai_instance=True,
- )
- service_status["embedding_store"] = {"status": "ok", "error": None}
+
+def _init_embedding_store():
+ """Background thread target. Builds the embedding store without
+ blocking module import — TigerGraph may be slow on first connect,
+ and we don't want app startup to wait on it.
+ """
+ global embedding_store
+ try:
+ conn = TigerGraphConnection(
+ host=db_config.get("hostname", "http://tigergraph"),
+ username=db_config.get("username", "tigergraph"),
+ password=db_config.get("password", "tigergraph"),
+ gsPort=db_config.get("gsPort", "14240"),
+ restppPort=db_config.get("restppPort", "9000"),
+ graphname=db_config.get("graphname", ""),
+ apiToken=db_config.get("apiToken", ""),
+ )
+ if not db_config.get("apiToken") and db_config.get("getToken"):
+ conn.getToken()
+
+ embedding_store = TigerGraphEmbeddingStore(
+ conn,
+ embedding_service,
+ support_ai_instance=True,
+ )
+ service_status["embedding_store"] = {"status": "ok", "error": None}
+ except Exception as e:
+ service_status["embedding_store"] = {"status": "error", "error": str(e)}
+ logger.error(f"Failed to initialize embedding store: {e}")
+ finally:
+ _embedding_store_ready.set()
+
+
+def get_embedding_store(timeout: float = 0):
+ """Return the embedding store if ready.
+
+ Args:
+ timeout: Seconds to wait for initialization. Default 0
+ (non-blocking — raises immediately if still initializing).
+
+ Raises:
+ RuntimeError: if not yet ready, timed out, or initialization failed.
+ """
+ if not _embedding_store_ready.wait(timeout=timeout):
+ raise RuntimeError(
+ "Embedding store is still initializing. Please try again shortly."
+ )
+ if embedding_store is None:
+ error = service_status.get("embedding_store", {}).get("error", "Unknown error")
+ raise RuntimeError(f"Embedding store failed to initialize: {error}")
+ return embedding_store
+
+
+if os.getenv("INIT_EMBED_STORE", "true") == "true":
+ threading.Thread(target=_init_embedding_store, daemon=True).start()
def reload_llm_config(new_llm_config: dict = None):
@@ -550,6 +627,14 @@ def reload_llm_config(new_llm_config: dict = None):
if svc_key in new_llm_config and "region_name" not in new_llm_config[svc_key]:
new_llm_config[svc_key]["region_name"] = new_llm_config["region_name"]
+ # Inject top-level prompt_path into LLM-prompted service configs
+ # if missing. Embedding service is excluded — embedding models
+ # never load prompt files.
+ if "prompt_path" in new_llm_config:
+ for svc_key in ["completion_service", "multimodal_service", "chat_service"]:
+ if svc_key in new_llm_config and "prompt_path" not in new_llm_config[svc_key]:
+ new_llm_config[svc_key]["prompt_path"] = new_llm_config["prompt_path"]
+
new_completion_config = new_llm_config.get("completion_service")
new_embedding_config = new_llm_config.get("embedding_service")
diff --git a/common/db/schema_extraction.py b/common/db/schema_extraction.py
new file mode 100644
index 0000000..b71794e
--- /dev/null
+++ b/common/db/schema_extraction.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Schema-extraction over sample documents (Phase 1, sample-doc path).
+
+The endpoint accepts up to N representative documents, this module
+turns them into a single concatenated markdown blob and asks the LLM
+to emit ``VERTEX`` / ``DIRECTED EDGE`` / ``UNDIRECTED EDGE``
+statements (the same GSQL form the *paste* path accepts), so both
+sources funnel through ``schema_utils.parse_gsql_schema``.
+
+Prompt loading is delegated to
+``common.llm_services.base_llm.LLM_Model.schema_extraction_prompt`` —
+the same per-graph-override → provider-default resolution used by every
+other customizable prompt. The prompt itself lives at
+``/schema_extraction.txt`` with a per-graph override at
+``configs/graph_configs//prompts/schema_extraction.txt``.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Iterable, List
+
+from langchain.prompts import PromptTemplate
+from langchain_core.output_parsers import StrOutputParser
+
+from common.db.schema_utils import (
+ GRAPHRAG_STRUCTURAL_EDGE_TYPES,
+ GRAPHRAG_STRUCTURAL_VERTEX_TYPES,
+ get_gsql_reserved_words,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _build_prompt(llm_service) -> PromptTemplate:
+ """Wrap *llm_service*'s ``schema_extraction_prompt`` text in a
+ ``PromptTemplate`` with the three required input variables.
+ """
+ template_str = llm_service.schema_extraction_prompt
+ return PromptTemplate(
+ template=template_str,
+ input_variables=["samples", "structural_types", "tg_keywords"],
+ )
+
+
+def concatenate_samples(
+ samples: Iterable[dict],
+ max_chars: int,
+) -> str:
+ """Concatenate sample-doc markdown into a single blob, with each
+ document preceded by an ``# `` heading. Truncates at
+ *max_chars* total characters; truncation is logged.
+
+ *samples* is an iterable of ``{"doc_id": str, "content": str}``
+ dicts (the same shape ``extract_text_from_file_with_images_as_docs``
+ returns).
+ """
+ parts: List[str] = []
+ total = 0
+ for s in samples:
+ doc_id = s.get("doc_id", "doc")
+ content = s.get("content", "") or ""
+ header = f"\n\n# {doc_id}\n\n"
+ budget = max_chars - total
+ if budget <= 0:
+ logger.warning("Sample doc budget exhausted; later files truncated.")
+ break
+ chunk = (header + content)[:budget]
+ parts.append(chunk)
+ total += len(chunk)
+ return "".join(parts).lstrip()
+
+
+def extract_schema_gsql(
+ llm_service,
+ samples: Iterable[dict],
+ max_chars: int = 200_000,
+) -> str:
+ """Run the schema-extraction prompt against *llm_service*. Returns
+ the raw GSQL string the model produced (caller passes it to
+ ``schema_utils.parse_gsql_schema``).
+
+ *llm_service* must expose ``schema_extraction_prompt`` (from
+ :class:`common.llm_services.base_llm.LLM_Model`) and the standard
+ ``invoke_with_parser(prompt, parser, inputs, caller_name)`` entry
+ point. Per-graph prompt overrides are picked up automatically by
+ ``schema_extraction_prompt``'s resolution chain.
+ """
+ prompt = _build_prompt(llm_service)
+ samples_blob = concatenate_samples(samples, max_chars=max_chars)
+ structural_types = ", ".join(
+ sorted(GRAPHRAG_STRUCTURAL_VERTEX_TYPES | GRAPHRAG_STRUCTURAL_EDGE_TYPES)
+ )
+ tg_keywords = ", ".join(sorted(get_gsql_reserved_words()))
+
+ raw = llm_service.invoke_with_parser(
+ prompt,
+ StrOutputParser(),
+ {
+ "samples": samples_blob,
+ "structural_types": structural_types,
+ "tg_keywords": tg_keywords,
+ },
+ caller_name="schema_extraction",
+ )
+ if isinstance(raw, str):
+ return raw.strip()
+ return str(raw).strip()
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
new file mode 100644
index 0000000..ce4a782
--- /dev/null
+++ b/common/db/schema_utils.py
@@ -0,0 +1,1259 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""
+Schema proposal and persistence for the schema-aware initialize_graph flow.
+
+A "schema proposal" is the user-supplied (or LLM-derived) **domain** schema
+the graph should adopt at init time, expressed as a small Python dict:
+
+ {
+ "vertices": [
+ {"name": "Company", "description": "..."},
+ {"name": "Report", "description": "..."},
+ ...
+ ],
+ "edges": [
+ {"name": "PUBLISHES",
+ "description": "...",
+ "pairs": [("Company", "Report"), ("Company", "Filing")]},
+ ...
+ ],
+ "domain_label": "Corporate Governance", # optional
+ }
+
+This module provides:
+
+* :data:`GRAPHRAG_STRUCTURAL_VERTEX_TYPES` / :data:`GRAPHRAG_STRUCTURAL_EDGE_TYPES`
+ — the GraphRAG-internal types that the user must not redefine.
+* :func:`parse_gsql_schema` — permissive scanner that turns pasted GSQL
+ (``ADD VERTEX/EDGE`` statements *or* ``gsql ls`` output) into a proposal.
+* :func:`emit_add_statements` — produce a list of ``ADD VERTEX/EDGE`` /
+ ``ALTER EDGE … ADD PAIR`` statements that bring an existing graph schema
+ up to the proposal (compare-and-add only; never drop).
+* :func:`emit_preview_gsql` — render the proposal as a self-contained GSQL
+ block for the UI's "Preview as GSQL" tab.
+
+The module is intentionally dependency-light (regex, dataclasses, stdlib
+only) so it's unit-testable without spinning up TigerGraph or the LLM.
+"""
+
+from __future__ import annotations
+
+import re
+import time
+import uuid
+from dataclasses import dataclass, field
+from typing import Iterable, List, Optional, Sequence, Set, Tuple
+
+
+# -----------------------------------------------------------------------------
+# Structural type registry
+# -----------------------------------------------------------------------------
+
+#: GraphRAG-internal vertex types. The user must not propose these as domain
+#: types; the permissive parser silently drops any line that names one of
+#: these (case-insensitive match).
+GRAPHRAG_STRUCTURAL_VERTEX_TYPES: frozenset = frozenset({
+ "Document",
+ "DocumentChunk",
+ "Entity",
+ "EntityType",
+ "RelationshipType",
+ "Content",
+ "Community",
+ "Image",
+})
+
+
+#: GraphRAG-internal edge types. The user must not propose these as domain
+#: types either. ``reverse_*`` companions are derived from ``WITH
+#: REVERSE_EDGE=…`` declarations and shouldn't be hand-written.
+GRAPHRAG_STRUCTURAL_EDGE_TYPES: frozenset = frozenset({
+ "HAS_CONTENT",
+ "IS_HEAD_OF",
+ "HAS_TAIL",
+ "CONTAINS_ENTITY",
+ "MENTIONS_RELATIONSHIP",
+ "MENTIONS_ENTITY_TYPE",
+ "IS_AFTER",
+ "HAS_CHILD",
+ "ENTITY_HAS_TYPE",
+ "RELATIONSHIP",
+ "ENTITY_LINKS_TO",
+ "IN_COMMUNITY",
+ "LINKS_TO",
+ "HAS_PARENT",
+ "HAS_IMAGE",
+ "REFERENCES_IMAGE",
+})
+
+
+_GSQL_RESERVED_CACHE: Optional[frozenset] = None
+
+
+def get_gsql_reserved_words() -> frozenset:
+ """Return the GSQL reserved-keyword set sourced from
+ ``pyTigerGraph.TigerGraphConnection.getReservedKeywords()``.
+
+ Memoized at first call. ``pyTigerGraph`` is a hard dependency of
+ this codebase, so an import failure here is a real configuration
+ error and we let it propagate.
+ """
+ global _GSQL_RESERVED_CACHE
+ if _GSQL_RESERVED_CACHE is None:
+ from pyTigerGraph import TigerGraphConnection
+
+ words = TigerGraphConnection.getReservedKeywords()
+ _GSQL_RESERVED_CACHE = (
+ words if isinstance(words, frozenset) else frozenset(words)
+ )
+ return _GSQL_RESERVED_CACHE
+
+
+def is_reserved_word(name: str) -> bool:
+ """Return True if *name* (case-insensitive) collides with a GSQL
+ reserved word per pyTigerGraph. Used by the permissive parser to
+ drop names that would error at schema-change time anyway.
+ """
+ if not name:
+ return False
+ return name.upper() in get_gsql_reserved_words()
+
+
+#: Network / transport-level failure markers that ``conn.gsql()``
+#: surfaces as a string return rather than an exception.
+_GSQL_TRANSPORT_FAILURE_MARKERS: tuple = (
+ "Response ended prematurely",
+ "Connection refused",
+ "Connection reset",
+ "Read timed out",
+ "Internal Server Error",
+)
+
+
+#: Server-reported failure markers that ``conn.gsql()`` includes in
+#: its string output without raising. Maintained locally — the
+#: pyTigerGraph private helper ``_wrap_gsql_result`` is documented as
+#: in flux upstream, so we don't depend on it. Keep this list aligned
+#: with upstream's ``_GSQL_ERROR_PATTERNS`` when it stabilizes.
+_GSQL_SERVER_ERROR_MARKERS: tuple = (
+ 'Encountered "',
+ "SEMANTIC ERROR",
+ "Syntax Error",
+ "Failed to create",
+ "does not exist",
+ "is not a valid",
+ "already exists",
+ "Invalid syntax",
+)
+
+
+def gsql_output_error(output: str) -> Optional[str]:
+ """Return a short error description if *output* (the string returned
+ by ``pyTigerGraph.TigerGraphConnection.gsql()``) indicates failure,
+ else ``None``.
+
+ Two layers, both checked locally so we don't depend on
+ pyTigerGraph private helpers:
+
+ 1. Transport-level errors (``Response ended prematurely``,
+ ``Connection refused``, etc.) — pyTigerGraph surfaces these as
+ a string return rather than an exception.
+ 2. Server-reported errors (``SEMANTIC ERROR``, ``Failed to
+ create``, ``Invalid syntax``, etc.) — string markers in the
+ gsql output.
+
+ Used by :func:`apply_proposal` to flip an "applied" return into
+ an error when the server reported a problem but pyTigerGraph
+ didn't raise.
+ """
+ if not output:
+ return None
+
+ folded = output.casefold()
+ for marker in _GSQL_TRANSPORT_FAILURE_MARKERS:
+ if marker.casefold() in folded:
+ idx = output.lower().find(marker.lower())
+ snippet = output[max(0, idx - 40): idx + len(marker) + 200]
+ return f"GSQL transport error: {marker!r}. Excerpt: {snippet!r}"
+
+ for marker in _GSQL_SERVER_ERROR_MARKERS:
+ if marker in output:
+ idx = output.find(marker)
+ snippet = output[max(0, idx - 40): idx + len(marker) + 200]
+ return f"GSQL server error: {snippet!r}"
+
+ return None
+
+
+def is_structural_type(name: str) -> bool:
+ """Return True if *name* (case-insensitive) is a GraphRAG structural
+ vertex or edge type, OR a ``reverse_*`` companion of one, OR a GSQL
+ reserved word that would fail at schema-change time.
+ """
+ if not name:
+ return False
+ folded = name.casefold()
+ if folded.startswith("reverse_"):
+ return True
+ structural = {t.casefold() for t in GRAPHRAG_STRUCTURAL_VERTEX_TYPES}
+ structural |= {t.casefold() for t in GRAPHRAG_STRUCTURAL_EDGE_TYPES}
+ if folded in structural:
+ return True
+ return is_reserved_word(name)
+
+
+# -----------------------------------------------------------------------------
+# Canonical proposal dataclass
+# -----------------------------------------------------------------------------
+
+
+#: TigerGraph GSQL primitive attribute types we accept on proposals.
+#: Anything else is dropped at parse time so the schema-change job
+#: never receives a non-primitive type.
+GSQL_PRIMITIVE_TYPES: frozenset = frozenset({
+ "STRING", "INT", "UINT", "DOUBLE", "FLOAT", "BOOL", "DATETIME",
+})
+
+
+@dataclass
+class AttributeProposal:
+ """One ``(name, type)`` pair on a vertex or edge type."""
+
+ name: str
+ type: str = "STRING"
+
+ def to_dict(self) -> dict:
+ return {"name": self.name, "type": self.type}
+
+
+@dataclass
+class VertexProposal:
+ name: str
+ description: str = ""
+ attributes: List[AttributeProposal] = field(default_factory=list)
+
+ def to_dict(self) -> dict:
+ return {
+ "name": self.name,
+ "description": self.description,
+ "attributes": [a.to_dict() for a in self.attributes],
+ }
+
+
+@dataclass
+class EdgeProposal:
+ name: str
+ pairs: List[Tuple[str, str]] = field(default_factory=list)
+ description: str = ""
+ attributes: List[AttributeProposal] = field(default_factory=list)
+ # ``True`` for ``DIRECTED EDGE`` (default), ``False`` for
+ # ``UNDIRECTED EDGE``. Captured from the parser; propagated to the
+ # emitter so the schema-change job uses the right keyword and
+ # WITH-clause shape (undirected edges have no REVERSE_EDGE).
+ directed: bool = True
+
+ def to_dict(self) -> dict:
+ return {
+ "name": self.name,
+ "description": self.description,
+ "pairs": [list(p) for p in self.pairs],
+ "attributes": [a.to_dict() for a in self.attributes],
+ "directed": self.directed,
+ }
+
+
+@dataclass
+class SchemaProposal:
+ """Canonical in-memory representation of a domain schema proposal."""
+
+ vertices: List[VertexProposal] = field(default_factory=list)
+ edges: List[EdgeProposal] = field(default_factory=list)
+ domain_label: Optional[str] = None
+
+ # --- Construction helpers -----------------------------------------------
+
+ def add_vertex(
+ self,
+ name: str,
+ description: str = "",
+ attributes: Optional[Iterable[Tuple[str, str]]] = None,
+ ) -> VertexProposal:
+ existing = self.find_vertex(name)
+ if existing is not None:
+ if description and not existing.description:
+ existing.description = description
+ if attributes:
+ self._merge_attrs(existing.attributes, attributes)
+ return existing
+ v = VertexProposal(name=name, description=description)
+ if attributes:
+ self._merge_attrs(v.attributes, attributes)
+ self.vertices.append(v)
+ return v
+
+ def add_edge_pair(
+ self,
+ name: str,
+ from_vt: str,
+ to_vt: str,
+ description: str = "",
+ attributes: Optional[Iterable[Tuple[str, str]]] = None,
+ directed: bool = True,
+ ) -> EdgeProposal:
+ existing = self.find_edge(name)
+ pair = (from_vt, to_vt)
+ if existing is None:
+ existing = EdgeProposal(
+ name=name,
+ pairs=[pair],
+ description=description,
+ directed=directed,
+ )
+ if attributes:
+ self._merge_attrs(existing.attributes, attributes)
+ self.edges.append(existing)
+ else:
+ if pair not in existing.pairs:
+ existing.pairs.append(pair)
+ if description and not existing.description:
+ existing.description = description
+ if attributes:
+ self._merge_attrs(existing.attributes, attributes)
+ # If the same edge name appears twice with mismatched
+ # direction, prefer the first declaration's choice and
+ # log nothing — schema-change time will reject anyway.
+ return existing
+
+ @staticmethod
+ def _merge_attrs(
+ target: List[AttributeProposal],
+ new_attrs: Iterable[Tuple[str, str]],
+ ) -> None:
+ """Merge new ``(name, type)`` tuples into *target*. Ignores
+ attributes whose name is already present (case-insensitive),
+ so the first declared type wins. Filters out attributes whose
+ type isn't a recognized GSQL primitive — those would error at
+ schema-change time, and we drop silently to keep the parser
+ permissive.
+ """
+ existing_names = {a.name.casefold() for a in target}
+ for name, type_str in new_attrs:
+ if not name:
+ continue
+ if name.casefold() in existing_names:
+ continue
+ if type_str.upper() not in GSQL_PRIMITIVE_TYPES:
+ continue
+ target.append(AttributeProposal(name=name, type=type_str.upper()))
+ existing_names.add(name.casefold())
+
+ # --- Lookup helpers -----------------------------------------------------
+
+ def find_vertex(self, name: str) -> Optional[VertexProposal]:
+ folded = name.casefold()
+ return next(
+ (v for v in self.vertices if v.name.casefold() == folded), None
+ )
+
+ def find_edge(self, name: str) -> Optional[EdgeProposal]:
+ folded = name.casefold()
+ return next(
+ (e for e in self.edges if e.name.casefold() == folded), None
+ )
+
+ def vertex_names(self) -> Set[str]:
+ return {v.name for v in self.vertices}
+
+ # --- Cleanup ------------------------------------------------------------
+
+ def drop_dangling_pairs(self) -> int:
+ """Remove ``(FROM, TO)`` pairs whose endpoints aren't in the
+ proposal's vertex set. Returns the number of pairs dropped.
+ Edges whose pair list becomes empty are removed entirely.
+ """
+ names = {v.name for v in self.vertices}
+ names_folded = {n.casefold() for n in names}
+ dropped = 0
+ kept_edges: List[EdgeProposal] = []
+ for edge in self.edges:
+ kept_pairs: List[Tuple[str, str]] = []
+ for src, tgt in edge.pairs:
+ if (
+ src.casefold() in names_folded
+ and tgt.casefold() in names_folded
+ ):
+ kept_pairs.append((src, tgt))
+ else:
+ dropped += 1
+ if kept_pairs:
+ edge.pairs = kept_pairs
+ kept_edges.append(edge)
+ else:
+ dropped += 0 # whole edge dropped, not counted as a pair-drop
+ self.edges = kept_edges
+ return dropped
+
+ # --- Serialization ------------------------------------------------------
+
+ def to_dict(self) -> dict:
+ out: dict = {
+ "vertices": [v.to_dict() for v in self.vertices],
+ "edges": [e.to_dict() for e in self.edges],
+ }
+ if self.domain_label:
+ out["domain_label"] = self.domain_label
+ return out
+
+ @classmethod
+ def from_dict(cls, data: dict) -> "SchemaProposal":
+ prop = cls(domain_label=data.get("domain_label"))
+ for v in data.get("vertices", []) or []:
+ attrs = [
+ (a.get("name", ""), a.get("type", "STRING"))
+ for a in v.get("attributes", []) or []
+ ]
+ prop.add_vertex(
+ name=v["name"],
+ description=v.get("description", ""),
+ attributes=attrs,
+ )
+ for e in data.get("edges", []) or []:
+ attrs = [
+ (a.get("name", ""), a.get("type", "STRING"))
+ for a in e.get("attributes", []) or []
+ ]
+ edge_directed = bool(e.get("directed", True))
+ for pair in e.get("pairs", []) or []:
+ prop.add_edge_pair(
+ name=e["name"],
+ from_vt=pair[0],
+ to_vt=pair[1],
+ description=e.get("description", ""),
+ attributes=attrs,
+ directed=edge_directed,
+ )
+ return prop
+
+
+# -----------------------------------------------------------------------------
+# Permissive GSQL parser
+# -----------------------------------------------------------------------------
+
+
+# A line that contains "VERTEX (...)" anywhere on it.
+# Captures the name and (optionally) the parenthesized attribute list.
+# Allows leading whitespace, optional dash, optional ADD prefix.
+_VERTEX_LINE_RE = re.compile(
+ r"""
+ ^ # start of line (re.MULTILINE)
+ [\s\-]* # leading whitespace, optional dash
+ (?:add\s+)? # optional ADD
+ vertex # VERTEX
+ \s+
+ (?P[A-Za-z_][A-Za-z0-9_]*) # type name
+ \s*
+ \( # opening paren of attribute list
+ (?P[^()]*) # attribute body (no nested parens)
+ \) # closing paren
+ """,
+ re.IGNORECASE | re.VERBOSE | re.MULTILINE | re.DOTALL,
+)
+
+
+# A line that contains "DIRECTED EDGE (...)" or
+# "UNDIRECTED EDGE (...)". Captures the direction keyword (so
+# the parser can preserve it on the proposal), the edge name, and the
+# FROM/TO body. Attribute / WITH-clause text after the closing paren
+# is intentionally not captured.
+_EDGE_LINE_RE = re.compile(
+ r"""
+ ^ # start of line
+ [\s\-]* # leading whitespace, optional dash
+ (?:add\s+)? # optional ADD
+ (?Pdirected|undirected) # DIRECTED or UNDIRECTED
+ \s+edge
+ \s+
+ (?P[A-Za-z_][A-Za-z0-9_]*) # edge name
+ \s*
+ \( # opening paren
+ (?P.*?) # FROM/TO body (non-greedy)
+ \) # closing paren
+ """,
+ re.IGNORECASE | re.VERBOSE | re.MULTILINE | re.DOTALL,
+)
+
+
+# Within an edge body, a single (FROM , TO ) clause. Multi-pair
+# bodies are separated by `|`.
+_EDGE_PAIR_RE = re.compile(
+ r"""
+ \bfrom\s+
+ (?P[A-Za-z_][A-Za-z0-9_]*)
+ \s*,\s*
+ \bto\s+
+ (?P[A-Za-z_][A-Za-z0-9_]*)
+ """,
+ re.IGNORECASE | re.VERBOSE,
+)
+
+
+# A single ``name `` token in an attribute body.
+_ATTR_TOKEN_RE = re.compile(
+ r"""
+ \b
+ (?P[A-Za-z_][A-Za-z0-9_]*)
+ \s+
+ (?PSTRING|INT|UINT|DOUBLE|FLOAT|BOOL|DATETIME)
+ \b
+ """,
+ re.IGNORECASE | re.VERBOSE,
+)
+
+
+# Strip ``PRIMARY_ID `` so the attribute
+# scanner doesn't collect the id field. The system always auto-adds
+# ``PRIMARY_ID id STRING``; user-supplied values are honored only if
+# they appear as the literal PRIMARY_ID — otherwise they're treated
+# as plain attributes.
+_PRIMARY_ID_RE = re.compile(
+ r"\bPRIMARY_ID\b\s+[A-Za-z_][A-Za-z0-9_]*\s+(?:STRING|INT|UINT|DOUBLE|FLOAT|BOOL|DATETIME)",
+ re.IGNORECASE,
+)
+
+
+# Strip a ``FROM , TO `` clause so the attribute scanner doesn't
+# accidentally pick up "FROM" / "TO" tokens or their vertex-type
+# placeholders. Used when scanning edge attribute bodies.
+_FROM_TO_CLAUSE_RE = re.compile(
+ r"\bfrom\s+[A-Za-z_][A-Za-z0-9_]*\s*,\s*to\s+[A-Za-z_][A-Za-z0-9_]*",
+ re.IGNORECASE,
+)
+
+
+def _extract_attributes(body: str, *, is_edge_body: bool) -> List[Tuple[str, str]]:
+ """Scan an attribute body and return ``(name, type)`` pairs that
+ look like primitive attribute declarations. Skips ``PRIMARY_ID``
+ entries (the system auto-adds those) and, for edge bodies, FROM/TO
+ pair clauses.
+ """
+ if not body:
+ return []
+ cleaned = _PRIMARY_ID_RE.sub("", body)
+ if is_edge_body:
+ cleaned = _FROM_TO_CLAUSE_RE.sub("", cleaned)
+ seen: Set[str] = set()
+ out: List[Tuple[str, str]] = []
+ for m in _ATTR_TOKEN_RE.finditer(cleaned):
+ name = m.group("name")
+ type_str = m.group("type").upper()
+ folded = name.casefold()
+ if folded in seen:
+ continue
+ # Skip GSQL keywords that may slip through (FROM/TO already
+ # stripped, but be defensive against other reserved tokens).
+ if folded in {"from", "to", "primary_id"}:
+ continue
+ seen.add(folded)
+ out.append((name, type_str))
+ return out
+
+
+# Matches a comment block:
+# * one or more `// ...` lines, or
+# * a single `/* ... */` block.
+# Used to find descriptions immediately preceding a VERTEX/EDGE line.
+_COMMENT_BLOCK_RE = re.compile(
+ r"""
+ (?:
+ (?:^[ \t]*//[ \t]?(?P.*)$\n?)+ # one+ // lines
+ |
+ ^[ \t]*/\*(?P.*?)\*/[ \t]*\n # /* … */ block
+ )
+ """,
+ re.MULTILINE | re.DOTALL | re.VERBOSE,
+)
+
+
+def _extract_description_for(text: str, decl_start: int) -> str:
+ """Return the comment block's text immediately preceding *decl_start*
+ in *text*, or an empty string if none is present.
+
+ A comment block is one or more consecutive ``//`` line comments, or a
+ single ``/* … */`` block, separated from the declaration by at most
+ blank lines.
+ """
+ # Walk backwards from decl_start over blank/whitespace-only lines.
+ cursor = decl_start
+ # Skip any whitespace immediately before the decl
+ while cursor > 0 and text[cursor - 1] in (" ", "\t"):
+ cursor -= 1
+ # Walk back one or more blank lines
+ while cursor > 0 and text[cursor - 1] == "\n":
+ # Look at the line before this newline
+ prev_line_end = cursor - 1
+ prev_line_start = text.rfind("\n", 0, prev_line_end) + 1
+ prev_line = text[prev_line_start:prev_line_end]
+ if prev_line.strip() == "":
+ cursor = prev_line_start
+ continue
+ break
+
+ # Now cursor points at the start of the line that's potentially a comment.
+ # Walk back over consecutive `//` lines collecting their bodies.
+ comment_lines: List[str] = []
+ while cursor > 0:
+ line_end = cursor - 1 # newline before cursor
+ line_start = text.rfind("\n", 0, line_end) + 1
+ line = text[line_start:line_end]
+ stripped = line.lstrip()
+ if stripped.startswith("//"):
+ comment_lines.insert(0, stripped[2:].lstrip())
+ cursor = line_start
+ elif stripped.startswith("/*") or stripped.endswith("*/"):
+ # Try a /* … */ block ending on this line
+ block_end = text.rfind("*/", 0, cursor)
+ block_start = text.rfind("/*", 0, block_end)
+ if block_start == -1 or block_end == -1:
+ break
+ body = text[block_start + 2:block_end]
+ # Strip leading * on each line (typical /* * … */ style)
+ cleaned = re.sub(r"^\s*\*?\s?", "", body, flags=re.MULTILINE)
+ comment_lines.insert(0, cleaned.strip())
+ break
+ else:
+ break
+
+ return " ".join(s.strip() for s in comment_lines if s.strip())
+
+
+def parse_gsql_schema(text: str) -> SchemaProposal:
+ """Permissively scan *text* for ``VERTEX`` / ``DIRECTED EDGE``
+ declarations and return a :class:`SchemaProposal`.
+
+ The scanner ignores everything that doesn't match the two declaration
+ patterns: section headers (``Vertex Types:``, ``Edge Types:``),
+ ``Indexes:``, ``Queries:`` blocks, ``CREATE GRAPH`` /
+ ``INSTALL QUERY`` / ``ALTER`` lines, blank lines, etc. ``ADD``
+ prefix and the ``- `` bullet from ``gsql ls`` output are both
+ accepted; ``;`` terminators are tolerated.
+
+ Lines naming a structural type (case-insensitive) are silently dropped.
+ ``reverse_*`` edges (auto-generated by ``WITH REVERSE_EDGE=…``) are
+ silently dropped. ``(FROM, TO)`` pairs whose endpoints don't resolve
+ to a vertex extracted from the same payload are dropped after parsing
+ (see :meth:`SchemaProposal.drop_dangling_pairs`).
+ """
+ proposal = SchemaProposal()
+
+ # Pass 1: vertices
+ for m in _VERTEX_LINE_RE.finditer(text):
+ name = m.group("name")
+ if is_structural_type(name):
+ continue
+ desc = _extract_description_for(text, m.start())
+ attrs = _extract_attributes(m.group("body") or "", is_edge_body=False)
+ proposal.add_vertex(name=name, description=desc, attributes=attrs)
+
+ # Pass 2: edges
+ for m in _EDGE_LINE_RE.finditer(text):
+ name = m.group("name")
+ if is_structural_type(name):
+ continue
+ if name.lower().startswith("reverse_"):
+ continue
+ body = m.group("body") or ""
+ desc = _extract_description_for(text, m.start())
+ attrs = _extract_attributes(body, is_edge_body=True)
+ directed = (m.group("dir") or "directed").lower() == "directed"
+ for pm in _EDGE_PAIR_RE.finditer(body):
+ from_vt = pm.group("from")
+ to_vt = pm.group("to")
+ if is_structural_type(from_vt) and is_structural_type(to_vt):
+ # Both endpoints are structural — definitely not a domain
+ # edge pair the user is trying to add. Drop it.
+ continue
+ proposal.add_edge_pair(
+ name=name,
+ from_vt=from_vt,
+ to_vt=to_vt,
+ description=desc,
+ attributes=attrs,
+ directed=directed,
+ )
+
+ # Filter dangling pairs (FROM/TO that don't resolve to a vertex we
+ # actually extracted from the same payload).
+ proposal.drop_dangling_pairs()
+ return proposal
+
+
+# -----------------------------------------------------------------------------
+# GSQL emission
+# -----------------------------------------------------------------------------
+
+
+@dataclass
+class ExistingSchema:
+ """Snapshot of what's already on the graph, used by the diff emitter.
+
+ ``vertex_types`` is the set of vertex-type names currently on the
+ graph (case-sensitive). ``edge_pairs`` maps an edge type name to the
+ set of ``(FROM, TO)`` pairs already declared for that edge.
+ """
+
+ vertex_types: Set[str] = field(default_factory=set)
+ edge_pairs: dict = field(default_factory=dict)
+
+ def has_vertex(self, name: str) -> bool:
+ folded = name.casefold()
+ return any(v.casefold() == folded for v in self.vertex_types)
+
+ def has_edge(self, name: str) -> bool:
+ return name in self.edge_pairs or any(
+ k.casefold() == name.casefold() for k in self.edge_pairs
+ )
+
+ def has_edge_pair(self, name: str, from_vt: str, to_vt: str) -> bool:
+ # Edge name lookup is case-insensitive
+ edge_key = next(
+ (k for k in self.edge_pairs if k.casefold() == name.casefold()),
+ None,
+ )
+ if edge_key is None:
+ return False
+ for src, tgt in self.edge_pairs.get(edge_key, set()):
+ if src.casefold() == from_vt.casefold() and tgt.casefold() == to_vt.casefold():
+ return True
+ return False
+
+
+def emit_add_statements(
+ proposal: SchemaProposal,
+ existing: Optional[ExistingSchema] = None,
+) -> List[str]:
+ """Diff *proposal* against *existing* and return a list of GSQL
+ statements (sans trailing ``;``) that, when run inside a
+ ``SCHEMA_CHANGE JOB`` against a graph in the *existing* state, bring
+ the graph up to the proposal.
+
+ Order is deterministic and dependency-safe:
+
+ 1. ``ADD VERTEX (PRIMARY_ID id STRING) WITH PRIMARY_ID_AS_ATTRIBUTE="true"``
+ for every domain vertex type that doesn't already exist.
+ 2. ``ADD DIRECTED EDGE (FROM , TO [| FROM …]) WITH REVERSE_EDGE="reverse_"``
+ for every domain edge type that doesn't exist on the graph at all.
+ 3. ``ALTER EDGE ADD PAIR (FROM , TO )`` for every
+ ``(FROM, TO)`` pair on an existing edge type that's missing.
+
+ No ``DROP``s are ever emitted — the diff is strictly additive.
+ """
+ if existing is None:
+ existing = ExistingSchema()
+
+ stmts: List[str] = []
+
+ # 1. New vertex types
+ for v in proposal.vertices:
+ if existing.has_vertex(v.name):
+ continue
+ attrs_part = ""
+ if v.attributes:
+ attrs_part = ", " + ", ".join(
+ f"{a.name} {a.type}" for a in v.attributes
+ )
+ stmts.append(
+ f'ADD VERTEX {v.name} (PRIMARY_ID id STRING{attrs_part}) '
+ f'WITH PRIMARY_ID_AS_ATTRIBUTE="true"'
+ )
+
+ # 2 + 3. Edges: fully new, or new pairs on an existing edge
+ for e in proposal.edges:
+ if not e.pairs:
+ continue
+ if not existing.has_edge(e.name):
+ pairs_str = " | ".join(
+ f"FROM {src}, TO {tgt}" for src, tgt in e.pairs
+ )
+ attrs_part = ""
+ if e.attributes:
+ attrs_part = ", " + ", ".join(
+ f"{a.name} {a.type}" for a in e.attributes
+ )
+ edge_kw = "DIRECTED EDGE" if e.directed else "UNDIRECTED EDGE"
+ # Undirected edges have no reverse companion, so omit the
+ # WITH REVERSE_EDGE clause.
+ with_clause = (
+ f' WITH REVERSE_EDGE="reverse_{e.name}"' if e.directed else ""
+ )
+ stmts.append(
+ f'ADD {edge_kw} {e.name} ({pairs_str}{attrs_part}){with_clause}'
+ )
+ else:
+ # Existing edge: only ALTER ADD PAIR is supported. Attributes
+ # of an existing edge can't be added at the same time; that's
+ # a separate ALTER ATTRIBUTE statement and is out of scope
+ # for the additive Phase 1 diff emitter.
+ for src, tgt in e.pairs:
+ if existing.has_edge_pair(e.name, src, tgt):
+ continue
+ stmts.append(
+ f"ALTER EDGE {e.name} ADD PAIR (FROM {src}, TO {tgt})"
+ )
+
+ return stmts
+
+
+def emit_structural_link_alters(
+ proposal: SchemaProposal,
+ existing: Optional[ExistingSchema] = None,
+) -> List[str]:
+ """For every domain vertex in *proposal*, emit ``ALTER EDGE … ADD
+ PAIR`` statements that connect it to the GraphRAG core via the
+ structural edges:
+
+ * ``CONTAINS_ENTITY`` — ``Document`` / ``DocumentChunk`` → domain vertex
+
+ The typed-relationship pattern (``IS_HEAD_OF`` / ``HAS_TAIL``) lives
+ at the meta-schema layer (``EntityType`` ↔ ``RelationshipType``) and
+ does NOT need per-domain-vertex pairs. The original schema
+ declaration covers the only pairs we ever traverse.
+
+ Pairs already on the graph (per *existing*) are skipped. The
+ statements are returned in a deterministic order so the schema
+ diff is reproducible.
+ """
+ if existing is None:
+ existing = ExistingSchema()
+
+ # Skip the structural-link emit entirely when the GraphRAG core
+ # types aren't on the graph — without them the ALTER would
+ # reference an undeclared endpoint and fail. In production these
+ # are always present by the time apply_proposal runs (init_supportai
+ # creates the structural schema first), but unit tests and
+ # bare-graph fixtures may not have them.
+ has_doc = existing.has_vertex("Document")
+ has_chunk = existing.has_vertex("DocumentChunk")
+
+ stmts: List[str] = []
+ for v in proposal.vertices:
+ # CONTAINS_ENTITY: Document / DocumentChunk →
+ if has_doc and not existing.has_edge_pair("CONTAINS_ENTITY", "Document", v.name):
+ stmts.append(
+ f"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO {v.name})"
+ )
+ if has_chunk and not existing.has_edge_pair("CONTAINS_ENTITY", "DocumentChunk", v.name):
+ stmts.append(
+ f"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO {v.name})"
+ )
+ return stmts
+
+
+def emit_preview_gsql(proposal: SchemaProposal) -> str:
+ """Render *proposal* as a self-contained GSQL block suitable for the
+ UI's "Preview as GSQL" tab. Comments above each declaration carry
+ the description, when set.
+ """
+ lines: List[str] = []
+ if proposal.domain_label:
+ lines.append(f"// Domain: {proposal.domain_label}")
+ lines.append("")
+
+ for v in proposal.vertices:
+ if v.description:
+ lines.append(f"// {v.description}")
+ attrs_part = ""
+ if v.attributes:
+ attrs_part = ", " + ", ".join(
+ f"{a.name} {a.type}" for a in v.attributes
+ )
+ lines.append(
+ f'ADD VERTEX {v.name} (PRIMARY_ID id STRING{attrs_part}) '
+ f'WITH PRIMARY_ID_AS_ATTRIBUTE="true";'
+ )
+ lines.append("")
+
+ for e in proposal.edges:
+ if not e.pairs:
+ continue
+ if e.description:
+ lines.append(f"// {e.description}")
+ pairs_str = " | ".join(f"FROM {src}, TO {tgt}" for src, tgt in e.pairs)
+ attrs_part = ""
+ if e.attributes:
+ attrs_part = ", " + ", ".join(
+ f"{a.name} {a.type}" for a in e.attributes
+ )
+ edge_kw = "DIRECTED EDGE" if e.directed else "UNDIRECTED EDGE"
+ with_clause = (
+ f' WITH REVERSE_EDGE="reverse_{e.name}"' if e.directed else ""
+ )
+ lines.append(
+ f'ADD {edge_kw} {e.name} ({pairs_str}{attrs_part}){with_clause};'
+ )
+ lines.append("")
+
+ return "\n".join(lines).rstrip() + "\n"
+
+
+# -----------------------------------------------------------------------------
+# TigerGraph-side schema reader
+# -----------------------------------------------------------------------------
+
+
+def read_existing_schema(conn) -> ExistingSchema:
+ """Read the current vertex / edge schema from a TigerGraph
+ connection and return an :class:`ExistingSchema` snapshot suitable
+ for :func:`emit_add_statements`.
+
+ Works with both ``pyTigerGraph.TigerGraphConnection`` and our
+ ``TigerGraphConnectionProxy`` wrapper. Only the synchronous
+ ``getVertexTypes`` / ``getEdgeTypes`` / ``getEdgeType`` API is used.
+
+ Edge pairs are extracted from the edge-type metadata returned by
+ pyTigerGraph. For single-pair edges the metadata exposes
+ ``FromVertexTypeName`` / ``ToVertexTypeName`` directly. For
+ multi-pair edges (where those fields are ``"*"``) the metadata
+ contains an ``EdgePairs`` list of ``{"From": ..., "To": ...}``
+ dicts. We accept both shapes.
+
+ Errors during schema introspection are not swallowed — the caller
+ needs to know if the snapshot is incomplete before diffing. If the
+ graph hasn't been initialized at all (no vertex types yet),
+ pyTigerGraph returns an empty list, which produces an
+ ``ExistingSchema`` with empty ``vertex_types`` / ``edge_pairs``
+ (the diff emitter then emits a full ``ADD`` for everything in the
+ proposal — which is the desired behavior on a fresh graph).
+ """
+ snapshot = ExistingSchema()
+
+ vertex_types = conn.getVertexTypes() or []
+ snapshot.vertex_types = set(vertex_types)
+
+ for et_name in conn.getEdgeTypes() or []:
+ meta = conn.getEdgeType(et_name) or {}
+ pairs: Set[Tuple[str, str]] = set()
+
+ from_v = meta.get("FromVertexTypeName")
+ to_v = meta.get("ToVertexTypeName")
+ if from_v and to_v and from_v != "*" and to_v != "*":
+ pairs.add((from_v, to_v))
+
+ # Multi-pair edges: an EdgePairs list either always (some TG
+ # versions) or only when From/To are "*" (other versions).
+ for ep in meta.get("EdgePairs", []) or []:
+ f = ep.get("From")
+ t = ep.get("To")
+ if f and t:
+ pairs.add((f, t))
+
+ if pairs:
+ snapshot.edge_pairs[et_name] = pairs
+
+ return snapshot
+
+
+# -----------------------------------------------------------------------------
+# Atomic apply
+# -----------------------------------------------------------------------------
+
+
+def build_schema_change_job(
+ graphname: str,
+ statements: Sequence[str],
+ job_name: Optional[str] = None,
+) -> Tuple[str, str]:
+ """Wrap *statements* into a single ``CREATE SCHEMA_CHANGE JOB`` /
+ ``RUN`` / ``DROP`` GSQL block for *graphname*.
+
+ Returns ``(gsql_block, job_name)``. The job name is generated with a
+ short uuid suffix so re-runs against the same graph don't collide
+ with a previously-created (but never dropped) job.
+
+ The returned block is intended to be passed verbatim to
+ ``conn.gsql(...)``; running every ``ADD`` / ``ALTER`` inside one job
+ is what makes the application atomic.
+ """
+ if not statements:
+ raise ValueError("build_schema_change_job: statements is empty")
+ if job_name is None:
+ job_name = f"add_domain_schema_{uuid.uuid4().hex[:8]}"
+
+ body = ";\n ".join(s.rstrip(";") for s in statements) + ";"
+ block = (
+ f"USE GRAPH {graphname}\n"
+ f"CREATE SCHEMA_CHANGE JOB {job_name} FOR GRAPH {graphname} {{\n"
+ f" {body}\n"
+ f"}}\n"
+ f"RUN SCHEMA_CHANGE JOB {job_name}\n"
+ f"DROP JOB {job_name}"
+ )
+ return block, job_name
+
+
+def read_type_metadata(conn) -> Tuple[dict, dict]:
+ """Read every ``EntityType`` / ``RelationshipType`` vertex from
+ *conn* and return two dicts:
+
+ (
+ {entity_type_id: description},
+ {relationship_type_id: definition},
+ )
+
+ Empty / missing values are dropped so callers can ``.get(name, "")``
+ without distinguishing "no row" from "row with empty description".
+ Errors propagate — callers needing best-effort behavior should wrap
+ in their own try/except.
+ """
+ entity_descs: dict = {}
+ rel_defs: dict = {}
+
+ try:
+ rows = conn.getVertices("EntityType") or []
+ except Exception:
+ rows = []
+ for row in rows:
+ attrs = row.get("attributes", row)
+ v_id = row.get("v_id") or attrs.get("id")
+ desc = (attrs.get("description") or "").strip()
+ if v_id and desc:
+ entity_descs[v_id] = desc
+
+ try:
+ rows = conn.getVertices("RelationshipType") or []
+ except Exception:
+ rows = []
+ for row in rows:
+ attrs = row.get("attributes", row)
+ v_id = row.get("v_id") or attrs.get("id")
+ defn = (attrs.get("definition") or "").strip()
+ if v_id and defn:
+ rel_defs[v_id] = defn
+
+ return entity_descs, rel_defs
+
+
+async def read_existing_schema_async(conn) -> "ExistingSchema":
+ """Async counterpart to :func:`read_existing_schema` — used by the
+ ECC pipeline where ``conn`` is an ``AsyncTigerGraphConnection``.
+
+ Returns a raw :class:`ExistingSchema` snapshot. Callers that want
+ to filter structural types (e.g. for domain-only consumers like
+ the extractor builder) should use :func:`is_structural_type` on
+ the returned vertex / edge names — this helper deliberately does
+ not couple the live-schema read to the proposal-time concept of
+ "domain vs structural", so live-schema consumers stay independent
+ of the proposal lifecycle.
+ """
+ snapshot = ExistingSchema()
+ snapshot.vertex_types = set(await conn.getVertexTypes() or [])
+ for et_name in await conn.getEdgeTypes() or []:
+ meta = await conn.getEdgeType(et_name) or {}
+ pairs: Set[Tuple[str, str]] = set()
+ from_v = meta.get("FromVertexTypeName")
+ to_v = meta.get("ToVertexTypeName")
+ if from_v and to_v and from_v != "*" and to_v != "*":
+ pairs.add((from_v, to_v))
+ for ep in meta.get("EdgePairs", []) or []:
+ f = ep.get("From")
+ t = ep.get("To")
+ if f and t:
+ pairs.add((f, t))
+ if pairs:
+ snapshot.edge_pairs[et_name] = pairs
+ return snapshot
+
+
+async def read_type_metadata_async(conn) -> Tuple[dict, dict]:
+ """Async counterpart to :func:`read_type_metadata` — used by the
+ ECC pipeline where the available connection is
+ ``pyTigerGraph.AsyncTigerGraphConnection``.
+
+ Same return shape: ``({entity_id: description}, {rel_id: definition})``.
+ Errors propagate to the caller.
+ """
+ entity_descs: dict = {}
+ rel_defs: dict = {}
+
+ try:
+ rows = await conn.getVertices("EntityType") or []
+ except Exception:
+ rows = []
+ for row in rows:
+ attrs = row.get("attributes", row)
+ v_id = row.get("v_id") or attrs.get("id")
+ desc = (attrs.get("description") or "").strip()
+ if v_id and desc:
+ entity_descs[v_id] = desc
+
+ try:
+ rows = await conn.getVertices("RelationshipType") or []
+ except Exception:
+ rows = []
+ for row in rows:
+ attrs = row.get("attributes", row)
+ v_id = row.get("v_id") or attrs.get("id")
+ defn = (attrs.get("definition") or "").strip()
+ if v_id and defn:
+ rel_defs[v_id] = defn
+
+ return entity_descs, rel_defs
+
+
+def _short_name(name: str) -> str:
+ """Lowercase, underscore-separated form of *name* — used as the
+ ``short_name`` attribute on ``RelationshipType`` vertices for display.
+ Trims to at most ~32 characters (the column has no length but display
+ is friendlier when short).
+ """
+ folded = re.sub(r"[^A-Za-z0-9]+", "_", name).strip("_").lower()
+ return folded[:32]
+
+
+def upsert_type_metadata(
+ conn,
+ proposal: SchemaProposal,
+) -> dict:
+ """Upsert ``EntityType`` / ``RelationshipType`` vertices with the
+ descriptions from *proposal*. Does not touch existing rows whose
+ description / definition is already non-empty unless the proposal
+ carries a non-empty value of its own (callers may opt to override
+ by passing a description; we always pass through what the proposal
+ has).
+
+ Returns ``{"entity_types": [...], "relationship_types": [...]}``
+ listing the ids upserted.
+ """
+ now = int(time.time())
+ entity_ids: List[str] = []
+ relationship_ids: List[str] = []
+
+ for v in proposal.vertices:
+ # EntityType schema: (id STRING, description STRING, epoch_added UINT)
+ attrs = {"epoch_added": now}
+ if v.description:
+ attrs["description"] = v.description
+ conn.upsertVertex("EntityType", v.name, attributes=attrs)
+ entity_ids.append(v.name)
+
+ for e in proposal.edges:
+ # RelationshipType schema:
+ # (id STRING, definition STRING, short_name STRING,
+ # epoch_added UINT, epoch_processing UINT, epoch_processed UINT)
+ attrs = {
+ "epoch_added": now,
+ "short_name": _short_name(e.name),
+ }
+ if e.description:
+ attrs["definition"] = e.description
+ conn.upsertVertex("RelationshipType", e.name, attributes=attrs)
+ relationship_ids.append(e.name)
+
+ return {
+ "entity_types": entity_ids,
+ "relationship_types": relationship_ids,
+ }
+
+
+def apply_proposal(
+ conn,
+ graphname: str,
+ proposal: SchemaProposal,
+ job_name: Optional[str] = None,
+) -> dict:
+ """Diff *proposal* against the current schema on *conn* and apply the
+ additive delta as a single atomic ``SCHEMA_CHANGE JOB``.
+
+ Returns a result dict::
+
+ {
+ "status": "applied" | "no-op",
+ "statements": [...], # ADD/ALTER statements that were emitted
+ "job_name": "",
+ "gsql_output": "",
+ "summary": {...}, # summarize(proposal)
+ }
+
+ Schema introspection errors propagate; the caller decides whether the
+ overall init flow should be marked as failed. The structural GraphRAG
+ schema must already exist on the graph (so the diff sees structural
+ types and only emits domain-side ADDs).
+ """
+ existing = read_existing_schema(conn)
+ domain_stmts = emit_add_statements(proposal, existing)
+ # Run the structural-link emitter against an *augmented* snapshot
+ # so vertices we're about to ADD are treated as present — otherwise
+ # has_edge_pair would always say "missing" and we'd over-emit.
+ augmented = ExistingSchema(
+ vertex_types=set(existing.vertex_types) | {v.name for v in proposal.vertices},
+ edge_pairs=dict(existing.edge_pairs),
+ )
+ structural_stmts = emit_structural_link_alters(proposal, augmented)
+ statements = domain_stmts + structural_stmts
+ summary = summarize(proposal)
+
+ if not statements:
+ # Even on no-op, refresh metadata so descriptions edited in the
+ # review panel land on EntityType / RelationshipType vertices.
+ metadata = upsert_type_metadata(conn, proposal)
+ return {
+ "status": "no-op",
+ "statements": [],
+ "job_name": None,
+ "gsql_output": "",
+ "summary": summary,
+ "metadata": metadata,
+ }
+
+ block, job_name = build_schema_change_job(graphname, statements, job_name)
+ output = conn.gsql(block)
+ err = gsql_output_error(output)
+ if err:
+ # pyTigerGraph's gsql() returned a failure response without
+ # raising — surface it explicitly so the caller doesn't
+ # falsely report "applied". Skip metadata upsert (the schema
+ # change didn't land, so writing EntityType vertices for
+ # types that don't exist would also fail).
+ return {
+ "status": "error",
+ "statements": statements,
+ "job_name": job_name,
+ "gsql_output": output,
+ "error": err,
+ "summary": summary,
+ "metadata": {"entity_types": [], "relationship_types": []},
+ }
+ metadata = upsert_type_metadata(conn, proposal)
+ return {
+ "status": "applied",
+ "statements": statements,
+ "job_name": job_name,
+ "gsql_output": output,
+ "summary": summary,
+ "metadata": metadata,
+ }
+
+
+# -----------------------------------------------------------------------------
+# Validation summary (informational, never blocking)
+# -----------------------------------------------------------------------------
+
+
+def summarize(proposal: SchemaProposal) -> dict:
+ """Return a small descriptive payload for logging / API responses
+ (counts and lists of names). Never raises.
+ """
+ return {
+ "vertex_count": len(proposal.vertices),
+ "edge_count": len(proposal.edges),
+ "vertex_names": [v.name for v in proposal.vertices],
+ "edge_names": [e.name for e in proposal.edges],
+ "edge_pair_count": sum(len(e.pairs) for e in proposal.edges),
+ "domain_label": proposal.domain_label,
+ }
diff --git a/common/embeddings/tigergraph_embedding_store.py b/common/embeddings/tigergraph_embedding_store.py
index 748f166..e4758b8 100644
--- a/common/embeddings/tigergraph_embedding_store.py
+++ b/common/embeddings/tigergraph_embedding_store.py
@@ -69,11 +69,7 @@ def __init__(
tg_version = self.conn.getVer()
ver = tg_version.split(".")
if int(ver[0]) >= 4 and int(ver[1]) >= 2:
- logger.info(f"Installing GDS library")
- q_res = self.conn.gsql(
- """USE GLOBAL\nimport package gds\ninstall function gds.**"""
- )
- logger.info(f"Done installing GDS library with status {q_res}")
+ self._ensure_gds_installed()
if self.conn.graphname and not self.conn.graphname == "MyGraph":
current_schema = self.conn.gsql(f"USE GRAPH {self.conn.graphname}\n ls")
if "(Dimension=" in current_schema:
@@ -82,6 +78,47 @@ def __init__(
else:
raise Exception(f"Current TigerGraph version {ver} does not support vector feature!")
+ def _ensure_gds_installed(self) -> None:
+ """Install the gds package only if the gds.vector sub-package
+ isn't already present.
+
+ Probes via ``SHOW PACKAGE gds`` whose output looks like
+ ``Packages "gds":\\n - Sub-Packages:\\n - vector\\n`` when
+ the sub-package is installed (verified empirically). Checking
+ the sub-package (rather than just the top-level ``gds``) also
+ catches a partial-install state where ``gds`` is present but
+ ``gds.vector`` isn't.
+
+ The probe is a fast catalog read (~60ms) compared to
+ ``install function gds.**`` which takes a global catalog
+ write lock for the duration of the install scan (~3 minutes
+ against a remote TG). Skipping the install when the package
+ is present avoids that lock on every container restart.
+
+ Falls through to the install on any probe failure — better to
+ occasionally re-install than to skip an install that's
+ actually missing.
+ """
+ try:
+ sub_packages = self.conn.gsql("SHOW PACKAGE gds")
+ except Exception as exc: # noqa: BLE001 — defensive
+ logger.warning(
+ f"GDS-presence probe failed: {exc}. Falling through to install."
+ )
+ sub_packages = ""
+ # The expected installed output is:
+ # 'Packages "gds":\n - Sub-Packages:\n - vector\n'
+ # When gds isn't installed at all, ``SHOW PACKAGE gds`` returns
+ # an error message instead, so this check fails closed.
+ if "- vector" in sub_packages:
+ logger.info("GDS library already installed; skipping install.")
+ return
+ logger.info("Installing GDS library")
+ q_res = self.conn.gsql(
+ """USE GLOBAL\nimport package gds\ninstall function gds.**"""
+ )
+ logger.info(f"Done installing GDS library with status {q_res}")
+
def install_vector_queries(self):
logger.info(f"Installing vector queries")
vector_queries = [
@@ -121,6 +158,12 @@ def set_graphname(self, graphname):
self.vector_attr_cache = {}
if self.conn.apiToken or self.conn.jwtToken:
self.conn.getToken()
+ # Re-verify GDS presence on every graphname switch. Cheap when
+ # the package is already installed (one LS call) and recovers
+ # from a mid-flight DROP ALL or admin-side wipe before the
+ # per-graph vector-query install below tries to reference
+ # missing gds.vector.* UDFs.
+ self._ensure_gds_installed()
if self.conn.graphname and not self.conn.graphname == "MyGraph":
current_schema = self.conn.gsql(f"USE GRAPH {self.conn.graphname}\n ls")
if "(Dimension=" in current_schema:
diff --git a/common/extractors/LLMEntityRelationshipExtractor.py b/common/extractors/LLMEntityRelationshipExtractor.py
index dec1753..877197b 100644
--- a/common/extractors/LLMEntityRelationshipExtractor.py
+++ b/common/extractors/LLMEntityRelationshipExtractor.py
@@ -32,11 +32,112 @@ def __init__(
allowed_entity_types: List[str] = None,
allowed_relationship_types: List[str] = None,
strict_mode: bool = False,
+ entity_type_definitions: dict = None,
+ relationship_type_definitions: dict = None,
+ domain_edge_endpoints: dict = None,
):
self.llm_service = llm_service
self.allowed_vertex_types = allowed_entity_types
self.allowed_edge_types = allowed_relationship_types
+ # When True the existing parser filter (drop nodes/rels whose
+ # type isn't in the allowed list) is enforced AND the prompt
+ # tells the LLM to stay within the schema. Read from
+ # graphrag_config.strict_mode by the ECC builder.
self.strict_mode = strict_mode
+ self.entity_type_definitions = dict(entity_type_definitions or {})
+ self.relationship_type_definitions = dict(
+ relationship_type_definitions or {}
+ )
+ # Per-edge ``{name: [(from_vt, to_vt), ...]}`` derived from the
+ # live schema. Used by the prompt to tell the LLM the valid
+ # source/target pairs per relationship type, and by the ingest
+ # worker to validate that an extracted relationship's endpoints
+ # match a declared pair before writing IS_HEAD_OF / HAS_TAIL.
+ self.domain_edge_endpoints = {
+ k: list(v) for k, v in (domain_edge_endpoints or {}).items()
+ }
+
+ def _format_definitions(self, defs: dict) -> str:
+ """Render a ``{type_name: definition}`` dict as one
+ ``- : `` line per type, sorted by name. Used
+ when assembling the schema-aware extraction prompt.
+ """
+ if not defs:
+ return ""
+ return "\n".join(
+ f"- {name}: {definition}"
+ for name, definition in sorted(defs.items())
+ if definition
+ )
+
+ def _format_edge_endpoints(self) -> str:
+ """Render ``{edge_name: [(from, to), ...]}`` as
+ ``- : -> [, -> ]`` lines, sorted
+ by edge name. Empty when no endpoints are configured.
+ """
+ if not self.domain_edge_endpoints:
+ return ""
+ lines = []
+ for name, pairs in sorted(self.domain_edge_endpoints.items()):
+ pair_strs = ", ".join(f"{f} -> {t}" for f, t in pairs) or ""
+ defn = self.relationship_type_definitions.get(name, "")
+ tail = f" — {defn}" if defn else ""
+ lines.append(f"- {name}: {pair_strs}{tail}")
+ return "\n".join(lines)
+
+ def _build_schema_prompt_messages(self) -> list:
+ """Return the human-message tuples that describe the domain
+ schema to the LLM. Used by both sync and async extraction paths.
+ Empty list when no schema is configured.
+ """
+ msgs = []
+ entity_def_block = self._format_definitions(self.entity_type_definitions)
+ rel_def_block = self._format_definitions(self.relationship_type_definitions)
+ endpoints_block = self._format_edge_endpoints()
+ if not (entity_def_block or rel_def_block or endpoints_block):
+ return msgs
+
+ if self.strict_mode:
+ msgs.append((
+ "human",
+ "STRICT SCHEMA MODE: only emit entities whose entity_type "
+ "matches one of the schema entity types listed below, and "
+ "only emit relationships whose relation_type matches a "
+ "schema relationship type AND whose source / target "
+ "entity types match a declared (FROM, TO) endpoint pair "
+ "for that relationship. Drop any entity or relationship "
+ "that doesn't fit. Do NOT invent new types.",
+ ))
+ else:
+ msgs.append((
+ "human",
+ "When deciding the entity_type / relationship_type for an "
+ "extraction, strongly prefer the schema types listed below "
+ "and use their definitions to disambiguate similar types. "
+ "Ignore page-structure / chart / layout artifacts (axes, "
+ "segments, percentages, page numbers, sections, navigation "
+ "menus, captions). Prefer concrete real-world entities over "
+ "abstract categorical groupings. Only invent a new type "
+ "when nothing in the schema fits.",
+ ))
+ if entity_def_block:
+ msgs.append((
+ "human",
+ f"Schema entity types with definitions:\n{entity_def_block}",
+ ))
+ if endpoints_block:
+ msgs.append((
+ "human",
+ "Schema relationship types — each line lists the valid "
+ "(source -> target) endpoint pairs for that relationship "
+ "and the relationship's definition:\n" + endpoints_block,
+ ))
+ elif rel_def_block:
+ msgs.append((
+ "human",
+ f"Schema relationship types with definitions:\n{rel_def_block}",
+ ))
+ return msgs
def _parse_json_output(self, content: str) -> dict:
"""Parse JSON from LLM output with multiple fallback strategies.
@@ -298,6 +399,7 @@ async def adocument_er_extraction(self, document):
prompt.append(("human", f"Allowed Node Types: {self.allowed_vertex_types}"))
if self.allowed_edge_types:
prompt.append(("human", f"Allowed Edge Types: {self.allowed_edge_types}"))
+ prompt.extend(self._build_schema_prompt_messages())
prompt = ChatPromptTemplate.from_messages(prompt)
chain = prompt | self.llm_service.llm # | parser
er = await self._aextract_kg_from_doc(document, chain, parser)
@@ -336,6 +438,7 @@ def document_er_extraction(self, document):
prompt.append(("human", f"Allowed Node Types: {self.allowed_vertex_types}"))
if self.allowed_edge_types:
prompt.append(("human", f"Allowed Edge Types: {self.allowed_edge_types}"))
+ prompt.extend(self._build_schema_prompt_messages())
prompt = ChatPromptTemplate.from_messages(prompt)
chain = prompt | self.llm_service.llm # | parser
er = self._extract_kg_from_doc(document, chain, parser)
diff --git a/common/gsql/supportai/SupportAI_Schema.gsql b/common/gsql/supportai/SupportAI_Schema.gsql
index c756fd3..79aa865 100644
--- a/common/gsql/supportai/SupportAI_Schema.gsql
+++ b/common/gsql/supportai/SupportAI_Schema.gsql
@@ -22,14 +22,13 @@ CREATE SCHEMA_CHANGE JOB add_supportai_schema {
ADD VERTEX Content(PRIMARY_ID id STRING, ctype STRING, text STRING, epoch_added UINT) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true";
ADD VERTEX EntityType(PRIMARY_ID id STRING, description STRING, epoch_added UINT) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true";
ADD DIRECTED EDGE HAS_CONTENT(FROM Document, TO Content|FROM DocumentChunk, TO Content) WITH REVERSE_EDGE="reverse_HAS_CONTENT";
- ADD DIRECTED EDGE IS_HEAD_OF(FROM Entity, TO RelationshipType) WITH REVERSE_EDGE="reverse_IS_HEAD_OF";
- ADD DIRECTED EDGE HAS_TAIL(FROM RelationshipType, TO Entity) WITH REVERSE_EDGE="reverse_HAS_TAIL";
+ ADD DIRECTED EDGE IS_HEAD_OF(FROM EntityType, TO RelationshipType) WITH REVERSE_EDGE="reverse_IS_HEAD_OF";
+ ADD DIRECTED EDGE HAS_TAIL(FROM RelationshipType, TO EntityType) WITH REVERSE_EDGE="reverse_HAS_TAIL";
ADD DIRECTED EDGE CONTAINS_ENTITY(FROM DocumentChunk, TO Entity|FROM Document, TO Entity) WITH REVERSE_EDGE="reverse_CONTAINS_ENTITY";
ADD DIRECTED EDGE MENTIONS_RELATIONSHIP(FROM DocumentChunk, TO RelationshipType|FROM Document, TO RelationshipType) WITH REVERSE_EDGE="reverse_MENTIONS_RELATIONSHIP";
ADD DIRECTED EDGE IS_AFTER(FROM DocumentChunk, TO DocumentChunk) WITH REVERSE_EDGE="reverse_IS_AFTER";
ADD DIRECTED EDGE HAS_CHILD(FROM Document, TO DocumentChunk) WITH REVERSE_EDGE="reverse_HAS_CHILD";
ADD DIRECTED EDGE ENTITY_HAS_TYPE(FROM Entity, TO EntityType) WITH REVERSE_EDGE="reverse_ENTITY_HAS_TYPE";
- ADD DIRECTED EDGE RELATIONSHIP_TYPE(FROM EntityType, TO EntityType, DISCRIMINATOR(relation_type STRING), frequency INT) WITH REVERSE_EDGE="reverse_RELATIONSHIP_TYPE";
// GraphRAG
ADD VERTEX Community (PRIMARY_ID id STRING, iteration UINT, description STRING) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true";
diff --git a/common/gsql/supportai/create_entity_type_relationships.gsql b/common/gsql/supportai/create_entity_type_relationships.gsql
deleted file mode 100644
index 860f55c..0000000
--- a/common/gsql/supportai/create_entity_type_relationships.gsql
+++ /dev/null
@@ -1,20 +0,0 @@
-CREATE OR REPLACE DISTRIBUTED QUERY create_entity_type_relationships(/* Parameters here */) SYNTAX v2{
- MapAccum>> @rel_type_count; // entity type, relationship type for entity type, frequency
- SumAccum @@rels_inserted;
- ents = {Entity.*};
- accum_types = SELECT et FROM ents:e -(RELATIONSHIP>:r)- Entity:e2 -(ENTITY_HAS_TYPE>:eht)- EntityType:et
- WHERE r.relation_type != "DOC_CHUNK_COOCCURRENCE"
- ACCUM
- e.@rel_type_count += (et.id -> (r.relation_type -> 1));
-
- ets = SELECT et FROM ents:e -(ENTITY_HAS_TYPE>:eht)- EntityType:et
- ACCUM
- FOREACH (entity_type, rel_type_freq) IN e.@rel_type_count DO
- FOREACH (rel_type, freq) IN e.@rel_type_count.get(entity_type) DO
- INSERT INTO RELATIONSHIP_TYPE VALUES (et.id, entity_type, rel_type, freq),
- @@rels_inserted += 1
- END
- END;
-
- PRINT @@rels_inserted as relationships_inserted;
-}
\ No newline at end of file
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
index ed917e0..6877c0a 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
@@ -44,6 +44,12 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Search(STRING json_list_v
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
+ ELSE IF s.type != "DocumentChunk" AND s.type != "Document"
+ AND s.type != "Content" AND s.type != "Image"
+ AND s.type != "EntityType" THEN
+ // Domain vertex type — surface ": " so the
+ // LLM sees the schema-aware label.
+ @@final_retrieval += (s.id -> s.type + ": " + replace(s.id, "_", " "))
END;
@@verbose_info += ("start_set" -> @@start_set_type);
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
index 24648d9..5d58f89 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
@@ -42,6 +42,12 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_ty
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
+ ELSE IF s.type != "DocumentChunk" AND s.type != "Document"
+ AND s.type != "Content" AND s.type != "Image"
+ AND s.type != "EntityType" THEN
+ // Domain vertex type — surface ": " so the
+ // LLM sees the schema-aware label.
+ @@final_retrieval += (s.id -> s.type + ": " + replace(s.id, "_", " "))
END;
@@verbose_info += ("start_set" -> @@start_set_type);
diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql
index 9d68c00..8a49e75 100644
--- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql
+++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql
@@ -49,9 +49,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search(STRING json_list_vts
start = SELECT t FROM start:s -((RELATIONSHIP>|
CONTAINS_ENTITY>|
reverse_CONTAINS_ENTITY>|
- IS_AFTER>|
- IS_HEAD_OF>|
- HAS_TAIL>):e)- :t
+ IS_AFTER>):e)- :t
WHERE s.@visited < 1 AND t NOT IN s.@parents
ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e
POST-ACCUM(t) @@tmp_set += t;
@@ -76,6 +74,14 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search(STRING json_list_vts
s.@context += tmp_dsc
ELSE IF s.type == "DocumentChunk" THEN
@@to_retrieve_content += s
+ ELSE
+ // Domain vertex type instance — surface its type label
+ // and id so the LLM sees "Company: acme corp" instead of
+ // an unlabeled identifier. Domain VTs mirror Entity
+ // instances by id, so the description data is already
+ // captured via the Entity branch above; this branch adds
+ // the type-aware grounding the schema-aware path provides.
+ s.@context += s.type + ": " + replace(s.id, "_", " ")
END
POST-ACCUM(s)
IF NOT (chunk_only OR doc_only) OR (chunk_only OR doc_only) AND s.type == "DocumentChunk" THEN
diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql
index 6a6f9b9..a5dcae5 100644
--- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql
+++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql
@@ -48,9 +48,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search_Display(STRING json_l
start = SELECT t FROM start:s -((RELATIONSHIP>|
CONTAINS_ENTITY>|
reverse_CONTAINS_ENTITY>|
- IS_AFTER>|
- IS_HEAD_OF>|
- HAS_TAIL>):e)- :t
+ IS_AFTER>):e)- :t
WHERE s.@visited < 1 AND t NOT IN s.@parents
ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e
POST-ACCUM(t) @@tmp_set += t, t.@seen_in_hop += to_string(hierachy),
@@ -80,6 +78,10 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search_Display(STRING json_l
s.@context += tmp_dsc
ELSE IF s.type == "DocumentChunk" THEN
@@to_retrieve_content += s
+ ELSE
+ // Domain vertex type — surface ": " so the
+ // LLM sees the schema-aware label.
+ s.@context += s.type + ": " + replace(s.id, "_", " ")
END
POST-ACCUM(s)
IF NOT chunk_only OR chunk_only AND s.type == "DocumentChunk" THEN
diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql
index 2816156..d9fc9b4 100644
--- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql
+++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql
@@ -55,9 +55,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Vector_Search(Set v_
start = SELECT t FROM start:s -((RELATIONSHIP>|
CONTAINS_ENTITY>|
reverse_CONTAINS_ENTITY>|
- IS_AFTER>|
- IS_HEAD_OF>|
- HAS_TAIL>):e)- :t
+ IS_AFTER>):e)- :t
WHERE s.@visited < 1 AND t NOT IN s.@parents
ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e
POST-ACCUM(t) @@tmp_set += t;
@@ -82,6 +80,10 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Vector_Search(Set v_
s.@context += tmp_dsc
ELSE IF s.type == "DocumentChunk" THEN
@@to_retrieve_content += s
+ ELSE
+ // Domain vertex type — surface ": " so the
+ // LLM sees the schema-aware label.
+ s.@context += s.type + ": " + replace(s.id, "_", " ")
END
POST-ACCUM(s)
IF NOT (chunk_only OR doc_only) OR (chunk_only OR doc_only) AND s.type == "DocumentChunk" THEN
diff --git a/common/llm_services/aws_bedrock_service.py b/common/llm_services/aws_bedrock_service.py
index de6143a..ce6056c 100644
--- a/common/llm_services/aws_bedrock_service.py
+++ b/common/llm_services/aws_bedrock_service.py
@@ -22,6 +22,70 @@
logger = logging.getLogger(__name__)
+#: Per-model-family ``max_tokens`` caps for Bedrock-hosted models.
+#: Keys are case-insensitive prefixes / substrings of the model id; the
+#: longest matching prefix wins. Models not matched fall back to the
+#: generic default (see :data:`_BEDROCK_MAX_TOKENS_DEFAULT`).
+#:
+#: References (cap = max output tokens supported by the model on Bedrock):
+#: - Anthropic Claude 3 / 3.5 Haiku / 3 Opus: 4096
+#: - Anthropic Claude 3.5 / 3.7 Sonnet: 8192
+#: - Anthropic Claude Sonnet 4 / 4.5: 64000 (we cap at 8192 for safety)
+#: - Amazon Titan Text: 4096
+#: - Amazon Nova: 5120
+#: - Cohere Command: 4000
+#: - Meta Llama 2: 2048; Llama 3: 4096
+#: - AI21 Jurassic / Jamba: 4096
+#: - Mistral: 8192
+_BEDROCK_MAX_TOKENS_BY_MODEL: tuple = (
+ # Anthropic Claude 3.x family — explicit capped at 4096
+ ("anthropic.claude-3-5-haiku", 4096),
+ ("anthropic.claude-3-haiku", 4096),
+ ("anthropic.claude-3-opus", 4096),
+ ("anthropic.claude-3-sonnet", 4096),
+ ("anthropic.claude-instant", 4096),
+ # Anthropic Claude 3.5 / 3.7 Sonnet — 8192
+ ("anthropic.claude-3-5-sonnet", 8192),
+ ("anthropic.claude-3-7-sonnet", 8192),
+ # Amazon Titan Text models — capped at 4096
+ ("amazon.titan-text", 4096),
+ ("amazon.titan-tg1", 4096),
+ # Cohere Command — 4000
+ ("cohere.command", 4000),
+ # Meta Llama
+ ("meta.llama2", 2048),
+ ("meta.llama3", 4096),
+ # AI21 Jamba / Jurassic
+ ("ai21.", 4096),
+)
+_BEDROCK_MAX_TOKENS_DEFAULT = 8192
+
+
+def _bedrock_max_tokens_for_model(model_id: str) -> int:
+ """Return the recommended ``max_tokens`` for the given Bedrock model
+ id. Falls back to :data:`_BEDROCK_MAX_TOKENS_DEFAULT` when no
+ family-specific cap is registered.
+ """
+ if not model_id:
+ return _BEDROCK_MAX_TOKENS_DEFAULT
+ mid = model_id.lower()
+ # Cross-region inference profiles are prefixed with the region
+ # short code (``us.``, ``eu.``, ``apac.``, ``us-gov.``); strip so
+ # ``us.anthropic.claude-3-haiku-...`` matches the same family.
+ for prefix in ("us.", "eu.", "apac.", "us-gov."):
+ if mid.startswith(prefix):
+ mid = mid[len(prefix):]
+ break
+ # Walk the table in order; longest-prefix match wins. The table is
+ # already sorted with more-specific entries first
+ # (``claude-3-5-haiku`` before ``claude-3-haiku``).
+ best: tuple = ("", _BEDROCK_MAX_TOKENS_DEFAULT)
+ for prefix, cap in _BEDROCK_MAX_TOKENS_BY_MODEL:
+ if mid.startswith(prefix) and len(prefix) > len(best[0]):
+ best = (prefix, cap)
+ return best[1]
+
+
class AWSBedrock(LLM_Model):
def __init__(self, config):
super().__init__(config)
@@ -45,11 +109,25 @@ def __init__(self, config):
"AWS_SECRET_ACCESS_KEY"
],
)
+ # Resolve ``max_tokens`` so the langchain-aws built-in default
+ # of 1024 (Anthropic Claude on InvokeModel) doesn't truncate
+ # large prompts. Priority:
+ # 1. ``model_kwargs["max_tokens"]`` — explicit per-deployment override
+ # 2. ``token_limit`` config field — shared with retrieval-side context cap
+ # 3. Known model-family cap (Claude 3.x, Titan, Cohere, etc.)
+ # 4. Generic fallback: 8192
+ merged_kwargs = dict(config.get("model_kwargs") or {"temperature": 0})
+ if "max_tokens" not in merged_kwargs:
+ cfg_limit = config.get("token_limit")
+ if isinstance(cfg_limit, int) and cfg_limit > 0:
+ merged_kwargs["max_tokens"] = cfg_limit
+ else:
+ merged_kwargs["max_tokens"] = _bedrock_max_tokens_for_model(model_name)
self.llm = ChatBedrock(
client=client,
model_id=model_name,
region_name=config.get("region_name", "us-east-1"),
- model_kwargs=config.get("model_kwargs", {"temperature": 0}),
+ model_kwargs=merged_kwargs,
)
self.prompt_path = config["prompt_path"]
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index ba1c770..ffd7a59 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -146,52 +146,172 @@ async def ainvoke_with_parser(
@property
def map_question_schema_prompt(self):
"""Property to get the prompt for the MapQuestionToSchema tool."""
- return self._read_prompt_file(self.prompt_path + "map_question_to_schema.txt")
+ result = self._read_prompt_file(self.prompt_path + "map_question_to_schema.txt")
+ if result is not None:
+ return result
+ return """# Map Question to Schema
+
+Replace entities and relationships in the question with their canonical schema names provided in the Inputs section below.
+
+## Rules
+- If an entity (e.g. "John Doe") is referred to by different names or pronouns ("Joe", "he"), use the most complete identifier ("John Doe") consistently.
+- Choose the better mapping between a vertex type and one of its attributes.
+- Ensure entities are either source or target vertices of the chosen relationships.
+- If an entity maps to a vertex attribute, consider generating a `WHERE` clause.
+- For synonyms, output the canonical form from the schema choices.
+- Generate the **complete** rewritten question. Keep the case of schema elements unchanged.
+- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question.
+
+## Inputs
+- **Vertices**: {vertices}
+- **Vertex attributes**: {verticesAttrs}
+- **Edges**: {edges}
+- **Edge source/target**: {edgesInfo}
+- **Question**: {question}
+- **Conversation**: {conversation}
+
+{format_instructions}
+"""
@property
def generate_function_prompt(self):
"""Property to get the prompt for the GenerateFunction tool."""
- return self._read_prompt_file(self.prompt_path + "generate_function.txt")
+ result = self._read_prompt_file(self.prompt_path + "generate_function.txt")
+ if result is not None:
+ return result
+ return """# pyTigerGraph Function Selection
+
+Use the schema below to write the pyTigerGraph function call that answers the question via a `pyTigerGraph` connection.
+
+## Selection Rules
+- For "how many", counts, totals, or graph-DB statistics, always pick a function whose name contains `Count` (e.g. `getVertexCount`, `getEdgeCount`).
+- Never pick a function not described in the docstrings below.
+- If entities map to vertex attributes, consider a `WHERE` clause.
+- When constructing `WHERE`, quote string attribute values properly. Example: `('Person', where='name="William Torres"')` — applies to every string attribute (name, email, address, etc.).
+- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question.
+- Pick exactly **one** function to execute.
+
+## Schema
+- **Vertex Types**: {vertex_types}
+- **Vertex Attributes**: {vertex_attributes}
+- **Vertex IDs**: {vertex_ids}
+- **Edge Types**: {edge_types}
+- **Edge Attributes**: {edge_attributes}
+
+## Question
+{question}
+
+## Reference Docstrings
+1. {doc1}
+2. {doc2}
+3. {doc3}
+4. {doc4}
+5. {doc5}
+6. {doc6}
+7. {doc7}
+8. {doc8}
+
+## Output
+- If the function output answers the user's question, return that answer immediately.
+- Output **valid JSON only** — no extra text would render the response invalid.
+
+{format_instructions}
+"""
@property
def entity_relationship_extraction_prompt(self):
"""Property to get the prompt for the EntityRelationshipExtraction tool."""
- return self._read_prompt_file(
+ result = self._read_prompt_file(
self.prompt_path + "entity_relationship_extraction.txt"
)
-
- @property
- def generate_cypher_prompt(self):
- """Property to get the prompt for the GenerateCypher tool."""
- result = self._read_prompt_file(self.prompt_path + "generate_cypher.txt")
if result is not None:
return result
- return """You're an expert in OpenCypher programming. Given the following schema and history, what is the OpenCypher query that retrieves the {question}
- Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
- Use attributes instead of primary id if attribute name is closer to the keyword type in the question.
- Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
- Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
- Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern.
- Always use double quotes for strings instead of single quotes.
+ return """# Knowledge Graph Extraction
+
+You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
+
+## Goals
+- **Nodes** represent entities, concepts, and properties of entities.
+- Aim for simplicity and clarity so the graph is accessible to a vast audience.
- Avoid generating invalid OpenCypher queries based on the errors from history below.
+## Node Labeling
+- **Consistency**: use basic or elementary types. Label a person as `person`, not `mathematician` / `scientist`.
+- **Node IDs**: never use integers. Use names or human-readable identifiers found in the text.
- Schema: {schema}
- History: {history}
+## Numerical Data and Dates
+- Incorporate as **attributes / properties** of the respective nodes.
+- Do NOT create separate nodes for dates or numerical values.
+- Properties are key-value. Use properties only for dates and numbers; string properties become new nodes.
+- Never use escaped single or double quotes within property values.
+- Use `camelCase` for property keys (e.g. `birthDate`).
- You cannot use the following clauses:
- OPTIONAL MATCH
- CREATE
- MERGE
- REMOVE
- UNION
- UNION ALL
- UNWIND
- SET
+## Coreference Resolution
+- Maintain entity consistency: if "John Doe" is referred to as "Joe" or "he", always use the most complete identifier (`John Doe`) throughout.
- Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types.
+## Strict Compliance
+- Follow these rules strictly. Non-compliance, including poor formatting, results in termination.
- ONLY write the OpenCypher query in the response. Do not include any other information in the response."""
+## No-Relationship Nodes
+- Include nodes that have no relationships. Add the node and leave the relationships section empty."""
+
+ @property
+ def generate_cypher_prompt(self):
+ """Property to get the prompt for the GenerateCypher tool."""
+ result = self._read_prompt_file(self.prompt_path + "generate_cypher.txt")
+ if result is not None:
+ return result
+ return """# OpenCypher Query Generation
+
+You are an expert in OpenCypher. Generate the best query that retrieves the answer to: **{question}**.
+
+## Schema and History
+- **Schema**: {schema}
+- **History**: {history}
+
+## Construction Rules
+- Distinguish entity **value** from entity **type** carefully.
+- Remove duplicate words with the same meaning in the question.
+- Only use attributes that exist in the schema. Pick the closest matching attribute name when multiple candidates exist.
+- Prefer attributes over primary IDs when an attribute name is more similar to the keyword in the question.
+- Keep the query minimal — fewest vertex types, edge types, and attributes possible.
+- Do NOT return attributes that aren't explicitly mentioned in the question. If only a vertex is mentioned, return only the vertex.
+- Always include the entity from the `WHERE` clause in the final `RETURN`. Use vertex name over ID when available.
+- Always use **undirected** edge patterns. Ensure edges connect correct vertex types per schema.
+- Use **double quotes** for strings.
+- For string comparisons in `WHERE`, convert with `toLower()`.
+- Use multi-word, underscore-joined aliases for `ORDER BY`. Aliases / attributes used in `ORDER BY` must be in `RETURN`. Always specify `ASC` / `DESC` based on data type.
+- For "summarize" / "write a summary" questions, fetch all neighbour nodes and edges.
+- Avoid invalid queries based on errors in the history above.
+
+## Supported
+- **Clauses**: `MATCH`, `OPTIONAL MATCH`, `MANDATORY MATCH`, `WHERE`, `RETURN`, `WITH`, `ORDER BY`, `SKIP`, `LIMIT`, `DELETE`, `DETACH DELETE`
+- **Operators**:
+ - Math: `+`, `-`, `*`, `/`, `%`, `^`
+ - Comparison: `=`, `<`, `<=`, `>`, `>=`, `<>`, `IS NULL`, `IS NOT NULL`
+ - Boolean: `AND`, `OR`, `NOT`, `XOR`
+ - String / list: `CONTAINS`, `STARTS WITH`, `ENDS WITH`, `IN`, `DISTINCT`, `[ ]`, `.`
+- **Functions**:
+ - Aggregation: `count`, `sum`, `avg`, `min`, `max`, `stDev`, `stDevP`
+ - Math: `abs`, `sqrt`, `log`, `exp`, `sin`, `cos`, `tan`, `radians`, `degrees`
+ - String: `left`, `right`, `substring`, `replace`, `trim`, `toLower`, `toUpper`, `split`
+ - List: `head`, `last`, `size`, `range`, `coalesce`, `tail`
+ - Other: `id`, `elementId`, `labels`, `properties`, `timestamp`
+- **Expressions**: `CASE`
+
+## Unsupported
+- **Clauses**: `CALL`, `CREATE`, `MERGE`, `REMOVE`, `SET`, `UNION`, `UNION ALL`, `UNWIND`
+- **Functions**: `collect`, `exists`, `keys`, `nodes`, `relationships`, `length`, `percentileCont`, `percentileDisc`, `startNode`, `endNode`, `reverse` (list form)
+- **Syntax limits**:
+ - `WITH` must group by exactly one vertex variable.
+ - Path variables (`p = (...)`) not supported.
+ - `MATCH` must reference variables from prior `WITH`.
+ - Disconnected `MATCH` fragments not supported.
+
+## Output
+- The query must return both the entity from the question AND the requested data.
+- Validate syntax before responding.
+- Aliases must NOT match vertex / edge types, operator / function names, or reserved keywords. Use multi-word underscore identifiers.
+- Output ONLY the OpenCypher query — no explanation."""
@property
def generate_gsql_prompt(self):
@@ -199,37 +319,30 @@ def generate_gsql_prompt(self):
result = self._read_prompt_file(self.prompt_path + "generate_gsql.txt")
if result is not None:
return result
- return """You're an expert in GSQL (Graph SQL) programming for TigerGraph. Given the following schema: {schema}, what is the GSQL query that retrieves the answer for question: {question}
- Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
- Use attributes instead of primary id if attribute name is more similar to the keyword type in the question.
- Use as few vertex types, edge types and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
- Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
- Always use double quotes for strings instead of single quotes.
- Use alias for ORDER BY if any, and make sure the alias or attributes used in ORDER BY is also in PRINT. Always add ASC or DESC for ORDER BY based on data type.
-
- Avoid generating invalid GSQL queries based on the errors from history below.
-
- Schema: {schema}
- History: {history}
-
- Additionally, you cannot use the following clauses:
- CREATE
- DELETE
- INSERT
- UPDATE
- UPSERT
-
- Here's some commonly used abbreviations:
- dt -> date
- pct -> percentage
- qty -> quantity
- lng -> longitude
- cm -> Contract Manufacturer
-
- Always make the GSQL query returns the entity in the original question together with the data to be queried.
- Make sure to have correct attribute names in the GSQL query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore.
-
- ONLY write the GSQL query in the response. Do not include any other information in the response."""
+ return """# GSQL Query Generation
+
+You are an expert in TigerGraph GSQL. Generate the GSQL query that retrieves the answer to: **{question}**.
+
+## Schema and History
+- **Schema**: {schema}
+- **History**: {history}
+
+## Construction Rules
+- Only use attributes in the schema. Never invent attributes.
+- Prefer attributes over primary IDs when the attribute name is more similar to a keyword in the question.
+- Keep the query minimal — fewest vertex types, edge types, and attributes possible.
+- Do NOT return attributes the question doesn't mention. If only a vertex is mentioned, return only the vertex.
+- Always use **double quotes** for strings.
+- Use aliases for `ORDER BY`. Aliases / attributes used in `ORDER BY` must also be in `PRINT`. Always specify `ASC` / `DESC` based on data type.
+- Avoid invalid queries based on errors in the history above.
+
+## Unsupported
+- **Clauses**: `CREATE`, `DELETE`, `INSERT`, `UPDATE`, `UPSERT`
+
+## Output
+- The query must return both the entity from the question AND the requested data.
+- Aliases must NOT match vertex / edge types, operator / function names, or reserved keywords. Use multi-word underscore identifiers.
+- Output ONLY the GSQL query — no explanation."""
@property
def route_response_prompt(self):
@@ -237,26 +350,34 @@ def route_response_prompt(self):
result = self._read_prompt_file(self.prompt_path + "route_response.txt")
if result is not None:
return result
- return """\
-You are an expert at routing a user question to a vectorstore, function calls, or conversation history.
-Use the conversation history for questions that are similar to previous ones or that reference earlier answers or responses.
-Use the vectorstore for questions that would be best suited by text documents.
-Use the function calls for questions that ask about structured data, or operations on structured data.
-Questions referring to same entities in a previous, earlier, or above answer or response should be routed to the conversation history.
-Keep in mind that some questions about documents such as "how many documents are there?" can be answered by function calls.
-The function calls can be used to answer questions about these entities: {v_types} and relationships: {e_types}.
-IMPORTANT: Questions about graph database statistics or metadata MUST be routed to function calls. This includes:
-- Counting vertices/nodes/edges (e.g. "how many vertices are there", "how many edges in the graph")
-- Listing or describing vertex/edge types, schema, or graph structure
-- Aggregations, totals, or summaries of data stored in the graph database
-- Any question mentioning "graph", "graph db", "graph database", "vertices", "nodes", or "edges" in the context of statistics or counts
-These are database queries, NOT document lookups — always route them to function calls.
-Otherwise, use vectorstore. Choose one of 'functions', 'vectorstore', or 'history' based on the question and conversation history.
-Return a JSON with a single key 'datasource' and no preamble or explanation.
-Question to route: {question}
-Conversation history: {conversation}
-Format: {format_instructions}\
-"""
+ return """# Route the Question
+
+Route the user question to one of: `functions`, `vectorstore`, or `history`.
+
+## Routing
+- **`history`**: questions similar to previous ones, or that reference earlier answers / responses, or that refer to the same entities mentioned in a previous answer.
+- **`vectorstore`**: questions best answered by text documents.
+- **`functions`**: questions about structured data or operations on structured data. Available entities: {v_types}; relationships: {e_types}. Some "how many documents are there?" style questions can be answered here.
+
+## Mandatory `functions` Routing
+Any question about graph database **statistics or metadata** MUST route to `functions`:
+- Counts of vertices / nodes / edges (e.g. "how many edges in the graph").
+- Listing or describing vertex / edge types, schema, or graph structure.
+- Aggregations, totals, or summaries of data in the graph database.
+- Any question mentioning "graph", "graph db", "graph database", "vertices", "nodes", or "edges" in the context of statistics / counts.
+
+These are **database queries, not document lookups** — always route them to `functions`.
+
+Otherwise, route to `vectorstore`.
+
+## Output
+Return JSON with a single key `datasource` (value: `functions`, `vectorstore`, or `history`). No preamble or explanation.
+
+## Inputs
+- **Question**: {question}
+- **Conversation history**: {conversation}
+
+{format_instructions}"""
@property
def hyde_prompt(self):
@@ -264,8 +385,13 @@ def hyde_prompt(self):
result = self._read_prompt_file(self.prompt_path + "hyde.txt")
if result is not None:
return result
- return """You are a helpful agent that is writing an example of a document that might answer this question: {question}
- Answer:"""
+ return """# Hypothetical Document
+
+Write an example of a document that might answer this question.
+
+**Question**: {question}
+
+**Answer**:"""
@property
def chatbot_response_prompt(self):
@@ -273,13 +399,27 @@ def chatbot_response_prompt(self):
result = self._read_prompt_file(self.prompt_path + "chatbot_response.txt")
if result is not None:
return result
- return """Given the answer context in JSON format, rephrase it to answer the question. \n
- Use only the provided information in context without adding any reasoning or additional logic. \n
- Make sure all information in the answer are covered in the generated answer.\n
-
- Question: {question} \n
- Answer: {context} \n
- Format: {format_instructions}"""
+ return """# AI-Powered Knowledge Graph Assistant
+
+You are a highly efficient, empathetic, and professional AI assistant. Use the provided contexts to answer the user's question.
+
+## Rules
+- The contexts arrive as JSON key-context pairs. **Combine and rephrase** them to answer the question.
+- **Score** each context for relevance and use only the high-scoring ones — do not invent additional logic.
+- **Cover** the relevant information, especially image references that carry critical visual information.
+- **Preserve** image links exactly as `` in the final answer when used. Do NOT modify or omit them.
+- **Format** the answer in Markdown — titles, paragraphs, bulleted / numbered lists, images, and tables. Place images and tables below the related text section.
+- **Tables**: every row, including the header, starts on a new line.
+- **Output as JSON** — escape characters as needed so the response is valid JSON. Include every field required by the format instructions; set unknown fields to empty.
+- Treat context keys as citations only when asked; otherwise do NOT include citations in the final answer.
+
+## Inputs
+- **Question**: {question}
+- **Contexts**: {context}
+- **Query**: {query}
+
+{format_instructions}
+"""
@property
def keyword_extraction_prompt(self):
@@ -287,7 +427,20 @@ def keyword_extraction_prompt(self):
result = self._read_prompt_file(self.prompt_path + "keyword_extraction.txt")
if result is not None:
return result
- return """You are a helpful assistant responsible for extracting key terms (glossary) from all the questions below to represent their original meaning as much as possible. Each term should only contain a couple of words. Include a quality score for the each extracted glossary, based on how important and frequent it's in the given questions. The quality score should range from 0 (poor) to 100 (excellent), with higher scores indicating terms that are both significant and frequent in the context of the questions.\nThe output should only contain the extracted terms and their quality scores using the required format.\n\nQuestion: {question}\n\n{format_instructions}\n"""
+ return """# Keyword Extraction
+
+Extract key terms (glossary) from the question(s) below to represent their original meaning as faithfully as possible.
+
+## Rules
+- Each term should contain only a couple of words.
+- Score each extracted term **0 (poor)** to **100 (excellent)** based on how important and frequent it is in the question(s). Higher scores indicate terms that are both significant and frequent.
+- Output ONLY the extracted terms with their quality scores in the required format.
+
+## Question
+{question}
+
+{format_instructions}
+"""
@property
def question_expansion_prompt(self):
@@ -295,7 +448,18 @@ def question_expansion_prompt(self):
result = self._read_prompt_file(self.prompt_path + "question_expansion.txt")
if result is not None:
return result
- return """You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way.\nInclude a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent).\n\nQuestion: {question}\n\n{format_instructions}\n"""
+ return """# Question Expansion
+
+Generate **10 new questions** similar to the original question below to express its meaning more clearly.
+
+## Scoring
+Include a quality score per generated question, **0 (poor)** to **100 (excellent)**, based on how well it represents the meaning of the original question.
+
+## Question
+{question}
+
+{format_instructions}
+"""
@property
def graphrag_scoring_prompt(self):
@@ -303,7 +467,19 @@ def graphrag_scoring_prompt(self):
result = self._read_prompt_file(self.prompt_path + "graphrag_scoring.txt")
if result is not None:
return result
- return """You are a helpful assistant responsible for generating an answer to the question below using the data provided.\nInclude a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).\n\nQuestion: {question}\nContext: {context}\n\n{format_instructions}\n"""
+ return """# Quality-Scored Answer
+
+Generate an answer to the question below using the provided data, and include a quality score.
+
+## Scoring
+The quality score is between **0 (poor)** and **100 (excellent)**, based on how well the answer addresses the question.
+
+## Inputs
+- **Question**: {question}
+- **Context**: {context}
+
+{format_instructions}
+"""
@property
def community_summarize_prompt(self):
@@ -311,10 +487,61 @@ def community_summarize_prompt(self):
result = self._read_prompt_file(self.prompt_path + "community_summarization.txt")
if result is not None:
return result
- raise FileNotFoundError(
- f"Community summarization prompt file not found in {self.prompt_path}. "
- "Please ensure community_summarization.txt exists in the configured prompt path."
- )
+ return """# Community Summary
+
+Generate a comprehensive summary of the data below.
+
+## Rules
+- Concatenate the descriptions into a single, comprehensive summary that includes information from **all** descriptions.
+- Resolve contradictions; do NOT add information that is not in the descriptions.
+- Write in **third person** and include the entity name(s) for full context.
+
+## Data
+- **Community Title**: {entity_name}
+- **Description List**: {description_list}
+"""
+
+ @property
+ def schema_extraction_prompt(self):
+ """Property to get the prompt for sample-doc schema extraction."""
+ result = self._read_prompt_file(self.prompt_path + "schema_extraction.txt")
+ if result is not None:
+ return result
+ return """# Schema Extraction
+
+You are a knowledge-graph schema architect. From the sample documents provided in the Inputs section below, produce a domain schema as TigerGraph GSQL `VERTEX` / `DIRECTED EDGE` / `UNDIRECTED EDGE` declarations (no leading `ADD`). Return GSQL only — no fences, no commentary, no JSON.
+
+## Rules
+
+1. **Vertex inclusion**: a vertex type's instances must be individuated in the source (each instance has its own identity), appear **2+ times**, and have at least one natural attribute beyond `name`. Concrete or conceptual is fine. Skip categorical wrappers — names ending in `_record`, `_management`, `_context`, `_grouping`, or labels of classes-of-classes.
+2. **Skip layout**: do NOT produce types for axes, page numbers, captions, table cells, or other document-rendering artifacts.
+3. **Edge naming**: use a specific action verb. Include an edge type ONLY IF the source documents contain **2+ concrete instances** of that relationship between named entities — do NOT propose merely-plausible edges. Avoid generic edges (`RELATED_TO`, `CONNECTED_TO`, `ASSOCIATED_WITH`, `HAS`, `BELONGS_TO`). Use `DIRECTED EDGE` for asymmetric verbs and `UNDIRECTED EDGE` only for genuinely symmetric peer relationships.
+4. **Reserved names**: do NOT use a name (case-insensitive) matching any of the reserved structural types or GSQL keywords listed in the Inputs section. Pick a synonym or qualifier (e.g. `KeywordRecord`).
+5. **Attributes**: each `VERTEX` has **1–5** attributes; each `EDGE` has **0–3**. Primitive types only: `STRING`, `INT`, `UINT`, `DOUBLE`, `FLOAT`, `BOOL`, `DATETIME`. Do NOT include any id / primary-key field.
+6. **Comments**: every `VERTEX` and `EDGE` MUST be preceded by exactly one `// ` line.
+7. **Size**: produce **8–25** vertex types and **8–25** edge types.
+
+## Example Output (illustrative — pick names that fit YOUR documents)
+
+ // A natural person referenced in the documents.
+ VERTEX Person(name STRING, role STRING);
+
+ // An organization or institutional body.
+ VERTEX Organization(name STRING, founded_at DATETIME);
+
+ // A person works for an organization in a given role.
+ DIRECTED EDGE WORKS_FOR(FROM Person, TO Organization, role STRING);
+
+ // Two people are colleagues — symmetric peer relationship.
+ UNDIRECTED EDGE COLLEAGUE_OF(FROM Person, TO Person);
+
+## Inputs
+- **Reserved structural types** (case-insensitive): {structural_types}
+- **Reserved GSQL keywords** (case-insensitive): {tg_keywords}
+- **Sample documents**:
+
+{samples}
+"""
@property
def contextualize_question_prompt(self):
@@ -325,13 +552,18 @@ def contextualize_question_prompt(self):
)
if result is not None:
return result
- return (
- "Given the following conversation history and a follow-up "
- "question, rewrite the follow-up question into a standalone, "
- "self-contained question suitable for searching a knowledge "
- "graph. Do NOT answer the question; only rewrite it.\n\n"
- "Conversation history:\n{history}\n\n"
- "Follow-up question: {question}\n\n"
- "Standalone question:"
- )
+ return """# Standalone Question Rewrite
+
+Given the conversation history and a follow-up question, rewrite the follow-up into a **standalone, self-contained** question suitable for searching a knowledge graph.
+
+Do **NOT** answer the question — only rewrite it.
+
+## Conversation History
+{history}
+
+## Follow-up Question
+{question}
+
+## Standalone Question
+"""
diff --git a/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt b/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt
deleted file mode 100644
index 6acdaf5..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism.
-
-Follow these guidelines:
-- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question.
-- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic.
-- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information.
-- Make sure to preserve the image links in markdown syntax "" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references.
-- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section.
-- Ensure that each row of every table, including the header row, starts on a new line.
-- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only.
-- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information.
-- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer
-
-Question: {question}
-Contexts: {context}
-Query: {query}
-Format: {format_instructions}
diff --git a/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt b/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt
deleted file mode 100644
index 50e4619..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-You are a helpful assistant responsible for generating a comprehensive summary of the data provided below.
-Given one or two entities, and a list of descriptions, all related to the same entity or group of entities.
-Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions.
-If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description.
-Make sure it is written in third person, and include the entity names so we the have full context.
-
-#######
--Data-
-Commuinty Title: {entity_name}
-Description List: {description_list}
-
diff --git a/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt b/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt b/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt
deleted file mode 100644
index 732ed49..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt
+++ /dev/null
@@ -1,85 +0,0 @@
-You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}.
-If there're multiple words in the question having same meaning then remove the duplication.
-Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB".
-Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
-Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates.
-Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
-Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema.
-Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
-Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available.
-Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema.
-Always use double quotes for strings instead of single quotes.
-Always convert strings to lower case using toLower() function for string comparision in WHERE clause.
-Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore.
-Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type.
-For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges.
-
-Avoid to generate invalid OpenCypher queries based on the errors from history below.
-
-Schema: {schema}
-History: {history}
-
-Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below:
-
-Supported Clauses:
-MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph.
-WHERE: Filter results.
-RETURN / WITH: Project query results, alias fields, chain query parts.
-ORDER BY / SKIP / LIMIT: Control output order, offset, and size.
-DELETE / DETACH DELETE: Delete nodes/edges.
-
-Supported Operators:
-Mathematical: +, -, *, /, %, ^ (exponent)
-Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL
-Boolean: AND, OR, NOT, XOR
-String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access)
-
-Supported Functions:
-Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP()
-Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees()
-String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split()
-List: head(), last(), size(), range(), coalesce(), tail()
-Others: id(), elementId(), labels(), properties(), timestamp()
-
-Supported Expressions:
-CASE: Conditional logic.
-
-Supported Operators:
-Comparison: IS NULL, IS NOT NULL
-
-Unsupported Features:
-Clauses Not Yet Supported
-CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND
-
-Unsupported Functions:
-collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form)
-
-Syntax Limitations:
-WITH clause must group by exactly one vertex variable.
-Path variables (e.g. p = (...)) not supported.
-MATCH must reference variables from prior WITH.
-Disconnected MATCH fragments not supported.
-
-Additionally, you cannot use the following clauses:
-CREATE
-MERGE
-REMOVE
-UNION
-UNION ALL
-UNWIND
-SET
-
-Here's some commonly used abbreviations:
-dt -> date
-wk -> week
-yr -> year
-pct -> percentage
-qty -> quantity
-lng -> longitude
-cm -> Contract Manufacturer
-
-Always make the cypher query returns the entity in the original question together with the data to be queried.
-Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore.
-Always validate the syntax for the generated OpenCypher query before writing to response.
-
-ONLY write the OpenCypher query in the response. Do not include any other information in the response.
diff --git a/common/prompts/aws_bedrock_claude3haiku/generate_function.txt b/common/prompts/aws_bedrock_claude3haiku/generate_function.txt
deleted file mode 100644
index 359b46c..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/generate_function.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-If the output of this function answers the user's question, immediately return that answer.
-
-Follow the output directions below on how to structure your response
-Only include valid JSON do not include any other texts which would render the response invalid JSON.
-{format_instructions}
diff --git a/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt b/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt
deleted file mode 100644
index 38ef643..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-You are a helpful assistant responsible for generating an answer to the question below using the data provided.
-Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-Context: {context}
-
-{format_instructions}
diff --git a/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt b/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt
deleted file mode 100644
index 8e4cf05..0000000
--- a/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-Respond in JSON (and only JSON). Follow the format instructions below:
-{format_instructions}
-question: {question}
-conversation: {conversation}
diff --git a/common/prompts/aws_bedrock_titan/generate_function.txt b/common/prompts/aws_bedrock_titan/generate_function.txt
deleted file mode 100644
index b0be05c..0000000
--- a/common/prompts/aws_bedrock_titan/generate_function.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertices}
-Edge Types: {edges}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Python Call: conn.
\ No newline at end of file
diff --git a/common/prompts/aws_bedrock_titan/map_question_to_schema.txt b/common/prompts/aws_bedrock_titan/map_question_to_schema.txt
deleted file mode 100644
index d9fb173..0000000
--- a/common/prompts/aws_bedrock_titan/map_question_to_schema.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-Example: How many universities are there?
-Response: How many vertices are University Vetexes?
-Example: What is the schema?
-Response: What is the schema?
-Example: How many transactions are there?
-Response: How many TRANSACTION Edges are there?
-{format_instructions}
-question: {question}
-conversation: {conversation}
diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt
deleted file mode 100644
index e0a83d0..0000000
--- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-Never add more than one function call in the response, and only use the functions provided below. Do not chain function calls together.
-For example, if the correct function is `getVertexCount()` do not use `getVertexCount().limit()`.
-If the correct function is `getEdges()` do not use `getEdges().count()`.
-
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-Follow the output directions below on how to structure your response, make sure to exactly match the output format.
-{format_instructions}
diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt
deleted file mode 100644
index d72726e..0000000
--- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-You are mapping a question from a user to entities represented in a graph database.
-The question is: {question}
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. If some entites are mapped to attributes, may consider to generate a where clause.
-Format your response following the directions below.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-{format_instructions}
-question: {question}
-conversation: {conversation}
\ No newline at end of file
diff --git a/common/prompts/custom/aml/chatbot_response.txt b/common/prompts/custom/aml/chatbot_response.txt
deleted file mode 100644
index 05532c4..0000000
--- a/common/prompts/custom/aml/chatbot_response.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-You are a highly efficient and empathetic AI-powered assistant in JSON parsing and generating.
-Given the following context in JSON format, rephrase it to answer the question.
-Use only the provided information in context without adding any reasoning or additional logic.
-Make sure all information in the context are covered in the generated answer.
-Make sure to extract and include the image links in markdown syntax in the generated answer when their summaries are referenced, and preserve the link URLs in their original format.
-Use compact markdown syntax to geneate the answer, including title, bulleted or numbered list, images and tables if any, and place images or tables below the related text section.
-Ensure that each row of every table, including the header row, starts on a new line.
-Always only return a JSON contains the answer and otheh required fields after validation. Assign an empty value to the field if you cannot determine it.
-
-For questions related to financial graph or transaction graph, create a suspicious activity report.
-- The narrative should be clear, comprehensive, and avoid institution-specific acronyms.
-- The narrative should include paragraphs for Summary of **Investigation**, **Suspicious Activity Overview**, **Details of Suspicious Activities**, **Investigation Conducted**, and **Conclusion**
-- The narrative must provide information about the subject to include phone numbers, email addresses, addresses and government IDs.
-- The narrative must include any suspicious transactions and the start of the suspicious transactions.
-- The narrative must specify the suspicious activity observed (types of transactions, amount, frequency).
-- The narrative must tell the complete story. A reviewer should understand the full picture without needing additional context.
-
-Narrative Writing Guidelines:
-- Write in clear, complete sentences
-- Spell out all acronyms (no institution-specific jargon)
-- Include specific dates, amounts, and account numbers
-- Describe the investigation conducted
-- Note any customer explanations received and why they were insufficient
-- Include any supporting documentation references
-
-Question: {question}
-Context: {context}
-Query: {query}
-Format: {format_instructions}
diff --git a/common/prompts/custom/aml/community_summarization.txt b/common/prompts/custom/aml/community_summarization.txt
deleted file mode 100644
index 50e4619..0000000
--- a/common/prompts/custom/aml/community_summarization.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-You are a helpful assistant responsible for generating a comprehensive summary of the data provided below.
-Given one or two entities, and a list of descriptions, all related to the same entity or group of entities.
-Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions.
-If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description.
-Make sure it is written in third person, and include the entity names so we the have full context.
-
-#######
--Data-
-Commuinty Title: {entity_name}
-Description List: {description_list}
-
diff --git a/common/prompts/custom/aml/entity_relationship_extraction.txt b/common/prompts/custom/aml/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/custom/aml/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/custom/aml/generate_cypher.txt b/common/prompts/custom/aml/generate_cypher.txt
deleted file mode 100644
index 732ed49..0000000
--- a/common/prompts/custom/aml/generate_cypher.txt
+++ /dev/null
@@ -1,85 +0,0 @@
-You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}.
-If there're multiple words in the question having same meaning then remove the duplication.
-Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB".
-Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
-Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates.
-Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
-Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema.
-Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
-Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available.
-Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema.
-Always use double quotes for strings instead of single quotes.
-Always convert strings to lower case using toLower() function for string comparision in WHERE clause.
-Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore.
-Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type.
-For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges.
-
-Avoid to generate invalid OpenCypher queries based on the errors from history below.
-
-Schema: {schema}
-History: {history}
-
-Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below:
-
-Supported Clauses:
-MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph.
-WHERE: Filter results.
-RETURN / WITH: Project query results, alias fields, chain query parts.
-ORDER BY / SKIP / LIMIT: Control output order, offset, and size.
-DELETE / DETACH DELETE: Delete nodes/edges.
-
-Supported Operators:
-Mathematical: +, -, *, /, %, ^ (exponent)
-Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL
-Boolean: AND, OR, NOT, XOR
-String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access)
-
-Supported Functions:
-Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP()
-Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees()
-String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split()
-List: head(), last(), size(), range(), coalesce(), tail()
-Others: id(), elementId(), labels(), properties(), timestamp()
-
-Supported Expressions:
-CASE: Conditional logic.
-
-Supported Operators:
-Comparison: IS NULL, IS NOT NULL
-
-Unsupported Features:
-Clauses Not Yet Supported
-CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND
-
-Unsupported Functions:
-collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form)
-
-Syntax Limitations:
-WITH clause must group by exactly one vertex variable.
-Path variables (e.g. p = (...)) not supported.
-MATCH must reference variables from prior WITH.
-Disconnected MATCH fragments not supported.
-
-Additionally, you cannot use the following clauses:
-CREATE
-MERGE
-REMOVE
-UNION
-UNION ALL
-UNWIND
-SET
-
-Here's some commonly used abbreviations:
-dt -> date
-wk -> week
-yr -> year
-pct -> percentage
-qty -> quantity
-lng -> longitude
-cm -> Contract Manufacturer
-
-Always make the cypher query returns the entity in the original question together with the data to be queried.
-Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore.
-Always validate the syntax for the generated OpenCypher query before writing to response.
-
-ONLY write the OpenCypher query in the response. Do not include any other information in the response.
diff --git a/common/prompts/custom/aml/generate_function.txt b/common/prompts/custom/aml/generate_function.txt
deleted file mode 100644
index 359b46c..0000000
--- a/common/prompts/custom/aml/generate_function.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-If the output of this function answers the user's question, immediately return that answer.
-
-Follow the output directions below on how to structure your response
-Only include valid JSON do not include any other texts which would render the response invalid JSON.
-{format_instructions}
diff --git a/common/prompts/custom/aml/graphrag_scoring.txt b/common/prompts/custom/aml/graphrag_scoring.txt
deleted file mode 100644
index 38ef643..0000000
--- a/common/prompts/custom/aml/graphrag_scoring.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-You are a helpful assistant responsible for generating an answer to the question below using the data provided.
-Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-Context: {context}
-
-{format_instructions}
diff --git a/common/prompts/custom/aml/map_question_to_schema.txt b/common/prompts/custom/aml/map_question_to_schema.txt
deleted file mode 100644
index 8e4cf05..0000000
--- a/common/prompts/custom/aml/map_question_to_schema.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-Respond in JSON (and only JSON). Follow the format instructions below:
-{format_instructions}
-question: {question}
-conversation: {conversation}
diff --git a/common/prompts/gcp_vertexai_palm/community_summarization.txt b/common/prompts/gcp_vertexai_palm/community_summarization.txt
deleted file mode 100644
index 50e4619..0000000
--- a/common/prompts/gcp_vertexai_palm/community_summarization.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-You are a helpful assistant responsible for generating a comprehensive summary of the data provided below.
-Given one or two entities, and a list of descriptions, all related to the same entity or group of entities.
-Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions.
-If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description.
-Make sure it is written in third person, and include the entity names so we the have full context.
-
-#######
--Data-
-Commuinty Title: {entity_name}
-Description List: {description_list}
-
diff --git a/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt b/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/gcp_vertexai_palm/generate_function.txt b/common/prompts/gcp_vertexai_palm/generate_function.txt
deleted file mode 100644
index fe7d3cc..0000000
--- a/common/prompts/gcp_vertexai_palm/generate_function.txt
+++ /dev/null
@@ -1,33 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-Never add more than one function call in the response, and only use the functions provided below. Do not chain function calls together.
-For example, if the correct function is `getVertexCount()` do not use `getVertexCount().limit()`.
-If the correct function is `getEdges()` do not use `getEdges().count()`.
-
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-Make sure to carefully read the question and the docstrings to determine the correct function to call.
-Choose the simplest function that will answer the question.
-Only choose one function to call, and do not change the syntax of the function call.
-
-Follow the output directions below on how to structure your response:
-{format_instructions}
diff --git a/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt b/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt
deleted file mode 100644
index b9051bc..0000000
--- a/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-{format_instructions}
-question: {question}
-conversation: {conversation}
diff --git a/common/prompts/google_gemini/chatbot_response.txt b/common/prompts/google_gemini/chatbot_response.txt
deleted file mode 100644
index 6acdaf5..0000000
--- a/common/prompts/google_gemini/chatbot_response.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism.
-
-Follow these guidelines:
-- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question.
-- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic.
-- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information.
-- Make sure to preserve the image links in markdown syntax "" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references.
-- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section.
-- Ensure that each row of every table, including the header row, starts on a new line.
-- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only.
-- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information.
-- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer
-
-Question: {question}
-Contexts: {context}
-Query: {query}
-Format: {format_instructions}
diff --git a/common/prompts/google_gemini/community_summarization.txt b/common/prompts/google_gemini/community_summarization.txt
deleted file mode 100644
index 50e4619..0000000
--- a/common/prompts/google_gemini/community_summarization.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-You are a helpful assistant responsible for generating a comprehensive summary of the data provided below.
-Given one or two entities, and a list of descriptions, all related to the same entity or group of entities.
-Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions.
-If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description.
-Make sure it is written in third person, and include the entity names so we the have full context.
-
-#######
--Data-
-Commuinty Title: {entity_name}
-Description List: {description_list}
-
diff --git a/common/prompts/google_gemini/entity_relationship_extraction.txt b/common/prompts/google_gemini/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/google_gemini/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/google_gemini/generate_cypher.txt b/common/prompts/google_gemini/generate_cypher.txt
deleted file mode 100644
index f347194..0000000
--- a/common/prompts/google_gemini/generate_cypher.txt
+++ /dev/null
@@ -1,84 +0,0 @@
-You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}.
-If there're multiple words in the question having same meaning then remove the duplication.
-Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB".
-Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
-Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates.
-Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
-Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema.
-Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
-Always return the entity from the WHERE clause together with the final result in the RETURN statement. Use vertex name instead of ID whenever available.
-Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema.
-Always use double quotes for strings instead of single quotes.
-Always convert strings to lower case using toLower() function for string comparision in WHERE clause.
-Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore.
-Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type.
-
-Avoid to generate invalid OpenCypher queries based on the errors from history below.
-
-Schema: {schema}
-History: {history}
-
-Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below:
-
-Supported Clauses:
-MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph.
-WHERE: Filter results.
-RETURN / WITH: Project query results, alias fields, chain query parts.
-ORDER BY / SKIP / LIMIT: Control output order, offset, and size.
-DELETE / DETACH DELETE: Delete nodes/edges.
-
-Supported Operators:
-Mathematical: +, -, *, /, %, ^ (exponent)
-Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL
-Boolean: AND, OR, NOT, XOR
-String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access)
-
-Supported Functions:
-Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP()
-Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees()
-String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split()
-List: head(), last(), size(), range(), coalesce(), tail()
-Others: id(), elementId(), labels(), properties(), timestamp()
-
-Supported Expressions:
-CASE: Conditional logic.
-
-Supported Operators:
-Comparison: IS NULL, IS NOT NULL
-
-Unsupported Features:
-Clauses Not Yet Supported
-CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND
-
-Unsupported Functions:
-collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form)
-
-Syntax Limitations:
-WITH clause must group by exactly one vertex variable.
-Path variables (e.g. p = (...)) not supported.
-MATCH must reference variables from prior WITH.
-Disconnected MATCH fragments not supported.
-
-Additionally, you cannot use the following clauses:
-CREATE
-MERGE
-REMOVE
-UNION
-UNION ALL
-UNWIND
-SET
-
-Here's some commonly used abbreviations:
-dt -> date
-wk -> week
-yr -> year
-pct -> percentage
-qty -> quantity
-lng -> longitude
-cm -> Contract Manufacturer
-
-Always make the cypher query returns the entity in the original question together with the data to be queried.
-Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore.
-Always validate the syntax for the generated OpenCypher query before writing to response.
-
-ONLY write the OpenCypher query in the response. Do not include any other information in the response.
diff --git a/common/prompts/google_gemini/generate_function.txt b/common/prompts/google_gemini/generate_function.txt
deleted file mode 100644
index a7e4ee0..0000000
--- a/common/prompts/google_gemini/generate_function.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-Follow the output directions below on how to structure your response:
-{format_instructions}
diff --git a/common/prompts/google_gemini/graphrag_scoring.txt b/common/prompts/google_gemini/graphrag_scoring.txt
deleted file mode 100644
index 38ef643..0000000
--- a/common/prompts/google_gemini/graphrag_scoring.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-You are a helpful assistant responsible for generating an answer to the question below using the data provided.
-Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-Context: {context}
-
-{format_instructions}
diff --git a/common/prompts/google_gemini/map_question_to_schema.txt b/common/prompts/google_gemini/map_question_to_schema.txt
deleted file mode 100644
index 81ed53d..0000000
--- a/common/prompts/google_gemini/map_question_to_schema.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-{format_instructions}
-question: {question}
-conversation: {conversation}
-
diff --git a/common/prompts/google_gemini/question_expansion.txt b/common/prompts/google_gemini/question_expansion.txt
deleted file mode 100644
index b04aed8..0000000
--- a/common/prompts/google_gemini/question_expansion.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way.
-Include a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-
-{format_instructions}
diff --git a/common/prompts/llama_70b/generate_function.txt b/common/prompts/llama_70b/generate_function.txt
deleted file mode 100644
index a7e4ee0..0000000
--- a/common/prompts/llama_70b/generate_function.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-Follow the output directions below on how to structure your response:
-{format_instructions}
diff --git a/common/prompts/llama_70b/map_question_to_schema.txt b/common/prompts/llama_70b/map_question_to_schema.txt
deleted file mode 100644
index 81ed53d..0000000
--- a/common/prompts/llama_70b/map_question_to_schema.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-{format_instructions}
-question: {question}
-conversation: {conversation}
-
diff --git a/common/prompts/openai_gpt4/chatbot_response.txt b/common/prompts/openai_gpt4/chatbot_response.txt
deleted file mode 100644
index 6acdaf5..0000000
--- a/common/prompts/openai_gpt4/chatbot_response.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism.
-
-Follow these guidelines:
-- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question.
-- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic.
-- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information.
-- Make sure to preserve the image links in markdown syntax "" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references.
-- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section.
-- Ensure that each row of every table, including the header row, starts on a new line.
-- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only.
-- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information.
-- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer
-
-Question: {question}
-Contexts: {context}
-Query: {query}
-Format: {format_instructions}
diff --git a/common/prompts/openai_gpt4/community_summarization.txt b/common/prompts/openai_gpt4/community_summarization.txt
deleted file mode 100644
index 50e4619..0000000
--- a/common/prompts/openai_gpt4/community_summarization.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-You are a helpful assistant responsible for generating a comprehensive summary of the data provided below.
-Given one or two entities, and a list of descriptions, all related to the same entity or group of entities.
-Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions.
-If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description.
-Make sure it is written in third person, and include the entity names so we the have full context.
-
-#######
--Data-
-Commuinty Title: {entity_name}
-Description List: {description_list}
-
diff --git a/common/prompts/openai_gpt4/entity_relationship_extraction.txt b/common/prompts/openai_gpt4/entity_relationship_extraction.txt
deleted file mode 100644
index 852dded..0000000
--- a/common/prompts/openai_gpt4/entity_relationship_extraction.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# Knowledge Graph Instructions for GPT-4
-## 1. Overview
-You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
-- **Nodes** represent entities, concepts, and properties of entities.
-- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
-## 2. Labeling Nodes
-- **Consistency**: Ensure you use basic or elementary types for node labels.
-- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
-- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
-## 3. Handling Numerical Data and Dates
-- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
-- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
-- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes.
-- **Quotation Marks**: Never use escaped single or double quotes within property values.
-- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
-## 4. Coreference Resolution
-- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
-If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.
-Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial.
-## 5. Strict Compliance
-Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting.
-## 6. Handling Instances with No Relationships
-If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty.
\ No newline at end of file
diff --git a/common/prompts/openai_gpt4/generate_cypher.txt b/common/prompts/openai_gpt4/generate_cypher.txt
deleted file mode 100644
index 732ed49..0000000
--- a/common/prompts/openai_gpt4/generate_cypher.txt
+++ /dev/null
@@ -1,85 +0,0 @@
-You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}.
-If there're multiple words in the question having same meaning then remove the duplication.
-Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB".
-Only include attributes that are found in the schema. Never include any attributes that are not found in the schema.
-Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates.
-Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query.
-Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema.
-Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex.
-Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available.
-Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema.
-Always use double quotes for strings instead of single quotes.
-Always convert strings to lower case using toLower() function for string comparision in WHERE clause.
-Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore.
-Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type.
-For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges.
-
-Avoid to generate invalid OpenCypher queries based on the errors from history below.
-
-Schema: {schema}
-History: {history}
-
-Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below:
-
-Supported Clauses:
-MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph.
-WHERE: Filter results.
-RETURN / WITH: Project query results, alias fields, chain query parts.
-ORDER BY / SKIP / LIMIT: Control output order, offset, and size.
-DELETE / DETACH DELETE: Delete nodes/edges.
-
-Supported Operators:
-Mathematical: +, -, *, /, %, ^ (exponent)
-Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL
-Boolean: AND, OR, NOT, XOR
-String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access)
-
-Supported Functions:
-Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP()
-Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees()
-String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split()
-List: head(), last(), size(), range(), coalesce(), tail()
-Others: id(), elementId(), labels(), properties(), timestamp()
-
-Supported Expressions:
-CASE: Conditional logic.
-
-Supported Operators:
-Comparison: IS NULL, IS NOT NULL
-
-Unsupported Features:
-Clauses Not Yet Supported
-CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND
-
-Unsupported Functions:
-collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form)
-
-Syntax Limitations:
-WITH clause must group by exactly one vertex variable.
-Path variables (e.g. p = (...)) not supported.
-MATCH must reference variables from prior WITH.
-Disconnected MATCH fragments not supported.
-
-Additionally, you cannot use the following clauses:
-CREATE
-MERGE
-REMOVE
-UNION
-UNION ALL
-UNWIND
-SET
-
-Here's some commonly used abbreviations:
-dt -> date
-wk -> week
-yr -> year
-pct -> percentage
-qty -> quantity
-lng -> longitude
-cm -> Contract Manufacturer
-
-Always make the cypher query returns the entity in the original question together with the data to be queried.
-Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore.
-Always validate the syntax for the generated OpenCypher query before writing to response.
-
-ONLY write the OpenCypher query in the response. Do not include any other information in the response.
diff --git a/common/prompts/openai_gpt4/generate_function.txt b/common/prompts/openai_gpt4/generate_function.txt
deleted file mode 100644
index 359b46c..0000000
--- a/common/prompts/openai_gpt4/generate_function.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection.
-When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted.
-For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on.
-Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-Vertex Types: {vertex_types}
-Vertex Attributes: {vertex_attributes}
-Vertex IDs: {vertex_ids}
-Edge Types: {edge_types}
-Edge Attributes: {edge_attributes}
-Question: {question}
-First Docstring: {doc1}
-Second Docstring: {doc2}
-Third Docstring: {doc3}
-Fourth Docstring: {doc4}
-Fifth Docstring: {doc5}
-Sixth Docstring: {doc6}
-Seventh Docstring: {doc7}
-Eighth Docstring: {doc8}
-
-If the output of this function answers the user's question, immediately return that answer.
-
-Follow the output directions below on how to structure your response
-Only include valid JSON do not include any other texts which would render the response invalid JSON.
-{format_instructions}
diff --git a/common/prompts/openai_gpt4/graphrag_scoring.txt b/common/prompts/openai_gpt4/graphrag_scoring.txt
deleted file mode 100644
index 38ef643..0000000
--- a/common/prompts/openai_gpt4/graphrag_scoring.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-You are a helpful assistant responsible for generating an answer to the question below using the data provided.
-Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-Context: {context}
-
-{format_instructions}
diff --git a/common/prompts/openai_gpt4/map_question_to_schema.txt b/common/prompts/openai_gpt4/map_question_to_schema.txt
deleted file mode 100644
index 81ed53d..0000000
--- a/common/prompts/openai_gpt4/map_question_to_schema.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Replace the entites mentioned in the question to one of these choices: {vertices}.
-If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"),
-always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity.
-Choose a better mapping between vertex type or its attributes: {verticesAttrs}.
-Replace the relationships mentioned in the question to one of these choices: {edges}.
-Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}.
-When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause.
-If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above.
-Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same.
-Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question.
-
-{format_instructions}
-question: {question}
-conversation: {conversation}
-
diff --git a/common/prompts/openai_gpt4/question_expansion.txt b/common/prompts/openai_gpt4/question_expansion.txt
deleted file mode 100644
index b04aed8..0000000
--- a/common/prompts/openai_gpt4/question_expansion.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way.
-Include a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent).
-
-Question: {question}
-
-{format_instructions}
diff --git a/common/utils/image_data_extractor.py b/common/utils/image_data_extractor.py
index 711c562..f925e9d 100644
--- a/common/utils/image_data_extractor.py
+++ b/common/utils/image_data_extractor.py
@@ -73,5 +73,8 @@ def describe_image_with_llm(file_path):
response = langchain_client.invoke(messages)
return response.content if hasattr(response, "content") else str(response)
except Exception as e:
+ error_str = str(e).lower()
+ if "throttl" in error_str or "rate" in error_str or "too many" in error_str:
+ raise # Let caller retry on rate limit
logger.error(f"Failed to describe image with LLM: {str(e)}")
return "Image: Error processing image description"
diff --git a/common/utils/prompt_validation.py b/common/utils/prompt_validation.py
new file mode 100644
index 0000000..51c5cfd
--- /dev/null
+++ b/common/utils/prompt_validation.py
@@ -0,0 +1,135 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Gatekeepers for user-customized prompt templates.
+
+When a user saves a customized prompt via the *Customize Prompts* UI,
+two things must hold before the file is written:
+
+1. **Required placeholders are present.** Every prompt type has a fixed
+ set of ``{var}`` tokens the calling code substitutes at runtime
+ (e.g. ``community_summarization`` always interpolates
+ ``{entity_name}`` and ``{description_list}``). If the user removes
+ one of these, the corresponding feature breaks at the next call.
+ ``validate_and_escape_prompt`` returns the missing list so the API
+ can reject the save with a 400.
+
+2. **Stray brace tokens are escaped.** Users frequently include literal
+ ``{example}`` or ``{TODO}`` text in their prompts as documentation
+ or examples. ``str.format`` / ``PromptTemplate`` interpret those as
+ placeholders and either substitute the wrong thing or raise
+ ``KeyError``. ``validate_and_escape_prompt`` rewrites any
+ ``{ident}`` whose name isn't a recognized placeholder for the
+ prompt type into ``{{ident}}`` so the runtime treats it as literal.
+
+The placeholder sets are derived from ``input_variables=[…]`` at the
+caller site (e.g. ``agent_generation.py``, ``community_summarizer.py``,
+``map_question_to_schema.py``). Add a new entry here when a new
+user-customizable prompt is wired up.
+"""
+
+from __future__ import annotations
+
+import re
+from typing import List, Set, Tuple
+
+
+#: Variables every customized prompt of this type MUST contain. Derived
+#: from the ``input_variables`` arguments passed to the
+#: ``PromptTemplate`` / ``ChatPromptTemplate`` constructors at the call
+#: sites that consume each prompt.
+REQUIRED_VARS_BY_PROMPT_TYPE: dict = {
+ # Used by graphrag/app/agent/agent_generation.py and the supportai
+ # retrievers' final answer step.
+ "chatbot_response": {"question", "context"},
+ # System message in LLMEntityRelationshipExtractor — input arrives
+ # via separate human messages, so the customizable prompt doesn't
+ # need any required placeholders of its own.
+ "entity_relationship": set(),
+ # ecc/app/graphrag/community_summarizer.py.
+ "community_summarization": {"entity_name", "description_list"},
+ # graphrag/app/tools/map_question_to_schema.py.
+ "query_generation": {
+ "question",
+ "conversation",
+ "vertices",
+ "verticesAttrs",
+ "edges",
+ "edgesInfo",
+ },
+ # common/db/schema_extraction.py.
+ "schema_extraction": {"samples", "structural_types", "tg_keywords"},
+}
+
+
+#: Variables the runtime supplies as ``partial_variables`` (or via a
+#: separate prompt message) — they MAY appear in the user content but
+#: aren't required. Listed so the escaper doesn't double-brace them.
+ALLOWED_PARTIALS_BY_PROMPT_TYPE: dict = {
+ "chatbot_response": {"format_instructions", "query", "history"},
+ "entity_relationship": {"format_instructions", "input"},
+ "community_summarization": {"format_instructions"},
+ "query_generation": {"format_instructions"},
+ "schema_extraction": set(),
+}
+
+
+# Match a single-brace placeholder like ``{ident}`` BUT NOT a
+# double-brace ``{{ident}}`` (Python's str.format escape) and NOT
+# ``{}`` / ``{123}`` (no leading letter or underscore).
+#
+# The negative lookbehind ``(? Tuple[str, List[str]]:
+ """Run both gatekeepers on *content* for *prompt_type*.
+
+ Returns ``(escaped_content, missing_required)`` where:
+
+ * ``escaped_content`` is *content* with every stray ``{ident}``
+ rewritten to ``{{ident}}``. Tokens whose name is in the
+ required + partials set are left as-is.
+ * ``missing_required`` lists the required placeholder names the
+ user did NOT include. Caller should reject the save when this
+ list is non-empty.
+
+ For unknown ``prompt_type`` (e.g. a future addition that this
+ module hasn't been updated for), returns ``(content, [])``
+ unchanged so the save isn't blocked — better to ship a forward-
+ compatible passthrough than fail-closed on a name typo.
+ """
+ if prompt_type not in REQUIRED_VARS_BY_PROMPT_TYPE:
+ return content, []
+
+ required: Set[str] = REQUIRED_VARS_BY_PROMPT_TYPE[prompt_type]
+ allowed_partials: Set[str] = ALLOWED_PARTIALS_BY_PROMPT_TYPE.get(
+ prompt_type, set()
+ )
+ legal: Set[str] = required | allowed_partials
+
+ found_idents: Set[str] = set()
+
+ def _replace(m: re.Match) -> str:
+ ident = m.group(1)
+ found_idents.add(ident)
+ if ident in legal:
+ return m.group(0)
+ return "{{" + ident + "}}"
+
+ escaped = _PLACEHOLDER_RE.sub(_replace, content)
+ missing = sorted(required - found_idents)
+ return escaped, missing
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 449ace5..4b9e545 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -235,6 +235,7 @@ def safe_walk(path):
files_to_process = []
jsonl_files_copied = []
+ cached_jsonl_skipped = []
for file_path in safe_walk(folder_path_obj):
if file_path.is_file():
if file_path.name.startswith(('.', '~', '$')) or 'BROMIUM' in file_path.name.upper():
@@ -252,9 +253,41 @@ def safe_walk(path):
})
logger.info(f"Copied JSONL file directly: {file_path.name} ({num_lines} documents)")
elif file_ext in self.supported_extensions:
- files_to_process.append(file_path)
-
- logger.info(f"Found {len(files_to_process)} files to process, {len(jsonl_files_copied)} JSONL files copied directly")
+ # If a previous run (e.g. schema extraction) already
+ # produced a matching JSONL in *temp_folder*, reuse
+ # it instead of re-converting the source file. This
+ # saves the per-file PDF / image conversion cost
+ # when the user uploaded sample files via the
+ # Initialize Graph dialog and is now ingesting them.
+ cached_jsonl = os.path.join(
+ temp_folder, f"{file_path.stem}.jsonl"
+ )
+ if os.path.exists(cached_jsonl):
+ try:
+ num_lines = sum(
+ 1 for _ in open(cached_jsonl, 'r', encoding='utf-8')
+ )
+ except Exception:
+ num_lines = 0
+ cached_jsonl_skipped.append({
+ 'file_path': str(file_path),
+ 'num_documents': num_lines,
+ 'jsonl_file': os.path.basename(cached_jsonl),
+ 'status': 'success',
+ 'cached': True,
+ })
+ logger.info(
+ f"Reusing cached JSONL for {file_path.name} "
+ f"({num_lines} documents) — skipping re-conversion"
+ )
+ else:
+ files_to_process.append(file_path)
+
+ logger.info(
+ f"Found {len(files_to_process)} files to process, "
+ f"{len(jsonl_files_copied)} JSONL files copied directly, "
+ f"{len(cached_jsonl_skipped)} skipped via cached JSONL"
+ )
semaphore = asyncio.Semaphore(max_concurrent)
@@ -265,8 +298,10 @@ async def process_with_semaphore(file_path):
tasks = [process_with_semaphore(fp) for fp in files_to_process]
results = await asyncio.gather(*tasks, return_exceptions=True)
- processed_files_info = list(jsonl_files_copied)
- total_docs = sum(f['num_documents'] for f in jsonl_files_copied)
+ processed_files_info = list(jsonl_files_copied) + list(cached_jsonl_skipped)
+ total_docs = sum(
+ f['num_documents'] for f in jsonl_files_copied + cached_jsonl_skipped
+ )
for result in results:
if isinstance(result, Exception):
@@ -457,54 +492,64 @@ def _extract_pdf_with_images_as_docs(file_path, base_doc_id, graphname=None):
"content": markdown_content,
"position": 0
}]
- image_entries = []
- image_counter = 0
- for img_ref in image_refs:
+ # Phase 1 — describe + base64-encode every image in parallel.
+ # Each worker hits Bedrock for the description and reads the
+ # image off disk, so they're I/O-bound; a small thread pool
+ # cuts wall-clock proportionally for image-heavy PDFs.
+ # Markdown mutations stay in phase 2 (next loop) because
+ # insert_description_by_id / replace_path_with_tg_protocol
+ # mutate the same shared string and must run in deterministic
+ # order. Concurrency cap is intentionally small to stay below
+ # Bedrock's per-account throttle.
+ image_workers = int(os.environ.get("PDF_IMAGE_CONCURRENCY", "8"))
+
+ def _describe_and_encode(img_ref: dict) -> dict:
+ """Run on a worker thread. Returns one of:
+ * ``{"ok": True, "img_ref", "description", "image_base64",
+ "width", "height"}``
+ * ``{"ok": False, "img_ref", "error"}``
+ Never raises.
+ """
try:
- img_path = Path(img_ref["path"]) # convert to Path
- image_id = img_ref["image_id"]
- # Image description
+ img_path = Path(img_ref["path"])
description = describe_image_with_llm(str(img_path))
- markdown_content = insert_description_by_id(
- markdown_content,
- image_id,
- description
- )
- # Convert image to base64
pil_image = PILImage.open(img_path)
- buffer = io.BytesIO()
-
if pil_image.mode != "RGB":
pil_image = pil_image.convert("RGB")
-
+ buffer = io.BytesIO()
pil_image.save(buffer, format="JPEG", quality=95)
image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
-
- image_counter += 1
- image_doc_id = f"{base_doc_id}_image_{image_counter}".lower()
-
- # Replace file path with tg:// protocol reference in markdown
- markdown_content = replace_path_with_tg_protocol(
- markdown_content,
- image_id,
- image_doc_id
- )
-
- image_entries.append({
- "doc_id": image_doc_id,
- "doc_type": "image",
- "image_description": description,
- "image_data": image_base64,
- "image_format": "jpg",
- "parent_doc": base_doc_id,
- "page_number": 0,
+ return {
+ "ok": True,
+ "img_ref": img_ref,
+ "description": description,
+ "image_base64": image_base64,
"width": pil_image.width,
"height": pil_image.height,
- "position": image_counter
- })
+ }
+ except Exception as img_error: # noqa: BLE001 — keep going
+ return {"ok": False, "img_ref": img_ref, "error": img_error}
+
+ if image_refs:
+ with ThreadPoolExecutor(
+ max_workers=max(1, min(image_workers, len(image_refs)))
+ ) as ex:
+ # executor.map preserves input ordering, which is what
+ # the markdown-mutation phase below relies on.
+ described = list(ex.map(_describe_and_encode, image_refs))
+ else:
+ described = []
- except Exception as img_error:
- logger.warning(f"Failed to process image {img_ref.get('path')}: {img_error}")
+ # Phase 2 — apply markdown mutations and build image_entries
+ # in deterministic order using the parallel results.
+ image_entries: list[dict] = []
+ image_counter = 0
+ for d in described:
+ img_ref = d["img_ref"]
+ if not d.get("ok"):
+ logger.warning(
+ f"Failed to process image {img_ref.get('path')}: {d.get('error')}"
+ )
failed_path = img_ref.get("path", "")
if failed_path:
markdown_content = re.sub(
@@ -512,6 +557,30 @@ def _extract_pdf_with_images_as_docs(file_path, base_doc_id, graphname=None):
"",
markdown_content,
)
+ continue
+
+ image_id = img_ref["image_id"]
+ markdown_content = insert_description_by_id(
+ markdown_content, image_id, d["description"]
+ )
+
+ image_counter += 1
+ image_doc_id = f"{base_doc_id}_image_{image_counter}".lower()
+ markdown_content = replace_path_with_tg_protocol(
+ markdown_content, image_id, image_doc_id
+ )
+ image_entries.append({
+ "doc_id": image_doc_id,
+ "doc_type": "image",
+ "image_description": d["description"],
+ "image_data": d["image_base64"],
+ "image_format": "jpg",
+ "parent_doc": base_doc_id,
+ "page_number": 0,
+ "width": d["width"],
+ "height": d["height"],
+ "position": image_counter,
+ })
# FINAL CLEANUP — delete folder after processing everything
if image_output_folder.exists() and image_output_folder.is_dir():
diff --git a/ecc/app/ecc_util.py b/ecc/app/ecc_util.py
index a28567a..7da80bc 100644
--- a/ecc/app/ecc_util.py
+++ b/ecc/app/ecc_util.py
@@ -1,5 +1,5 @@
from common.chunkers import character_chunker, regex_chunker, semantic_chunker, markdown_chunker, recursive_chunker, html_chunker, single_chunker
-from common.config import get_graphrag_config, embedding_service
+from common.config import get_graphrag_config, get_embedding_service
def get_chunker(chunker_type: str = "", graphname: str = None):
cfg = get_graphrag_config(graphname)
@@ -8,7 +8,7 @@ def get_chunker(chunker_type: str = "", graphname: str = None):
chunker_config = cfg.get("chunker_config", {})
if chunker_type == "semantic":
chunker = semantic_chunker.SemanticChunker(
- embedding_service,
+ get_embedding_service(),
chunker_config.get("method", "percentile"),
chunker_config.get("threshold", 0.95),
)
diff --git a/ecc/app/eventual_consistency_checker.py b/ecc/app/eventual_consistency_checker.py
index 1c28b53..fb501fe 100644
--- a/ecc/app/eventual_consistency_checker.py
+++ b/ecc/app/eventual_consistency_checker.py
@@ -155,24 +155,10 @@ def _upsert_rels(self, src_id, src_type, relationships):
for x in relationships
],
)
- self.conn.upsertEdges(
- "Entity",
- "IS_HEAD_OF",
- "RelationshipType",
- [
- (x["source"], x["source"] + ":" + x["type"] + ":" + x["target"], {})
- for x in relationships
- ],
- )
- self.conn.upsertEdges(
- "RelationshipType",
- "HAS_TAIL",
- "Entity",
- [
- (x["source"] + ":" + x["type"] + ":" + x["target"], x["target"], {})
- for x in relationships
- ],
- )
+ # IS_HEAD_OF / HAS_TAIL are meta-schema edges (EntityType ↔
+ # RelationshipType); not per-instance. The legacy ECC path
+ # writes only MENTIONS_RELATIONSHIP from the chunk/document
+ # source to the RelationshipType meta-vertex.
self.conn.upsertEdges(
src_type,
"MENTIONS_RELATIONSHIP",
diff --git a/ecc/app/graphrag/community_summarizer.py b/ecc/app/graphrag/community_summarizer.py
index 532b94f..3586e9b 100644
--- a/ecc/app/graphrag/community_summarizer.py
+++ b/ecc/app/graphrag/community_summarizer.py
@@ -26,7 +26,7 @@
# src: https://github.com/microsoft/graphrag/blob/main/graphrag/index/graph/extractors/summarize/prompts.py
-id_pat = re.compile(r"[_\d]*")
+id_pat = re.compile(r"(_\d+)+$")
class CommunitySummarizer:
diff --git a/ecc/app/graphrag/graph_rag.py b/ecc/app/graphrag/graph_rag.py
index c7ef5be..3435916 100644
--- a/ecc/app/graphrag/graph_rag.py
+++ b/ecc/app/graphrag/graph_rag.py
@@ -34,7 +34,6 @@
stream_ids,
tg_sem,
upsert_batch,
- add_rels_between_types
)
from pyTigerGraph import AsyncTigerGraphConnection
@@ -506,16 +505,10 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
init_end = time.perf_counter()
logger.info("Doc Processing End")
- # Type Resolution
+ # Type Resolution — IS_HEAD_OF / HAS_TAIL writes happen inline in
+ # the per-relationship extract step (workers.py); no post-processing
+ # query needed.
type_start = time.perf_counter()
- if entity_extraction_switch:
- logger.info("Type Processing Start")
- res = await add_rels_between_types(conn)
- if res.get("error", False):
- logger.error(f"Error adding relationships between types: {res}")
- else:
- logger.info(f"Added relationships between types: {res}")
- logger.info("Type Processing End")
type_end = time.perf_counter()
# Community Detection
diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py
index 094d95b..7973107 100644
--- a/ecc/app/graphrag/util.py
+++ b/ecc/app/graphrag/util.py
@@ -30,6 +30,11 @@
get_completion_config,
get_graphrag_config,
)
+from common.db.schema_utils import (
+ is_structural_type,
+ read_existing_schema_async,
+ read_type_metadata_async,
+)
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.tigergraph_embedding_store import TigerGraphEmbeddingStore
from common.extractors import GraphExtractor, LLMEntityRelationshipExtractor
@@ -61,7 +66,6 @@
"common/gsql/graphrag/StreamChunkContent",
"common/gsql/graphrag/SetEpochProcessing",
"common/gsql/graphrag/get_vertices_or_remove",
- "common/gsql/supportai/create_entity_type_relationships",
]
load_q = reusable_channel.ReuseableChannel()
@@ -142,7 +146,68 @@ async def init(
if graph_cfg.get("extractor") == "graphrag":
extractor = GraphExtractor()
elif graph_cfg.get("extractor") == "llm":
- extractor = LLMEntityRelationshipExtractor(get_llm_service(get_completion_config()))
+ # Read the live schema directly (without going through the
+ # proposal-flow SchemaProposal type). This intentionally
+ # supports graphs whose domain types were created outside of
+ # the proposal flow — admin UI, prior releases,
+ # external migration scripts — as long as the domain types
+ # and the EntityType / RelationshipType metadata are on the
+ # graph, ECC will use them.
+ try:
+ existing = await read_existing_schema_async(conn)
+ except Exception as exc:
+ logger.warning(f"Loading live schema for extractor failed: {exc}")
+ from common.db.schema_utils import ExistingSchema
+ existing = ExistingSchema()
+ try:
+ entity_descs, rel_defs = await read_type_metadata_async(conn)
+ except Exception as exc:
+ logger.warning(f"Loading type metadata for extractor failed: {exc}")
+ entity_descs, rel_defs = {}, {}
+
+ # Filter to domain types (drop GraphRAG structural types and
+ # any pair whose endpoint touches a structural vertex).
+ domain_vertex_types = sorted(
+ v for v in existing.vertex_types if not is_structural_type(v)
+ )
+ domain_edge_endpoints: dict = {}
+ for edge_name, pairs in existing.edge_pairs.items():
+ if is_structural_type(edge_name):
+ continue
+ domain_pairs = [
+ (s, t)
+ for s, t in pairs
+ if not is_structural_type(s) and not is_structural_type(t)
+ ]
+ if domain_pairs:
+ domain_edge_endpoints[edge_name] = domain_pairs
+ domain_edge_types = sorted(domain_edge_endpoints.keys())
+
+ # Trim the descriptions to domain types only.
+ domain_entity_defs = {
+ vt: entity_descs[vt]
+ for vt in domain_vertex_types
+ if entity_descs.get(vt)
+ }
+ domain_rel_defs = {
+ et: rel_defs[et]
+ for et in domain_edge_types
+ if rel_defs.get(et)
+ }
+
+ # Strict-mode comes from graphrag_config; default false (legacy
+ # fallback to plain Entity vertices for non-domain extractions).
+ strict_mode = bool(graph_cfg.get("strict_mode", False))
+
+ extractor = LLMEntityRelationshipExtractor(
+ get_llm_service(get_completion_config(conn.graphname)),
+ allowed_entity_types=domain_vertex_types or None,
+ allowed_relationship_types=domain_edge_types or None,
+ strict_mode=strict_mode,
+ entity_type_definitions=domain_entity_defs,
+ relationship_type_definitions=domain_rel_defs,
+ domain_edge_endpoints=domain_edge_endpoints,
+ )
else:
raise ValueError("Invalid extractor type")
@@ -216,6 +281,55 @@ def process_id(v_id: str):
return v_id
+# Suffixes the LLM commonly tacks onto type labels without adding
+# semantic distinction. Stripped during meta-layer normalization so
+# ``Company_Type``, ``Company_Class``, ``Company_Entity`` collapse onto
+# the same canonical name.
+_TYPE_SUFFIXES = ("_type", "_class", "_entity", "_data", "_info", "_record")
+
+
+def normalize_type_name(name: str) -> str:
+ """Normalize an LLM-emitted vertex / edge type label so trivial
+ variants collapse onto a single canonical id.
+
+ Applies in order:
+
+ 1. ``process_id`` (lowercase, whitespace → ``-``, strip parens).
+ 2. Strip a single trailing semantic-suffix from
+ :data:`_TYPE_SUFFIXES` (e.g. ``company_type`` → ``company``).
+ 3. Singularize trailing ``ies`` → ``y`` (``companies`` →
+ ``company``); strip a single trailing ``s`` only when the
+ preceding char is a consonant other than ``s``, ``i``, or ``u``
+ (``reports`` → ``report``; preserves ``series``, ``status``,
+ ``news``, ``business``).
+
+ Used only for the EntityType / RelationshipType meta-layer in
+ Case 1 (no domain types declared) — instance ids stay
+ untouched. Synonym consolidation (``Company`` vs ``Corporation``)
+ is out of scope for this deterministic pass.
+ """
+ base = process_id(name)
+ if not base:
+ return ""
+ for suffix in _TYPE_SUFFIXES:
+ if base.endswith(suffix) and len(base) > len(suffix):
+ base = base[: -len(suffix)]
+ break
+ # Singularize defensively. Length thresholds keep short words
+ # whose final ``s`` / ``ies`` is part of the singular stem
+ # (``News``, ``Series``, ``Bus``, ``Status``, ``Yes``).
+ if base.endswith("ies") and len(base) > 6:
+ base = base[:-3] + "y"
+ elif (
+ base.endswith("s")
+ and len(base) > 4
+ and base[-2] not in "siu"
+ and not base[-2].isdigit()
+ ):
+ base = base[:-1]
+ return base
+
+
async def upsert_vertex(
conn: AsyncTigerGraphConnection,
vertex_type: str,
@@ -319,17 +433,6 @@ async def get_commuinty_children(conn, i: int, c: str):
return descrs
-async def add_rels_between_types(conn):
- try:
- async with tg_sem:
- resp = await conn.runInstalledQuery(
- "create_entity_type_relationships"
- )
- except Exception as e:
- logger.error(f"Check Vert EntityType err:\n{e}")
- return {"error": True, "message": e}
- return resp[0]
-
async def check_vertex_has_desc(conn, i: int):
try:
async with tg_sem:
diff --git a/ecc/app/graphrag/workers.py b/ecc/app/graphrag/workers.py
index 516b83c..bc5819f 100644
--- a/ecc/app/graphrag/workers.py
+++ b/ecc/app/graphrag/workers.py
@@ -264,13 +264,65 @@ async def extract(
logger.error(f"Failed to extract chunk {chunk_id}: {e}")
extracted = []
+ # Schema-aware ingest helpers — derive case-insensitive
+ # lookups from the extractor once per chunk so the loops below
+ # can map LLM-emitted type strings back to canonical schema names.
+ domain_vt_canonical: dict = {}
+ domain_edge_canonical: dict = {}
+ edge_endpoint_pairs: dict = {}
+ strict_mode = False
+ if isinstance(extractor, LLMEntityRelationshipExtractor):
+ domain_vt_canonical = {
+ v.casefold(): v for v in (extractor.allowed_vertex_types or [])
+ }
+ domain_edge_canonical = {
+ e.casefold(): e for e in (extractor.allowed_edge_types or [])
+ }
+ edge_endpoint_pairs = {
+ name.casefold(): {(f.casefold(), t.casefold()) for f, t in pairs}
+ for name, pairs in (extractor.domain_edge_endpoints or {}).items()
+ }
+ strict_mode = bool(extractor.strict_mode)
+
+ # ``has_domain_types`` distinguishes the two meta-layer cases:
+ # Case 1: no domain types on the graph or extracted — the
+ # EntityType / RelationshipType layer becomes a free-text
+ # catalog of whatever the LLM emitted (legacy behaviour).
+ # Case 2: at least one domain type exists — the meta-layer
+ # is restricted to the declared / matched domain types
+ # only. Non-matched extractions still write to the legacy
+ # Entity / RELATIONSHIP layer but DO NOT pollute the meta
+ # layer.
+ has_domain_types = bool(domain_vt_canonical) or bool(domain_edge_canonical)
+
# upsert nodes and edges to the graph
for doc in extracted:
+ # Build a node_id → node_type lookup so the relationship
+ # loop below knows the source/target types (the parser
+ # currently doesn't carry endpoint types per relationship).
+ node_type_by_id: dict = {}
+ for n in doc.nodes:
+ if not n.id or not n.type:
+ continue
+ pid = util.process_id(str(n.id))
+ if pid:
+ node_type_by_id[pid] = n.type
+
for i, node in enumerate(doc.nodes):
logger.info(f"extract writes entity vert to upsert\nNode: {node.id}")
v_id = util.process_id(str(node.id))
if len(v_id) == 0:
continue
+ node_type_lower = (node.type or "").casefold()
+ domain_vt = domain_vt_canonical.get(node_type_lower)
+
+ # Strict mode: drop nodes whose type isn't in the
+ # schema. The legacy raw-Entity fallback applies only
+ # when strict_mode is off OR the node matches a domain
+ # type.
+ if strict_mode and domain_vt is None:
+ continue
+
desc = await get_vert_desc(conn, v_id, node)
if len(desc[0]) == 0:
@@ -290,22 +342,38 @@ async def extract(
),
)
)
+ # Meta-layer (EntityType / ENTITY_HAS_TYPE) population:
+ # Case 1 (no domain types): write for every extracted
+ # node using a normalized form of the LLM-emitted
+ # type label so trivial variants
+ # (``Company`` / ``Companies`` / ``company_type``)
+ # collapse onto one EntityType row.
+ # Case 2 (domain types exist): write only when the
+ # node matches a declared domain VT, using the
+ # canonical domain-VT name as the EntityType id.
+ meta_type_id = ""
if isinstance(extractor, LLMEntityRelationshipExtractor):
+ if not has_domain_types:
+ meta_type_id = util.normalize_type_name(node.type)
+ elif domain_vt is not None:
+ # Preserve the canonical schema casing
+ # (``InvestmentFund``) so the EntityType id
+ # matches what ``upsert_type_metadata`` writes
+ # at schema-apply time. Lowercasing here would
+ # produce a duplicate row keyed
+ # ``investmentfund``.
+ meta_type_id = domain_vt
+ if meta_type_id:
logger.info("extract writes type vert to upsert")
- type_id = util.process_id(node.type)
- if len(type_id) == 0:
- continue
await upsert_chan.put(
(
- util.upsert_vertex, # func to call
+ util.upsert_vertex,
(
conn,
- "EntityType", # v_type
- type_id, # v_id
- { # attrs
- "epoch_added": int(time.time()),
- },
- )
+ "EntityType",
+ meta_type_id,
+ {"epoch_added": int(time.time())},
+ ),
)
)
logger.info("extract writes entity_has_type edge to upsert")
@@ -314,12 +382,12 @@ async def extract(
util.upsert_edge,
(
conn,
- "Entity", # src_type
- v_id, # src_id
- "ENTITY_HAS_TYPE", # edgeType
- "EntityType", # tgt_type
- type_id, # tgt_id
- None, # attributes
+ "Entity",
+ v_id,
+ "ENTITY_HAS_TYPE",
+ "EntityType",
+ meta_type_id,
+ None,
),
)
)
@@ -340,6 +408,44 @@ async def extract(
),
)
)
+
+ # Schema-aware: when the node's type matches a domain
+ # vertex type from the live schema, ALSO upsert the
+ # vertex as that domain type and link it back to the
+ # chunk via the multi-pair CONTAINS_ENTITY pair we
+ # added at init time.
+ if domain_vt is not None:
+ logger.info(
+ f"extract writes domain {domain_vt} vert + CONTAINS_ENTITY pair"
+ )
+ # Domain VTs don't carry the ECC bookkeeping
+ # ``epoch_added`` attribute — sending it makes TG
+ # reject the whole batch.
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (
+ conn,
+ domain_vt,
+ v_id,
+ {},
+ ),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_edge,
+ (
+ conn,
+ "DocumentChunk",
+ chunk_id,
+ "CONTAINS_ENTITY",
+ domain_vt,
+ v_id,
+ None,
+ ),
+ )
+ )
for node2 in doc.nodes[i + 1:]:
v_id2 = util.process_id(str(node2.id))
if len(v_id2) == 0:
@@ -363,66 +469,227 @@ async def extract(
logger.info(
f"extract writes relates edge to upsert:{edge.source.id} -({edge.type})-> {edge.target.id}"
)
- # upsert verts first to make sure their ID becomes an attr
- v_id = util.process_id(edge.source.id) # src_id
- if len(v_id) == 0:
+ src_id = util.process_id(edge.source.id)
+ tgt_id = util.process_id(edge.target.id)
+ if len(src_id) == 0 or len(tgt_id) == 0:
continue
- desc = await get_vert_desc(conn, v_id, edge.source)
- if len(desc[0]) == 0:
- desc[0] = edge.source.id
+
+ # Look up the source / target types from the per-doc
+ # node lookup (the parser doesn't currently carry
+ # endpoint types per relationship).
+ src_type = node_type_by_id.get(src_id, "")
+ tgt_type = node_type_by_id.get(tgt_id, "")
+
+ rel_type_lower = (edge.type or "").casefold()
+ canonical_rel = domain_edge_canonical.get(rel_type_lower)
+ valid_pair = (
+ canonical_rel is not None
+ and (src_type.casefold(), tgt_type.casefold())
+ in edge_endpoint_pairs.get(rel_type_lower, set())
+ )
+
+ # Strict mode: only write the typed pattern. Legacy
+ # Entity → RELATIONSHIP → Entity fallback applies only
+ # when strict_mode is off.
+ if strict_mode and not valid_pair:
+ continue
+
+ # ---- Legacy raw layer: Entity src + Entity tgt + RELATIONSHIP edge ----
+ src_desc = await get_vert_desc(conn, src_id, edge.source)
+ if len(src_desc[0]) == 0:
+ src_desc[0] = edge.source.id
await upsert_chan.put(
(
- util.upsert_vertex, # func to call
+ util.upsert_vertex,
(
conn,
- "Entity", # v_type
- v_id,
- { # attrs
- "description": desc,
+ "Entity",
+ src_id,
+ {
+ "description": src_desc,
"epoch_added": int(time.time()),
},
),
)
)
- v_id = util.process_id(edge.target.id)
- if len(v_id) == 0:
- continue
- desc = await get_vert_desc(conn, v_id, edge.target)
- if len(desc[0]) == 0:
- desc[0] = edge.target.id
+ tgt_desc = await get_vert_desc(conn, tgt_id, edge.target)
+ if len(tgt_desc[0]) == 0:
+ tgt_desc[0] = edge.target.id
await upsert_chan.put(
(
- util.upsert_vertex, # func to call
+ util.upsert_vertex,
(
conn,
- "Entity", # v_type
- v_id, # src_id
- { # attrs
- "description": desc,
+ "Entity",
+ tgt_id,
+ {
+ "description": tgt_desc,
"epoch_added": int(time.time()),
},
),
)
)
-
- # upsert the edge between the two entities
await upsert_chan.put(
(
util.upsert_edge,
(
conn,
- "Entity", # src_type
- util.process_id(edge.source.id), # src_id
- "RELATIONSHIP", # edgeType
- "Entity", # tgt_type
- util.process_id(edge.target.id), # tgt_id
- {"relation_type": edge.type}, # attributes
+ "Entity",
+ src_id,
+ "RELATIONSHIP",
+ "Entity",
+ tgt_id,
+ {"relation_type": edge.type},
),
)
)
- # embed "RelationshipType",
- # (v_id, content, index_name)
- # right now, we're not embedding relationships in graphrag
+
+ # ---- Meta-schema typed-relationship layer ----
+ # Two cases:
+ # Case 1 (no domain types): every extracted
+ # relationship contributes RelationshipType (via
+ # LLM-emitted edge.type) and IS_HEAD_OF / HAS_TAIL
+ # edges between the corresponding EntityType
+ # vertices (via LLM-emitted src_type / tgt_type).
+ # Case 2 (domain types exist) with valid_pair:
+ # same writes but using canonical (declared) names.
+ # Case 2 without valid_pair: skip the meta-layer
+ # entirely. The legacy Entity / RELATIONSHIP write
+ # above is the only persistence for unmatched
+ # extractions.
+ #
+ # IS_HEAD_OF / HAS_TAIL connect EntityType ↔
+ # RelationshipType (meta layer), NOT individual domain
+ # vertex instances. Per-instance domain edges (e.g.
+ # ``Company → PUBLISHES → Report``) are written
+ # separately when valid_pair holds.
+ meta_rel_id = ""
+ meta_src_et_id = ""
+ meta_tgt_et_id = ""
+ if valid_pair:
+ meta_rel_id = canonical_rel
+ # Preserve canonical schema casing so the EntityType
+ # id matches the entity-side write and the row that
+ # ``upsert_type_metadata`` lays down at schema-apply
+ # time (``InvestmentFund``, not
+ # ``investmentfund``).
+ meta_src_et_id = domain_vt_canonical.get(
+ src_type.casefold(), ""
+ )
+ meta_tgt_et_id = domain_vt_canonical.get(
+ tgt_type.casefold(), ""
+ )
+ elif not has_domain_types:
+ # Case 1: dedup variants via normalize_type_name so
+ # the meta-layer doesn't overflow with near-duplicate
+ # labels (``Company``/``Companies``,
+ # ``WORKS_FOR``/``works_for_type``, etc.).
+ meta_rel_id = util.normalize_type_name(edge.type)
+ meta_src_et_id = util.normalize_type_name(src_type)
+ meta_tgt_et_id = util.normalize_type_name(tgt_type)
+
+ if meta_rel_id and meta_src_et_id and meta_tgt_et_id:
+ now = int(time.time())
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (conn, "RelationshipType", meta_rel_id, {"epoch_added": now}),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (conn, "EntityType", meta_src_et_id, {"epoch_added": now}),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (conn, "EntityType", meta_tgt_et_id, {"epoch_added": now}),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_edge,
+ (
+ conn,
+ "EntityType",
+ meta_src_et_id,
+ "IS_HEAD_OF",
+ "RelationshipType",
+ meta_rel_id,
+ None,
+ ),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_edge,
+ (
+ conn,
+ "RelationshipType",
+ meta_rel_id,
+ "HAS_TAIL",
+ "EntityType",
+ meta_tgt_et_id,
+ None,
+ ),
+ )
+ )
+ # Chunk → RelationshipType — fires whenever any
+ # meta-layer write fires (Case 1 always, Case 2 on
+ # valid_pair).
+ await upsert_chan.put(
+ (
+ util.upsert_edge,
+ (
+ conn,
+ "DocumentChunk",
+ chunk_id,
+ "MENTIONS_RELATIONSHIP",
+ "RelationshipType",
+ meta_rel_id,
+ None,
+ ),
+ )
+ )
+
+ if valid_pair:
+ # Schema-aware: also write the canonical domain VT
+ # instances and the per-instance domain edge (the
+ # schema-declared edge name like ``PUBLISHES``).
+ # Domain VTs don't carry the ECC bookkeeping
+ # ``epoch_added`` attribute — sending it makes TG
+ # reject the whole batch with ``Unknown vertex
+ # attribute or vector name: epoch_added``.
+ canonical_src_vt = domain_vt_canonical.get(src_type.casefold())
+ canonical_tgt_vt = domain_vt_canonical.get(tgt_type.casefold())
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (conn, canonical_src_vt, src_id, {}),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_vertex,
+ (conn, canonical_tgt_vt, tgt_id, {}),
+ )
+ )
+ await upsert_chan.put(
+ (
+ util.upsert_edge,
+ (
+ conn,
+ canonical_src_vt,
+ src_id,
+ canonical_rel,
+ canonical_tgt_vt,
+ tgt_id,
+ None,
+ ),
+ )
+ )
comm_sem = asyncio.Semaphore(util._worker_concurrency)
diff --git a/ecc/app/main.py b/ecc/app/main.py
index 58751bb..f4ba391 100644
--- a/ecc/app/main.py
+++ b/ecc/app/main.py
@@ -33,7 +33,7 @@
from common.config import (
db_config,
graphrag_config,
- embedding_service,
+ get_embedding_service,
get_llm_service,
get_completion_config,
get_graphrag_config,
@@ -92,13 +92,18 @@ def initialize_eventual_consistency_checker(
try:
maj, minor, patch = conn.getVer().split(".")
- if maj >= "4" and minor >= "2":
+ if maj >= "4" and minor >= "2":
# TigerGraph native vector support
embedding_store = TigerGraphEmbeddingStore(
conn,
- embedding_service,
+ get_embedding_service(),
support_ai_instance=False,
)
+ else:
+ raise ValueError(
+ f"TigerGraph version {maj}.{minor}.{patch} is not supported. "
+ "Requires >= 4.2."
+ )
graph_cfg = get_graphrag_config(graphname)
index_names = graph_cfg.get(
"indexes",
@@ -108,7 +113,9 @@ def initialize_eventual_consistency_checker(
if graph_cfg.get("extractor") == "llm":
from common.extractors import LLMEntityRelationshipExtractor
- extractor = LLMEntityRelationshipExtractor(get_llm_service(get_completion_config()))
+ extractor = LLMEntityRelationshipExtractor(
+ get_llm_service(get_completion_config(graphname))
+ )
else:
raise ValueError("Invalid extractor type")
@@ -116,7 +123,7 @@ def initialize_eventual_consistency_checker(
graph_cfg.get("process_interval_seconds", 300),
graph_cfg.get("cleanup_interval_seconds", 300),
graphname,
- embedding_service,
+ get_embedding_service(),
embedding_store,
index_names,
conn,
diff --git a/ecc/app/supportai/supportai_init.py b/ecc/app/supportai/supportai_init.py
index 8d59a5b..db18ab0 100644
--- a/ecc/app/supportai/supportai_init.py
+++ b/ecc/app/supportai/supportai_init.py
@@ -21,7 +21,7 @@
from aiochannel import Channel
from pyTigerGraph import TigerGraphConnection
-from common.config import embedding_service, entity_extraction_switch, doc_process_switch
+from common.config import get_embedding_service, entity_extraction_switch, doc_process_switch
from common.embeddings.base_embedding_store import EmbeddingStore
from common.extractors.BaseExtractor import BaseExtractor
from supportai import workers
@@ -140,7 +140,7 @@ async def embed(
logger.info(f"Embed to {graphname}_{index_name}: {v_id}")
sp.create_task(
workers.embed(
- embedding_service,
+ get_embedding_service(),
embedding_store,
v_id,
content,
diff --git a/ecc/app/supportai/util.py b/ecc/app/supportai/util.py
index 3e3f07a..e3dd36f 100644
--- a/ecc/app/supportai/util.py
+++ b/ecc/app/supportai/util.py
@@ -12,7 +12,7 @@
from pyTigerGraph import TigerGraphConnection
from common.config import (
- embedding_service,
+ get_embedding_service,
graphrag_config,
get_llm_service,
get_completion_config,
@@ -122,7 +122,7 @@ async def init(
embedding_store = TigerGraphEmbeddingStore(
conn,
- embedding_service,
+ get_embedding_service(),
support_ai_instance=True,
)
embedding_store.set_graphname(conn.graphname)
diff --git a/ecc/app/supportai/workers.py b/ecc/app/supportai/workers.py
index 07104fb..3afc44f 100644
--- a/ecc/app/supportai/workers.py
+++ b/ecc/app/supportai/workers.py
@@ -303,34 +303,10 @@ async def extract(
)
)
- # upsert the edge between the two entities
- await upsert_chan.put(
- (
- util.upsert_edge,
- (
- conn,
- "Entity", # src_type
- util.process_id(edge.source.id), # src_id
- "IS_HEAD_OF", # edgeType
- "RelationshipType", # tgt_type
- edge.type, # tgt_id
- ),
- )
- )
- await upsert_chan.put(
- (
- util.upsert_edge,
- (
- conn,
- "RelationshipType", # src_type
- edge.type, # src_id
- "HAS_TAIL", # edgeType
- "Entity", # tgt_type
- util.process_id(edge.target.id), # tgt_id
- ),
- )
- )
-
+ # IS_HEAD_OF / HAS_TAIL are meta-schema edges between
+ # EntityType ↔ RelationshipType — not written here per
+ # Entity instance. Legacy supportai ECC paths without
+ # per-instance entity_type info skip the meta-layer.
# link the relationship to the chunk it came from
logger.info("extract writes mentions edge to upsert")
await upsert_chan.put(
diff --git a/graphrag-ui/src/components/SideMenu.tsx b/graphrag-ui/src/components/SideMenu.tsx
index c4072db..a7594b3 100644
--- a/graphrag-ui/src/components/SideMenu.tsx
+++ b/graphrag-ui/src/components/SideMenu.tsx
@@ -9,6 +9,7 @@ import { HiOutlineChatBubbleOvalLeft } from "react-icons/hi2";
import { MdKeyboardArrowDown, MdKeyboardArrowUp } from "react-icons/md";
import { IoIosArrowForward } from "react-icons/io";
import { useTheme } from "@/components/ThemeProvider";
+import { safeJson } from "@/utils/safeJson";
import { GoGear } from "react-icons/go";
import { useState } from "react";
import {
@@ -98,7 +99,7 @@ const SideMenu = ({ height, setGetConversationId }: { height?: string, setGetCon
return;
}
- const data = await response.json();
+ const data = await safeJson(response);
if (!Array.isArray(data) || data.length === 0) {
setConversationId([]);
@@ -120,7 +121,7 @@ const SideMenu = ({ height, setGetConversationId }: { height?: string, setGetCon
if (!response2.ok) {
return null;
}
- const content = await response2.json();
+ const content = await safeJson(response2);
// Get the most recent message timestamp for sorting
let lastUpdateTime = item.update_ts || item.create_ts;
@@ -204,7 +205,7 @@ const SideMenu = ({ height, setGetConversationId }: { height?: string, setGetCon
return;
}
- const data = await response.json();
+ const data = await safeJson(response);
setConversationId2(data);
// Store the conversation data in sessionStorage for the chat component
diff --git a/graphrag-ui/src/pages/Setup.tsx b/graphrag-ui/src/pages/Setup.tsx
index d5674ac..168ac65 100644
--- a/graphrag-ui/src/pages/Setup.tsx
+++ b/graphrag-ui/src/pages/Setup.tsx
@@ -20,6 +20,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useConfirm } from "@/hooks/useConfirm";
+import { safeJson } from "@/utils/safeJson";
const DEFAULT_MAX_UPLOAD_SIZE_MB = 100;
const envUploadLimit = Number(import.meta.env.VITE_MAX_UPLOAD_SIZE_MB);
@@ -106,7 +107,7 @@ const [activeTab, setActiveTab] = useState("upload");
const response = await fetch(`/ui/${ingestGraphName}/uploads/list`, {
headers: { Authorization: `Basic ${creds}` },
});
- const data = await response.json();
+ const data = await safeJson(response);
setUploadedFiles(data.files || []);
} catch (error) {
console.error("Error fetching files:", error);
@@ -162,11 +163,11 @@ const [activeTab, setActiveTab] = useState("upload");
});
if (!response.ok) {
- const errorData = await response.json();
+ const errorData = await safeJson(response);
throw new Error(errorData.detail || `Upload failed: ${response.statusText}`);
}
- const data = await response.json();
+ const data = await safeJson(response);
if (data.status === "success") {
const uploadedCount = selectedFiles?.length || 0;
setUploadMessage("✅ Successfully uploaded the files. Processing...");
@@ -223,11 +224,11 @@ const [activeTab, setActiveTab] = useState("upload");
});
if (!response.ok) {
- const errorData = await response.json();
+ const errorData = await safeJson(response);
throw new Error(errorData.detail || `Upload failed with status ${response.status}`);
}
- const data = await response.json();
+ const data = await safeJson(response);
if (data.status === "success") {
uploadedCount++;
} else {
diff --git a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
index dc33689..b07c76f 100644
--- a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
+++ b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
@@ -34,6 +34,10 @@ const GraphRAGConfig = () => {
const [upsertDelay, setUpsertDelay] = useState("0");
const [maxConcurrency, setMaxConcurrency] = useState("10");
+ // Schema-aware initialization (Phase 1 sample-doc path)
+ const [schemaMaxSampleFiles, setSchemaMaxSampleFiles] = useState("5");
+ const [schemaMaxTotalMb, setSchemaMaxTotalMb] = useState("50");
+
// Chunker-specific settings
const [chunkSize, setChunkSize] = useState("");
const [overlapSize, setOverlapSize] = useState("");
@@ -75,6 +79,8 @@ const GraphRAGConfig = () => {
setLoadBatchSize(String(graphragConfig.load_batch_size ?? 500));
setUpsertDelay(String(graphragConfig.upsert_delay ?? 0));
setMaxConcurrency(String(graphragConfig.default_concurrency ?? 10));
+ setSchemaMaxSampleFiles(String(graphragConfig.schema_max_sample_files ?? 5));
+ setSchemaMaxTotalMb(String(graphragConfig.schema_max_total_mb ?? 50));
const chunkerConfig = graphragConfig.chunker_config || {};
setChunkSize(String(chunkerConfig.chunk_size ?? ""));
@@ -157,6 +163,8 @@ const GraphRAGConfig = () => {
load_batch_size: parseInt(loadBatchSize),
upsert_delay: parseInt(upsertDelay),
default_concurrency: parseInt(maxConcurrency),
+ schema_max_sample_files: parseInt(schemaMaxSampleFiles),
+ schema_max_total_mb: parseInt(schemaMaxTotalMb),
};
// Display defaults — used to avoid saving values the user never changed
@@ -173,6 +181,8 @@ const GraphRAGConfig = () => {
load_batch_size: 500,
upsert_delay: 0,
default_concurrency: 10,
+ schema_max_sample_files: 5,
+ schema_max_total_mb: 50,
};
// Determine which config to diff against based on scope
@@ -691,6 +701,54 @@ const GraphRAGConfig = () => {
)}
+ {/* Schema-aware initialization (Phase 1) */}
+
+
+ Schema Initialization
+
+
+ Limits for the Generate from sample documents path on
+ the Initialize Knowledge Graph dialog.
+
+
+
+
+
+ Max Sample Files
+
+
setSchemaMaxSampleFiles(e.target.value)}
+ />
+
+ Maximum number of sample documents per schema-extraction run
+
+
+
+
+
+ Max Total Size (MB)
+
+
setSchemaMaxTotalMb(e.target.value)}
+ />
+
+ Combined upload cap across all sample files (per-file cap is fixed at 10 MB)
+
+
+
+
+
{/* Service Endpoints (global only) */}
{configScope !== "graph" && (
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index 0579376..572c9fa 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -31,18 +31,32 @@ const KGAdmin = () => {
const [initializeDialogOpen, setInitializeDialogOpen] = useState(false);
const [refreshDialogOpen, setRefreshDialogOpen] = useState(false);
const [ingestDialogOpen, setIngestDialogOpen] = useState(false);
-
// Reset states when dialogs close
const handleInitializeDialogChange = (open: boolean) => {
if (!open && isConfirmDialogOpen) {
return;
}
+ // Closing the dialog (X, Esc, click-outside-prevented, or the
+ // Cancel button) intentionally PRESERVES state — schema source,
+ // typed graph name, picked sample files, the in-flight extract
+ // spinner, and any returned draft GSQL all stay so the user can
+ // reopen and pick up where they left off. State is only reset
+ // when the user clicks the success "Done" button below
+ // (handleInitializeReset).
setInitializeDialogOpen(open);
- if (!open) {
- setGraphName("");
- setStatusMessage("");
- setStatusType("");
- }
+ };
+
+ const handleInitializeReset = () => {
+ setGraphName("");
+ setStatusMessage("");
+ setStatusType("");
+ setSchemaSource("none");
+ setPasteGsql("");
+ setDraftProposal(null);
+ setSampleFiles([]);
+ setExtractedFingerprint(null);
+ setAttributesCollapsed(false);
+ setIsInitComplete(false);
};
const handleRefreshDialogChange = (open: boolean) => {
@@ -61,6 +75,113 @@ const KGAdmin = () => {
const [isInitializing, setIsInitializing] = useState(false);
const [statusMessage, setStatusMessage] = useState("");
const [statusType, setStatusType] = useState<"success" | "error" | "">("");
+ // True only after the full create-graph + initialize-graph round
+ // succeeds. The "Done" button gates on this — extraction success
+ // alone (statusType === "success" mid-flow) must NOT show Done,
+ // because the user still needs to click Initialize.
+ const [isInitComplete, setIsInitComplete] = useState(false);
+ // Schema-source state (Phase 1). 'none' = legacy auto-create path;
+ // 'gsql' = user pastes ADD VERTEX/EDGE statements (or `gsql ls`
+ // output); 'samples' = user uploads a few representative documents,
+ // the backend runs schema_extraction LLM, returns GSQL, and the
+ // textarea is populated for review/edit before /initialize_graph.
+ const [schemaSource, setSchemaSource] = useState<"none" | "gsql" | "samples">("none");
+ // Two distinct buffers — Paste GSQL is the user's verbatim text for
+ // the strict-syntax path; Generate-from-samples populates a
+ // structured proposal (vertices / edges / attributes) the UI edits
+ // in form mode.
+ const [pasteGsql, setPasteGsql] = useState("");
+ const [draftProposal, setDraftProposal] = useState<{
+ vertices: Array<{
+ name: string;
+ description: string;
+ attributes: Array<{ name: string; type: string }>;
+ }>;
+ edges: Array<{
+ name: string;
+ description: string;
+ pairs: Array<[string, string]>;
+ attributes: Array<{ name: string; type: string }>;
+ }>;
+ domain_label?: string;
+ } | null>(null);
+ const [sampleFiles, setSampleFiles] = useState([]);
+ const [maxSampleFiles, setMaxSampleFiles] = useState(5);
+ const [maxTotalMb, setMaxTotalMb] = useState(50);
+ const [isExtractingSchema, setIsExtractingSchema] = useState(false);
+ // Fingerprint of the file set used for the most recent successful
+ // extraction. Used to disable the *Extract draft schema* button
+ // when the same files are selected (no new work to do).
+ const [extractedFingerprint, setExtractedFingerprint] = useState(null);
+ // True when the form-mode editor's per-card attribute lists are
+ // hidden, for a cleaner overview of types.
+ const [attributesCollapsed, setAttributesCollapsed] = useState(false);
+
+ const fingerprintFiles = (files: File[]): string =>
+ files
+ .map((f) => `${f.name}:${f.size}:${f.lastModified}`)
+ .sort()
+ .join("|");
+
+ const sampleFingerprint = fingerprintFiles(sampleFiles);
+
+ const PRIMITIVE_TYPES = [
+ "STRING",
+ "INT",
+ "UINT",
+ "DOUBLE",
+ "FLOAT",
+ "BOOL",
+ "DATETIME",
+ ];
+
+ // Render the form-mode draft proposal back into ADD VERTEX / ADD
+ // DIRECTED EDGE GSQL for submission to /initialize_graph. Mirrors
+ // schema_proposal.emit_preview_gsql on the backend so a round-trip
+ // produces identical output.
+ const draftProposalToGsql = (
+ proposal: NonNullable
+ ): string => {
+ const lines: string[] = [];
+ if (proposal.domain_label) {
+ lines.push(`// Domain: ${proposal.domain_label}`);
+ lines.push("");
+ }
+ for (const v of proposal.vertices) {
+ if (!v.name.trim()) continue;
+ if (v.description) lines.push(`// ${v.description}`);
+ const attrs = v.attributes
+ .filter((a) => a.name.trim())
+ .map((a) => `${a.name} ${a.type}`)
+ .join(", ");
+ const attrPart = attrs ? `, ${attrs}` : "";
+ lines.push(
+ `ADD VERTEX ${v.name} (PRIMARY_ID id STRING${attrPart}) ` +
+ `WITH PRIMARY_ID_AS_ATTRIBUTE="true";`
+ );
+ lines.push("");
+ }
+ for (const e of proposal.edges) {
+ if (!e.name.trim() || e.pairs.length === 0) continue;
+ if (e.description) lines.push(`// ${e.description}`);
+ const pairs = e.pairs
+ .filter(([f, t]) => f.trim() && t.trim())
+ .map(([f, t]) => `FROM ${f}, TO ${t}`)
+ .join(" | ");
+ if (!pairs) continue;
+ const attrs = e.attributes
+ .filter((a) => a.name.trim())
+ .map((a) => `${a.name} ${a.type}`)
+ .join(", ");
+ const attrPart = attrs ? `, ${attrs}` : "";
+ lines.push(
+ `ADD DIRECTED EDGE ${e.name} (${pairs}${attrPart}) ` +
+ `WITH REVERSE_EDGE="reverse_${e.name}";`
+ );
+ lines.push("");
+ }
+ return lines.join("\n").trimEnd() + "\n";
+ };
// Refresh state
const [refreshGraphName, setRefreshGraphName] = useState("");
@@ -82,6 +203,166 @@ const KGAdmin = () => {
}
}, []);
+ // Pull schema-init caps from /ui/config when the Initialize dialog opens.
+ // Read-only here; the values are edited on the GraphRAG Config page.
+ useEffect(() => {
+ if (!initializeDialogOpen) return;
+ // If there's pending sample-flow state (extraction in flight or a
+ // returned draft), force the "Generate from sample documents"
+ // radio to be selected so the user immediately sees the spinner /
+ // form on reopen, instead of landing on the previously-selected
+ // option.
+ if (isExtractingSchema || draftProposal) {
+ setSchemaSource("samples");
+ }
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) return;
+ fetch(`/ui/config`, { headers: { Authorization: `Basic ${creds}` } })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ const cfg = data?.graphrag_config || {};
+ if (typeof cfg.schema_max_sample_files === "number")
+ setMaxSampleFiles(cfg.schema_max_sample_files);
+ if (typeof cfg.schema_max_total_mb === "number")
+ setMaxTotalMb(cfg.schema_max_total_mb);
+ })
+ .catch(() => {
+ /* fall back to defaults */
+ });
+ }, [initializeDialogOpen]);
+
+ const handleSampleFileSelect = (e: React.ChangeEvent) => {
+ const list = Array.from(e.target.files || []);
+ if (list.length > maxSampleFiles) {
+ setStatusMessage(`Too many files: pick at most ${maxSampleFiles}.`);
+ setStatusType("error");
+ e.target.value = "";
+ return;
+ }
+ const totalBytes = list.reduce((sum, f) => sum + f.size, 0);
+ if (totalBytes > maxTotalMb * 1024 * 1024) {
+ setStatusMessage(`Total size exceeds ${maxTotalMb} MB cap.`);
+ setStatusType("error");
+ e.target.value = "";
+ return;
+ }
+ const oversize = list.find((f) => f.size > 10 * 1024 * 1024);
+ if (oversize) {
+ setStatusMessage(`File ${oversize.name} exceeds the 10 MB per-file cap.`);
+ setStatusType("error");
+ e.target.value = "";
+ return;
+ }
+ setSampleFiles(list);
+ setStatusMessage("");
+ setStatusType("");
+ };
+
+ const handleExtractFromSamples = async () => {
+ if (!graphName.trim()) {
+ setStatusMessage("Enter a graph name before extracting a draft schema.");
+ setStatusType("error");
+ return;
+ }
+ if (sampleFiles.length === 0) {
+ setStatusMessage("Pick at least one sample document first.");
+ setStatusType("error");
+ return;
+ }
+ setIsExtractingSchema(true);
+ setStatusMessage(
+ `Step 1/2: Converting ${sampleFiles.length} uploaded file${sampleFiles.length === 1 ? "" : "s"} to text…`
+ );
+ setStatusType("");
+ try {
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) throw new Error("Not authenticated. Please login first.");
+
+ // Step 1/2: upload + convert. Returns the saved filenames so we
+ // know exactly which JSONLs to feed to the LLM in step 2.
+ const form = new FormData();
+ sampleFiles.forEach((f) => form.append("files", f));
+ const convertResp = await fetch(
+ `/ui/${graphName}/convert_sample_files`,
+ {
+ method: "POST",
+ headers: { Authorization: `Basic ${creds}` },
+ body: form,
+ }
+ );
+ const convertData = await convertResp.json();
+ if (!convertResp.ok) {
+ throw new Error(
+ convertData.detail || `Conversion failed: ${convertResp.statusText}`
+ );
+ }
+
+ // Step 2/2: LLM call. The status flip now reflects the real
+ // backend phase change, not a timer.
+ setStatusMessage("Step 2/2: Extracting schema with LLM…");
+ const resp = await fetch(
+ `/ui/${graphName}/extract_schema_from_jsonl`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Basic ${creds}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ filenames: convertData.saved_files || [] }),
+ }
+ );
+ const data = await resp.json();
+ if (!resp.ok) {
+ throw new Error(data.detail || `Extraction failed: ${resp.statusText}`);
+ }
+ const proposal = data.proposal;
+ if (
+ !proposal ||
+ ((proposal.vertices?.length ?? 0) === 0 &&
+ (proposal.edges?.length ?? 0) === 0)
+ ) {
+ throw new Error("LLM returned no schema. Try different sample files.");
+ }
+ // Normalize so every record has the optional fields the form
+ // editor expects (defensive — backend always sets them today).
+ setDraftProposal({
+ domain_label: proposal.domain_label,
+ vertices: (proposal.vertices || []).map((v: any) => ({
+ name: v.name || "",
+ description: v.description || "",
+ attributes: (v.attributes || []).map((a: any) => ({
+ name: a.name || "",
+ type: a.type || "STRING",
+ })),
+ })),
+ edges: (proposal.edges || []).map((e: any) => ({
+ name: e.name || "",
+ description: e.description || "",
+ pairs: (e.pairs || []).map((p: any) => [
+ p?.[0] || "",
+ p?.[1] || "",
+ ]) as Array<[string, string]>,
+ attributes: (e.attributes || []).map((a: any) => ({
+ name: a.name || "",
+ type: a.type || "STRING",
+ })),
+ })),
+ });
+ setExtractedFingerprint(fingerprintFiles(sampleFiles));
+ setStatusMessage(
+ `Draft schema ready (${data.summary?.vertex_count ?? "?"} vertex types, ` +
+ `${data.summary?.edge_count ?? "?"} edge types). Review/edit below, then click Initialize.`
+ );
+ setStatusType("success");
+ } catch (error: any) {
+ console.error("Schema extraction error:", error);
+ setStatusMessage(`❌ ${error.message}`);
+ setStatusType("error");
+ } finally {
+ setIsExtractingSchema(false);
+ }
+ };
+
// Initialize Graph
const handleInitializeGraph = async () => {
if (!graphName.trim()) {
@@ -143,9 +424,20 @@ const KGAdmin = () => {
}
setStatusMessage("Step 2/2: Initializing GraphRAG schema...");
+ const initBody: { schema_gsql?: string } = {};
+ if (schemaSource === "gsql" && pasteGsql.trim()) {
+ initBody.schema_gsql = pasteGsql;
+ } else if (schemaSource === "samples" && draftProposal) {
+ const gsql = draftProposalToGsql(draftProposal).trim();
+ if (gsql) initBody.schema_gsql = gsql;
+ }
const initResponse = await fetch(`/ui/${graphName}/initialize_graph`, {
method: "POST",
- headers: { Authorization: `Basic ${creds}` },
+ headers: {
+ Authorization: `Basic ${creds}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(initBody),
});
const initData = await initResponse.json();
@@ -165,10 +457,19 @@ const KGAdmin = () => {
return;
}
+ const domain = initData.domain_schema_status;
+ let domainNote = "";
+ if (domain && domain.status === "applied") {
+ const stmts = domain.statements?.length ?? 0;
+ domainNote = ` Domain schema applied (${stmts} statement${stmts === 1 ? "" : "s"}).`;
+ } else if (domain && domain.status === "no-op") {
+ domainNote = " Domain schema already up-to-date.";
+ }
setStatusMessage(
- `✅ Graph "${graphName}" created and initialized successfully! You can now close this dialog.`
+ `✅ Graph "${graphName}" created and initialized successfully!${domainNote} You can now close this dialog.`
);
setStatusType("success");
+ setIsInitComplete(true);
const newGraph = graphName;
setAvailableGraphs(prev => {
@@ -374,7 +675,7 @@ const KGAdmin = () => {
{/* Card Grid */}
-
+
{/* Initialize Card */}
@@ -446,12 +747,13 @@ const KGAdmin = () => {
+
{/* Initialize Dialog */}
e.preventDefault()}
>
@@ -470,16 +772,753 @@ const KGAdmin = () => {
placeholder="e.g., MyKnowledgeGraph"
value={graphName}
onChange={(e) => setGraphName(e.target.value)}
- disabled={isInitializing}
+ disabled={isInitializing || isExtractingSchema}
className="dark:border-[#3D3D3D] dark:bg-shadeA"
onKeyDown={(e) => {
- if (e.key === "Enter" && !isInitializing) {
+ if (e.key === "Enter" && !isInitializing && !isExtractingSchema) {
handleInitializeGraph();
}
}}
/>
+
+
+ Domain schema (optional)
+
+
+
+ setSchemaSource("none")}
+ // Only disable when init or extraction is in
+ // flight AND this is NOT the currently-selected
+ // option — that way the active radio keeps its
+ // full "selected" styling so the user can clearly
+ // see which source is running.
+ disabled={
+ (isInitializing || isExtractingSchema) &&
+ schemaSource !== "none"
+ }
+ />
+ None — only create the GraphRAG structural schema
+
+
+ setSchemaSource("samples")}
+ disabled={
+ (isInitializing || isExtractingSchema) &&
+ schemaSource !== "samples"
+ }
+ />
+ Generate from sample documents
+
+
+ setSchemaSource("gsql")}
+ disabled={
+ (isInitializing || isExtractingSchema) &&
+ schemaSource !== "gsql"
+ }
+ />
+ Paste GSQL schema
+
+
+
+ {schemaSource === "samples" && (
+
+
+
+ Up to {maxSampleFiles} files, ≤10 MB each, ≤{maxTotalMb} MB total.
+ Selected: {sampleFiles.length}
+ {sampleFiles.length > 0 &&
+ ` (${(sampleFiles.reduce((s, f) => s + f.size, 0) / (1024 * 1024)).toFixed(1)} MB)`}
+
+
+ {isExtractingSchema ? (
+ <>
+
+ Extracting…
+ >
+ ) : (
+ <>Extract draft schema>
+ )}
+
+
+ {draftProposal && (
+
+
+
+ Review and edit the draft below. Each vertex auto-gets a primary
+ key id (STRING) — you don't need to add it. Click
+ Initialize when ready.
+
+
setAttributesCollapsed((c) => !c)}
+ disabled={isInitializing || isExtractingSchema}
+ className="text-xs text-blue-600 hover:underline disabled:opacity-50 whitespace-nowrap"
+ >
+ {attributesCollapsed ? "Expand attributes" : "Collapse attributes"}
+
+
+
+ {/* Vertex types */}
+
+
+
+ Vertex types ({draftProposal.vertices.length})
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: [
+ ...p.vertices,
+ { name: "", description: "", attributes: [] },
+ ],
+ }
+ : p
+ )
+ }
+ className="text-xs h-7 dark:border-[#3D3D3D]"
+ >
+ + Add vertex
+
+
+
+ {draftProposal.vertices.map((v, vIdx) => (
+
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx ? { ...vv, name: e.target.value } : vv
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.filter((_, i) => i !== vIdx),
+ }
+ : p
+ )
+ }
+ className="text-xs text-red-600 hover:underline disabled:opacity-50"
+ >
+ Remove
+
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx
+ ? { ...vv, description: e.target.value }
+ : vv
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ Attributes ({v.attributes.length}); primary key id auto-added
+ {attributesCollapsed && (
+ — collapsed
+ )}
+
+ {!attributesCollapsed && v.attributes.map((a, aIdx) => (
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx
+ ? {
+ ...vv,
+ attributes: vv.attributes.map(
+ (aa, j) =>
+ j === aIdx
+ ? {
+ ...aa,
+ // Auto-replace whitespace
+ // with underscores so the
+ // displayed name always
+ // matches the GSQL
+ // identifier that will be
+ // emitted (whitespace is
+ // not a valid char in
+ // GSQL idents).
+ name: e.target.value.replace(
+ /\s+/g,
+ "_"
+ ),
+ }
+ : aa
+ ),
+ }
+ : vv
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-7 text-xs font-mono dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx
+ ? {
+ ...vv,
+ attributes: vv.attributes.map(
+ (aa, j) =>
+ j === aIdx
+ ? { ...aa, type: e.target.value }
+ : aa
+ ),
+ }
+ : vv
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="h-7 text-xs border rounded px-1 dark:border-[#3D3D3D] dark:bg-shadeA"
+ >
+ {PRIMITIVE_TYPES.map((t) => (
+
+ {t}
+
+ ))}
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx
+ ? {
+ ...vv,
+ attributes: vv.attributes.filter(
+ (_, j) => j !== aIdx
+ ),
+ }
+ : vv
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-red-600 hover:underline disabled:opacity-50"
+ >
+ ✕
+
+
+ ))}
+ {!attributesCollapsed && (
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: p.vertices.map((vv, i) =>
+ i === vIdx
+ ? {
+ ...vv,
+ attributes: [
+ ...vv.attributes,
+ { name: "", type: "STRING" },
+ ],
+ }
+ : vv
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-blue-600 hover:underline disabled:opacity-50"
+ >
+ + Add attribute
+
+ )}
+
+ ))}
+
+
+
+ {/* Edge types */}
+
+
+
+ Edge types ({draftProposal.edges.length})
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: [
+ ...p.edges,
+ {
+ name: "",
+ description: "",
+ pairs: [["", ""]],
+ attributes: [],
+ },
+ ],
+ }
+ : p
+ )
+ }
+ className="text-xs h-7 dark:border-[#3D3D3D]"
+ >
+ + Add edge
+
+
+
+ {draftProposal.edges.map((e, eIdx) => (
+
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? { ...ee, name: ev.target.value }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.filter((_, i) => i !== eIdx),
+ }
+ : p
+ )
+ }
+ className="text-xs text-red-600 hover:underline disabled:opacity-50"
+ >
+ Remove
+
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? { ...ee, description: ev.target.value }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ Endpoints (FROM → TO):
+
+ {e.pairs.map((pair, pIdx) => (
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ pairs: ee.pairs.map((pr, j) =>
+ j === pIdx
+ ? [ev.target.value, pr[1]]
+ : pr
+ ) as Array<[string, string]>,
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-7 text-xs dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+ →
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ pairs: ee.pairs.map((pr, j) =>
+ j === pIdx
+ ? [pr[0], ev.target.value]
+ : pr
+ ) as Array<[string, string]>,
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-7 text-xs dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ pairs: ee.pairs.filter(
+ (_, j) => j !== pIdx
+ ),
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-red-600 hover:underline disabled:opacity-50"
+ >
+ ✕
+
+
+ ))}
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ pairs: [...ee.pairs, ["", ""]] as Array<
+ [string, string]
+ >,
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-blue-600 hover:underline disabled:opacity-50"
+ >
+ + Add pair
+
+
+ Attributes ({e.attributes.length}, optional)
+ {attributesCollapsed && (
+ — collapsed
+ )}
+
+ {!attributesCollapsed && e.attributes.map((a, aIdx) => (
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ attributes: ee.attributes.map(
+ (aa, j) =>
+ j === aIdx
+ ? {
+ ...aa,
+ // Auto-replace whitespace
+ // with underscores —
+ // GSQL idents can't have
+ // spaces, and rendering
+ // them as `_` makes the
+ // visual unambiguous.
+ name: ev.target.value.replace(
+ /\s+/g,
+ "_"
+ ),
+ }
+ : aa
+ ),
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 h-7 text-xs font-mono dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ attributes: ee.attributes.map(
+ (aa, j) =>
+ j === aIdx
+ ? { ...aa, type: ev.target.value }
+ : aa
+ ),
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ disabled={isInitializing || isExtractingSchema}
+ className="h-7 text-xs border rounded px-1 dark:border-[#3D3D3D] dark:bg-shadeA"
+ >
+ {PRIMITIVE_TYPES.map((t) => (
+
+ {t}
+
+ ))}
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ attributes: ee.attributes.filter(
+ (_, j) => j !== aIdx
+ ),
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-red-600 hover:underline disabled:opacity-50"
+ >
+ ✕
+
+
+ ))}
+ {!attributesCollapsed && (
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: p.edges.map((ee, i) =>
+ i === eIdx
+ ? {
+ ...ee,
+ attributes: [
+ ...ee.attributes,
+ { name: "", type: "STRING" },
+ ],
+ }
+ : ee
+ ),
+ }
+ : p
+ )
+ }
+ className="text-xs text-blue-600 hover:underline disabled:opacity-50"
+ >
+ + Add attribute
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+ {schemaSource === "gsql" && (
+
+
+ Paste TigerGraph GSQL ADD VERTEX /
+ ADD [UN]DIRECTED EDGE statements (or output of
+ gsql ls). If you don't include a
+ PRIMARY_ID, the system auto-adds
+ PRIMARY_ID id STRING. Lines that don't match
+ VERTEX / EDGE patterns are silently ignored.
+
+
+ )}
+
+
{statusMessage && (
{
- {statusType === "success" ? (
+ {isInitComplete ? (
{
+ handleInitializeReset();
setInitializeDialogOpen(false);
- setGraphName("");
- setStatusMessage("");
- setStatusType("");
}}
>
Done
@@ -520,7 +1557,21 @@ const KGAdmin = () => {
{isInitializing ? (
diff --git a/graphrag-ui/src/utils/safeJson.ts b/graphrag-ui/src/utils/safeJson.ts
new file mode 100644
index 0000000..79904b1
--- /dev/null
+++ b/graphrag-ui/src/utils/safeJson.ts
@@ -0,0 +1,18 @@
+/**
+ * Safely parse a fetch Response as JSON.
+ * If the body is not valid JSON (e.g. an HTML error page from nginx),
+ * returns a synthetic error object so callers never crash on `.json()`.
+ */
+export async function safeJson(response: Response): Promise {
+ const text = await response.text();
+ try {
+ return JSON.parse(text);
+ } catch {
+ return {
+ error: true,
+ detail: response.ok
+ ? "Received an invalid response from the server."
+ : `Server error (HTTP ${response.status}): ${response.statusText}`,
+ };
+ }
+}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 49b8552..2235890 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -8,7 +8,7 @@
from fastapi import WebSocket
from tools import GenerateCypher, GenerateFunction, MapQuestionToSchema
-from common.config import embedding_service, embedding_store, llm_config, get_completion_config, get_chat_config, get_llm_service
+from common.config import get_embedding_service, get_embedding_store, llm_config, get_completion_config, get_chat_config, get_llm_service
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.embedding_services import EmbeddingModel
from common.llm_services.base_llm import LLM_Model
@@ -165,8 +165,8 @@ def make_agent(graphname, conn, use_cypher, ws: WebSocket = None, supportai_retr
agent = TigerGraphAgent(
llm_provider,
conn,
- embedding_service,
- embedding_store,
+ get_embedding_service(),
+ get_embedding_store(),
use_cypher=use_cypher,
ws=ws,
supportai_retriever=supportai_retriever
diff --git a/graphrag/app/routers/inquiryai.py b/graphrag/app/routers/inquiryai.py
index 5cecf88..e21564e 100644
--- a/graphrag/app/routers/inquiryai.py
+++ b/graphrag/app/routers/inquiryai.py
@@ -10,7 +10,7 @@
from fastapi.security.http import HTTPBase
from tools.validation_utils import MapQuestionToSchemaException
-from common.config import embedding_service, embedding_store, session_handler, service_status
+from common.config import get_embedding_service, get_embedding_store, session_handler, service_status
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.metrics.prometheus_metrics import metrics as pmetrics
@@ -26,11 +26,14 @@
def check_embedding_store_status():
- if service_status["embedding_store"]["error"]:
- return HTTPException(
- status_code=503,
- detail=service_status["embedding_store"]["error"]
- )
+ """Validate embedding store is ready, raising 503 if not.
+
+ Also returns the store instance so callers can use it directly.
+ """
+ try:
+ return get_embedding_store(timeout=0)
+ except RuntimeError as e:
+ raise HTTPException(status_code=503, detail=str(e))
@router.post("/{graphname}/query")
@@ -59,7 +62,7 @@ def retrieve_answer(
try:
resp = agent.question_for_agent(query.query)
# Note: tg:// protocol conversion happens in agent_graph.py
- pmetrics.llm_success_response_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_success_response_total.labels(get_embedding_service().model_name).inc()
except MapQuestionToSchemaException:
resp.natural_language_response = (
"A schema mapping error occurred. Please try rephrasing your question."
@@ -69,7 +72,7 @@ def retrieve_answer(
LogWriter.warning(
f"/{graphname}/query request_id={req_id_cv.get()} agent execution failed due to MapQuestionToSchemaException"
)
- pmetrics.llm_query_error_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_query_error_total.labels(get_embedding_service().model_name).inc()
exc = traceback.format_exc()
logger.debug_pii(
f"/{graphname}/query request_id={req_id_cv.get()} Exception Trace:\n{exc}"
@@ -89,7 +92,7 @@ def retrieve_answer(
logger.debug_pii(
f"/{graphname}/query request_id={req_id_cv.get()} Exception Trace:\n{exc}"
)
- pmetrics.llm_query_error_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_query_error_total.labels(get_embedding_service().model_name).inc()
return resp
@@ -148,7 +151,7 @@ def retrieve_answer_with_chathistory(
resp.natural_language_response
)
- pmetrics.llm_success_response_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_success_response_total.labels(get_embedding_service().model_name).inc()
conversation_history.append(
{"query": query.query, "response": resp.natural_language_response}
@@ -163,7 +166,7 @@ def retrieve_answer_with_chathistory(
LogWriter.warning(
f"/{graphname}/query_with_history request_id={req_id_cv.get()} agent execution failed due to MapQuestionToSchemaException"
)
- pmetrics.llm_query_error_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_query_error_total.labels(get_embedding_service().model_name).inc()
exc = traceback.format_exc()
logger.debug_pii(
f"/{graphname}/query_with_history request_id={req_id_cv.get()} Exception Trace:\n{exc}"
@@ -184,7 +187,7 @@ def retrieve_answer_with_chathistory(
logger.debug_pii(
f"/{graphname}/query_with_history request_id={req_id_cv.get()} Exception Trace:\n{exc}"
)
- pmetrics.llm_query_error_total.labels(embedding_service.model_name).inc()
+ pmetrics.llm_query_error_total.labels(get_embedding_service().model_name).inc()
return resp
@@ -196,13 +199,13 @@ def list_registered_queries(
check_embedding_store_status()
conn = conn.state.conn
if conn.getVer().split(".")[0] <= "3":
- query_descs = embedding_store.list_registered_documents(
+ query_descs = get_embedding_store().list_registered_documents(
graphname=graphname,
only_custom=True,
output_fields=["function_header", "text"],
)
else:
- queries = embedding_store.list_registered_documents(
+ queries = get_embedding_store().list_registered_documents(
graphname=graphname, only_custom=True, output_fields=["function_header"]
)
if not queries:
@@ -218,7 +221,7 @@ def get_query_embedding(graphname, query: NaturalLanguageQuery):
f"/{graphname}/getqueryembedding request_id={req_id_cv.get()} question={query.query}"
)
- return embedding_service.embed_query(query.query)
+ return get_embedding_service().embed_query(query.query)
@router.post("/{graphname}/register_docs")
@@ -235,7 +238,7 @@ def register_docs(
conn.echo()
except Exception as e:
raise HTTPException(status_code=401, detail="Invalid credentials")
- logger.debug(f"Using embedding store: {embedding_store}")
+ logger.debug(f"Using embedding store: {get_embedding_store()}")
results = []
if not isinstance(query_list, list):
@@ -246,9 +249,9 @@ def register_docs(
f"/{graphname}/register_docs request_id={req_id_cv.get()} registering {query_info.function_header}"
)
- vec = embedding_service.embed_query(query_info.docstring)
+ vec = get_embedding_service().embed_query(query_info.docstring)
param_types = conn.getQueryMetadata(query_info.function_header)["input"]
- res = embedding_store.add_embeddings(
+ res = get_embedding_store().add_embeddings(
[(query_info.docstring +
".\nRun with runInstalledQuery('" +
query_info.function_header +
@@ -384,7 +387,7 @@ def upsert_docs(
try:
# expr = f"function_header in ['{query_info.function_header}']"
expr = f"function_header == '{query_info.function_header}'"
- id = embedding_store.get_pks(expr)
+ id = get_embedding_store().get_pks(expr)
if id:
id = str(id[0])
logger.info(
@@ -405,8 +408,8 @@ def upsert_docs(
f"/{graphname}/upsert_docs request_id={req_id_cv.get()} upserting document(s)"
)
param_types = conn.getQueryMetadata(query_info.function_header)["input"]
- vec = embedding_service.embed_query(query_info.docstring)
- res = embedding_store.upsert_embeddings(
+ vec = get_embedding_service().embed_query(query_info.docstring)
+ res = get_embedding_store().upsert_embeddings(
id,
[(query_info.docstring +
".\nRun with runInstalledQuery('" +
@@ -468,10 +471,10 @@ def delete_docs(
# Call the remove_embeddings method based on provided IDs or expression
try:
if expr:
- res = embedding_store.remove_embeddings(expr=expr)
+ res = get_embedding_store().remove_embeddings(expr=expr)
return res
elif ids:
- res = embedding_store.remove_embeddings(ids=ids)
+ res = get_embedding_store().remove_embeddings(ids=ids)
return res
else:
raise HTTPException(
@@ -492,8 +495,8 @@ def retrieve_docs(
f"/{graphname}/retrieve_docs request_id={req_id_cv.get()} top_k={top_k} question={query.query}"
)
check_embedding_store_status()
- return embedding_store.retrieve_similar(
- embedding_service.embed_query(query.query), top_k=top_k
+ return get_embedding_store().retrieve_similar(
+ get_embedding_service().embed_query(query.query), top_k=top_k
)
diff --git a/graphrag/app/routers/supportai.py b/graphrag/app/routers/supportai.py
index dac8db8..97266d9 100644
--- a/graphrag/app/routers/supportai.py
+++ b/graphrag/app/routers/supportai.py
@@ -30,8 +30,8 @@
from common.config import (
db_config,
graphrag_config,
- embedding_service,
- embedding_store,
+ get_embedding_service,
+ get_embedding_store,
get_chat_config,
get_llm_service,
service_status,
@@ -52,10 +52,15 @@
def check_embedding_store_status():
- if service_status["embedding_store"]["error"]:
- return HTTPException(
- status_code=503, detail=service_status["embedding_store"]["error"]
- )
+ """Return the embedding store if ready, else raise 503.
+
+ Replaces the old behavior that returned (rather than raised) an
+ HTTPException, leaving callers thinking the check succeeded.
+ """
+ try:
+ return get_embedding_store(timeout=0)
+ except RuntimeError as e:
+ raise HTTPException(status_code=503, detail=str(e))
@router.post("/{graphname}/graphrag/initialize")
@@ -119,7 +124,7 @@ def search(
query.method_params["verbose"] = False
if query.method.lower() == "hybrid":
retriever = HybridRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
if "method" not in query.method_params:
query.method_params["method"] = "similarity"
@@ -146,7 +151,7 @@ def search(
if "index" not in query.method_params:
raise Exception("Index name not provided")
retriever = SimilarityRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.search(
query.question,
@@ -160,7 +165,7 @@ def search(
if "index" not in query.method_params:
raise Exception("Index name not provided")
retriever = SiblingRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.search(
query.question,
@@ -174,12 +179,12 @@ def search(
)
elif query.method.lower() == "entityrelationship":
retriever = EntityRelationshipRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.search(query.question, query.method_params["top_k"])
elif query.method.lower() == "community":
retriever = CommunityRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
if "with_chunk" not in query.method_params:
query.method_params["with_chunk"] = True
@@ -222,7 +227,7 @@ def answer_question(
query.method_params["verbose"] = False
if query.method.lower() == "hybrid":
retriever = HybridRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
if "method" not in query.method_params:
query.method_params["method"] = "Similarity"
@@ -250,7 +255,7 @@ def answer_question(
if "index" not in query.method_params:
raise Exception("Index name not provided")
retriever = SimilarityRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.retrieve_answer(
query.question,
@@ -265,7 +270,7 @@ def answer_question(
if "index" not in query.method_params:
raise Exception("Index name not provided")
retriever = SiblingRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.retrieve_answer(
query.question,
@@ -280,13 +285,13 @@ def answer_question(
)
elif query.method.lower() == "entityrelationship":
retriever = EntityRelationshipRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
res = retriever.retrieve_answer(query.question, query.method_params["top_k"])
elif query.method.lower() == "community":
retriever = CommunityRetriever(
- embedding_service, embedding_store, get_llm_service(get_chat_config(graphname)), conn
+ get_embedding_service(), get_embedding_store(), get_llm_service(get_chat_config(graphname)), conn
)
if "with_chunk" not in query.method_params:
query.method_params["with_chunk"] = True
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 400435d..7a89719 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -53,6 +53,9 @@
from common.config import db_config, graphrag_config, embedding_service, llm_config, service_status, get_chat_config, get_completion_config, get_embedding_config, get_multimodal_config, validate_graphname, get_llm_service, resolve_llm_services
from common.db.connections import get_db_connection_pwd_manual
+from common.db import schema_utils as schema_utils_mod
+from common.db import schema_extraction as schema_extraction_mod
+from common.utils.text_extractors import TextExtractor
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.metrics.prometheus_metrics import metrics as pmetrics
@@ -387,11 +390,24 @@ def create_graph(
def init_graph(
graphname: ValidGraphName,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+ payload: Annotated[dict | None, Body()] = None,
):
"""
Initialize a TigerGraph knowledge graph with GraphRAG schema.
- This initializes the graph with SupportAI/GraphRAG schema, indexes, and queries.
- Uses HTTP Basic Authentication to get credentials and create a connection.
+
+ The structural GraphRAG schema (Document, DocumentChunk, Entity,
+ EntityType, RelationshipType, Content, Community, Image and their
+ structural edges) is always created if missing.
+
+ Optionally accepts a JSON body with a domain-schema proposal:
+
+ {"schema_gsql": "ADD VERTEX Company(...); ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);"}
+
+ When ``schema_gsql`` is provided, the pasted text is parsed
+ permissively (``ADD`` form *or* ``gsql ls`` output), structural-type
+ collisions and dangling pairs are silently dropped, the diff against
+ the current graph is computed, and the additive delta is applied as a
+ single atomic ``SCHEMA_CHANGE JOB``. Existing types are never dropped.
"""
try:
# Extract credentials from the dependency (same pattern as other endpoints)
@@ -404,9 +420,41 @@ def init_graph(
resp = supportai.init_supportai(conn, graphname)
schema_res, index_res, query_res = resp[0], resp[1], resp[2]
+ domain_schema_status: dict | None = None
+ schema_gsql = (payload or {}).get("schema_gsql") if isinstance(payload, dict) else None
+ if isinstance(schema_gsql, str) and schema_gsql.strip():
+ LogWriter.info(f"Applying domain schema proposal for graph: {graphname}")
+ proposal = schema_utils_mod.parse_gsql_schema(schema_gsql)
+ proposal.drop_dangling_pairs()
+ domain_schema_status = schema_utils_mod.apply_proposal(
+ conn, graphname, proposal
+ )
+ LogWriter.info(
+ f"Domain schema status for {graphname}: "
+ f"{domain_schema_status['status']} "
+ f"({len(domain_schema_status['statements'])} stmts)"
+ )
+ # apply_proposal returns status=error when the gsql output
+ # contains a known failure marker. Surface it as a 5xx so
+ # the caller doesn't falsely think the schema landed.
+ if domain_schema_status.get("status") == "error":
+ LogWriter.error(
+ f"Domain schema apply failed for {graphname}: "
+ f"{domain_schema_status.get('error')}"
+ )
+ raise HTTPException(
+ status_code=500,
+ detail={
+ "message": "Domain schema apply failed",
+ "error": domain_schema_status.get("error"),
+ "gsql_output": domain_schema_status.get("gsql_output", "")[:1000],
+ "statements": domain_schema_status.get("statements", []),
+ },
+ )
+
LogWriter.info(f"Graph initialization completed for: {graphname}")
- return {
+ result = {
"status": "success",
"message": f"Graph '{graphname}' initialized successfully",
"graphname": graphname,
@@ -415,6 +463,9 @@ def init_graph(
"index_creation_status": json.dumps(index_res),
"query_creation_status": json.dumps(query_res),
}
+ if domain_schema_status is not None:
+ result["domain_schema_status"] = domain_schema_status
+ return result
except Exception as e:
LogWriter.error(f"Error initializing graph {graphname}: {str(e)}")
@@ -425,6 +476,179 @@ def init_graph(
}
+@router.post(route_prefix + "/{graphname}/convert_sample_files")
+async def convert_sample_files(
+ graphname: ValidGraphName,
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+ files: Annotated[list[UploadFile], File(description="Sample documents (≤5)")],
+):
+ """
+ Step 1/2 of the sample-doc schema extraction flow:
+
+ Save uploaded sample files to ``uploads//`` and convert
+ each to JSONL under ``uploads/ingestion_temp//``. Files
+ are persisted so the Ingest Document dialog can reuse them, and
+ the JSONL cache means a subsequent Ingest run won't re-convert.
+
+ Returns the list of saved filenames so the caller can pass them
+ to ``POST /ui//extract_schema_from_jsonl``.
+
+ No LLM call. Caps come from ``graphrag_config``:
+ * ``schema_max_sample_files`` (default 5)
+ * ``schema_max_total_mb`` (default 50)
+ """
+ max_files = int(graphrag_config.get("schema_max_sample_files", 5))
+ max_total_mb = int(graphrag_config.get("schema_max_total_mb", 50))
+ max_total_bytes = max_total_mb * 1024 * 1024
+ per_file_max_bytes = 10 * 1024 * 1024 # 10 MB per file (Phase 1 cap)
+
+ if len(files) > max_files:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Too many files: got {len(files)}, max is {max_files}.",
+ )
+ if not files:
+ raise HTTPException(status_code=400, detail="No files supplied.")
+
+ upload_dir = os.path.join("uploads", graphname)
+ os.makedirs(upload_dir, exist_ok=True)
+ temp_folder = os.path.join("uploads", "ingestion_temp", graphname)
+ os.makedirs(temp_folder, exist_ok=True)
+
+ saved_basenames: list[str] = []
+ total_bytes = 0
+ for f in files:
+ data = await f.read()
+ if len(data) > per_file_max_bytes:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"File {f.filename} exceeds the 10 MB per-file cap."
+ ),
+ )
+ total_bytes += len(data)
+ if total_bytes > max_total_bytes:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"Total upload exceeds {max_total_mb} MB cap."
+ ),
+ )
+ safe_name = os.path.basename(f.filename or "sample")
+ target = os.path.join(upload_dir, safe_name)
+ with open(target, "wb") as out:
+ out.write(data)
+ saved_basenames.append(safe_name)
+
+ extractor = TextExtractor()
+ try:
+ result = await extractor._process_folder_async(
+ upload_dir, graphname, temp_folder
+ )
+ except Exception as exc:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Text extraction failed: {exc}",
+ )
+
+ LogWriter.info(
+ f"Converted sample files for {graphname}: {len(files)} uploaded, "
+ f"{result.get('num_documents', 0)} docs in JSONL"
+ )
+ return {
+ "status": "success",
+ "graphname": graphname,
+ "saved_files": list(saved_basenames),
+ "num_documents": result.get("num_documents", 0),
+ }
+
+
+@router.post(route_prefix + "/{graphname}/extract_schema_from_jsonl")
+def extract_schema_from_jsonl(
+ graphname: ValidGraphName,
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+ payload: Annotated[dict | None, Body()] = None,
+):
+ """
+ Step 2/2 of the sample-doc schema extraction flow:
+
+ Read the previously-converted JSONLs (from ``convert_sample_files``)
+ and run the schema-extraction LLM over them. Returns the proposed
+ domain schema as GSQL plus a structured proposal dict for the
+ form-mode editor.
+
+ Body:
+ ``{"filenames": ["report1.pdf", "report2.docx"]}``
+ The endpoint reads ``uploads/ingestion_temp//.jsonl``
+ for each name. If ``filenames`` is absent or empty, every JSONL in
+ the temp folder is consumed.
+ """
+ temp_folder = os.path.join("uploads", "ingestion_temp", graphname)
+ if not os.path.isdir(temp_folder):
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"No converted JSONLs found for graph {graphname}. "
+ "Run convert_sample_files first."
+ ),
+ )
+
+ requested = []
+ if isinstance(payload, dict):
+ requested = payload.get("filenames") or []
+
+ if requested:
+ jsonl_paths = []
+ for name in requested:
+ stem = os.path.splitext(os.path.basename(name))[0]
+ p = os.path.join(temp_folder, f"{stem}.jsonl")
+ if os.path.exists(p):
+ jsonl_paths.append(p)
+ else:
+ jsonl_paths = [
+ os.path.join(temp_folder, fn)
+ for fn in os.listdir(temp_folder)
+ if fn.endswith(".jsonl")
+ ]
+
+ samples: list[dict] = []
+ for jp in jsonl_paths:
+ with open(jp, "r", encoding="utf-8") as jf:
+ for line in jf:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ samples.append(json.loads(line))
+ except json.JSONDecodeError:
+ pass
+
+ if not samples:
+ raise HTTPException(
+ status_code=400,
+ detail="No extractable text in the converted files.",
+ )
+
+ LogWriter.info(
+ f"Running schema extraction LLM for {graphname} "
+ f"({len(jsonl_paths)} JSONLs, {len(samples)} doc parts)"
+ )
+ llm_service = get_llm_service(get_completion_config(graphname))
+ gsql_text = schema_extraction_mod.extract_schema_gsql(
+ llm_service, samples
+ )
+ proposal = schema_utils_mod.parse_gsql_schema(gsql_text)
+ proposal.drop_dangling_pairs()
+ return {
+ "status": "success",
+ "graphname": graphname,
+ "schema_gsql": gsql_text,
+ "preview_gsql": schema_utils_mod.emit_preview_gsql(proposal),
+ "proposal": proposal.to_dict(),
+ "summary": schema_utils_mod.summarize(proposal),
+ }
+
+
@router.post(route_prefix + "/{graphname}/rebuild_graph")
async def forceupdate(
graphname: ValidGraphName,
@@ -2156,6 +2380,28 @@ def _prepare_llm_config(llm_config_data: dict):
if isinstance(svc, dict) and svc.get("region_name") == top_region:
del svc["region_name"]
+ # Normalize prompt_path: promote from completion_service to top
+ # level, strip per-service copies if identical (same pattern as
+ # auth / region). The UI doesn't expose per-service prompt_paths;
+ # in practice all services share the completion value.
+ if "prompt_path" not in llm_config_data:
+ completion_svc = llm_config_data.get("completion_service")
+ if isinstance(completion_svc, dict) and "prompt_path" in completion_svc:
+ llm_config_data["prompt_path"] = completion_svc["prompt_path"]
+
+ top_prompt_path = llm_config_data.get("prompt_path")
+ if top_prompt_path:
+ # Embedding excluded — embedding services never use prompt_path.
+ for svc_key in ["completion_service", "multimodal_service", "chat_service"]:
+ svc = llm_config_data.get(svc_key)
+ if isinstance(svc, dict) and svc.get("prompt_path") == top_prompt_path:
+ del svc["prompt_path"]
+ # If embedding_service somehow has a prompt_path on disk, strip
+ # it — it's never read.
+ emb = llm_config_data.get("embedding_service")
+ if isinstance(emb, dict) and "prompt_path" in emb:
+ del emb["prompt_path"]
+
return llm_config_data, graphname, scope
@@ -2504,35 +2750,57 @@ async def save_graphrag_config(
raise HTTPException(status_code=500, detail=f"Failed to save GraphRAG config: {str(e)}")
+#: Per-prompt-type list of regex patterns that mark the start of the
+#: placeholder-variables block. The first matching pattern wins.
+#: Patterns are tried in order so the canonical Markdown headers
+#: (``## Inputs`` / ``## Data``) match first; legacy patterns are
+#: kept as fallbacks for any older saved files.
+_TEMPLATE_VAR_MARKERS = {
+ "chatbot_response": [
+ r'(?ms)^##\s*Inputs\b.*$',
+ r'(?ms)^Question:\s*\{question\}.*$',
+ ],
+ "entity_relationship": [
+ # No placeholders in the entity-relationship system prompt.
+ # The whole content is editable.
+ ],
+ "community_summarization": [
+ r'(?ms)^##\s*Data\b.*$',
+ r'(?ms)^##\s*Inputs\b.*$',
+ r'(?ms)^#######\s*-Data-.*$',
+ ],
+ "query_generation": [
+ r'(?ms)^##\s*Inputs\b.*$',
+ r'(?ms)^\{format_instructions\}.*$',
+ ],
+ "schema_extraction": [
+ r'(?ms)^##\s*Inputs\b.*$',
+ ],
+}
+
+
def split_prompt_template(prompt_content: str, prompt_type: str) -> dict:
- """
- Split prompt into editable content and template variables that users should not modify.
- Returns: {"editable_content": str, "template_variables": str}
- """
- if prompt_type == "chatbot_response":
- pattern = r'(Question: \{question\}.*?)$'
- match = re.search(pattern, prompt_content, re.DOTALL)
- if match:
- template_vars = match.group(1).strip()
- editable = prompt_content[:match.start()].strip()
- return {"editable_content": editable, "template_variables": template_vars}
+ """Split a prompt into editable prose and the trailing placeholder
+ block that users should not modify.
- elif prompt_type == "query_generation":
- pattern = r'(\{format_instructions\}.*?)$'
- match = re.search(pattern, prompt_content, re.DOTALL)
- if match:
- template_vars = match.group(1).strip()
- editable = prompt_content[:match.start()].strip()
- return {"editable_content": editable, "template_variables": template_vars}
+ The placeholder block — everything from a canonical marker to end
+ of file — is preserved verbatim so the saved file always renders
+ with the original ``{placeholder}`` set even when the user's edit
+ inadvertently removes them from the prose. POST ``/prompts``
+ re-concatenates ``editable_content + "\\n\\n" + template_variables``
+ on save.
- elif prompt_type == "community_summarization":
- pattern = r'(#######\s*-Data-.*?)$'
- match = re.search(pattern, prompt_content, re.DOTALL)
+ Returns ``{"editable_content": str, "template_variables": str}``.
+ """
+ for pattern in _TEMPLATE_VAR_MARKERS.get(prompt_type, []):
+ match = re.search(pattern, prompt_content)
if match:
- template_vars = match.group(1).strip()
- editable = prompt_content[:match.start()].strip()
- return {"editable_content": editable, "template_variables": template_vars}
-
+ template_vars = prompt_content[match.start():].strip()
+ editable = prompt_content[:match.start()].rstrip()
+ return {
+ "editable_content": editable,
+ "template_variables": template_vars,
+ }
return {"editable_content": prompt_content, "template_variables": ""}
@@ -2547,43 +2815,64 @@ async def get_prompts(
"""
try:
access_level = _require_prompt_access(credentials, graphname)
- active_config = get_chat_config(graphname)
- default_prompt_path = active_config.get("prompt_path", "./common/prompts/openai_gpt4/")
+ chat_cfg = dict(get_chat_config(graphname))
+ completion_cfg = dict(get_completion_config(graphname))
+ if graphname:
+ chat_cfg["graphname"] = graphname
+ completion_cfg["graphname"] = graphname
+
+ # ``chatbot_response`` is consumed by the chat agent and must
+ # resolve through the chat service's ``prompt_path``. Every
+ # other prompt is consumed by completion-side code paths
+ # (entity / relationship extraction, schema extraction,
+ # community summarization, schema mapping) and resolves
+ # through the completion service's ``prompt_path``. When no
+ # ``chat_service`` is configured, ``get_chat_config`` already
+ # falls back to ``completion_service`` so this routing stays
+ # correct for single-service deployments.
+ chat_llm = get_llm_service(chat_cfg)
+ completion_llm = get_llm_service(completion_cfg)
+
+ # Each entry: (LLM service, base_llm property name). The
+ # property's resolution chain is graph-override →
+ # ``prompt_path`` file → hardcoded default in base_llm.py, so
+ # this single delegation gives the editor the right text in
+ # every case.
+ _PROMPT_SOURCE = {
+ "chatbot_response":
+ (chat_llm, "chatbot_response_prompt"),
+ "entity_relationship":
+ (completion_llm, "entity_relationship_extraction_prompt"),
+ "community_summarization":
+ (completion_llm, "community_summarize_prompt"),
+ "query_generation":
+ (completion_llm, "map_question_schema_prompt"),
+ "schema_extraction":
+ (completion_llm, "schema_extraction_prompt"),
+ }
+
+ def _get_prompt(prompt_type: str) -> dict:
+ svc, prop = _PROMPT_SOURCE[prompt_type]
+ try:
+ text = getattr(svc, prop, "") or ""
+ except Exception as exc:
+ logger.warning(
+ f"Falling back to empty content for {prompt_type}: {exc}"
+ )
+ text = ""
+ if not text:
+ return {"editable_content": "", "template_variables": ""}
+ return split_prompt_template(text, prompt_type)
+
+ prompts = {pt: _get_prompt(pt) for pt in _PROMPT_SOURCE}
+
+ default_prompt_path = chat_cfg.get(
+ "prompt_path", "./common/prompts/openai_gpt4/"
+ )
if default_prompt_path.startswith("./"):
default_prompt_path = default_prompt_path[2:]
default_prompt_path = default_prompt_path.rstrip("/")
- # Per-graph prompt overrides directory (only contains customized files)
- graph_prompt_dir = f"configs/graph_configs/{graphname}/prompts" if graphname else None
-
- def _resolve_prompt_file(filename: str) -> str | None:
- """Find prompt file: graph override first, then default."""
- if graph_prompt_dir:
- graph_file = os.path.join(graph_prompt_dir, filename)
- if os.path.exists(graph_file):
- return graph_file
- default_file = os.path.join(default_prompt_path, filename)
- if os.path.exists(default_file):
- return default_file
- return None
-
- def _read_prompt(filename: str, prompt_type: str) -> dict:
- filepath = _resolve_prompt_file(filename)
- if filepath:
- with open(filepath, "r", encoding="utf-8") as f:
- return split_prompt_template(f.read(), prompt_type)
- return {"editable_content": "", "template_variables": ""}
-
- prompts = {}
- prompts["chatbot_response"] = _read_prompt("chatbot_response.txt", "chatbot_response")
- prompts["entity_relationship"] = _read_prompt("entity_relationship_extraction.txt", "entity_relationship")
- prompts["community_summarization"] = _read_prompt("community_summarization.txt", "community_summarization")
-
- query_gen = _read_prompt("map_question_to_schema.txt", "query_generation")
- if not query_gen["editable_content"]:
- query_gen = _read_prompt("query_generation.txt", "query_generation")
- prompts["query_generation"] = query_gen
-
# Graph-admin (chatbot_only) only sees chatbot_response
if access_level == "chatbot_only":
prompts = {"chatbot_response": prompts.get("chatbot_response", {"editable_content": "", "template_variables": ""})}
@@ -2591,7 +2880,7 @@ def _read_prompt(filename: str, prompt_type: str) -> dict:
return {
"prompts": prompts,
"prompt_path": default_prompt_path,
- "configured_provider": active_config.get("llm_service", "openai")
+ "configured_provider": chat_cfg.get("llm_service", "openai"),
}
except HTTPException:
@@ -2644,45 +2933,87 @@ async def save_prompts(
os.makedirs(graph_prompt_dir, exist_ok=True)
prompt_path = graph_prompt_dir
else:
- # Global: seed persistent dir from defaults if needed
- default_prompt_path = get_chat_config().get("prompt_path", "./common/prompts/openai_gpt4/")
- if default_prompt_path.startswith("./"):
- default_prompt_path = default_prompt_path[2:]
- default_prompt_path = default_prompt_path.rstrip("/")
-
+ # Global: route writes to the persistent override dir
+ # ``configs/prompts/`` so user edits survive container
+ # restarts. The dir starts empty — base_llm.py serves the
+ # hardcoded default for every prompt the user hasn't
+ # touched.
+ #
+ # ``prompt_path`` lives at the top level of ``llm_config``
+ # and is injected into every service that doesn't override
+ # it (mirrors the ``authentication_configuration`` /
+ # ``region_name`` pattern). One write here suffices for
+ # every consumer (chatbot_response via chat_service,
+ # entity_relationship / schema_extraction via
+ # completion_service, multimodal via multimodal_service).
persistent_prompt_dir = "configs/prompts"
- if not default_prompt_path.startswith("configs/"):
- os.makedirs(persistent_prompt_dir, exist_ok=True)
- if os.path.exists(default_prompt_path):
- for fname in os.listdir(default_prompt_path):
- src = os.path.join(default_prompt_path, fname)
- dst = os.path.join(persistent_prompt_dir, fname)
- if os.path.isfile(src) and not os.path.exists(dst):
- shutil.copy2(src, dst)
- from common.config import reload_llm_config, _config_file_lock
- with _config_file_lock:
- with open(SERVER_CONFIG, "r") as f:
- server_cfg = json.load(f)
- server_cfg["llm_config"]["completion_service"]["prompt_path"] = f"./{persistent_prompt_dir}/"
+ os.makedirs(persistent_prompt_dir, exist_ok=True)
+ new_path = f"./{persistent_prompt_dir}/"
+
+ from common.config import reload_llm_config, _config_file_lock, SERVER_CONFIG
+ # Acquire the lock to read-modify-write the server config,
+ # then RELEASE before calling ``reload_llm_config()`` —
+ # reload acquires the same lock internally, so calling it
+ # while held would deadlock.
+ changed = False
+ with _config_file_lock:
+ with open(SERVER_CONFIG, "r") as f:
+ server_cfg = json.load(f)
+ llm_cfg = server_cfg.setdefault("llm_config", {})
+ if (llm_cfg.get("prompt_path") or "").rstrip("/") != new_path.rstrip("/"):
+ llm_cfg["prompt_path"] = new_path
+ changed = True
+ # Strip per-service copies — they're redundant once the
+ # top-level field is set. Keeps the config clean and
+ # avoids stale per-service entries shadowing future
+ # global changes. ``embedding_service`` is included
+ # only to scrub stray legacy entries; embedding models
+ # never read prompt_path.
+ for svc_key in (
+ "completion_service",
+ "chat_service",
+ "multimodal_service",
+ "embedding_service",
+ ):
+ svc = llm_cfg.get(svc_key)
+ if isinstance(svc, dict) and "prompt_path" in svc:
+ del svc["prompt_path"]
+ changed = True
+ if changed:
temp_file = f"{SERVER_CONFIG}.tmp"
with open(temp_file, "w") as f:
json.dump(server_cfg, f, indent=2)
os.replace(temp_file, SERVER_CONFIG)
+ if changed:
reload_llm_config()
- prompt_path = persistent_prompt_dir
- else:
- prompt_path = default_prompt_path
+ prompt_path = persistent_prompt_dir
prompt_type_to_file = {
"chatbot_response": "chatbot_response.txt",
"entity_relationship": "entity_relationship_extraction.txt",
"community_summarization": "community_summarization.txt",
"query_generation": "map_question_to_schema.txt",
+ "schema_extraction": "schema_extraction.txt",
}
if prompt_type not in prompt_type_to_file:
raise HTTPException(status_code=400, detail=f"Invalid prompt_type: {prompt_type}")
+ # Gatekeepers — escape stray ``{token}`` occurrences (so user
+ # examples like ``{example}`` don't crash str.format at call
+ # time) and reject saves that miss a required placeholder.
+ from common.utils.prompt_validation import validate_and_escape_prompt
+ content, missing = validate_and_escape_prompt(content, prompt_type)
+ if missing:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ "Prompt is missing required placeholders: "
+ + ", ".join("{" + m + "}" for m in missing)
+ + ". Add them to the prompt before saving."
+ ),
+ )
+
file_path = os.path.join(prompt_path, prompt_type_to_file[prompt_type])
temp_file = f"{file_path}.tmp"
with open(temp_file, "w", encoding="utf-8") as f:
@@ -2694,8 +3025,9 @@ async def save_prompts(
"entity_relationship": "Entity relationship prompt saved successfully",
"community_summarization": "Community summarization prompt saved successfully",
"query_generation": "Schema instructions prompt saved successfully",
+ "schema_extraction": "Schema extraction prompt saved successfully",
}
- return {"status": "success", "message": messages[prompt_type]}
+ return {"status": "success", "message": messages.get(prompt_type, "Prompt saved successfully")}
except HTTPException:
raise
diff --git a/graphrag/app/supportai/supportai_ingest.py b/graphrag/app/supportai/supportai_ingest.py
index e312f25..4d29729 100644
--- a/graphrag/app/supportai/supportai_ingest.py
+++ b/graphrag/app/supportai/supportai_ingest.py
@@ -166,32 +166,12 @@ def upsert_chunk(self, chunk: DocumentChunk):
for x in chunk.relationships
],
)
- self.conn.upsertEdges(
- "Entity",
- "IS_HEAD_OF",
- "RelationshipType",
- [
- (
- x["source"],
- x["source"] + ":" + x["type"] + ":" + x["target"],
- {},
- )
- for x in chunk.relationships
- ],
- )
- self.conn.upsertEdges(
- "RelationshipType",
- "HAS_TAIL",
- "Entity",
- [
- (
- x["source"] + ":" + x["type"] + ":" + x["target"],
- x["target"],
- {},
- )
- for x in chunk.relationships
- ],
- )
+ # IS_HEAD_OF / HAS_TAIL live at the meta-schema layer
+ # (EntityType ↔ RelationshipType) — they are NOT written
+ # per-relationship-instance here. The schema-aware ECC
+ # path writes them when it knows the EntityType for the
+ # source / target. Legacy supportai chunks without
+ # entity_type info skip the meta-layer edges.
self.conn.upsertEdges(
"DocumentChunk",
"MENTIONS_RELATIONSHIP",
@@ -269,32 +249,11 @@ def upsert_document(self, document: Document):
for x in document.relationships
],
)
- self.conn.upsertEdges(
- "Entity",
- "IS_HEAD_OF",
- "RelationshipType",
- [
- (
- x["source"],
- x["source"] + ":" + x["type"] + ":" + x["target"],
- {},
- )
- for x in document.relationships
- ],
- )
- self.conn.upsertEdges(
- "RelationshipType",
- "HAS_TAIL",
- "Entity",
- [
- (
- x["source"] + ":" + x["type"] + ":" + x["target"],
- x["target"],
- {},
- )
- for x in document.relationships
- ],
- )
+ # IS_HEAD_OF / HAS_TAIL are meta-schema edges between
+ # EntityType and RelationshipType — see chunk path
+ # comment above. Legacy document-level supportai ingest
+ # writes only MENTIONS_RELATIONSHIP from Document to
+ # RelationshipType.
self.conn.upsertEdges(
"Document",
"MENTIONS_RELATIONSHIP",
diff --git a/graphrag/app/tools/generate_cypher.py b/graphrag/app/tools/generate_cypher.py
index 8e7ffcb..b749fe9 100644
--- a/graphrag/app/tools/generate_cypher.py
+++ b/graphrag/app/tools/generate_cypher.py
@@ -20,6 +20,7 @@
from langchain.llms.base import LLM
from common.metrics.tg_proxy import TigerGraphConnectionProxy
from common.db.connections import get_schema_ver
+from common.db.schema_utils import read_type_metadata
from common.logs.logwriter import LogWriter
from common.logs.log import req_id_cv
@@ -60,14 +61,22 @@ def _generate_schema_rep(self):
return self.schema_rep
verts = self.conn.getVertexTypes()
edges = self.conn.getEdgeTypes()
+ try:
+ entity_descs, rel_defs = read_type_metadata(self.conn)
+ except Exception as exc:
+ logger.warning(f"read_type_metadata failed: {exc}")
+ entity_descs, rel_defs = {}, {}
vertex_schema = []
for vert in verts:
primary_id = self.conn.getVertexType(vert)["PrimaryId"]["AttributeName"]
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
+ attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
for attr in self.conn.getVertexType(vert)["Attributes"]])
if attributes == "":
attributes = "No attributes"
- vertex_schema.append(f"{vert}\n\tPrimary Id Attribute: {primary_id}\n\tAttributes: \n\t\t{attributes}")
+ defn_line = ""
+ if entity_descs.get(vert):
+ defn_line = f"\n\tDefinition: {entity_descs[vert]}"
+ vertex_schema.append(f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id}\n\tAttributes: \n\t\t{attributes}")
edge_schema = []
for edge in edges:
@@ -75,18 +84,21 @@ def _generate_schema_rep(self):
to_vertex = self.conn.getEdgeType(edge)["ToVertexTypeName"]
direction = "Directed" if self.conn.getEdgeType(edge)["IsDirected"] else "Undirected"
#reverse_edge = conn.getEdgeType(edge)["Config"].get("REVERSE_EDGE")
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
+ attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
for attr in self.conn.getEdgeType(edge)["Attributes"]])
if attributes == "":
attributes = "No attributes"
+ defn_line = ""
+ if rel_defs.get(edge):
+ defn_line = f"\n\tDefinition: {rel_defs[edge]}"
if from_vertex == "*" or to_vertex == "*":
edge_pairs = self.conn.getEdgeType(edge)["EdgePairs"]
for an_edge in edge_pairs:
edge_info = f"""From Vertex: {an_edge["From"]}\n\tTo Vertex: {an_edge["To"]}"""
- edge_schema.append(f"""{edge}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
+ edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
else:
edge_info = f"""From Vertex: {from_vertex}\n\tTo Vertex: {to_vertex}"""
- edge_schema.append(f"""{edge}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
+ edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
self.schema_rep = f"""The schema of the graph is as follows:
Vertex Types:
diff --git a/graphrag/app/tools/generate_gsql.py b/graphrag/app/tools/generate_gsql.py
index 5afc42c..02675b7 100644
--- a/graphrag/app/tools/generate_gsql.py
+++ b/graphrag/app/tools/generate_gsql.py
@@ -20,6 +20,7 @@
from langchain.llms.base import LLM
from common.metrics.tg_proxy import TigerGraphConnectionProxy
from common.db.connections import get_schema_ver
+from common.db.schema_utils import read_type_metadata
from common.logs.logwriter import LogWriter
from common.logs.log import req_id_cv
@@ -60,14 +61,22 @@ def _generate_schema_rep(self):
return self.schema_rep
verts = self.conn.getVertexTypes()
edges = self.conn.getEdgeTypes()
+ try:
+ entity_descs, rel_defs = read_type_metadata(self.conn)
+ except Exception as exc:
+ logger.warning(f"read_type_metadata failed: {exc}")
+ entity_descs, rel_defs = {}, {}
vertex_schema = []
for vert in verts:
primary_id = self.conn.getVertexType(vert)["PrimaryId"]["AttributeName"]
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
+ attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
for attr in self.conn.getVertexType(vert)["Attributes"]])
if attributes == "":
attributes = "No attributes"
- vertex_schema.append(f"{vert}\n\tPrimary Id Attribute: {primary_id}\n\tAttributes: \n\t\t{attributes}")
+ defn_line = ""
+ if entity_descs.get(vert):
+ defn_line = f"\n\tDefinition: {entity_descs[vert]}"
+ vertex_schema.append(f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id}\n\tAttributes: \n\t\t{attributes}")
edge_schema = []
for edge in edges:
@@ -75,18 +84,21 @@ def _generate_schema_rep(self):
to_vertex = self.conn.getEdgeType(edge)["ToVertexTypeName"]
direction = "Directed" if self.conn.getEdgeType(edge)["IsDirected"] else "Undirected"
#reverse_edge = conn.getEdgeType(edge)["Config"].get("REVERSE_EDGE")
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
+ attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
for attr in self.conn.getEdgeType(edge)["Attributes"]])
if attributes == "":
attributes = "No attributes"
+ defn_line = ""
+ if rel_defs.get(edge):
+ defn_line = f"\n\tDefinition: {rel_defs[edge]}"
if from_vertex == "*" or to_vertex == "*":
edge_pairs = self.conn.getEdgeType(edge)["EdgePairs"]
for an_edge in edge_pairs:
edge_info = f"""From Vertex: {an_edge["From"]}\n\tTo Vertex: {an_edge["To"]}"""
- edge_schema.append(f"""{edge}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
+ edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
else:
edge_info = f"""From Vertex: {from_vertex}\n\tTo Vertex: {to_vertex}"""
- edge_schema.append(f"""{edge}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
+ edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
self.schema_rep = f"""The schema of the graph {self.conn.graphname} is as follows:
Vertex Types:
diff --git a/graphrag/tests/conftest.py b/graphrag/tests/conftest.py
index 1d32643..46f669b 100644
--- a/graphrag/tests/conftest.py
+++ b/graphrag/tests/conftest.py
@@ -1,16 +1,8 @@
-import pytest
-
-def pytest_collection_modifyitems(config, items):
- """
- Hook to modify collected test items.
- """
- deselected_modules = set()
- for item in items:
- try:
- # Attempt to collect the test
- config.hook.pytest_runtest_protocol(item=item, nextitem=None)
- except Exception as e:
- # Check if the error message contains the specified substring
- error_message = str(e)
- # Remove the deselected modules from the test items list
- items[:] = [item for item in items if item.module.__name__ not in deselected_modules]
+# Intentionally empty.
+#
+# A previous version of this file defined ``pytest_collection_modifyitems``
+# that called ``config.hook.pytest_runtest_protocol`` for each item — that
+# actually executed every test once during the collection phase and again
+# during the normal runtest phase, so every test ran twice. The
+# ``deselected_modules`` set it built was also never populated, so the
+# hook had no useful effect besides the double-run.
diff --git a/graphrag/tests/test_e2e_prompt_customization.py b/graphrag/tests/test_e2e_prompt_customization.py
new file mode 100644
index 0000000..bffb4bb
--- /dev/null
+++ b/graphrag/tests/test_e2e_prompt_customization.py
@@ -0,0 +1,300 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""End-to-end test for the customizable-prompt round-trip.
+
+Stages:
+ 1. GET ``/ui/prompts`` returns the in-code default for every
+ UI-editable prompt; ``editable_content`` is non-empty and
+ contains zero ``{placeholder}`` occurrences; placeholders the
+ prompt requires live exclusively in ``template_variables``.
+ 2. POST ``/ui/prompts`` saves a customized ``chatbot_response``;
+ a fresh GET returns the customized text (still with
+ placeholders hidden).
+ 3. POST ``/ui/prompts`` reverts ``chatbot_response`` to the
+ original; GET returns the original again.
+ 4. Same load → save → revert flow for ``schema_extraction``.
+
+Requires a live GraphRAG service; ``GRAPHRAG_URL`` env enables the
+suite (default ``http://localhost:80``). Test runs against the global
+scope (no graphname); per-graph overrides are exercised separately.
+
+Default credentials: ``tigergraph`` / ``tigergraph``. Override via
+``TG_USERNAME`` / ``TG_PASSWORD`` env if your TG instance differs.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+
+import pytest
+import requests
+
+
+GRAPHRAG_URL = os.getenv("GRAPHRAG_URL", "http://localhost:80")
+USERNAME = os.getenv("TG_USERNAME", "tigergraph")
+PASSWORD = os.getenv("TG_PASSWORD", "tigergraph")
+AUTH = (USERNAME, PASSWORD)
+
+# Prompt types the UI exposes through ``/ui/prompts``. Every entry here
+# must round-trip through GET → POST → GET → revert.
+EDITABLE_PROMPT_TYPES = (
+ "chatbot_response",
+ "entity_relationship",
+ "community_summarization",
+ "query_generation",
+ "schema_extraction",
+)
+
+# Required placeholders per prompt type — these MUST appear in the
+# template_variables block returned by GET /prompts. ``entity_relationship``
+# is the system-message prompt and has no required placeholders.
+REQUIRED_PLACEHOLDERS = {
+ "chatbot_response": {"question", "context", "format_instructions"},
+ "entity_relationship": set(),
+ "community_summarization": {"entity_name", "description_list"},
+ "query_generation": {
+ "question", "conversation",
+ "vertices", "verticesAttrs",
+ "edges", "edgesInfo",
+ },
+ "schema_extraction": {"samples", "structural_types", "tg_keywords"},
+}
+
+
+skip_unless_graphrag = pytest.mark.skipif(
+ not os.getenv("GRAPHRAG_URL"),
+ reason="E2E tests require a live GraphRAG service. Set GRAPHRAG_URL to run.",
+)
+
+
+_PLACEHOLDER_RE = re.compile(r"(? set:
+ """Return single-brace ``{ident}`` placeholders, ignoring escaped
+ ``{{ident}}`` literals.
+ """
+ return set(_PLACEHOLDER_RE.findall(text or ""))
+
+
+# Shared state across ordered stages.
+_state: dict = {}
+
+
+@skip_unless_graphrag
+def test_01_get_returns_defaults_with_placeholders_hidden():
+ """Every editable prompt resolves to a non-empty default; the
+ editable portion is placeholder-free; required placeholders all
+ live in template_variables.
+ """
+ print("\n--- Stage 1: GET defaults; verify placeholder split ---")
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ prompts = body.get("prompts", {})
+
+ originals: dict = {}
+ for ptype in EDITABLE_PROMPT_TYPES:
+ assert ptype in prompts, f"GET /ui/prompts missing {ptype!r}"
+ entry = prompts[ptype]
+ editable = entry.get("editable_content", "")
+ template_vars = entry.get("template_variables", "")
+ assert editable, f"{ptype}: empty editable_content (expected in-code default)"
+
+ placeholders_in_editable = _placeholder_set(editable)
+ assert not placeholders_in_editable, (
+ f"{ptype}: placeholders leaked into editable_content: "
+ f"{sorted(placeholders_in_editable)}"
+ )
+
+ required = REQUIRED_PLACEHOLDERS[ptype]
+ if required:
+ placeholders_in_tv = _placeholder_set(template_vars)
+ missing = required - placeholders_in_tv
+ assert not missing, (
+ f"{ptype}: required placeholders missing from "
+ f"template_variables: {sorted(missing)}"
+ )
+
+ originals[ptype] = entry
+ print(
+ f" {ptype}: editable={len(editable)}b, "
+ f"template={len(template_vars)}b, "
+ f"hidden={sorted(_placeholder_set(template_vars))}"
+ )
+
+ _state["originals"] = originals
+
+
+@skip_unless_graphrag
+def test_02_save_customized_chatbot_response_round_trips():
+ """Saving a customized chatbot_response prompt persists it; a
+ follow-up GET returns the customized text with placeholders still
+ hidden.
+ """
+ if "originals" not in _state:
+ pytest.skip("Skipped because Stage 1 did not capture originals")
+ print("\n--- Stage 2: customize chatbot_response; verify round-trip ---")
+
+ original = _state["originals"]["chatbot_response"]
+ custom_marker = "[E2E TEST EDIT — chatbot_response]"
+ new_editable = f"{custom_marker}\n\n{original['editable_content']}"
+
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "chatbot_response",
+ "editable_content": new_editable,
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ after = resp.json()["prompts"]["chatbot_response"]
+ assert custom_marker in after["editable_content"], (
+ "Customized marker missing from chatbot_response after save+reload"
+ )
+ placeholders_in_editable = _placeholder_set(after["editable_content"])
+ assert not placeholders_in_editable, (
+ f"Placeholders leaked into editable_content after customize: "
+ f"{sorted(placeholders_in_editable)}"
+ )
+ required = REQUIRED_PLACEHOLDERS["chatbot_response"]
+ placeholders_in_tv = _placeholder_set(after["template_variables"])
+ missing = required - placeholders_in_tv
+ assert not missing, (
+ f"Required placeholders dropped during round-trip: {sorted(missing)}"
+ )
+ _state["chatbot_customized"] = True
+
+
+@skip_unless_graphrag
+def test_03_revert_chatbot_response_to_original():
+ """Saving the original ``editable_content`` back removes the
+ customization.
+ """
+ if not _state.get("chatbot_customized"):
+ pytest.skip("Skipped — Stage 2 did not customize")
+ print("\n--- Stage 3: revert chatbot_response to original ---")
+
+ original = _state["originals"]["chatbot_response"]
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "chatbot_response",
+ "editable_content": original["editable_content"],
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ after = resp.json()["prompts"]["chatbot_response"]
+ custom_marker = "[E2E TEST EDIT — chatbot_response]"
+ assert custom_marker not in after["editable_content"], (
+ "Customization marker survived revert"
+ )
+
+
+@skip_unless_graphrag
+def test_04_save_customized_schema_extraction_round_trips():
+ """Same round-trip flow for schema_extraction (the prompt with
+ the largest set of required placeholders / structural-context
+ template variables).
+ """
+ if "originals" not in _state:
+ pytest.skip("Skipped because Stage 1 did not capture originals")
+ print("\n--- Stage 4: customize schema_extraction; verify round-trip ---")
+
+ original = _state["originals"]["schema_extraction"]
+ custom_marker = "[E2E TEST EDIT — schema_extraction]"
+ new_editable = f"{custom_marker}\n\n{original['editable_content']}"
+
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "schema_extraction",
+ "editable_content": new_editable,
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ after = resp.json()["prompts"]["schema_extraction"]
+ assert custom_marker in after["editable_content"], (
+ "Customized marker missing from schema_extraction after save+reload"
+ )
+ placeholders_in_editable = _placeholder_set(after["editable_content"])
+ assert not placeholders_in_editable, (
+ f"Placeholders leaked into editable_content after customize: "
+ f"{sorted(placeholders_in_editable)}"
+ )
+ required = REQUIRED_PLACEHOLDERS["schema_extraction"]
+ placeholders_in_tv = _placeholder_set(after["template_variables"])
+ missing = required - placeholders_in_tv
+ assert not missing, (
+ f"Required placeholders dropped during round-trip: {sorted(missing)}"
+ )
+
+ # Revert to keep the test idempotent.
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "schema_extraction",
+ "editable_content": original["editable_content"],
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+
+
+@skip_unless_graphrag
+def test_05_post_rejects_missing_required_placeholders():
+ """Saving a query_generation prompt that drops a required
+ placeholder must return 400 — the server-side validator catches
+ it before persisting.
+ """
+ if "originals" not in _state:
+ pytest.skip("Skipped because Stage 1 did not capture originals")
+ print("\n--- Stage 5: validator rejects missing required placeholder ---")
+
+ original = _state["originals"]["query_generation"]
+ # Strip the placeholder block entirely — the validator should
+ # reject because required placeholders (e.g. {question}) are gone.
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "query_generation",
+ "editable_content": original["editable_content"],
+ "template_variables": "",
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 400, (
+ f"Expected 400 for missing-placeholder save, got {resp.status_code}: {resp.text}"
+ )
+ detail = (resp.json() or {}).get("detail", "").lower()
+ assert "missing" in detail or "placeholder" in detail, (
+ f"Expected error detail to mention missing placeholders, got: {detail}"
+ )
diff --git a/graphrag/tests/test_e2e_schema_aware_ingest.py b/graphrag/tests/test_e2e_schema_aware_ingest.py
new file mode 100644
index 0000000..ebf31c5
--- /dev/null
+++ b/graphrag/tests/test_e2e_schema_aware_ingest.py
@@ -0,0 +1,692 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""
+End-to-end test for schema-aware initialization (Phase 1).
+
+Walks the full lifecycle a UI user would drive:
+
+ 1. Create graph
+ 2. Upload sample PDFs via convert_sample_files (stores files +
+ converts to JSONL, NO LLM call)
+ 3. Run schema extraction over the JSONLs (LLM call, returns draft)
+ 4. Initialize the graph with the LLM-produced schema_gsql
+ (applies domain types as a single atomic schema-change job)
+ 5. Validate the live schema and metadata vertices match the proposal
+ 6. Run ingestion against the SAME files (JSONL cache reused —
+ no second-round PDF conversion)
+ 7. Trigger rebuild and wait for completion
+ 8. Validate the final knowledge graph (documents, chunks, entities,
+ EntityType definitions populated, communities formed)
+
+Requires a running GraphRAG stack against a live TigerGraph instance.
+The default test corpus is the 2 Barclays PDFs at
+``~/Downloads/BarclaysDocs/`` — point ``TEST_FILES`` elsewhere to use
+a different sample.
+
+Usage::
+
+ GRAPHRAG_URL=http://localhost:80 \\
+ TEST_FILES=$HOME/Downloads/BarclaysDocs/Inspired_ESG-Report_2022.pdf,\\
+$HOME/Downloads/BarclaysDocs/QuarterlyInvestmentReport_uss.pdf \\
+ pytest graphrag/tests/test_e2e_schema_aware_ingest.py -v -s
+
+Environment variables:
+ GRAPHRAG_URL Base URL of running GraphRAG service (required to run)
+ DB_CONFIG Path to db_config.json (default: ./configs/db_config.json)
+ TG_USERNAME / TG_PASSWORD Fallbacks if DB_CONFIG is missing
+ TEST_GRAPH Graph name (default: SchemaAwareE2E_)
+ TEST_FILES Comma-separated file paths (default: BarclaysDocs PDFs)
+ REBUILD_TIMEOUT Max seconds to wait for rebuild (default: 7200)
+ SCHEMA_EXTRACT_TIMEOUT Max seconds for the LLM extract call (default: 300)
+ EXPECTED_MIN_VERTICES Minimum domain vertex types the LLM must produce (default: 3)
+ EXPECTED_MIN_EDGES Minimum domain edge types the LLM must produce (default: 2)
+ SKIP_CLEANUP Set to "1" to keep the graph after the test
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import time
+
+import pytest
+import requests
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+
+GRAPHRAG_URL = os.getenv("GRAPHRAG_URL", "http://localhost:80")
+
+_db_config_path = os.getenv("DB_CONFIG", "./configs/db_config.json")
+try:
+ with open(_db_config_path) as _f:
+ _db = json.load(_f)
+ USERNAME = _db.get("username", "tigergraph")
+ PASSWORD = _db.get("password", "tigergraph")
+except Exception:
+ USERNAME = os.getenv("TG_USERNAME", "tigergraph")
+ PASSWORD = os.getenv("TG_PASSWORD", "tigergraph")
+
+REBUILD_TIMEOUT = int(os.getenv("REBUILD_TIMEOUT", "7200"))
+SCHEMA_EXTRACT_TIMEOUT = int(os.getenv("SCHEMA_EXTRACT_TIMEOUT", "300"))
+EXPECTED_MIN_VERTICES = int(os.getenv("EXPECTED_MIN_VERTICES", "3"))
+EXPECTED_MIN_EDGES = int(os.getenv("EXPECTED_MIN_EDGES", "2"))
+
+AUTH = (USERNAME, PASSWORD)
+GRAPH_NAME = os.getenv("TEST_GRAPH", f"SchemaAwareE2E_{int(time.time())}")
+
+_default_pdfs = [
+ os.path.expanduser("~/Downloads/BarclaysDocs/Inspired_ESG-Report_2022.pdf"),
+ os.path.expanduser("~/Downloads/BarclaysDocs/QuarterlyInvestmentReport_uss.pdf"),
+]
+_raw_files = os.getenv("TEST_FILES")
+if _raw_files:
+ TEST_FILES = [f.strip() for f in _raw_files.split(",") if f.strip()]
+else:
+ TEST_FILES = [p for p in _default_pdfs if os.path.exists(p)]
+
+
+# Shared state across ordered test stages. Each stage records its
+# success under a distinct key so downstream stages can early-skip
+# instead of producing cascade failures with confusing tracebacks.
+_state: dict = {}
+
+skip_unless_graphrag = pytest.mark.skipif(
+ not os.getenv("GRAPHRAG_URL"),
+ reason="E2E tests require a live GraphRAG service. Set GRAPHRAG_URL to run.",
+)
+
+
+def _require_stage(stage_key: str):
+ if stage_key not in _state:
+ pytest.skip(f"Skipped because prior stage '{stage_key}' did not succeed")
+
+
+# ---------------------------------------------------------------------------
+# Stages
+# ---------------------------------------------------------------------------
+
+
+@skip_unless_graphrag
+def test_01_create_graph():
+ """Create an empty TigerGraph graph."""
+ print(f"\n--- Stage 1: Creating graph '{GRAPH_NAME}' ---")
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/create_graph",
+ auth=AUTH,
+ # Mirror the UI's fetch (no client read timeout); nginx caps at 3600s.
+ timeout=(60, None),
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ assert body["status"] == "success", body
+ _state["created"] = True
+ print(f"Graph '{GRAPH_NAME}' created.")
+
+
+@skip_unless_graphrag
+def test_02_convert_sample_files():
+ """Upload PDFs to convert_sample_files — stores files under
+ uploads// and writes JSONL under uploads/ingestion_temp//.
+ No LLM call. Returns the saved-file list we use to drive the next
+ stage and ingest later.
+ """
+ _require_stage("created")
+ if not TEST_FILES:
+ pytest.skip(
+ "No test files. Set TEST_FILES env var or place the Barclays PDFs at "
+ "~/Downloads/BarclaysDocs/."
+ )
+ print(f"\n--- Stage 2: Converting {len(TEST_FILES)} sample file(s) ---")
+ files = []
+ for fpath in TEST_FILES:
+ abs_path = os.path.abspath(fpath)
+ assert os.path.exists(abs_path), f"Test file not found: {abs_path}"
+ files.append(("files", (os.path.basename(abs_path), open(abs_path, "rb"))))
+ try:
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/convert_sample_files",
+ files=files,
+ auth=AUTH,
+ timeout=(60, None),
+ )
+ finally:
+ for _, (_, fobj) in files:
+ fobj.close()
+
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ assert body["status"] == "success", body
+ saved = body.get("saved_files") or []
+ assert saved, f"convert_sample_files returned empty saved_files: {body}"
+ print(f"Saved files: {saved}")
+ print(f"Total documents: {body.get('num_documents')}")
+ _state["saved_files"] = saved
+
+
+@skip_unless_graphrag
+def test_03_extract_schema_from_jsonl():
+ """Run the schema-extraction LLM and validate the returned proposal."""
+ _require_stage("saved_files")
+ print(f"\n--- Stage 3: Running schema extraction ---")
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/extract_schema_from_jsonl",
+ json={"filenames": _state["saved_files"]},
+ auth=AUTH,
+ timeout=SCHEMA_EXTRACT_TIMEOUT,
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ proposal = body.get("proposal") or {}
+ summary = body.get("summary") or {}
+ schema_gsql = body.get("schema_gsql") or ""
+
+ print(f"Vertex types: {summary.get('vertex_count')}")
+ print(f"Edge types: {summary.get('edge_count')}")
+ print(f"Vertex names: {summary.get('vertex_names')}")
+ print(f"Edge names: {summary.get('edge_names')}")
+
+ assert summary.get("vertex_count", 0) >= EXPECTED_MIN_VERTICES, (
+ f"LLM produced too few vertex types: got {summary.get('vertex_count')}, "
+ f"need >= {EXPECTED_MIN_VERTICES}. Proposal: {summary}"
+ )
+ assert summary.get("edge_count", 0) >= EXPECTED_MIN_EDGES, (
+ f"LLM produced too few edge types: got {summary.get('edge_count')}, "
+ f"need >= {EXPECTED_MIN_EDGES}. Proposal: {summary}"
+ )
+ assert schema_gsql.strip(), "Empty schema_gsql in response"
+
+ # Every vertex in the proposal must have a description (the LLM
+ # is prompted to emit a // comment per declaration).
+ no_desc_vertices = [
+ v["name"] for v in proposal.get("vertices", []) if not v.get("description")
+ ]
+ no_desc_edges = [
+ e["name"] for e in proposal.get("edges", []) if not e.get("description")
+ ]
+ if no_desc_vertices:
+ print(f"WARN: vertices without description: {no_desc_vertices}")
+ if no_desc_edges:
+ print(f"WARN: edges without description: {no_desc_edges}")
+
+ _state["proposal"] = proposal
+ _state["schema_gsql"] = schema_gsql
+ _state["expected_vertex_names"] = {v["name"] for v in proposal.get("vertices", [])}
+ _state["expected_edge_names"] = {e["name"] for e in proposal.get("edges", [])}
+
+
+@skip_unless_graphrag
+def test_04_initialize_graph_with_schema():
+ """Apply the LLM-produced schema as part of initialize_graph.
+
+ The endpoint creates the structural GraphRAG schema first, then
+ applies the domain types in a single atomic schema-change job.
+ """
+ _require_stage("schema_gsql")
+ print(f"\n--- Stage 4: Initializing graph with extracted schema ---")
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/initialize_graph",
+ json={"schema_gsql": _state["schema_gsql"]},
+ auth=AUTH,
+ timeout=(60, None),
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ assert body["status"] == "success", body.get("message")
+ domain_status = body.get("domain_schema_status") or {}
+ print(f"Domain schema status: {domain_status.get('status')}")
+ print(f"Statements applied: {len(domain_status.get('statements', []))}")
+ if domain_status.get("metadata"):
+ md = domain_status["metadata"]
+ print(
+ f"Metadata: {len(md.get('entity_types', []))} EntityType, "
+ f"{len(md.get('relationship_types', []))} RelationshipType"
+ )
+ assert domain_status.get("status") in ("applied", "no-op"), domain_status
+ _state["initialized"] = True
+
+
+@skip_unless_graphrag
+def test_05_validate_live_schema():
+ """The live graph should now have the structural types AND every
+ domain type from the proposal. Verified by reading the live schema
+ directly via pyTigerGraph (the graphrag service no longer exposes
+ a schema-export endpoint — schemas are read from TG directly).
+ """
+ _require_stage("initialized")
+ print(f"\n--- Stage 5: Validating live schema ---")
+
+ from pyTigerGraph import TigerGraphConnection
+
+ tg_host = os.getenv("TG_HOST", "http://192.168.11.11")
+ conn = TigerGraphConnection(
+ host=tg_host,
+ graphname=GRAPH_NAME,
+ username=USERNAME,
+ password=PASSWORD,
+ )
+ conn.getToken()
+ actual_vertex_names = set(conn.getVertexTypes())
+ actual_edge_names = set(conn.getEdgeTypes())
+
+ expected_vertices = _state["expected_vertex_names"]
+ expected_edges = _state["expected_edge_names"]
+
+ missing_vertices = expected_vertices - actual_vertex_names
+ missing_edges = expected_edges - actual_edge_names
+ assert not missing_vertices, (
+ f"Vertex types missing on graph: {missing_vertices}. "
+ f"Got: {actual_vertex_names}"
+ )
+ assert not missing_edges, (
+ f"Edge types missing on graph: {missing_edges}. "
+ f"Got: {actual_edge_names}"
+ )
+ print(f"All {len(expected_vertices)} domain vertex types present.")
+ print(f"All {len(expected_edges)} domain edge types present.")
+
+
+@skip_unless_graphrag
+def test_06_create_ingest():
+ """Create the loading job for ingest. The JSONLs are already in
+ uploads/ingestion_temp// from stage 2, so the underlying
+ process_folder run will hit the cached_jsonl_skipped path and
+ skip re-conversion.
+ """
+ _require_stage("initialized")
+ print(f"\n--- Stage 6: Creating ingest configuration ---")
+ payload = {
+ "data_source": "server",
+ "data_source_config": {"data_path": f"uploads/{GRAPH_NAME}"},
+ "file_format": "multi",
+ }
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/create_ingest",
+ json=payload,
+ auth=AUTH,
+ timeout=(60, None),
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ assert "load_job_id" in body, body
+ _state["ingest_config"] = body
+
+
+@skip_unless_graphrag
+def test_07_run_ingest():
+ """Run the loading job."""
+ _require_stage("ingest_config")
+ cfg = _state["ingest_config"]
+ print(f"\n--- Stage 7: Running ingestion ---")
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/ingest",
+ json={
+ "load_job_id": cfg["load_job_id"],
+ "data_source_id": cfg["data_source_id"],
+ "file_path": cfg.get("data_path", ""),
+ },
+ auth=AUTH,
+ timeout=(60, None),
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ print(f"Ingest result: {json.dumps(body)[:300]}")
+ _state["ingested"] = True
+
+
+@skip_unless_graphrag
+def test_08_rebuild_graph():
+ """Trigger ECC rebuild and poll until completion.
+
+ Completion is detected by tailing the graphrag-ecc container log
+ for the canonical end-of-run marker:
+ ``Completed ECC task: :graphrag``
+ The rebuild_status REST endpoint is also checked, but the log
+ marker is authoritative — the REST status flips ``completed``
+ when ``run_with_tracking`` returns, which can race with the final
+ flush of upserts; the log line is emitted strictly after the full
+ pipeline (including community detection / post-pipeline checks)
+ has finished.
+ """
+ import subprocess
+
+ _require_stage("ingested")
+ print(f"\n--- Stage 8: Triggering rebuild ---")
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/rebuild_graph",
+ auth=AUTH,
+ timeout=60,
+ )
+ assert resp.status_code == 200, resp.text
+ body = resp.json()
+ assert body.get("status") == "submitted", body
+ print("Rebuild submitted; polling for ECC completion marker in container log...")
+
+ completion_marker = f"Completed ECC task: {GRAPH_NAME}:graphrag"
+ failure_marker = f"ECC task failed: {GRAPH_NAME}:graphrag"
+ poll_start = time.time()
+ start_time = poll_start
+ last_status = ""
+ while time.time() - start_time < REBUILD_TIMEOUT:
+ elapsed = int(time.time() - start_time)
+ # Check the canonical log marker first.
+ try:
+ log_tail = subprocess.check_output(
+ ["docker", "logs", "--tail", "2000", "graphrag-ecc"],
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ if failure_marker in log_tail:
+ pytest.fail(f"ECC reported task failure in container log for {GRAPH_NAME}")
+ if completion_marker in log_tail:
+ print(f" ECC completion marker observed in container log ({elapsed}s).")
+ _state["rebuilt"] = True
+ return
+ except (subprocess.CalledProcessError, FileNotFoundError) as exc:
+ print(f" [{elapsed}s] docker-logs check failed: {exc}")
+
+ # Secondary signal — REST status. Useful for log lines on its
+ # state transitions; not authoritative for completion.
+ try:
+ sr = requests.get(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/rebuild_status",
+ auth=AUTH,
+ timeout=120,
+ )
+ if sr.status_code == 200:
+ sd = sr.json()
+ status = sd.get("status", "unknown")
+ if status != last_status:
+ print(f" [{elapsed}s] rebuild_status={status} (informational)")
+ last_status = status
+ if status == "failed":
+ pytest.fail(f"Rebuild failed per REST status: {sd}")
+ except Exception as exc:
+ print(f" [{elapsed}s] rebuild_status poll error: {exc}")
+
+ time.sleep(15)
+
+ pytest.fail(
+ f"ECC completion marker not seen in {REBUILD_TIMEOUT}s for graph {GRAPH_NAME}"
+ )
+
+
+@skip_unless_graphrag
+def test_09_validate_final_graph():
+ """Validate the rebuilt graph has the data we expect.
+
+ Per-type vertex AND edge counts are pulled directly from
+ TigerGraph via pyTigerGraph (the graphrag service does not expose
+ a per-type statistics endpoint). Every structural and every
+ domain type must have a non-zero count — empty types signal that
+ extraction did not produce data for them.
+ """
+ _require_stage("rebuilt")
+ print(f"\n--- Stage 9: Validating final graph data ---")
+
+ from pyTigerGraph import TigerGraphConnection
+
+ tg_host = os.getenv("TG_HOST", "http://192.168.11.11")
+ conn = TigerGraphConnection(
+ host=tg_host,
+ graphname=GRAPH_NAME,
+ username=USERNAME,
+ password=PASSWORD,
+ )
+ conn.getToken()
+
+ # Read the live schema directly from TG to discover which domain
+ # types we expect. Domain types = everything not in the GraphRAG
+ # structural set (which is fixed across releases).
+ structural_vertex_types = [
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType",
+ ]
+ structural_edge_types = [
+ "HAS_CONTENT", "CONTAINS_ENTITY",
+ "IS_HEAD_OF", "HAS_TAIL", "MENTIONS_RELATIONSHIP",
+ ]
+ # Other GraphRAG-structural types that may exist on the graph but
+ # whose presence isn't required (Content / Image / Community and
+ # their structural edges).
+ structural_optional = {
+ "Content", "Image", "Community",
+ "HAS_CHILD", "IS_AFTER", "HAS_IMAGE",
+ "REFERENCES_IMAGE", "LINKS_TO", "HAS_PARENT",
+ "IN_COMMUNITY", "ENTITY_HAS_TYPE",
+ "RELATIONSHIP",
+ }
+ structural_skip = (
+ set(structural_vertex_types)
+ | set(structural_edge_types)
+ | structural_optional
+ )
+ all_vertex_types = conn.getVertexTypes()
+ all_edge_types = conn.getEdgeTypes()
+ domain_vertex_types = sorted(
+ v for v in all_vertex_types if v not in structural_skip
+ )
+ domain_edge_types = sorted(
+ e for e in all_edge_types if e not in structural_skip
+ )
+ expected_v = structural_vertex_types + domain_vertex_types
+ expected_e = structural_edge_types + domain_edge_types
+
+ # `saw_progress` must only flip on EXTRACTION signals — types that
+ # ingest never populates on its own. Document / DocumentChunk are
+ # already non-zero after stage 7 (ingest), so they don't tell us
+ # anything about extraction having started.
+ extraction_signal_v = {"Entity", "EntityType", "RelationshipType"} | set(
+ domain_vertex_types
+ )
+ extraction_signal_e = {
+ "CONTAINS_ENTITY", "IS_HEAD_OF", "HAS_TAIL",
+ "MENTIONS_RELATIONSHIP",
+ } | set(domain_edge_types)
+
+ # ECC's `rebuild_status: completed` reports when the outer rebuild
+ # task returns, but extraction upserts can keep flowing into TG for
+ # many minutes after that. Poll until every expected type is
+ # non-zero, OR until the counts have been stable for several
+ # consecutive samples AFTER at least one extraction-signal type
+ # has crossed zero (stability at all-zeros just means extraction
+ # hasn't started yet, not that it's done).
+ poll_interval = 30
+ stability_window = 4 # 4 stable samples * 30s = 2 min idle
+ max_wait = int(os.getenv("STAGE9_POLL_MAX_S", "1800"))
+ start = time.time()
+ last_signature = None
+ stable_samples = 0
+ saw_progress = False
+ vertex_counts: dict = {}
+ edge_counts: dict = {}
+ while True:
+ vertex_counts = conn.getVertexCount("*")
+ edge_counts = conn.getEdgeCount("*")
+ signature = tuple(
+ (t, vertex_counts.get(t, 0)) for t in expected_v
+ ) + tuple(
+ ("E:" + t, edge_counts.get(t, 0)) for t in expected_e
+ )
+ empty_now = [t for t in expected_v if vertex_counts.get(t, 0) == 0] + [
+ "E:" + t for t in expected_e if edge_counts.get(t, 0) == 0
+ ]
+ # Progress flips only on extraction-signal types crossing zero.
+ progress_v = sum(
+ 1 for t in extraction_signal_v if vertex_counts.get(t, 0) > 0
+ )
+ progress_e = sum(
+ 1 for t in extraction_signal_e if edge_counts.get(t, 0) > 0
+ )
+ if (progress_v + progress_e) > 0:
+ saw_progress = True
+ elapsed = int(time.time() - start)
+ print(
+ f" [{elapsed}s] missing={len(empty_now)} types, "
+ f"saw_progress={saw_progress}"
+ )
+ if not empty_now:
+ break
+ if signature == last_signature:
+ stable_samples += 1
+ if saw_progress and stable_samples >= stability_window:
+ print(
+ f" Counts stable for {stability_window} samples after "
+ "first extraction progress; extraction appears done."
+ )
+ break
+ else:
+ stable_samples = 0
+ last_signature = signature
+ if elapsed >= max_wait:
+ print(f" Hit max_wait={max_wait}s; reporting current state.")
+ break
+ time.sleep(poll_interval)
+
+ print(f"Vertex counts: {json.dumps(vertex_counts)}")
+ print(f"Edge counts: {json.dumps(edge_counts)}")
+
+ # Hard requirement: every structural vertex / edge type the
+ # extraction-write path is supposed to populate MUST be non-zero.
+ required_v = list(structural_vertex_types)
+ required_e = list(structural_edge_types)
+ # Every domain vertex must have data — domain VTs are extracted
+ # entity instances, and a zero count means extraction missed an
+ # entire class declared in the schema.
+ required_v += list(domain_vertex_types)
+ # Domain edges are best-effort — the LLM can propose an edge type
+ # in the schema and then not actually extract any instances of it
+ # from the corpus (the schema is a superset of the realized graph).
+ # We require at least 50% of declared domain edges to have data,
+ # warn-print the empties, and fail if coverage is below that
+ # threshold.
+ DOMAIN_EDGE_MIN_COVERAGE = float(os.getenv("DOMAIN_EDGE_MIN_COVERAGE", "0.5"))
+
+ empty: list = []
+ for vt in expected_v:
+ c = vertex_counts.get(vt, 0)
+ if c == 0 and vt in required_v:
+ empty.append(f"VERTEX {vt}")
+ print(f" V {vt}: {c}")
+
+ empty_domain_edges: list = []
+ for et in expected_e:
+ c = edge_counts.get(et, 0)
+ if c == 0:
+ if et in required_e:
+ empty.append(f"EDGE {et}")
+ elif et in domain_edge_types:
+ empty_domain_edges.append(et)
+ print(f" E {et}: {c}")
+
+ if empty_domain_edges:
+ print(
+ f" WARN: {len(empty_domain_edges)}/{len(domain_edge_types)} "
+ f"domain edges empty (LLM proposed but not extracted): "
+ f"{empty_domain_edges}"
+ )
+ if domain_edge_types:
+ coverage = (
+ len(domain_edge_types) - len(empty_domain_edges)
+ ) / len(domain_edge_types)
+ assert coverage >= DOMAIN_EDGE_MIN_COVERAGE, (
+ f"Domain-edge coverage {coverage:.0%} below "
+ f"{DOMAIN_EDGE_MIN_COVERAGE:.0%}: empties = {empty_domain_edges}"
+ )
+
+ if empty:
+ print(f"Expected structural V: {structural_vertex_types}")
+ print(f"Expected domain V: {domain_vertex_types}")
+ print(f"Expected structural E: {structural_edge_types}")
+ print(f"Expected domain E: {domain_edge_types}")
+ assert not empty, f"Empty types after ingest+rebuild: {empty}"
+
+ # EntityType / RelationshipType metadata vertices should be populated
+ # with descriptions sourced from the LLM-emitted ``// `` comments
+ # above each declaration. Vertex descriptions are reliably emitted
+ # by Claude; edge descriptions are LLM-flaky (the model often skips
+ # the comment lines above EDGE declarations under load), so we
+ # require strict coverage on vertices and only require a soft
+ # majority on edges (warn on misses, fail only if more than half
+ # are missing).
+ et_rows = conn.getVerticesById("EntityType", domain_vertex_types) if domain_vertex_types else []
+ rt_rows = conn.getVerticesById("RelationshipType", domain_edge_types) if domain_edge_types else []
+ described_v = sum(
+ 1 for r in et_rows
+ if (r.get("attributes", {}) or {}).get("description")
+ )
+ described_e = sum(
+ 1 for r in rt_rows
+ if (r.get("attributes", {}) or {}).get("description")
+ )
+ print(
+ f"Type metadata descriptions: {described_v}/{len(domain_vertex_types)} V, "
+ f"{described_e}/{len(domain_edge_types)} E"
+ )
+ if domain_vertex_types:
+ assert described_v == len(domain_vertex_types), (
+ f"Only {described_v}/{len(domain_vertex_types)} domain "
+ f"vertex types carry a description — EntityType metadata "
+ f"may not have been populated."
+ )
+ if domain_edge_types:
+ # Warn-only by default — Claude routinely skips ``// ``
+ # comments above some edge declarations and the variance is
+ # high run-to-run. Bump via ``EDGE_DESC_MIN_COVERAGE`` env if
+ # you want strict enforcement.
+ edge_min_coverage = float(os.getenv("EDGE_DESC_MIN_COVERAGE", "0"))
+ edge_coverage = described_e / len(domain_edge_types)
+ if edge_min_coverage > 0 and edge_coverage < edge_min_coverage:
+ assert False, (
+ f"RelationshipType description coverage "
+ f"{edge_coverage:.0%} below {edge_min_coverage:.0%} "
+ f"({described_e}/{len(domain_edge_types)}) — LLM "
+ f"omitted ``// `` comments on too many EDGE "
+ f"declarations."
+ )
+ elif described_e < len(domain_edge_types):
+ print(
+ f" WARN: {len(domain_edge_types) - described_e}/"
+ f"{len(domain_edge_types)} domain edges missing "
+ f"descriptions (LLM-flaky behavior, accepted)."
+ )
+
+ print("Final graph validation passed.")
+
+
+@skip_unless_graphrag
+def test_99_cleanup():
+ """Clean up: clear graph data and delete uploaded files."""
+ if os.getenv("SKIP_CLEANUP") == "1":
+ print(f"\n[cleanup] SKIP_CLEANUP=1, keeping graph '{GRAPH_NAME}'")
+ pytest.skip("SKIP_CLEANUP=1")
+ if "created" not in _state:
+ pytest.skip("Graph was never created")
+
+ print(f"\n--- Cleanup: removing graph data for '{GRAPH_NAME}' ---")
+ try:
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/clear_graph_data",
+ auth=AUTH,
+ timeout=120,
+ )
+ print(f"clear_graph_data: {resp.status_code} {resp.text[:200]}")
+ except Exception as e:
+ print(f"clear_graph_data failed: {e}")
+
+ try:
+ resp = requests.delete(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/uploads",
+ auth=AUTH,
+ timeout=30,
+ )
+ print(f"delete uploads: {resp.status_code}")
+ except Exception as e:
+ print(f"delete uploads failed: {e}")
diff --git a/graphrag/tests/test_llm_entity_relationship_extractor.py b/graphrag/tests/test_llm_entity_relationship_extractor.py
new file mode 100644
index 0000000..07a8861
--- /dev/null
+++ b/graphrag/tests/test_llm_entity_relationship_extractor.py
@@ -0,0 +1,143 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Unit tests for the schema-aware extension of
+``LLMEntityRelationshipExtractor`` — definitions are surfaced in the
+prompt without breaking the legacy ``allowed_*_types`` arguments.
+
+We do not invoke the LLM. The tests exercise prompt assembly in
+``adocument_er_extraction`` / ``document_er_extraction`` by capturing
+the messages passed to ``ChatPromptTemplate.from_messages``.
+"""
+
+from __future__ import annotations
+
+from common.extractors.LLMEntityRelationshipExtractor import (
+ LLMEntityRelationshipExtractor,
+)
+
+
+class _StubLLMService:
+ entity_relationship_extraction_prompt = "(system prompt)"
+ llm = object()
+
+
+def _capture_prompt_messages(monkeypatch):
+ """Patch ChatPromptTemplate.from_messages so we can inspect what
+ messages the extractor assembles. Returns the captured list.
+ """
+ captured: list = []
+
+ class _FakeTemplate:
+ def __init__(self, msgs):
+ self._msgs = msgs
+
+ def __or__(self, other):
+ return self # short-circuit chain composition
+
+ def _from_messages(msgs):
+ captured.extend(msgs)
+ return _FakeTemplate(msgs)
+
+ monkeypatch.setattr(
+ "langchain.prompts.ChatPromptTemplate.from_messages", _from_messages
+ )
+ return captured
+
+
+def test_extractor_without_definitions_leaves_prompt_unchanged(monkeypatch):
+ captured = _capture_prompt_messages(monkeypatch)
+ ext = LLMEntityRelationshipExtractor(_StubLLMService())
+
+ # We patch _extract_kg_from_doc to a no-op so the chain isn't actually
+ # invoked.
+ monkeypatch.setattr(
+ ext, "_extract_kg_from_doc", lambda *a, **kw: []
+ )
+
+ ext.document_er_extraction("hello world")
+
+ rendered = "\n".join(m[1] for m in captured)
+ assert "Schema entity types with definitions" not in rendered
+ assert "Schema relationship types with definitions" not in rendered
+ assert "(system prompt)" in rendered
+
+
+def test_extractor_with_entity_definitions_appends_block(monkeypatch):
+ captured = _capture_prompt_messages(monkeypatch)
+ ext = LLMEntityRelationshipExtractor(
+ _StubLLMService(),
+ entity_type_definitions={
+ "Company": "A corporate entity.",
+ "Fund": "An investment vehicle pooling capital.",
+ },
+ )
+ monkeypatch.setattr(ext, "_extract_kg_from_doc", lambda *a, **kw: [])
+
+ ext.document_er_extraction("hello")
+
+ rendered = "\n".join(m[1] for m in captured)
+ assert "Schema entity types with definitions" in rendered
+ assert "- Company: A corporate entity." in rendered
+ assert "- Fund: An investment vehicle pooling capital." in rendered
+ # Sorted ordering: Company (C) before Fund (F)
+ assert rendered.index("Company") < rendered.index("Fund")
+ # Disambiguation tip is present
+ assert "disambiguate" in rendered.lower()
+
+
+def test_extractor_renders_relationship_definitions(monkeypatch):
+ captured = _capture_prompt_messages(monkeypatch)
+ ext = LLMEntityRelationshipExtractor(
+ _StubLLMService(),
+ relationship_type_definitions={
+ "PUBLISHES": "A company publishes a report.",
+ },
+ )
+ monkeypatch.setattr(ext, "_extract_kg_from_doc", lambda *a, **kw: [])
+
+ ext.document_er_extraction("hello")
+
+ rendered = "\n".join(m[1] for m in captured)
+ assert "Schema relationship types with definitions" in rendered
+ assert "- PUBLISHES: A company publishes a report." in rendered
+
+
+def test_extractor_skips_empty_definitions(monkeypatch):
+ captured = _capture_prompt_messages(monkeypatch)
+ ext = LLMEntityRelationshipExtractor(
+ _StubLLMService(),
+ entity_type_definitions={"Company": "", "Fund": ""},
+ )
+ monkeypatch.setattr(ext, "_extract_kg_from_doc", lambda *a, **kw: [])
+
+ ext.document_er_extraction("hello")
+
+ rendered = "\n".join(m[1] for m in captured)
+ # No definitions block when every value is empty.
+ assert "Schema entity types with definitions" not in rendered
+
+
+def test_extractor_combines_allowed_types_and_definitions(monkeypatch):
+ """Legacy ``allowed_entity_types`` still renders, AND definitions
+ block also renders below it. They are not mutually exclusive.
+ """
+ captured = _capture_prompt_messages(monkeypatch)
+ ext = LLMEntityRelationshipExtractor(
+ _StubLLMService(),
+ allowed_entity_types=["Company", "Fund"],
+ entity_type_definitions={"Company": "A corp."},
+ )
+ monkeypatch.setattr(ext, "_extract_kg_from_doc", lambda *a, **kw: [])
+
+ ext.document_er_extraction("hello")
+
+ rendered = "\n".join(m[1] for m in captured)
+ assert "Allowed Node Types" in rendered
+ assert "Schema entity types with definitions" in rendered
+ assert "- Company: A corp." in rendered
diff --git a/graphrag/tests/test_prompt_validation.py b/graphrag/tests/test_prompt_validation.py
new file mode 100644
index 0000000..b87bc97
--- /dev/null
+++ b/graphrag/tests/test_prompt_validation.py
@@ -0,0 +1,184 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Tests for ``common.utils.prompt_validation``."""
+
+from __future__ import annotations
+
+from common.utils.prompt_validation import validate_and_escape_prompt
+
+
+# ---------------------------------------------------------------------------
+# Required-placeholder validation
+# ---------------------------------------------------------------------------
+
+
+def test_chatbot_response_missing_required_returns_list():
+ out, missing = validate_and_escape_prompt(
+ "Hi! Just answer the {question} please.", "chatbot_response"
+ )
+ assert missing == ["context"]
+ # The provided required placeholder is preserved.
+ assert "{question}" in out
+
+
+def test_chatbot_response_all_required_present_returns_empty():
+ out, missing = validate_and_escape_prompt(
+ "You are a helpful assistant.\n\n"
+ "Context: {context}\n"
+ "Question: {question}\n",
+ "chatbot_response",
+ )
+ assert missing == []
+ assert "{context}" in out and "{question}" in out
+
+
+def test_community_summarization_required_set():
+ template = "Summarize {entity_name} given:\n{description_list}\n"
+ out, missing = validate_and_escape_prompt(template, "community_summarization")
+ assert missing == []
+ assert "{entity_name}" in out and "{description_list}" in out
+
+
+def test_query_generation_lists_all_missing_placeholders():
+ out, missing = validate_and_escape_prompt(
+ "Pick a query for {question} given vertices {vertices}.",
+ "query_generation",
+ )
+ assert set(missing) == {"conversation", "edges", "edgesInfo", "verticesAttrs"}
+ # Sorted, so we can assert the exact ordering for stability.
+ assert missing == sorted(missing)
+
+
+def test_entity_relationship_has_no_required_placeholders():
+ """``entity_relationship`` is a system-message-only prompt — its
+ customizable body doesn't need any required placeholders."""
+ out, missing = validate_and_escape_prompt(
+ "You are a knowledge-graph extractor. Bias toward concrete nouns.",
+ "entity_relationship",
+ )
+ assert missing == []
+
+
+def test_unknown_prompt_type_passes_through_unchanged():
+ """Forward-compatible: a prompt_type this module doesn't know about
+ must NOT block the save (avoids fail-closed regressions when a
+ new prompt type is added before this module is updated)."""
+ out, missing = validate_and_escape_prompt(
+ "Hello {world}!", "future_prompt_type_xyz"
+ )
+ assert out == "Hello {world}!"
+ assert missing == []
+
+
+# ---------------------------------------------------------------------------
+# Stray-placeholder escaping
+# ---------------------------------------------------------------------------
+
+
+def test_stray_placeholders_are_double_braced():
+ """Tokens that look like placeholders but aren't recognized for the
+ prompt type get escaped so str.format / PromptTemplate treats them
+ as literal text instead of trying to bind them."""
+ template = (
+ "Context: {context}\n"
+ "Question: {question}\n"
+ "For example: when the user asks {example_topic}, respond with "
+ "{TODO_fill_in_later}.\n"
+ )
+ out, missing = validate_and_escape_prompt(template, "chatbot_response")
+ assert missing == []
+ # Recognized placeholders unchanged.
+ assert "{context}" in out
+ assert "{question}" in out
+ # Stray placeholders escaped.
+ assert "{{example_topic}}" in out
+ assert "{{TODO_fill_in_later}}" in out
+ # And NOT left as bare braces.
+ assert "{example_topic}" not in out.replace("{{example_topic}}", "")
+ assert "{TODO_fill_in_later}" not in out.replace("{{TODO_fill_in_later}}", "")
+
+
+def test_already_escaped_double_braces_left_untouched():
+ """``{{ident}}`` is the format-string escape for a literal
+ ``{ident}``. Don't re-escape these."""
+ template = "Context: {context}\nThe user types {{not_a_placeholder}}.\n"
+ # Required is missing here; we still verify escaping is idempotent.
+ out, _ = validate_and_escape_prompt(template, "chatbot_response")
+ assert "{{not_a_placeholder}}" in out
+ # Make sure we didn't escape it AGAIN to {{{{...}}}}
+ assert "{{{{not_a_placeholder}}}}" not in out
+
+
+def test_partial_variables_are_recognized_not_escaped():
+ """``{format_instructions}`` is provided by the runtime as a
+ partial — appearance in user content is fine and must not be
+ escaped."""
+ template = (
+ "Context: {context}\n"
+ "Question: {question}\n"
+ "Output as: {format_instructions}\n"
+ )
+ out, missing = validate_and_escape_prompt(template, "chatbot_response")
+ assert missing == []
+ assert "{format_instructions}" in out # not escaped
+
+
+def test_escape_does_not_affect_required_placeholders_when_other_strays_present():
+ template = (
+ "Hi {question}.\n"
+ "Use {context} for facts.\n"
+ "Don't say {sensitive_word}.\n"
+ "Optional: {history}\n"
+ )
+ out, missing = validate_and_escape_prompt(template, "chatbot_response")
+ assert missing == []
+ assert "{question}" in out
+ assert "{context}" in out
+ assert "{history}" in out # in the "allowed partials" set
+ assert "{{sensitive_word}}" in out # stray → escaped
+
+
+def test_numeric_or_empty_brace_tokens_left_alone():
+ """``{}`` and ``{123}`` aren't valid Python identifiers; the regex
+ requires a leading letter / underscore. They should pass through
+ untouched."""
+ template = (
+ "Context: {context}\n"
+ "Question: {question}\n"
+ "Empty: {}, numeric-leading: {1abc}, full numeric: {123}.\n"
+ )
+ out, missing = validate_and_escape_prompt(template, "chatbot_response")
+ assert missing == []
+ assert "{}" in out
+ assert "{1abc}" in out
+ assert "{123}" in out
+
+
+def test_multiline_content_with_strays():
+ template = """You are a helpful assistant.
+
+When the user asks {question}, look at:
+
+ - The provided context: {context}
+ - Optional: {history}
+
+Examples of malformed inputs to ignore:
+ {bad_input_1}
+ {bad_input_2}
+ {bad_input_3}
+
+Respond as: {format_instructions}
+"""
+ out, missing = validate_and_escape_prompt(template, "chatbot_response")
+ assert missing == []
+ assert "{question}" in out and "{context}" in out
+ assert "{format_instructions}" in out
+ assert "{{bad_input_1}}" in out
+ assert "{{bad_input_2}}" in out
+ assert "{{bad_input_3}}" in out
diff --git a/graphrag/tests/test_schema_extraction.py b/graphrag/tests/test_schema_extraction.py
new file mode 100644
index 0000000..5b5d383
--- /dev/null
+++ b/graphrag/tests/test_schema_extraction.py
@@ -0,0 +1,200 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Unit tests for ``common.db.schema_extraction`` — the sample-doc
+schema-extraction prompt + concatenation helper.
+
+We do not invoke the LLM. The tests verify (a) the concat helper's
+truncation policy, (b) the prompt template renders the reserved-types
+list, (c) ``extract_schema_gsql`` calls ``invoke_with_parser`` with
+the right inputs and returns the LLM's text verbatim.
+"""
+
+from __future__ import annotations
+
+from common.db import schema_extraction
+
+
+_GENERIC_PROMPT_TEMPLATE = (
+ "Stub schema-extraction prompt for tests.\n"
+ "STRUCTURAL: {structural_types}\n"
+ "KEYWORDS: {tg_keywords}\n"
+ "SAMPLES:\n{samples}\n"
+)
+
+
+class _CapturingLLM:
+ def __init__(self, response: str = ""):
+ self.response = response
+ self.calls: list = []
+
+ @property
+ def schema_extraction_prompt(self) -> str:
+ return _GENERIC_PROMPT_TEMPLATE
+
+ def invoke_with_parser(self, prompt, parser, inputs, caller_name="x"):
+ self.calls.append({"prompt": prompt, "inputs": inputs, "caller_name": caller_name})
+ return self.response
+
+
+def test_concatenate_samples_joins_doc_id_headers():
+ samples = [
+ {"doc_id": "report1", "content": "Hello world."},
+ {"doc_id": "report2", "content": "Second body."},
+ ]
+ blob = schema_extraction.concatenate_samples(samples, max_chars=10_000)
+ assert "# report1" in blob
+ assert "# report2" in blob
+ assert "Hello world." in blob
+ assert "Second body." in blob
+
+
+def test_concatenate_samples_truncates_at_max_chars():
+ samples = [
+ {"doc_id": "a", "content": "x" * 1_000},
+ {"doc_id": "b", "content": "y" * 1_000},
+ ]
+ blob = schema_extraction.concatenate_samples(samples, max_chars=300)
+ assert len(blob) <= 300
+
+
+def test_concatenate_samples_handles_empty_content():
+ samples = [{"doc_id": "empty", "content": ""}]
+ blob = schema_extraction.concatenate_samples(samples, max_chars=1_000)
+ assert "# empty" in blob
+
+
+def test_extract_schema_gsql_passes_structural_and_keyword_lists_to_llm():
+ llm = _CapturingLLM(response="// A company.\nADD VERTEX Company();")
+ samples = [{"doc_id": "x", "content": "Acme Corp issues bonds."}]
+ out = schema_extraction.extract_schema_gsql(llm, samples)
+
+ assert out.startswith("// A company.")
+ assert len(llm.calls) == 1
+ inputs = llm.calls[0]["inputs"]
+ assert "samples" in inputs
+ assert "structural_types" in inputs
+ assert "tg_keywords" in inputs
+ # Structural-type names appear in the structural list — both vertex
+ # and edge types so the LLM doesn't propose either category.
+ assert "Document" in inputs["structural_types"]
+ assert "EntityType" in inputs["structural_types"]
+ assert "HAS_CONTENT" in inputs["structural_types"]
+ # GSQL keywords sourced from pyTigerGraph appear in the
+ # tg_keywords list — at least the high-frequency ones must be
+ # present so the LLM avoids common business-name collisions.
+ keyword_blob = inputs["tg_keywords"]
+ assert "TYPE" in keyword_blob
+ assert "VERTEX" in keyword_blob
+ assert "FROM" in keyword_blob
+ # Sample text is present in the rendered samples blob.
+ assert "Acme Corp" in inputs["samples"]
+ assert llm.calls[0]["caller_name"] == "schema_extraction"
+
+
+def test_extract_schema_gsql_returns_str_for_object_response():
+ """If the LLM returns a non-string (e.g. a Pydantic object), the
+ helper must coerce to str so the GSQL parser can consume it.
+ """
+
+ class _ObjResp:
+ def __str__(self): # noqa: D401
+ return "ADD VERTEX Foo();"
+
+ llm = _CapturingLLM(response=_ObjResp())
+ out = schema_extraction.extract_schema_gsql(
+ llm, [{"doc_id": "x", "content": "y"}]
+ )
+ assert "ADD VERTEX Foo" in out
+
+
+def test_extract_schema_gsql_round_trips_through_parser():
+ """End-to-end: the LLM's GSQL output, when fed back through the
+ permissive parser, produces a non-empty SchemaProposal. This pins
+ the contract between schema_extraction and schema_utils.
+
+ Exercises both ``ADD``-prefixed and bare ``VERTEX`` / ``EDGE``
+ forms (the new prompt asks the LLM for the bare form, the parser
+ still has to accept whichever the LLM produces) and both
+ ``DIRECTED`` and ``UNDIRECTED`` edges.
+ """
+ from common.db.schema_utils import parse_gsql_schema
+
+ response = (
+ "// A natural person.\n"
+ "VERTEX Person(name STRING, role STRING);\n"
+ "// An organization.\n"
+ "VERTEX Organization(name STRING);\n"
+ "// A person works for an organization.\n"
+ "DIRECTED EDGE WORKS_FOR(FROM Person, TO Organization, role STRING);\n"
+ "// Two people are colleagues.\n"
+ "UNDIRECTED EDGE COLLEAGUE_OF(FROM Person, TO Person);\n"
+ )
+ llm = _CapturingLLM(response=response)
+ gsql = schema_extraction.extract_schema_gsql(
+ llm, [{"doc_id": "x", "content": "y"}]
+ )
+ proposal = parse_gsql_schema(gsql)
+ proposal.drop_dangling_pairs()
+ assert {v.name for v in proposal.vertices} == {"Person", "Organization"}
+ edge_names = {e.name for e in proposal.edges}
+ assert "WORKS_FOR" in edge_names
+ assert "COLLEAGUE_OF" in edge_names
+ works_for = next(e for e in proposal.edges if e.name == "WORKS_FOR")
+ colleague_of = next(e for e in proposal.edges if e.name == "COLLEAGUE_OF")
+ assert works_for.directed is True
+ assert colleague_of.directed is False
+
+
+def test_extract_schema_gsql_uses_llm_service_prompt_getter():
+ """Prompt loading is delegated to llm_service.schema_extraction_prompt
+ (the centralized base_llm getter that handles per-graph override
+ resolution). The extract helper must read it via that property —
+ no duplicate path-resolution code in this module.
+ """
+
+ class _StubLLM(_CapturingLLM):
+ @property
+ def schema_extraction_prompt(self) -> str:
+ return (
+ "STUB PROMPT\n"
+ "{structural_types}\n"
+ "{tg_keywords}\n"
+ "{samples}\n"
+ )
+
+ llm = _StubLLM(response="// V.\nVERTEX V();")
+ out = schema_extraction.extract_schema_gsql(
+ llm, [{"doc_id": "x", "content": "y"}]
+ )
+ assert "VERTEX V" in out
+ inputs = llm.calls[0]["inputs"]
+ # The prompt template was rendered against the three required
+ # placeholders the stub exposed.
+ assert "samples" in inputs
+ assert "structural_types" in inputs
+ assert "tg_keywords" in inputs
+
+
+def test_extract_schema_gsql_propagates_missing_prompt_file():
+ """If llm_service.schema_extraction_prompt raises FileNotFoundError,
+ extract_schema_gsql must propagate — no silent fallback. The
+ file is expected to be present in every shipped provider dir.
+ """
+
+ class _MissingPromptLLM(_CapturingLLM):
+ @property
+ def schema_extraction_prompt(self) -> str:
+ raise FileNotFoundError("schema_extraction.txt not found")
+
+ import pytest as _pytest
+ llm = _MissingPromptLLM()
+ with _pytest.raises(FileNotFoundError):
+ schema_extraction.extract_schema_gsql(
+ llm, [{"doc_id": "x", "content": "y"}]
+ )
diff --git a/graphrag/tests/test_schema_utils.py b/graphrag/tests/test_schema_utils.py
new file mode 100644
index 0000000..5ed6e2a
--- /dev/null
+++ b/graphrag/tests/test_schema_utils.py
@@ -0,0 +1,1133 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Unit tests for ``common.db.schema_utils``.
+
+Covers the permissive GSQL parser, the additive GSQL emitter, the
+structural-type filter, comment-as-description extraction, and the
+``gsql ls`` output form.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from common.db.schema_utils import (
+ ExistingSchema,
+ SchemaProposal,
+ apply_proposal,
+ build_schema_change_job,
+ emit_add_statements,
+ emit_preview_gsql,
+ emit_structural_link_alters,
+ is_structural_type,
+ parse_gsql_schema,
+ read_existing_schema,
+ read_type_metadata,
+ summarize,
+ upsert_type_metadata,
+)
+
+
+class _FakeConn:
+ """Minimal pyTigerGraph-shaped connection for read_existing_schema tests."""
+
+ def __init__(self, vertex_types, edge_metadata, gsql_response="OK"):
+ self._vertex_types = list(vertex_types)
+ self._edge_metadata = dict(edge_metadata)
+ self._gsql_response = gsql_response
+ self.gsql_calls = []
+ self.upsert_calls = []
+
+ def getVertexTypes(self):
+ return list(self._vertex_types)
+
+ def getEdgeTypes(self):
+ return list(self._edge_metadata.keys())
+
+ def getEdgeType(self, name):
+ return self._edge_metadata.get(name, {})
+
+ def gsql(self, command):
+ self.gsql_calls.append(command)
+ return self._gsql_response
+
+ def upsertVertex(self, vertex_type, vertex_id, attributes=None):
+ self.upsert_calls.append((vertex_type, vertex_id, dict(attributes or {})))
+
+
+class _FakeConnWithVertices(_FakeConn):
+ """Extends _FakeConn with a getVertices() that returns canned rows."""
+
+ def __init__(self, vertex_types, edge_metadata, vertices_by_type):
+ super().__init__(vertex_types, edge_metadata)
+ self._vertices_by_type = dict(vertices_by_type)
+
+ def getVertices(self, vertexType, **kwargs):
+ return list(self._vertices_by_type.get(vertexType, []))
+
+
+# ---------------------------------------------------------------------------
+# Structural-type filter
+# ---------------------------------------------------------------------------
+
+
+def test_is_structural_type_recognises_canonical_set():
+ assert is_structural_type("Document")
+ assert is_structural_type("EntityType")
+ assert is_structural_type("HAS_CONTENT")
+ # Case-insensitive
+ assert is_structural_type("document")
+ assert is_structural_type("entitytype")
+
+
+def test_is_structural_type_recognises_reverse_companions():
+ assert is_structural_type("reverse_HAS_CONTENT")
+ assert is_structural_type("reverse_RELATIONSHIP")
+ # Reverse of arbitrary edge names too — we don't try to parse them
+ assert is_structural_type("reverse_PUBLISHES")
+
+
+def test_is_structural_type_rejects_domain_names():
+ assert not is_structural_type("Company")
+ assert not is_structural_type("Fund")
+ assert not is_structural_type("PUBLISHES")
+ assert not is_structural_type("")
+
+
+# ---------------------------------------------------------------------------
+# Parser — happy paths
+# ---------------------------------------------------------------------------
+
+
+def test_parse_simple_add_form():
+ text = """
+ ADD VERTEX Company();
+ ADD VERTEX Report();
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);
+ """
+ proposal = parse_gsql_schema(text)
+ assert {v.name for v in proposal.vertices} == {"Company", "Report"}
+ assert len(proposal.edges) == 1
+ assert proposal.edges[0].name == "PUBLISHES"
+ assert proposal.edges[0].pairs == [("Company", "Report")]
+
+
+def test_parse_with_attributes_captures_attrs_and_strips_primary_id():
+ """The parser captures primitive (name, type) attributes and skips
+ PRIMARY_ID (which is system-generated). WITH-clauses are ignored.
+ """
+ text = """
+ ADD VERTEX Company(PRIMARY_ID id STRING, name STRING, founded_year INT)
+ WITH PRIMARY_ID_AS_ATTRIBUTE="true", STATS="OUTDEGREE_BY_EDGETYPE";
+ ADD DIRECTED EDGE OWNS(FROM Company, TO Company, percent_owned DOUBLE)
+ WITH REVERSE_EDGE="reverse_OWNS";
+ """
+ proposal = parse_gsql_schema(text)
+ company = proposal.find_vertex("Company")
+ assert company is not None
+ attr_names = [a.name for a in company.attributes]
+ attr_types = [a.type for a in company.attributes]
+ assert attr_names == ["name", "founded_year"]
+ assert attr_types == ["STRING", "INT"]
+ # PRIMARY_ID's "id" must NOT appear as a regular attribute.
+ assert "id" not in [a.name for a in company.attributes]
+
+ edge = proposal.find_edge("OWNS")
+ assert edge is not None and edge.pairs == [("Company", "Company")]
+ edge_attrs = [(a.name, a.type) for a in edge.attributes]
+ assert edge_attrs == [("percent_owned", "DOUBLE")]
+
+
+def test_emit_includes_attributes_in_add_vertex_and_edge():
+ """The emitter renders attributes after the auto-added PRIMARY_ID
+ on vertices and after the FROM/TO pairs on edges.
+ """
+ proposal = SchemaProposal()
+ proposal.add_vertex(
+ "Company",
+ attributes=[("name", "STRING"), ("founded_year", "INT")],
+ )
+ proposal.add_vertex("Report", attributes=[("title", "STRING")])
+ proposal.add_edge_pair(
+ "PUBLISHES",
+ "Company",
+ "Report",
+ attributes=[("effective_date", "STRING")],
+ )
+
+ stmts = emit_add_statements(proposal)
+ company_stmt = next(s for s in stmts if "ADD VERTEX Company" in s)
+ assert "PRIMARY_ID id STRING" in company_stmt
+ assert "name STRING" in company_stmt
+ assert "founded_year INT" in company_stmt
+
+ edge_stmt = next(s for s in stmts if "ADD DIRECTED EDGE PUBLISHES" in s)
+ assert "FROM Company, TO Report" in edge_stmt
+ assert "effective_date STRING" in edge_stmt
+
+
+def test_emit_filters_unknown_primitive_types():
+ """Attributes whose type isn't a known GSQL primitive are dropped
+ silently — they would error at schema-change time otherwise.
+ """
+ proposal = SchemaProposal()
+ proposal.add_vertex(
+ "Company",
+ attributes=[
+ ("name", "STRING"),
+ ("note", "VARCHAR"), # not a GSQL primitive
+ ("count", "INT"),
+ ],
+ )
+ company = proposal.find_vertex("Company")
+ assert [a.name for a in company.attributes] == ["name", "count"]
+
+
+def test_parse_multi_pair_edge_produces_one_edge_with_multiple_pairs():
+ text = """
+ ADD VERTEX Company();
+ ADD VERTEX Report();
+ ADD VERTEX Filing();
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report | FROM Company, TO Filing);
+ """
+ proposal = parse_gsql_schema(text)
+ edge = proposal.find_edge("PUBLISHES")
+ assert edge is not None
+ assert edge.pairs == [("Company", "Report"), ("Company", "Filing")]
+
+
+def test_parse_descriptions_from_double_slash_comments():
+ text = """
+ // A corporate or business entity.
+ ADD VERTEX Company();
+
+ // A formal document
+ // summarising performance.
+ ADD VERTEX Report();
+
+ // Company publishes a Report.
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);
+ """
+ proposal = parse_gsql_schema(text)
+ assert proposal.find_vertex("Company").description == \
+ "A corporate or business entity."
+ # Multi-line // comments are joined
+ assert "summarising performance" in proposal.find_vertex("Report").description
+ assert proposal.find_edge("PUBLISHES").description.startswith(
+ "Company publishes"
+ )
+
+
+def test_parse_descriptions_from_block_comment():
+ text = """
+ /*
+ * A corporate or business entity.
+ */
+ ADD VERTEX Company();
+ """
+ proposal = parse_gsql_schema(text)
+ assert "corporate or business entity" in (
+ proposal.find_vertex("Company").description
+ )
+
+
+# ---------------------------------------------------------------------------
+# Parser — `gsql ls` output form
+# ---------------------------------------------------------------------------
+
+
+def test_parse_ls_output_form():
+ text = """
+ Vertex Types:
+ - VERTEX DocumentChunk(PRIMARY_ID id STRING) WITH STATS="..."
+ - VERTEX Company(PRIMARY_ID id STRING, name STRING) WITH PRIMARY_ID_AS_ATTRIBUTE="true"
+ - VERTEX Report(PRIMARY_ID id STRING) WITH PRIMARY_ID_AS_ATTRIBUTE="true"
+ Edge Types:
+ - DIRECTED EDGE HAS_CONTENT(FROM DocumentChunk, TO Content) WITH REVERSE_EDGE="reverse_HAS_CONTENT"
+ - DIRECTED EDGE reverse_HAS_CONTENT(FROM Content, TO DocumentChunk) WITH REVERSE_EDGE="HAS_CONTENT"
+ - DIRECTED EDGE PUBLISHES(FROM Company, TO Report) WITH REVERSE_EDGE="reverse_PUBLISHES"
+ - DIRECTED EDGE reverse_PUBLISHES(FROM Report, TO Company) WITH REVERSE_EDGE="PUBLISHES"
+ Indexes:
+ - some_index:Foo(bar)
+ """
+ proposal = parse_gsql_schema(text)
+ # Structural types (DocumentChunk, HAS_CONTENT, reverse_HAS_CONTENT)
+ # silently dropped; only Company, Report, PUBLISHES survive.
+ assert {v.name for v in proposal.vertices} == {"Company", "Report"}
+ assert [e.name for e in proposal.edges] == ["PUBLISHES"]
+ assert proposal.find_edge("PUBLISHES").pairs == [("Company", "Report")]
+ # Section headers and Indexes block silently ignored — no errors raised.
+
+
+# ---------------------------------------------------------------------------
+# Parser — permissive: noise is silently dropped
+# ---------------------------------------------------------------------------
+
+
+def test_parse_silently_ignores_unrelated_statements():
+ text = """
+ CREATE GRAPH FooBar(*);
+ INSTALL QUERY ALL;
+ USE GRAPH FooBar;
+
+ Some prose preamble line.
+
+ ADD VERTEX Company();
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);
+ ADD VERTEX Report();
+
+ DROP QUERY ALL;
+ """
+ proposal = parse_gsql_schema(text)
+ # Order of vertex declarations doesn't matter — pairs are filtered after.
+ assert {v.name for v in proposal.vertices} == {"Company", "Report"}
+ assert proposal.find_edge("PUBLISHES").pairs == [("Company", "Report")]
+
+
+def test_parse_drops_pairs_with_unknown_endpoint():
+ text = """
+ ADD VERTEX Company();
+ // Report is missing on purpose
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report | FROM Company, TO Filing);
+ ADD VERTEX Filing();
+ """
+ proposal = parse_gsql_schema(text)
+ # The Company → Report pair has a dangling endpoint → dropped.
+ # Company → Filing survives because Filing is declared.
+ assert proposal.find_edge("PUBLISHES").pairs == [("Company", "Filing")]
+
+
+def test_parse_drops_structural_type_collisions():
+ text = """
+ ADD VERTEX Document(); // dropped — structural
+ ADD VERTEX Company(); // kept
+ ADD VERTEX HAS_CONTENT(); // dropped — structural edge name
+ ADD DIRECTED EDGE HAS_CONTENT(FROM Document, TO Content); // dropped
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);
+ ADD VERTEX Report();
+ """
+ proposal = parse_gsql_schema(text)
+ names = {v.name for v in proposal.vertices}
+ assert "Document" not in names
+ assert "HAS_CONTENT" not in names
+ assert names == {"Company", "Report"}
+ assert [e.name for e in proposal.edges] == ["PUBLISHES"]
+
+
+def test_parse_empty_input_yields_empty_proposal():
+ proposal = parse_gsql_schema("")
+ assert proposal.vertices == []
+ assert proposal.edges == []
+ proposal2 = parse_gsql_schema("Just some prose, no GSQL here.")
+ assert proposal2.vertices == []
+ assert proposal2.edges == []
+
+
+# ---------------------------------------------------------------------------
+# Emitter — diff against existing
+# ---------------------------------------------------------------------------
+
+
+def test_emit_against_empty_existing_adds_everything():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Report")
+
+ stmts = emit_add_statements(proposal, existing=ExistingSchema())
+ assert any(s.startswith("ADD VERTEX Company") for s in stmts)
+ assert any(s.startswith("ADD VERTEX Report") for s in stmts)
+ assert any(
+ s.startswith("ADD DIRECTED EDGE PUBLISHES") and "FROM Company, TO Report" in s
+ for s in stmts
+ )
+
+
+def test_emit_skips_vertices_already_in_graph():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+
+ existing = ExistingSchema(vertex_types={"Company"})
+ stmts = emit_add_statements(proposal, existing=existing)
+ assert not any("ADD VERTEX Company" in s for s in stmts)
+ assert any("ADD VERTEX Report" in s for s in stmts)
+
+
+def test_emit_alter_edge_when_pair_is_new_on_existing_edge():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Filing")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Filing")
+
+ existing = ExistingSchema(
+ vertex_types={"Company"},
+ edge_pairs={"PUBLISHES": {("Company", "Report")}},
+ )
+ stmts = emit_add_statements(proposal, existing=existing)
+ # New vertex:
+ assert any("ADD VERTEX Filing" in s for s in stmts)
+ # No fresh ADD DIRECTED EDGE — PUBLISHES already exists:
+ assert not any(s.startswith("ADD DIRECTED EDGE PUBLISHES") for s in stmts)
+ # ALTER … ADD PAIR for the new pair:
+ assert any(
+ "ALTER EDGE PUBLISHES ADD PAIR (FROM Company, TO Filing)" in s
+ for s in stmts
+ )
+
+
+def test_emit_skips_pair_already_on_edge():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Report")
+
+ existing = ExistingSchema(
+ vertex_types={"Company", "Report"},
+ edge_pairs={"PUBLISHES": {("Company", "Report")}},
+ )
+ stmts = emit_add_statements(proposal, existing=existing)
+ assert stmts == [] # everything already exists
+
+
+# ---------------------------------------------------------------------------
+# Preview rendering
+# ---------------------------------------------------------------------------
+
+
+def test_emit_preview_gsql_round_trips_through_parser():
+ proposal = SchemaProposal(domain_label="Corp Gov")
+ proposal.add_vertex("Company", description="A corporate entity.")
+ proposal.add_vertex("Report", description="A formal document.")
+ proposal.add_edge_pair(
+ "PUBLISHES", "Company", "Report",
+ description="Company publishes a Report.",
+ )
+
+ preview = emit_preview_gsql(proposal)
+ assert "ADD VERTEX Company" in preview
+ assert "ADD DIRECTED EDGE PUBLISHES" in preview
+ assert "FROM Company, TO Report" in preview
+
+ # Round-trip: parse the preview back and verify it reconstructs the
+ # same vertex / edge / pair set (descriptions are preserved too).
+ reparsed = parse_gsql_schema(preview)
+ assert {v.name for v in reparsed.vertices} == {"Company", "Report"}
+ assert [(e.name, e.pairs) for e in reparsed.edges] == [
+ ("PUBLISHES", [("Company", "Report")])
+ ]
+ assert reparsed.find_vertex("Company").description == "A corporate entity."
+ assert reparsed.find_edge("PUBLISHES").description.startswith(
+ "Company publishes"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Summary helper
+# ---------------------------------------------------------------------------
+
+
+def test_summarize_returns_counts_and_names():
+ proposal = SchemaProposal(domain_label="Corp")
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Report")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Filing")
+
+ summary = summarize(proposal)
+ assert summary["vertex_count"] == 2
+ assert summary["edge_count"] == 1
+ assert summary["edge_pair_count"] == 2
+ assert summary["domain_label"] == "Corp"
+ assert set(summary["vertex_names"]) == {"Company", "Report"}
+ assert summary["edge_names"] == ["PUBLISHES"]
+
+
+# ---------------------------------------------------------------------------
+# from_dict / to_dict round-trip
+# ---------------------------------------------------------------------------
+
+
+# ---------------------------------------------------------------------------
+# read_existing_schema — TigerGraph-side reader
+# ---------------------------------------------------------------------------
+
+
+def test_read_existing_schema_empty_graph():
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+ snapshot = read_existing_schema(conn)
+ assert snapshot.vertex_types == set()
+ assert snapshot.edge_pairs == {}
+
+
+def test_read_existing_schema_single_pair_edge():
+ conn = _FakeConn(
+ vertex_types=["Document", "Entity", "Company"],
+ edge_metadata={
+ "PUBLISHES": {
+ "Name": "PUBLISHES",
+ "FromVertexTypeName": "Company",
+ "ToVertexTypeName": "Report",
+ },
+ },
+ )
+ snapshot = read_existing_schema(conn)
+ assert snapshot.has_vertex("Company")
+ assert snapshot.has_edge("PUBLISHES")
+ assert snapshot.has_edge_pair("PUBLISHES", "Company", "Report")
+
+
+def test_read_existing_schema_multi_pair_edge():
+ """When FromVertexTypeName/ToVertexTypeName are '*', the metadata's
+ EdgePairs list carries the actual (FROM, TO) pairs.
+ """
+ conn = _FakeConn(
+ vertex_types=["DocumentChunk", "Entity", "Document"],
+ edge_metadata={
+ "CONTAINS_ENTITY": {
+ "Name": "CONTAINS_ENTITY",
+ "FromVertexTypeName": "*",
+ "ToVertexTypeName": "*",
+ "EdgePairs": [
+ {"From": "DocumentChunk", "To": "Entity"},
+ {"From": "Document", "To": "Entity"},
+ ],
+ },
+ },
+ )
+ snapshot = read_existing_schema(conn)
+ assert snapshot.has_edge_pair("CONTAINS_ENTITY", "DocumentChunk", "Entity")
+ assert snapshot.has_edge_pair("CONTAINS_ENTITY", "Document", "Entity")
+
+
+def test_read_existing_schema_feeds_emit_add_statements_diff():
+ """End-to-end: existing graph has Company + PUBLISHES(Company→Report),
+ proposal wants to add Filing + PUBLISHES(Company→Filing). The emitter
+ should produce one ADD VERTEX (for Filing) and one ALTER EDGE … ADD PAIR.
+ """
+ conn = _FakeConn(
+ vertex_types=["Company", "Report"],
+ edge_metadata={
+ "PUBLISHES": {
+ "FromVertexTypeName": "Company",
+ "ToVertexTypeName": "Report",
+ },
+ },
+ )
+ existing = read_existing_schema(conn)
+
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+ proposal.add_vertex("Filing")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Report") # already there
+ proposal.add_edge_pair("PUBLISHES", "Company", "Filing") # new pair
+
+ stmts = emit_add_statements(proposal, existing=existing)
+ assert any("ADD VERTEX Filing" in s for s in stmts)
+ assert not any("ADD VERTEX Company" in s for s in stmts)
+ assert any(
+ "ALTER EDGE PUBLISHES ADD PAIR (FROM Company, TO Filing)" in s
+ for s in stmts
+ )
+ # No fresh ADD DIRECTED EDGE since PUBLISHES already exists.
+ assert not any(s.startswith("ADD DIRECTED EDGE PUBLISHES") for s in stmts)
+
+
+def test_to_from_dict_round_trip():
+ original = SchemaProposal(domain_label="Corp")
+ original.add_vertex("Company", description="A corporate entity.")
+ original.add_vertex("Report")
+ original.add_edge_pair(
+ "PUBLISHES", "Company", "Report",
+ description="Company publishes a Report.",
+ )
+
+ data = original.to_dict()
+ reconstructed = SchemaProposal.from_dict(data)
+ assert reconstructed.domain_label == "Corp"
+ assert {v.name for v in reconstructed.vertices} == {"Company", "Report"}
+ edge = reconstructed.find_edge("PUBLISHES")
+ assert edge.pairs == [("Company", "Report")]
+ assert edge.description.startswith("Company publishes")
+
+
+# ---------------------------------------------------------------------------
+# build_schema_change_job
+# ---------------------------------------------------------------------------
+
+
+def test_build_schema_change_job_wraps_statements_in_atomic_block():
+ stmts = [
+ 'ADD VERTEX Company (PRIMARY_ID id STRING) WITH PRIMARY_ID_AS_ATTRIBUTE="true"',
+ "ALTER EDGE PUBLISHES ADD PAIR (FROM Company, TO Filing)",
+ ]
+ block, job_name = build_schema_change_job("MyGraph", stmts)
+
+ assert job_name.startswith("add_domain_schema_")
+ assert "USE GRAPH MyGraph" in block
+ assert f"CREATE SCHEMA_CHANGE JOB {job_name} FOR GRAPH MyGraph" in block
+ assert f"RUN SCHEMA_CHANGE JOB {job_name}" in block
+ assert f"DROP JOB {job_name}" in block
+ # All inner statements appear and are terminated with ';'
+ for s in stmts:
+ assert s in block
+ assert block.count(";") == len(stmts)
+
+
+def test_build_schema_change_job_strips_trailing_semicolons_to_avoid_duplicates():
+ stmts = ["ADD VERTEX Company (PRIMARY_ID id STRING);"]
+ block, _ = build_schema_change_job("g", stmts)
+ # Exactly one terminator inside the body — the helper added it back.
+ assert block.count(";;") == 0
+ assert block.count(";") == 1
+
+
+def test_build_schema_change_job_respects_explicit_job_name():
+ block, job_name = build_schema_change_job(
+ "g",
+ ["ADD VERTEX Company (PRIMARY_ID id STRING)"],
+ job_name="fixed_job",
+ )
+ assert job_name == "fixed_job"
+ assert "CREATE SCHEMA_CHANGE JOB fixed_job" in block
+ assert "RUN SCHEMA_CHANGE JOB fixed_job" in block
+
+
+def test_build_schema_change_job_empty_statements_raises():
+ with pytest.raises(ValueError):
+ build_schema_change_job("g", [])
+
+
+# ---------------------------------------------------------------------------
+# apply_proposal
+# ---------------------------------------------------------------------------
+
+
+def test_apply_proposal_no_op_when_diff_is_empty():
+ """If the existing graph already has every type in the proposal, the
+ helper must not call gsql at all and must report status='no-op'.
+ """
+ conn = _FakeConn(
+ vertex_types=["Company", "Report"],
+ edge_metadata={
+ "PUBLISHES": {
+ "FromVertexTypeName": "Company",
+ "ToVertexTypeName": "Report",
+ },
+ },
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+ proposal.add_edge_pair("PUBLISHES", "Company", "Report")
+
+ result = apply_proposal(conn, "g", proposal)
+
+ assert result["status"] == "no-op"
+ assert result["statements"] == []
+ assert result["job_name"] is None
+ assert result["gsql_output"] == ""
+ assert conn.gsql_calls == []
+ assert result["summary"]["vertex_count"] == 2
+
+
+def test_apply_proposal_runs_single_gsql_call_with_diff():
+ """On a partially-populated graph the helper should issue exactly one
+ gsql() call — the wrapped CREATE / RUN / DROP block — containing only
+ the missing ADD/ALTER statements.
+ """
+ conn = _FakeConn(
+ vertex_types=["Company"],
+ edge_metadata={},
+ gsql_response="JOB add_domain_schema_xxx COMPLETED",
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Filing")
+ proposal.add_edge_pair("OWNS", "Company", "Filing")
+
+ result = apply_proposal(conn, "MyGraph", proposal)
+
+ assert result["status"] == "applied"
+ assert len(conn.gsql_calls) == 1
+ cmd = conn.gsql_calls[0]
+ assert "USE GRAPH MyGraph" in cmd
+ assert "ADD VERTEX Filing" in cmd
+ assert "ADD DIRECTED EDGE OWNS" in cmd
+ # Must NOT re-add the existing Company vertex.
+ assert "ADD VERTEX Company" not in cmd
+ assert "JOB COMPLETED" in result["gsql_output"] or "COMPLETED" in result["gsql_output"]
+ assert result["job_name"].startswith("add_domain_schema_")
+ assert any("ADD VERTEX Filing" in s for s in result["statements"])
+
+
+def test_apply_proposal_drops_structural_collisions_before_diff():
+ """parse_gsql_schema is the public contract for ingesting user input,
+ but apply_proposal also receives proposals constructed in code. If a
+ caller builds a proposal that names a structural type, the diff
+ against an empty graph should still skip it (no ADD VERTEX Document)
+ because read_existing_schema's snapshot won't contain it on a fresh
+ graph — so we rely on parse_gsql_schema's filter, *or* the caller
+ must filter manually. This test pins the contract: apply_proposal
+ does NOT silently filter; the caller is responsible. Used to detect
+ regressions if someone adds 'helpful' filtering to apply_proposal.
+ """
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+ proposal = SchemaProposal()
+ proposal.add_vertex("Document") # structural — caller's mistake
+
+ result = apply_proposal(conn, "g", proposal)
+ # Statement is emitted because apply_proposal does NOT re-filter;
+ # the parse step is where structural collisions are dropped.
+ assert any("ADD VERTEX Document" in s for s in result["statements"])
+
+
+def test_apply_proposal_end_to_end_from_pasted_gsql():
+ """Driving the full Slice 1c happy path: pasted GSQL → parser →
+ drop_dangling_pairs → apply_proposal against a fresh graph runs one
+ job containing everything.
+ """
+ pasted = """
+ // A corporate entity.
+ ADD VERTEX Company();
+ // A regulatory filing.
+ ADD VERTEX Filing();
+ // A company publishes a filing.
+ ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Filing);
+ """
+ proposal = parse_gsql_schema(pasted)
+ proposal.drop_dangling_pairs()
+
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+ result = apply_proposal(conn, "FreshGraph", proposal)
+
+ assert result["status"] == "applied"
+ assert len(conn.gsql_calls) == 1
+ cmd = conn.gsql_calls[0]
+ assert "USE GRAPH FreshGraph" in cmd
+ assert "ADD VERTEX Company" in cmd
+ assert "ADD VERTEX Filing" in cmd
+ assert "ADD DIRECTED EDGE PUBLISHES" in cmd
+ assert "FROM Company, TO Filing" in cmd
+ assert result["summary"]["vertex_count"] == 2
+ assert result["summary"]["edge_count"] == 1
+
+
+# ---------------------------------------------------------------------------
+# upsert_type_metadata
+# ---------------------------------------------------------------------------
+
+
+def test_upsert_type_metadata_writes_entity_and_relationship_rows():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company", description="A corporate entity.")
+ proposal.add_vertex("Report") # no description → omits attribute
+ proposal.add_edge_pair(
+ "PUBLISHES", "Company", "Report",
+ description="Company publishes a Report.",
+ )
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+
+ result = upsert_type_metadata(conn, proposal)
+
+ assert result == {
+ "entity_types": ["Company", "Report"],
+ "relationship_types": ["PUBLISHES"],
+ }
+ types_written = [(c[0], c[1]) for c in conn.upsert_calls]
+ assert ("EntityType", "Company") in types_written
+ assert ("EntityType", "Report") in types_written
+ assert ("RelationshipType", "PUBLISHES") in types_written
+
+ company_call = next(c for c in conn.upsert_calls if c[1] == "Company")
+ assert company_call[2]["description"] == "A corporate entity."
+ assert "epoch_added" in company_call[2]
+
+ report_call = next(c for c in conn.upsert_calls if c[1] == "Report")
+ assert "description" not in report_call[2]
+ assert "epoch_added" in report_call[2]
+
+ pub_call = next(c for c in conn.upsert_calls if c[1] == "PUBLISHES")
+ assert pub_call[2]["definition"] == "Company publishes a Report."
+ assert pub_call[2]["short_name"] == "publishes"
+ assert "epoch_added" in pub_call[2]
+
+
+def test_apply_proposal_populates_metadata_on_apply():
+ """End-to-end Slice 1d: when apply_proposal runs the schema-change
+ job, metadata vertices must also be written for every type in the
+ proposal. The result dict must surface the upsert summary so callers
+ can log / return it.
+ """
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company", description="A corp.")
+ proposal.add_edge_pair(
+ "OWNS", "Company", "Company", description="Self-ownership."
+ )
+
+ result = apply_proposal(conn, "g", proposal)
+
+ assert result["status"] == "applied"
+ assert result["metadata"]["entity_types"] == ["Company"]
+ assert result["metadata"]["relationship_types"] == ["OWNS"]
+ assert any(c[0] == "EntityType" and c[1] == "Company" for c in conn.upsert_calls)
+ assert any(
+ c[0] == "RelationshipType" and c[1] == "OWNS" for c in conn.upsert_calls
+ )
+
+
+def test_apply_proposal_populates_metadata_on_no_op():
+ """Even when the schema diff is empty, metadata vertices must be
+ upserted so descriptions edited on the review screen land in the
+ graph (the schema is already there from a prior init).
+ """
+ conn = _FakeConn(
+ vertex_types=["Company"],
+ edge_metadata={
+ "OWNS": {
+ "FromVertexTypeName": "Company",
+ "ToVertexTypeName": "Company",
+ },
+ },
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company", description="Updated description.")
+ proposal.add_edge_pair(
+ "OWNS", "Company", "Company", description="Updated definition."
+ )
+
+ result = apply_proposal(conn, "g", proposal)
+
+ assert result["status"] == "no-op"
+ assert result["metadata"]["entity_types"] == ["Company"]
+ assert result["metadata"]["relationship_types"] == ["OWNS"]
+ company = next(c for c in conn.upsert_calls if c[1] == "Company")
+ assert company[2]["description"] == "Updated description."
+ owns = next(c for c in conn.upsert_calls if c[1] == "OWNS")
+ assert owns[2]["definition"] == "Updated definition."
+
+
+# ---------------------------------------------------------------------------
+# read_type_metadata
+# ---------------------------------------------------------------------------
+
+
+def test_read_type_metadata_returns_descriptions_and_definitions():
+ conn = _FakeConnWithVertices(
+ vertex_types=[],
+ edge_metadata={},
+ vertices_by_type={
+ "EntityType": [
+ {"v_id": "Company", "attributes": {"description": "A corp."}},
+ {"v_id": "Report", "attributes": {"description": ""}},
+ {"v_id": "Filing", "attributes": {"description": "A filing."}},
+ ],
+ "RelationshipType": [
+ {
+ "v_id": "PUBLISHES",
+ "attributes": {"definition": "Company publishes a Report."},
+ },
+ {
+ "v_id": "OWNS",
+ "attributes": {"definition": ""},
+ },
+ ],
+ },
+ )
+
+ entity_descs, rel_defs = read_type_metadata(conn)
+
+ assert entity_descs == {"Company": "A corp.", "Filing": "A filing."}
+ assert rel_defs == {"PUBLISHES": "Company publishes a Report."}
+
+
+def test_read_type_metadata_returns_empty_on_missing_method():
+ """When conn lacks getVertices (older mock / stub), behave gracefully
+ rather than raising — schema_rep must still render.
+ """
+ conn = _FakeConn(vertex_types=[], edge_metadata={}) # no getVertices
+ entity_descs, rel_defs = read_type_metadata(conn)
+ assert entity_descs == {}
+ assert rel_defs == {}
+
+
+# ---------------------------------------------------------------------------
+# emit_structural_link_alters
+# ---------------------------------------------------------------------------
+
+
+def test_emit_structural_links_for_new_domain_vertices():
+ """Each new domain vertex must get CONTAINS_ENTITY pairs from
+ Document and DocumentChunk. IS_HEAD_OF / HAS_TAIL are NOT added
+ per-domain-vertex — they live at the EntityType ↔ RelationshipType
+ meta-schema layer and the original schema declaration covers the
+ only pair we ever traverse.
+ """
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+
+ existing = ExistingSchema(
+ vertex_types={
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType",
+ "Company", "Report",
+ },
+ edge_pairs={
+ "CONTAINS_ENTITY": {("Document", "Entity"), ("DocumentChunk", "Entity")},
+ "IS_HEAD_OF": {("EntityType", "RelationshipType")},
+ "HAS_TAIL": {("RelationshipType", "EntityType")},
+ },
+ )
+
+ stmts = emit_structural_link_alters(proposal, existing)
+
+ # Each vertex gets two CONTAINS_ENTITY pair-additions: 2*2 = 4.
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in stmts
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in stmts
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Report)" in stmts
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Report)" in stmts
+ # No per-domain-vertex IS_HEAD_OF / HAS_TAIL emitted.
+ assert not any("IS_HEAD_OF" in s for s in stmts)
+ assert not any("HAS_TAIL" in s for s in stmts)
+ assert len(stmts) == 4
+
+
+def test_emit_structural_links_skips_already_present_pairs():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ existing = ExistingSchema(
+ vertex_types={
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType",
+ "Company",
+ },
+ edge_pairs={
+ "CONTAINS_ENTITY": {
+ ("Document", "Entity"),
+ ("DocumentChunk", "Entity"),
+ ("Document", "Company"), # already there
+ },
+ "IS_HEAD_OF": {("EntityType", "RelationshipType")},
+ "HAS_TAIL": {("RelationshipType", "EntityType")},
+ },
+ )
+
+ stmts = emit_structural_link_alters(proposal, existing)
+ # Only the missing CONTAINS_ENTITY pair (DocumentChunk → Company).
+ assert stmts == [
+ "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)",
+ ]
+
+
+def test_apply_proposal_emits_structural_links_alongside_domain_adds():
+ """End-to-end: apply_proposal runs both emit_add_statements and
+ emit_structural_link_alters in a single schema-change job. The
+ fake graph has the GraphRAG structural types in place (production
+ invariant — init_supportai runs before apply_proposal).
+ """
+ conn = _FakeConn(
+ vertex_types=[
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType",
+ ],
+ edge_metadata={},
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ result = apply_proposal(conn, "g", proposal)
+ assert result["status"] == "applied"
+ assert len(conn.gsql_calls) == 1
+ cmd = conn.gsql_calls[0]
+ # Domain ADD VERTEX is in there.
+ assert "ADD VERTEX Company" in cmd
+ # CONTAINS_ENTITY pair-additions for Company are in the same job.
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in cmd
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in cmd
+ # No per-domain-vertex IS_HEAD_OF / HAS_TAIL — those live at
+ # EntityType ↔ RelationshipType in the structural schema.
+ assert "IS_HEAD_OF ADD PAIR" not in cmd
+ assert "HAS_TAIL ADD PAIR" not in cmd
+
+
+def test_apply_proposal_skips_structural_links_when_core_types_missing():
+ """Defensive: if Document / DocumentChunk / RelationshipType aren't
+ on the graph yet, the structural-link emitter must NOT emit
+ references that would fail at schema-change time.
+ """
+ conn = _FakeConn(vertex_types=[], edge_metadata={})
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ result = apply_proposal(conn, "g", proposal)
+ cmd = conn.gsql_calls[0] if conn.gsql_calls else ""
+ assert "ALTER EDGE CONTAINS_ENTITY" not in cmd
+ assert "ALTER EDGE IS_HEAD_OF" not in cmd
+ assert "ALTER EDGE HAS_TAIL" not in cmd
+
+
+# ---------------------------------------------------------------------------
+# Reserved-words + gsql-output error checks
+# ---------------------------------------------------------------------------
+
+
+def test_get_gsql_reserved_words_returns_pytigergraph_set():
+ from common.db.schema_utils import get_gsql_reserved_words
+
+ words = get_gsql_reserved_words()
+ assert isinstance(words, frozenset)
+ # Sanity check — pyTigerGraph's set must include core GSQL keywords.
+ assert "VERTEX" in words
+ assert "FROM" in words
+ assert "TYPE" in words
+
+
+def test_is_reserved_word_case_insensitive():
+ from common.db.schema_utils import is_reserved_word
+
+ assert is_reserved_word("VERTEX")
+ assert is_reserved_word("vertex")
+ assert is_reserved_word("Vertex")
+ assert not is_reserved_word("Company")
+ assert not is_reserved_word("")
+
+
+def test_is_structural_type_now_blocks_reserved_words():
+ """The structural-type filter — used by parse_gsql_schema to drop
+ LLM-proposed names that would error at schema-change time — now
+ also drops GSQL reserved words.
+ """
+ assert is_structural_type("Document") # graphrag structural — blocked
+ assert is_structural_type("VERTEX") # GSQL keyword — blocked
+ assert is_structural_type("Vertex") # case-insensitive
+ assert not is_structural_type("Company") # ordinary domain name
+ assert not is_structural_type("Filing")
+
+
+def test_gsql_output_error_catches_transport_failures():
+ from common.db.schema_utils import gsql_output_error
+
+ # Premature disconnect from the TG gsql server — pyTigerGraph
+ # returns this as a string, not an exception.
+ err = gsql_output_error("Response ended prematurely")
+ assert err is not None
+ assert "Response ended prematurely" in err
+
+ err = gsql_output_error("Connection refused")
+ assert err is not None
+ assert "Connection refused" in err
+
+ # Empty / None inputs are not errors.
+ assert gsql_output_error("") is None
+ assert gsql_output_error(None) is None
+
+
+def test_gsql_output_error_catches_pytg_server_errors():
+ """The server-error pattern list must flag server-reported errors —
+ semantic, syntax, "Failed to create".
+ """
+ from common.db.schema_utils import gsql_output_error
+
+ err = gsql_output_error("...\nFailed to create vertex types: …\n")
+ assert err is not None and "GSQL server error" in err
+
+ err = gsql_output_error("Encountered \"FROM\" at line 12, column 3.\nSyntax Error.")
+ assert err is not None
+
+ # A successful "OK" output is not an error.
+ assert gsql_output_error("Using graph 'foo'\nOK\n") is None
+
+
+def test_apply_proposal_returns_error_status_on_gsql_failure():
+ """When ``conn.gsql()`` returns a failure-marker string instead of
+ raising, apply_proposal must return ``status=error`` rather than
+ falsely reporting "applied".
+ """
+ conn = _FakeConn(
+ vertex_types=["Document", "DocumentChunk", "Entity", "RelationshipType"],
+ edge_metadata={},
+ gsql_response="Response ended prematurely",
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ result = apply_proposal(conn, "g", proposal)
+ assert result["status"] == "error"
+ assert "Response ended prematurely" in result["error"]
+ # Metadata upsert is skipped on failure.
+ assert result["metadata"] == {"entity_types": [], "relationship_types": []}
+ assert not conn.upsert_calls
+
+
+# ---------------------------------------------------------------------------
+# UNDIRECTED EDGE support
+# ---------------------------------------------------------------------------
+
+
+def test_parser_recognises_undirected_edge():
+ text = """
+ ADD VERTEX Company();
+ ADD VERTEX Investor();
+ ADD UNDIRECTED EDGE PARTNERS_WITH(FROM Company, TO Investor);
+ """
+ proposal = parse_gsql_schema(text)
+ edge = proposal.find_edge("PARTNERS_WITH")
+ assert edge is not None
+ assert edge.directed is False
+ assert edge.pairs == [("Company", "Investor")]
+
+
+def test_parser_keeps_directed_edge_directed():
+ text = """
+ ADD VERTEX A();
+ ADD VERTEX B();
+ ADD DIRECTED EDGE PUSHES(FROM A, TO B);
+ """
+ edge = parse_gsql_schema(text).find_edge("PUSHES")
+ assert edge is not None
+ assert edge.directed is True
+
+
+def test_emitter_writes_undirected_edge_without_reverse_clause():
+ proposal = SchemaProposal()
+ proposal.add_vertex("A")
+ proposal.add_vertex("B")
+ proposal.add_edge_pair("PARTNERS_WITH", "A", "B", directed=False)
+
+ stmts = emit_add_statements(proposal)
+ edge_stmt = next(s for s in stmts if "EDGE PARTNERS_WITH" in s)
+ assert edge_stmt.startswith("ADD UNDIRECTED EDGE")
+ assert "REVERSE_EDGE" not in edge_stmt
+
+
+def test_preview_writes_undirected_edge():
+ proposal = SchemaProposal()
+ proposal.add_vertex("A")
+ proposal.add_vertex("B")
+ proposal.add_edge_pair("PARTNERS_WITH", "A", "B", directed=False)
+
+ preview = emit_preview_gsql(proposal)
+ assert "ADD UNDIRECTED EDGE PARTNERS_WITH" in preview
+ assert "REVERSE_EDGE" not in preview
+
+
+def test_to_from_dict_preserves_directed_flag():
+ p = SchemaProposal()
+ p.add_vertex("A")
+ p.add_vertex("B")
+ p.add_edge_pair("E1", "A", "B", directed=True)
+ p.add_edge_pair("E2", "A", "B", directed=False)
+ data = p.to_dict()
+ p2 = SchemaProposal.from_dict(data)
+ assert p2.find_edge("E1").directed is True
+ assert p2.find_edge("E2").directed is False
From 6da2c97f046bafee8f407c7715b82a31659880b8 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 5 May 2026 23:09:47 +0530
Subject: [PATCH 21/70] feat(GML-2086): show unsupported file type warning in
upload UI
---
graphrag-ui/src/pages/setup/IngestGraph.tsx | 36 +++++++++++++++------
1 file changed, 26 insertions(+), 10 deletions(-)
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index d920b87..37a0d7a 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -1006,16 +1006,32 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
? `Upload destination: uploads/${ingestGraphName}/`
: ""}
- {selectedFiles && Array.from(selectedFiles).some((f) =>
- [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase())
- ) && (
-
-
ℹ️
-
- CSV and Excel files will be treated as unstructured text documents.
-
-
- )}
+ {selectedFiles && (() => {
+ const SUPPORTED_EXTENSIONS = new Set([".txt", ".md", ".pdf", ".docx", ".doc", ".html", ".htm", ".json", ".csv", ".xlsx", ".xls", ".xml", ".jpeg", ".jpg", ".png", ".gif", ".jsonl"]);
+ const files = Array.from(selectedFiles);
+ const unsupported = files.filter((f) => !SUPPORTED_EXTENSIONS.has(f.name.slice(f.name.lastIndexOf(".")).toLowerCase()));
+ const hasCsvExcel = files.some((f) => [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase()));
+ return (
+ <>
+ {unsupported.length > 0 && (
+
+
⚠️
+
+ Unsupported file type{unsupported.length > 1 ? "s" : ""}: {unsupported.map((f) => f.name).join(", ")} . These files will be skipped during ingestion.
+
+
+ )}
+ {hasCsvExcel && (
+
+
ℹ️
+
+ CSV and Excel files will be treated as unstructured text documents.
+
+
+ )}
+ >
+ );
+ })()}
From b561896fe8f5141c9d01fa9b848cdfb4ed0ec061 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Wed, 6 May 2026 20:20:36 +0530
Subject: [PATCH 22/70] feat: move Citations tab first, add 30-day trace log
cleanup
---
graphrag-ui/src/pages/TraceLogs.tsx | 26 +++++++++++++-------------
graphrag/app/routers/ui.py | 15 +++++++++++++++
2 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index bddf861..1438fa8 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -864,8 +864,17 @@ const TraceLogs: FC = () => {
{/* Tabs */}
-
+
+
+ Citations
+
+ {trace.citations.length}
+
+
{
{trace.toolCalls.length}
-
- Citations
-
- {trace.citations.length}
-
-
{
+
+
+
-
-
-
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 79dff2a..b5265a7 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -72,9 +72,24 @@
TRACE_LOGS_DIR = os.environ.get("TRACE_LOGS_DIR", "/code/trace_logs")
+def _cleanup_old_traces(max_age_days: int = 30):
+ """Delete trace log files older than max_age_days."""
+ try:
+ cutoff = time.time() - (max_age_days * 86400)
+ for filename in os.listdir(TRACE_LOGS_DIR):
+ if not filename.endswith(".json"):
+ continue
+ filepath = os.path.join(TRACE_LOGS_DIR, filename)
+ if os.path.getmtime(filepath) < cutoff:
+ os.remove(filepath)
+ except Exception:
+ logger.warning("Failed to clean up old trace logs", exc_info=True)
+
+
def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
try:
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+ _cleanup_old_traces()
# Strip chunk text from query_sources to keep trace files small.
# final_retrieval contains the full text of every retrieved chunk.
From a7bf04c25e0ad66c3d932ecfcef4d6ed865cb1e4 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Tue, 14 Apr 2026 22:07:51 -0700
Subject: [PATCH 23/70] Add UI test
---
graphrag-ui/package-lock.json | 10906 ++++++++++++++++++++++++++++++++
graphrag-ui/package.json | 6 +-
2 files changed, 10909 insertions(+), 3 deletions(-)
create mode 100644 graphrag-ui/package-lock.json
diff --git a/graphrag-ui/package-lock.json b/graphrag-ui/package-lock.json
new file mode 100644
index 0000000..7def4e4
--- /dev/null
+++ b/graphrag-ui/package-lock.json
@@ -0,0 +1,10906 @@
+{
+ "name": "tg-cbot-v5",
+ "version": "0.0.5",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tg-cbot-v5",
+ "version": "0.0.5",
+ "dependencies": {
+ "@hookform/resolvers": "^3.6.0",
+ "@radix-ui/react-dialog": "^1.1.1",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-popover": "^1.1.1",
+ "@radix-ui/react-radio-group": "^1.2.0",
+ "@radix-ui/react-select": "^2.1.1",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@react-three/drei": "9.56.1",
+ "@react-three/fiber": "8.13.3",
+ "@tailwindcss/typography": "^0.5.18",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "i18next": "^23.11.5",
+ "install": "^0.13.0",
+ "lucide-react": "^0.390.0",
+ "npm": "^10.8.1",
+ "react": "^18.3.1",
+ "react-chatbot-kit": "^2.2.2",
+ "react-dom": "^18.3.1",
+ "react-hook-form": "^7.51.5",
+ "react-i18next": "^14.1.2",
+ "react-icons": "^5.2.1",
+ "react-markdown": "^9.0.1",
+ "react-router-dom": "^6.23.1",
+ "react-use-websocket": "^4.8.1",
+ "reagraph": "4.15.19",
+ "remark-gfm": "^4.0.0",
+ "tailwind-merge": "^2.3.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.59.1",
+ "@types/node": "^25.6.0",
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@typescript-eslint/eslint-plugin": "^7.5.0",
+ "@typescript-eslint/parser": "^7.5.0",
+ "@vitejs/plugin-react-swc": "^3.6.0",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^6.1.1",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.18",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
+ "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
+ "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
+ "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/animated": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
+ "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/core": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
+ "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/rafz": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
+ "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/shared": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
+ "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/three": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
+ "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/core": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/@react-spring/types": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
+ "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "9.56.1",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.56.1.tgz",
+ "integrity": "sha512-xHQHMqqn4ww62YVDoXLazFhhrM5pkzoaA/2v5ytjbKjU9hP2iHos3odxGxQEKUS0WXwduziP6ScRkdSevpDFsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "@react-spring/three": "^9.3.1",
+ "@use-gesture/react": "^10.2.0",
+ "camera-controls": "^1.38.0",
+ "detect-gpu": "^5.0.8",
+ "glsl-noise": "^0.0.0",
+ "lodash.clamp": "^4.0.3",
+ "lodash.omit": "^4.5.0",
+ "lodash.pick": "^4.4.0",
+ "maath": "^0.5.2",
+ "meshline": "^3.1.6",
+ "react-composer": "^5.0.3",
+ "react-merge-refs": "^1.1.0",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.0.8",
+ "three-mesh-bvh": "^0.5.22",
+ "three-stdlib": "^2.21.6",
+ "troika-three-text": "^0.47.1",
+ "utility-types": "^3.10.0",
+ "zustand": "^3.5.13"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=8.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "8.13.3",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.3.tgz",
+ "integrity": "sha512-mCdTUB8D1kwlsOSxGhUg5nuGHt3HN3aNFc0s9I/N7ayk+nzT2ttLdn49c56nrHu+YK+SU1xnrxe6LqftZgIRmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.1",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-gl": ">=11.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz",
+ "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.26"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.15.24",
+ "@swc/core-darwin-x64": "1.15.24",
+ "@swc/core-linux-arm-gnueabihf": "1.15.24",
+ "@swc/core-linux-arm64-gnu": "1.15.24",
+ "@swc/core-linux-arm64-musl": "1.15.24",
+ "@swc/core-linux-ppc64-gnu": "1.15.24",
+ "@swc/core-linux-s390x-gnu": "1.15.24",
+ "@swc/core-linux-x64-gnu": "1.15.24",
+ "@swc/core-linux-x64-musl": "1.15.24",
+ "@swc/core-win32-arm64-msvc": "1.15.24",
+ "@swc/core-win32-ia32-msvc": "1.15.24",
+ "@swc/core-win32-x64-msvc": "1.15.24"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz",
+ "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz",
+ "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz",
+ "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz",
+ "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz",
+ "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-ppc64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz",
+ "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-s390x-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz",
+ "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz",
+ "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz",
+ "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz",
+ "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz",
+ "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.15.24",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz",
+ "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.26",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
+ "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
+ "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+ "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.183.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
+ "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.0.1"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/type-utils": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+ "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+ "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
+ "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@swc/core": "^1.12.11"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.69",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
+ "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@yomguithereal/helpers": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@yomguithereal/helpers/-/helpers-1.1.1.tgz",
+ "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==",
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.17",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz",
+ "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/camera-controls": {
+ "version": "1.38.2",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.38.2.tgz",
+ "integrity": "sha512-EfzbovxLssyWpJVG9uKcazSDDIEcd1hUsPhPF/OWWnICsKY9WbLY/2S4UPW73HHbvnVeR/Z9wsWaQKtANy/2Yg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
+ "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",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/ctrl-keys": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/ctrl-keys/-/ctrl-keys-1.0.6.tgz",
+ "integrity": "sha512-fENSKrbIfvX83uHxruP3S/9GizirvgT66vHhgKHOCTVHK+22Xpud/vttg5c5IifRl+6Gom/GjE+ZSXJKf0DMTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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-binarytree": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+ "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+ "license": "MIT"
+ },
+ "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-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force-3d": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+ "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-binarytree": "1",
+ "d3-dispatch": "1 - 3",
+ "d3-octree": "1",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "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-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "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-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "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-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",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.334",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
+ "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ellipsize": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.5.1.tgz",
+ "integrity": "sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz",
+ "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "zod": "^3.22.4 || ^4.0.0",
+ "zod-validation-error": "^3.0.3 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "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",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glodrei": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/glodrei/-/glodrei-0.0.1.tgz",
+ "integrity": "sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "@mediapipe/tasks-vision": "0.10.8",
+ "@react-spring/three": "~9.6.1",
+ "@use-gesture/react": "^10.2.24",
+ "camera-controls": "^2.4.2",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.28",
+ "glsl-noise": "^0.0.0",
+ "maath": "^0.10.7",
+ "meshline": "^3.1.6",
+ "react-composer": "^5.0.3",
+ "react-merge-refs": "^1.1.0",
+ "stats-gl": "^2.0.0",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.7.0",
+ "three-stdlib": "^2.29.4",
+ "troika-three-text": "^0.47.2",
+ "tunnel-rat": "^0.1.2",
+ "utility-types": "^3.10.0",
+ "uuid": "^9.0.1",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=8.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/animated": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
+ "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/core": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
+ "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/rafz": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
+ "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
+ "license": "MIT"
+ },
+ "node_modules/glodrei/node_modules/@react-spring/shared": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
+ "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/three": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
+ "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/core": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/glodrei/node_modules/@react-spring/types": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
+ "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/glodrei/node_modules/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/glodrei/node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/glodrei/node_modules/three-mesh-bvh": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz",
+ "integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.151.0"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/graphology": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz",
+ "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0",
+ "obliterator": "^2.0.2"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.24.0"
+ }
+ },
+ "node_modules/graphology-indices": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz",
+ "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.4.2",
+ "mnemonist": "^0.39.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-layout": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz",
+ "integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.3.0",
+ "pandemonium": "^2.4.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-layout-forceatlas2": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
+ "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.1.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-layout-noverlap": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz",
+ "integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.3.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-metrics": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/graphology-metrics/-/graphology-metrics-2.4.0.tgz",
+ "integrity": "sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-indices": "^0.17.0",
+ "graphology-shortest-path": "^2.0.0",
+ "graphology-utils": "^2.4.4",
+ "mnemonist": "^0.39.0",
+ "pandemonium": "2.4.1"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-shortest-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz",
+ "integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@yomguithereal/helpers": "^1.1.1",
+ "graphology-indices": "^0.17.0",
+ "graphology-utils": "^2.4.3",
+ "mnemonist": "^0.39.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-types": {
+ "version": "0.24.8",
+ "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
+ "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/graphology-utils": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
+ "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphology-types": ">=0.23.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hold-event": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/hold-event/-/hold-event-0.2.0.tgz",
+ "integrity": "sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "23.16.8",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
+ "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/install": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
+ "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "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-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
+ "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.clamp": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
+ "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.omit": {
+ "version": "4.18.0",
+ "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.18.0.tgz",
+ "integrity": "sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==",
+ "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.",
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.390.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.390.0.tgz",
+ "integrity": "sha512-APqbfEcVuHnZbiy3E97gYWLeBdkE4e6NbY6AuVETZDZVn/bQCHYUoHyxcUHyvRopfPOHhFUEvDyyQzHwM+S9/w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.5.3.tgz",
+ "integrity": "sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.144.0",
+ "three": ">=0.144.0"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
+ "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
+ "license": "MIT"
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mnemonist": {
+ "version": "0.39.8",
+ "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
+ "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "obliterator": "^2.0.1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm": {
+ "version": "10.9.8",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.8.tgz",
+ "integrity": "sha512-fYwb6ODSmHkqrJQQaCxY3M2lPf/mpgC7ik0HSzzIwG5CGtabRp4bNqikatvCoT42b5INQSqudVH0R7yVmC9hVg==",
+ "bundleDependencies": [
+ "@isaacs/string-locale-compare",
+ "@npmcli/arborist",
+ "@npmcli/config",
+ "@npmcli/fs",
+ "@npmcli/map-workspaces",
+ "@npmcli/package-json",
+ "@npmcli/promise-spawn",
+ "@npmcli/redact",
+ "@npmcli/run-script",
+ "@sigstore/tuf",
+ "abbrev",
+ "archy",
+ "cacache",
+ "chalk",
+ "ci-info",
+ "cli-columns",
+ "fastest-levenshtein",
+ "fs-minipass",
+ "glob",
+ "graceful-fs",
+ "hosted-git-info",
+ "ini",
+ "init-package-json",
+ "is-cidr",
+ "json-parse-even-better-errors",
+ "libnpmaccess",
+ "libnpmdiff",
+ "libnpmexec",
+ "libnpmfund",
+ "libnpmhook",
+ "libnpmorg",
+ "libnpmpack",
+ "libnpmpublish",
+ "libnpmsearch",
+ "libnpmteam",
+ "libnpmversion",
+ "make-fetch-happen",
+ "minimatch",
+ "minipass",
+ "minipass-pipeline",
+ "ms",
+ "node-gyp",
+ "nopt",
+ "normalize-package-data",
+ "npm-audit-report",
+ "npm-install-checks",
+ "npm-package-arg",
+ "npm-pick-manifest",
+ "npm-profile",
+ "npm-registry-fetch",
+ "npm-user-validate",
+ "p-map",
+ "pacote",
+ "parse-conflict-json",
+ "proc-log",
+ "qrcode-terminal",
+ "read",
+ "semver",
+ "spdx-expression-parse",
+ "ssri",
+ "supports-color",
+ "tar",
+ "text-table",
+ "tiny-relative-date",
+ "treeverse",
+ "validate-npm-package-name",
+ "which",
+ "write-file-atomic"
+ ],
+ "license": "Artistic-2.0",
+ "workspaces": [
+ "docs",
+ "smoke-tests",
+ "mock-globals",
+ "mock-registry",
+ "workspaces/*"
+ ],
+ "dependencies": {
+ "@isaacs/string-locale-compare": "^1.1.0",
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/config": "^9.0.0",
+ "@npmcli/fs": "^4.0.0",
+ "@npmcli/map-workspaces": "^4.0.2",
+ "@npmcli/package-json": "^6.2.0",
+ "@npmcli/promise-spawn": "^8.0.3",
+ "@npmcli/redact": "^3.2.2",
+ "@npmcli/run-script": "^9.1.0",
+ "@sigstore/tuf": "^3.1.1",
+ "abbrev": "^3.0.1",
+ "archy": "~1.0.0",
+ "cacache": "^19.0.1",
+ "chalk": "^5.6.2",
+ "ci-info": "^4.4.0",
+ "cli-columns": "^4.0.0",
+ "fastest-levenshtein": "^1.0.16",
+ "fs-minipass": "^3.0.3",
+ "glob": "^10.5.0",
+ "graceful-fs": "^4.2.11",
+ "hosted-git-info": "^8.1.0",
+ "ini": "^5.0.0",
+ "init-package-json": "^7.0.2",
+ "is-cidr": "^5.1.1",
+ "json-parse-even-better-errors": "^4.0.0",
+ "libnpmaccess": "^9.0.0",
+ "libnpmdiff": "^7.0.5",
+ "libnpmexec": "^9.0.5",
+ "libnpmfund": "^6.0.5",
+ "libnpmhook": "^11.0.0",
+ "libnpmorg": "^7.0.0",
+ "libnpmpack": "^8.0.5",
+ "libnpmpublish": "^10.0.2",
+ "libnpmsearch": "^8.0.0",
+ "libnpmteam": "^7.0.0",
+ "libnpmversion": "^7.0.0",
+ "make-fetch-happen": "^14.0.3",
+ "minimatch": "^9.0.9",
+ "minipass": "^7.1.3",
+ "minipass-pipeline": "^1.2.4",
+ "ms": "^2.1.2",
+ "node-gyp": "^11.5.0",
+ "nopt": "^8.1.0",
+ "normalize-package-data": "^7.0.1",
+ "npm-audit-report": "^6.0.0",
+ "npm-install-checks": "^7.1.2",
+ "npm-package-arg": "^12.0.2",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-profile": "^11.0.1",
+ "npm-registry-fetch": "^18.0.2",
+ "npm-user-validate": "^3.0.0",
+ "p-map": "^7.0.4",
+ "pacote": "^19.0.1",
+ "parse-conflict-json": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "qrcode-terminal": "^0.12.0",
+ "read": "^4.1.0",
+ "semver": "^7.7.4",
+ "spdx-expression-parse": "^4.0.0",
+ "ssri": "^12.0.0",
+ "supports-color": "^9.4.0",
+ "tar": "^7.5.11",
+ "text-table": "~0.2.0",
+ "tiny-relative-date": "^1.3.0",
+ "treeverse": "^3.0.0",
+ "validate-npm-package-name": "^6.0.2",
+ "which": "^5.0.0",
+ "write-file-atomic": "^6.0.0"
+ },
+ "bin": {
+ "npm": "bin/npm-cli.js",
+ "npx": "bin/npx-cli.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/@isaacs/string-locale-compare": {
+ "version": "1.1.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/@npmcli/agent": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^10.0.1",
+ "socks-proxy-agent": "^8.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/arborist": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/string-locale-compare": "^1.1.0",
+ "@npmcli/fs": "^4.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/map-workspaces": "^4.0.1",
+ "@npmcli/metavuln-calculator": "^8.0.0",
+ "@npmcli/name-from-folder": "^3.0.0",
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.1",
+ "@npmcli/query": "^4.0.0",
+ "@npmcli/redact": "^3.0.0",
+ "@npmcli/run-script": "^9.0.1",
+ "bin-links": "^5.0.0",
+ "cacache": "^19.0.1",
+ "common-ancestor-path": "^1.0.1",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "json-stringify-nice": "^1.1.4",
+ "lru-cache": "^10.2.2",
+ "minimatch": "^9.0.4",
+ "nopt": "^8.0.0",
+ "npm-install-checks": "^7.1.0",
+ "npm-package-arg": "^12.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.1",
+ "pacote": "^19.0.0",
+ "parse-conflict-json": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "proggy": "^3.0.0",
+ "promise-all-reject-late": "^1.0.0",
+ "promise-call-limit": "^3.0.1",
+ "promise-retry": "^2.0.1",
+ "read-package-json-fast": "^4.0.0",
+ "semver": "^7.3.7",
+ "ssri": "^12.0.0",
+ "treeverse": "^3.0.0",
+ "walk-up-path": "^3.0.1"
+ },
+ "bin": {
+ "arborist": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/config": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/map-workspaces": "^4.0.1",
+ "@npmcli/package-json": "^6.0.1",
+ "ci-info": "^4.0.0",
+ "ini": "^5.0.0",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "walk-up-path": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/fs": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/git": {
+ "version": "6.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^8.0.0",
+ "ini": "^5.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^10.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/installed-package-contents": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-bundled": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/map-workspaces": {
+ "version": "4.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/name-from-folder": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "glob": "^10.2.2",
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/metavuln-calculator": {
+ "version": "8.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cacache": "^19.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "pacote": "^20.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": {
+ "version": "20.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^7.5.10"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/name-from-folder": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/node-gyp": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/package-json": {
+ "version": "6.2.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.5.3",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/promise-spawn": {
+ "version": "8.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/query": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/redact": {
+ "version": "3.2.2",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@npmcli/run-script": {
+ "version": "9.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "node-gyp": "^11.0.0",
+ "proc-log": "^5.0.0",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/bundle": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/core": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/protobuf-specs": {
+ "version": "0.4.3",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/sign": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "make-fetch-happen": "^14.0.2",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/tuf": {
+ "version": "3.1.1",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.1",
+ "tuf-js": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@sigstore/verify": {
+ "version": "2.1.1",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/abbrev": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/agent-base": {
+ "version": "7.1.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/aproba": {
+ "version": "2.1.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/archy": {
+ "version": "1.0.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/bin-links": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cmd-shim": "^7.0.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "read-cmd-shim": "^5.0.0",
+ "write-file-atomic": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/cacache": {
+ "version": "19.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^4.0.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^10.0.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^7.0.2",
+ "ssri": "^12.0.0",
+ "tar": "^7.4.3",
+ "unique-filename": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/chalk": {
+ "version": "5.6.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/chownr": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/ci-info": {
+ "version": "4.4.0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/cidr-regex": {
+ "version": "4.1.3",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "ip-regex": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/cli-columns": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/npm/node_modules/cmd-shim": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/color-convert": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/color-name": {
+ "version": "1.1.4",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/common-ancestor-path": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/cssesc": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm/node_modules/debug": {
+ "version": "4.4.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/npm/node_modules/diff": {
+ "version": "5.2.2",
+ "inBundle": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/npm/node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/encoding": {
+ "version": "0.1.13",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/npm/node_modules/env-paths": {
+ "version": "2.2.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npm/node_modules/err-code": {
+ "version": "2.0.3",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "inBundle": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/npm/node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/npm/node_modules/fdir": {
+ "version": "6.5.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/npm/node_modules/foreground-child": {
+ "version": "3.3.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/glob": {
+ "version": "10.5.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/hosted-git-info": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "inBundle": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/npm/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm/node_modules/ignore-walk": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/npm/node_modules/ini": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/init-package-json": {
+ "version": "7.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/package-json": "^6.0.0",
+ "npm-package-arg": "^12.0.0",
+ "promzard": "^2.0.0",
+ "read": "^4.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/ip-address": {
+ "version": "10.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/npm/node_modules/ip-regex": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/is-cidr": {
+ "version": "5.1.1",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "cidr-regex": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/npm/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/isexe": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/jackspeak": {
+ "version": "3.4.3",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/npm/node_modules/json-parse-even-better-errors": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/json-stringify-nice": {
+ "version": "1.1.4",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/jsonparse": {
+ "version": "1.3.1",
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/just-diff": {
+ "version": "6.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/just-diff-apply": {
+ "version": "5.5.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/libnpmaccess": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-package-arg": "^12.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmdiff": {
+ "version": "7.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "binary-extensions": "^2.3.0",
+ "diff": "^5.1.0",
+ "minimatch": "^9.0.4",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0",
+ "tar": "^7.5.11"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmexec": {
+ "version": "9.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/run-script": "^9.0.1",
+ "ci-info": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0",
+ "proc-log": "^5.0.0",
+ "read": "^4.0.0",
+ "read-package-json-fast": "^4.0.0",
+ "semver": "^7.3.7",
+ "walk-up-path": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmfund": {
+ "version": "6.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmhook": {
+ "version": "11.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmorg": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmpack": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/arborist": "^8.0.5",
+ "@npmcli/run-script": "^9.0.1",
+ "npm-package-arg": "^12.0.0",
+ "pacote": "^19.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmpublish": {
+ "version": "10.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "ci-info": "^4.0.0",
+ "normalize-package-data": "^7.0.0",
+ "npm-package-arg": "^12.0.0",
+ "npm-registry-fetch": "^18.0.1",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.7",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmsearch": {
+ "version": "8.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmteam": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^2.0.0",
+ "npm-registry-fetch": "^18.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/libnpmversion": {
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.1",
+ "@npmcli/run-script": "^9.0.1",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/make-fetch-happen": {
+ "version": "14.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/agent": "^3.0.0",
+ "cacache": "^19.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^1.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/minimatch": {
+ "version": "9.0.9",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/minipass": {
+ "version": "7.1.3",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-fetch": {
+ "version": "4.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/minizlib": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/npm/node_modules/ms": {
+ "version": "2.1.3",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/mute-stream": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/negotiator": {
+ "version": "1.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/npm/node_modules/node-gyp": {
+ "version": "11.5.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^14.0.3",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.4.3",
+ "tinyglobby": "^0.2.12",
+ "which": "^5.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/nopt": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^3.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/normalize-package-data": {
+ "version": "7.0.1",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-audit-report": {
+ "version": "6.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-bundled": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-install-checks": {
+ "version": "7.1.2",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-normalize-package-bin": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-package-arg": {
+ "version": "12.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-packlist": {
+ "version": "9.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "ignore-walk": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-pick-manifest": {
+ "version": "10.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-install-checks": "^7.1.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-profile": {
+ "version": "11.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-registry-fetch": {
+ "version": "18.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/redact": "^3.0.0",
+ "jsonparse": "^1.3.1",
+ "make-fetch-happen": "^14.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minizlib": "^3.0.1",
+ "npm-package-arg": "^12.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/npm-user-validate": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/p-map": {
+ "version": "7.0.4",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/npm/node_modules/pacote": {
+ "version": "19.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^7.5.10"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/parse-conflict-json": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^4.0.0",
+ "just-diff": "^6.0.0",
+ "just-diff-apply": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/path-key": {
+ "version": "3.1.1",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/picomatch": {
+ "version": "4.0.3",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/npm/node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm/node_modules/proc-log": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/proggy": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/promise-all-reject-late": {
+ "version": "1.0.1",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/promise-call-limit": {
+ "version": "3.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/promise-retry": {
+ "version": "2.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm/node_modules/promzard": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "read": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/qrcode-terminal": {
+ "version": "0.12.0",
+ "inBundle": true,
+ "bin": {
+ "qrcode-terminal": "bin/qrcode-terminal.js"
+ }
+ },
+ "node_modules/npm/node_modules/read": {
+ "version": "4.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "mute-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/read-cmd-shim": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/read-package-json-fast": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/retry": {
+ "version": "0.12.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm/node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/npm/node_modules/semver": {
+ "version": "7.7.4",
+ "inBundle": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/npm/node_modules/shebang-command": {
+ "version": "2.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/npm/node_modules/sigstore": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "@sigstore/sign": "^3.1.0",
+ "@sigstore/tuf": "^3.1.0",
+ "@sigstore/verify": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/socks": {
+ "version": "2.8.7",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "inBundle": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/npm/node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "inBundle": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/npm/node_modules/ssri": {
+ "version": "12.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/string-width": {
+ "version": "4.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm/node_modules/supports-color": {
+ "version": "9.4.0",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/tar": {
+ "version": "7.5.11",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/text-table": {
+ "version": "0.2.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/tiny-relative-date": {
+ "version": "1.3.0",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/npm/node_modules/treeverse": {
+ "version": "3.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/tuf-js": {
+ "version": "3.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/models": "3.0.1",
+ "debug": "^4.4.1",
+ "make-fetch-happen": "^14.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^9.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/unique-filename": {
+ "version": "4.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/unique-slug": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "inBundle": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/npm/node_modules/validate-npm-package-name": {
+ "version": "6.0.2",
+ "inBundle": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/walk-up-path": {
+ "version": "3.0.1",
+ "inBundle": true,
+ "license": "ISC"
+ },
+ "node_modules/npm/node_modules/which": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/which/node_modules/isexe": {
+ "version": "3.1.5",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "inBundle": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "inBundle": true,
+ "license": "MIT"
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "5.1.2",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "inBundle": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/npm/node_modules/write-file-atomic": {
+ "version": "6.0.0",
+ "inBundle": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm/node_modules/yallist": {
+ "version": "5.0.0",
+ "inBundle": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/obliterator": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
+ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pandemonium": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz",
+ "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==",
+ "license": "MIT",
+ "dependencies": {
+ "mnemonist": "^0.39.2"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-chatbot-kit": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-chatbot-kit/-/react-chatbot-kit-2.2.2.tgz",
+ "integrity": "sha512-8p/i0KkzkhoyG2XsL6Pb6f72k9j7GYNAc5SOa4f9OZwbCD3Q34uEruNPc06qa1wZHKfT6aFna19PA2plFuO2NA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-conditionally-render": "^1.0.2"
+ }
+ },
+ "node_modules/react-composer": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
+ "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-conditionally-render": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/react-conditionally-render/-/react-conditionally-render-1.0.2.tgz",
+ "integrity": "sha512-CtjIgaLHVDSgHis3gv/PT/8EnD6GPUL8PrhUjh7DP6S5Y3p56dGu7y2nVg6pYv1kv+fGznRhRmX3assr/vRw3A==",
+ "license": "ISC"
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.72.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz",
+ "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "node_modules/react-i18next": {
+ "version": "14.1.3",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz",
+ "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 23.2.3",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-merge-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz",
+ "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/react-reconciler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+ "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.21.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-gesture": {
+ "version": "9.1.3",
+ "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz",
+ "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==",
+ "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-websocket": {
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz",
+ "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==",
+ "license": "MIT"
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reagraph": {
+ "version": "4.15.19",
+ "resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.15.19.tgz",
+ "integrity": "sha512-acM2agUYyNKyKLzKhnEoMNbBc58KxpBQ5wzIqYvsoVa3Se2weuB8npVfdjJZV9AxW9BaSaeu90NwCrcO3XATTg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-spring/three": "9.6.1",
+ "@react-three/fiber": "8.13.5",
+ "camera-controls": "^2.8.3",
+ "classnames": "^2.5.1",
+ "d3-array": "^3.2.4",
+ "d3-force-3d": "^3.0.3",
+ "d3-hierarchy": "^3.1.2",
+ "d3-scale": "^4.0.2",
+ "ellipsize": "^0.5.1",
+ "glodrei": "^0.0.1",
+ "graphology": "^0.25.4",
+ "graphology-layout": "^0.6.1",
+ "graphology-layout-forceatlas2": "^0.10.1",
+ "graphology-layout-noverlap": "^0.4.2",
+ "graphology-metrics": "^2.1.0",
+ "graphology-shortest-path": "^2.0.2",
+ "hold-event": "^0.2.0",
+ "react-use-gesture": "^9.1.3",
+ "reakeys": "^2.0.0",
+ "three": "^0.154.0",
+ "three-stdlib": "^2.23.13",
+ "zustand": "4.3.9"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/animated": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
+ "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/core": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
+ "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/rafz": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
+ "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/@react-spring/shared": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
+ "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/three": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
+ "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.6.1",
+ "@react-spring/core": "~9.6.1",
+ "@react-spring/shared": "~9.6.1",
+ "@react-spring/types": "~9.6.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-spring/types": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
+ "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/@react-three/fiber": {
+ "version": "8.13.5",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.5.tgz",
+ "integrity": "sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.1",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-gl": ">=11.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reagraph/node_modules/@react-three/fiber/node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reagraph/node_modules/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/reagraph/node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/reagraph/node_modules/three": {
+ "version": "0.154.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
+ "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==",
+ "license": "MIT"
+ },
+ "node_modules/reagraph/node_modules/zustand": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz",
+ "integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reakeys": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/reakeys/-/reakeys-2.0.6.tgz",
+ "integrity": "sha512-dmZPhOwU3NuLjy61CLqf3dGEhhetx4Du7m/DlX1eqZrBKcKrDqpR0O1tHyYMB95KVdhVRjrfcuFFawI7EqGyxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ctrl-keys": "^1.0.6"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+ "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/suspend-react": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.0.8.tgz",
+ "integrity": "sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
+ "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.183.2",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
+ "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.5.24.tgz",
+ "integrity": "sha512-VTIgfjz8aFoPKTQoMIQQv9jJD4ybFRZuKKE1/kqy78FQcuHQ0+iIWv7C5cSb2inlvs7bNMVY3yRx3RXGZfrvzQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.123.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/troika-three-text": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz",
+ "integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.47.2",
+ "troika-worker-utils": "^0.47.2",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz",
+ "integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.47.2",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz",
+ "integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==",
+ "license": "MIT"
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/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/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/graphrag-ui/package.json b/graphrag-ui/package.json
index 4ef113e..12cca39 100755
--- a/graphrag-ui/package.json
+++ b/graphrag-ui/package.json
@@ -19,6 +19,9 @@
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.0",
+ "@react-three/drei": "9.56.1",
+ "@react-three/fiber": "8.13.3",
+ "@tailwindcss/typography": "^0.5.18",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"i18next": "^23.11.5",
@@ -35,12 +38,9 @@
"react-router-dom": "^6.23.1",
"react-use-websocket": "^4.8.1",
"reagraph": "4.15.19",
- "@react-three/fiber": "8.13.3",
- "@react-three/drei": "9.56.1",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
- "@tailwindcss/typography": "^0.5.18",
"zod": "^3.23.8"
},
"devDependencies": {
From 222d680a65c376fd2f7660877f863dcc6e75c712 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Tue, 21 Apr 2026 22:21:21 -0700
Subject: [PATCH 24/70] Update version number
---
VERSION | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/VERSION b/VERSION
index 3a3cd8c..88c5fb8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.3.1
+1.4.0
From 4d6918f4f262f96e4ed21839cb2f2e263883d6b2 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 16:09:25 +0530
Subject: [PATCH 25/70] feat: Add Trace Logs UI for agent execution
observability
---
graphrag-ui/src/actions/ActionProvider.tsx | 8 +-
.../src/components/CustomChatMessage.tsx | 14 +
graphrag-ui/src/main.tsx | 5 +
graphrag-ui/src/pages/TraceLogs.tsx | 697 ++++++++++++++++++
graphrag/app/agent/agent.py | 22 +-
5 files changed, 742 insertions(+), 4 deletions(-)
create mode 100644 graphrag-ui/src/pages/TraceLogs.tsx
diff --git a/graphrag-ui/src/actions/ActionProvider.tsx b/graphrag-ui/src/actions/ActionProvider.tsx
index 58fc7aa..4bda231 100644
--- a/graphrag-ui/src/actions/ActionProvider.tsx
+++ b/graphrag-ui/src/actions/ActionProvider.tsx
@@ -1,4 +1,4 @@
-import React, {useState, useCallback, useEffect, useContext} from 'react';
+import React, {useState, useRef, useCallback, useEffect, useContext} from 'react';
import {createClientMessage} from 'react-chatbot-kit';
import useWebSocket, {ReadyState} from 'react-use-websocket';
import Loader from '../components/Loader';
@@ -81,6 +81,7 @@ const ActionProvider: React.FC = ({
}) => {
const selectedGraph = useContext(SelectedGraphContext);
const selectedRagPattern = useContext(RagPatternContext);
+ const lastUserQueryRef = useRef("");
const WS_URL = "/ui/" + selectedGraph + "/chat" + "?rag_pattern=" + selectedRagPattern;
const [messageHistory, setMessageHistory] = useState[]>(
[],
@@ -205,6 +206,7 @@ const ActionProvider: React.FC = ({
};
const defaultQuestions = (msg: string) => {
+ lastUserQueryRef.current = msg;
const clientMessage = createClientMessage(msg, {
delay: 300,
});
@@ -213,6 +215,7 @@ const ActionProvider: React.FC = ({
};
const queryGraphragWs = (msg) => {
+ lastUserQueryRef.current = msg;
const queryGraphragWsTest = (msg: string) => {
sendMessage(msg);
};
@@ -269,6 +272,9 @@ const ActionProvider: React.FC = ({
return; // Don't create a bot message for conversation ID
}
+ // Attach the user query so the trace page can display it
+ messageData.userQuery = lastUserQueryRef.current;
+
// Handle regular bot messages
const botMessage = createChatBotMessage(messageData);
setState((prev) => {
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 0aef2ea..285ac8f 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,6 +10,8 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
+import { LuActivity } from "react-icons/lu";
+import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
@@ -127,6 +129,7 @@ const AuthenticatedImage: FC<{ src: string; alt: string }> = ({ src, alt }) => {
export const CustomChatMessage: FC = ({
message,
}) => {
+ const navigate = useNavigate();
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
@@ -191,6 +194,17 @@ export const CustomChatMessage: FC = ({
showTable={handleShowTable}
showGraph={handleShowGraph}
/>
+ {(message.response_type !== "progress" && (message.query_sources?.result || message.query_sources?.reasoning)) && (
+ {
+ navigate("/trace", { state: { message, userQuery: message.userQuery || "" } });
+ }}
+ >
+
+ View Trace
+
+ )}
{showGraphVis ? (
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 70a14d3..2f6c599 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -4,6 +4,7 @@ import "./index.css";
import { Outlet, RouterProvider, createBrowserRouter, Navigate } from "react-router-dom";
import Chat from "./pages/Chat";
import ChatDialog from "./pages/ChatDialog.tsx";
+import TraceLogs from "./pages/TraceLogs.tsx";
import SetupLayout from "./pages/setup/SetupLayout.tsx";
import KGAdmin from "./pages/setup/KGAdmin.tsx";
import IngestGraph from "./pages/setup/IngestGraph.tsx";
@@ -56,6 +57,10 @@ const router = createBrowserRouter([
path: "/preferences",
element: ,
},
+ {
+ path: "/trace",
+ element: ,
+ },
{
path: "/setup",
element: ,
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
new file mode 100644
index 0000000..033a726
--- /dev/null
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -0,0 +1,697 @@
+import { FC, useState, useMemo } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import {
+ LuArrowLeft,
+ LuChevronDown,
+ LuChevronUp,
+ LuCopy,
+ LuDownload,
+ LuClock,
+ LuWrench,
+ LuBookOpen,
+ LuActivity,
+} from "react-icons/lu";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+interface TraceLogEntry {
+ id: number;
+ type: "tool_call" | "tool_result" | "citation";
+ timestamp: string;
+ label: string;
+ detail?: string;
+ durationMs?: number;
+ content?: string;
+ step?: number;
+}
+
+interface ToolCallEntry {
+ id: number;
+ name: string;
+ timestamp: string;
+ durationMs: number;
+ input?: string;
+ output?: string;
+}
+
+interface CitationEntry {
+ id: number;
+ source: string;
+ cited: boolean;
+ text: string;
+}
+
+interface TimelineStep {
+ step: number;
+ name: string;
+ durationMs: number;
+}
+
+interface TraceData {
+ originalQuery: string;
+ conversationContext: string[];
+ status: "completed" | "in_progress" | "failed";
+ sessionId: string;
+ timing: {
+ totalDuration: number;
+ toolExecution: number;
+ llmThinking: number;
+ startTime: string;
+ endTime: string;
+ };
+ logs: TraceLogEntry[];
+ toolCalls: ToolCallEntry[];
+ citations: CitationEntry[];
+ timeline: TimelineStep[];
+ finalResponse: string;
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function formatDuration(seconds: number): string {
+ if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`;
+ return `${seconds.toFixed(2)}s`;
+}
+
+function safeJson(obj: any, maxLen = 2000): string {
+ if (obj == null) return "N/A";
+ if (typeof obj === "string") {
+ try {
+ const parsed = JSON.parse(obj);
+ const pretty = JSON.stringify(parsed, null, 2);
+ return pretty.length > maxLen ? pretty.slice(0, maxLen) + "\n…truncated" : pretty;
+ } catch {
+ return obj.length > maxLen ? obj.slice(0, maxLen) + "\n…truncated" : obj;
+ }
+ }
+ try {
+ const s = JSON.stringify(obj, null, 2);
+ return s.length > maxLen ? s.slice(0, maxLen) + "\n…truncated" : s;
+ } catch {
+ return String(obj);
+ }
+}
+
+function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
+ const now = new Date();
+ const sessionTs = now.toISOString().replace(/[-:T]/g, "").slice(0, 15);
+ const sessionId = `chat_${sessionTs}`;
+
+ const query = userQuery || message?.originalQuery || message?.query || "N/A";
+ const qs = message?.query_sources || {};
+ const responseType = message?.response_type || "";
+ const totalResponseTime = message?.response_time || 0;
+ const ts = now.toLocaleTimeString();
+
+ // ── Tool Calls ──────────────────────────────────────────────────────────
+ // query_sources.agent_steps = array of { node, duration_s } from the
+ // actual LangGraph agent execution (collected in agent.py)
+ const toolCalls: ToolCallEntry[] = [];
+ const agentSteps: { node: string; duration_s: number }[] =
+ qs.agent_steps || [];
+
+ if (agentSteps.length > 0) {
+ agentSteps.forEach((step: { node: string; duration_s: number }, i: number) => {
+ toolCalls.push({
+ id: i + 1,
+ name: step.node,
+ timestamp: ts,
+ durationMs: Math.round(step.duration_s * 1000),
+ input: "",
+ output: "",
+ });
+ });
+ }
+
+ // ── Citations ───────────────────────────────────────────────────────────
+ const rawReasoning = qs.reasoning;
+ const finalRetrieval =
+ typeof qs.result === "object" && qs.result?.final_retrieval
+ ? qs.result.final_retrieval
+ : null;
+ const citations: CitationEntry[] = [];
+
+ if (rawReasoning && Array.isArray(rawReasoning)) {
+ rawReasoning.forEach((src: any, i: number) => {
+ if (src == null) return;
+ const raw = typeof src === "string" ? src : String(src);
+ const cited = raw.startsWith("* ");
+ const chunkName = raw.replace(/^\*\s*/, "");
+
+ let chunkText = "";
+ if (finalRetrieval && finalRetrieval[chunkName]) {
+ const val = finalRetrieval[chunkName];
+ chunkText = Array.isArray(val) ? val.join("\n\n") : String(val);
+ }
+
+ citations.push({
+ id: i + 1,
+ source: chunkName,
+ cited,
+ text: chunkText,
+ });
+ });
+ }
+
+ // ── Logs ────────────────────────────────────────────────────────────────
+ const logs: TraceLogEntry[] = [];
+ let logId = 0;
+ toolCalls.forEach((tc) => {
+ logs.push({
+ id: logId++,
+ type: "tool_call",
+ timestamp: tc.timestamp,
+ label: `${tc.name} - Input`,
+ content: tc.input,
+ });
+ logs.push({
+ id: logId++,
+ type: "tool_result",
+ timestamp: tc.timestamp,
+ label: `${tc.name} - Result`,
+ content: tc.output,
+ });
+ });
+
+ // ── Timeline ────────────────────────────────────────────────────────────
+ const timeline: TimelineStep[] = toolCalls.map((tc, i) => ({
+ step: i + 1,
+ name: tc.name,
+ durationMs: tc.durationMs,
+ }));
+
+ const totalToolSec = agentSteps.reduce(
+ (sum: number, s: { duration_s: number }) => sum + s.duration_s,
+ 0
+ );
+ const llmThinking = Math.max(0, totalResponseTime - totalToolSec);
+ const endTime = new Date(now.getTime() + totalResponseTime * 1000);
+
+ return {
+ originalQuery: query,
+ conversationContext: [`user: ${query}`],
+ status: "completed",
+ sessionId,
+ timing: {
+ totalDuration: totalResponseTime,
+ toolExecution: totalToolSec,
+ llmThinking,
+ startTime: now.toLocaleTimeString(),
+ endTime: endTime.toLocaleTimeString(),
+ },
+ logs,
+ toolCalls,
+ citations,
+ timeline,
+ finalResponse: message?.content || "",
+ };
+}
+
+// ─── Sub-components ───────────────────────────────────────────────────────────
+
+const StatusBadge: FC<{ status: string }> = ({ status }) => {
+ const color =
+ status === "completed"
+ ? "bg-emerald-500"
+ : status === "in_progress"
+ ? "bg-blue-500"
+ : "bg-red-500";
+ return (
+
+ {status}
+
+ );
+};
+
+const TimingRow: FC<{
+ items: { value: string; label: string; color: string }[];
+}> = ({ items }) => (
+
+ {items.map((item, i) => (
+
+
+ {item.value}
+
+
+ {item.label}
+
+
+ ))}
+
+);
+
+const ExpandableRow: FC<{
+ children: React.ReactNode;
+ content?: string;
+ defaultOpen?: boolean;
+}> = ({ children, content, defaultOpen = false }) => {
+ const [open, setOpen] = useState(defaultOpen);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+ {children}
+
+
+ {content && (
+ {
+ e.stopPropagation();
+ navigator.clipboard.writeText(content);
+ }}
+ title="Copy"
+ >
+
+
+ )}
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && content && (
+
+ )}
+
+ );
+};
+
+// ─── Tab Panels ───────────────────────────────────────────────────────────────
+
+const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
+ const [collapsed, setCollapsed] = useState(false);
+ const toolCount = trace.logs.filter(
+ (l) => l.type === "tool_call" || l.type === "tool_result"
+ ).length;
+ const citationCount = trace.citations.length;
+ const totalEvents = trace.logs.length;
+
+ return (
+
+
+
+
+ {trace.logs.length} log entries ({totalEvents} events)
+
+
+ Tools ({toolCount})
+
+
+ Citations ({citationCount})
+
+
+
setCollapsed((p) => !p)}
+ >
+ {collapsed ? "Expand All" : "Collapse All"}
+
+
+
+
+ {trace.logs.map((log) => {
+ const isToolCall = log.type === "tool_call";
+ const isToolResult = log.type === "tool_result";
+
+ const dotColor = isToolResult
+ ? "bg-emerald-500"
+ : "bg-blue-500";
+ const iconBg = isToolResult
+ ? "text-emerald-600 dark:text-emerald-400"
+ : "text-blue-600 dark:text-blue-400";
+ const typeLabel = isToolCall
+ ? "Tool Call"
+ : "Tool Result";
+
+ return (
+
+
+
+
+
+ {isToolCall && }
+ {isToolResult && "✓ "}
+ {typeLabel}
+
+
+ {log.timestamp}
+
+
+ {log.label}
+
+ {log.durationMs && (
+
+ ({log.durationMs}ms)
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+
+ {tc.id}
+
+ {tc.name}
+ {tc.timestamp}
+
+
+ {tc.durationMs > 0 && (
+
+ {tc.durationMs}ms
+
+ )}
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && (
+
+
+
+ Input
+
+
+ {tc.input || "N/A"}
+
+
+
+
+ Result
+
+
+ {tc.output || "N/A"}
+
+
+
+ )}
+
+ );
+};
+
+const ToolCallsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.toolCalls.map((tc) => (
+
+ ))}
+
+);
+
+
+const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((p) => !p)}
+ >
+
+
+
+ [{c.source}]
+
+ {c.cited && (
+
+ Cited
+
+ )}
+
+
+ {open ? (
+
+ ) : (
+
+ )}
+
+
+ {open && (
+
+ {c.text || "No content retrieved for this chunk."}
+
+ )}
+
+ );
+};
+
+const CitationsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.citations.length === 0 ? (
+
+ No citations available for this trace.
+
+ ) : (
+ trace.citations.map((c) =>
)
+ )}
+
+);
+
+const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
+
+ {trace.timeline.map((item, i) => (
+
+
+ Step {item.step}
+
+
+
+ {item.durationMs}ms
+
+ {item.name}
+
+
+ ))}
+
+);
+
+// ─── Main Page ────────────────────────────────────────────────────────────────
+
+const TraceLogs: FC = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const message = location.state?.message;
+ const userQuery = location.state?.userQuery;
+
+ const trace = useMemo(
+ () => buildTraceFromMessage(message, userQuery),
+ [message, userQuery]
+ );
+
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleDownload = () => {
+ const blob = new Blob([JSON.stringify(trace, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `trace_${trace.sessionId}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Back to Chat
+
+
Trace Logs
+
+
+
+
+ Session: {trace.sessionId}
+
+
+
+ Download
+
+
+
+
+
+
+ {/* Original Query */}
+
+
Original Query
+
+ {trace.originalQuery}
+
+
+
+ {/* Conversation Context */}
+
+
Conversation Context
+
+ {trace.conversationContext.map((line, i) => (
+
+ {line}
+
+ ))}
+
+
+
+ {/* Timing Overview */}
+
+
Timing Overview
+
+ {/* Timeline bar */}
+
+
+
+ Start
+ {trace.timing.startTime}
+ {trace.timing.endTime}
+
+
+
+
+ {/* Tabs */}
+
+
+
+
+ Logs
+
+
+ Tool Calls
+
+ {trace.toolCalls.length}
+
+
+
+ Citations
+
+ {trace.citations.length}
+
+
+
+ Timeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Final Response */}
+ {trace.finalResponse && (
+
+
Final Response
+
+
+ {trace.finalResponse}
+
+
+
+ )}
+
+
+ );
+};
+
+export default TraceLogs;
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 49b8552..735565c 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,14 +129,30 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
+ agent_steps = []
+ step_start = time.time()
+
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
- # logger.info(f"testing steps {key}: {value}")
- LogWriter.info(f"request_id={req_id_cv.get()} executed node {key}")
+ step_end = time.time()
+ step_duration = round(step_end - step_start, 3)
+ agent_steps.append({
+ "node": key,
+ "duration_s": step_duration,
+ })
+ LogWriter.info(
+ f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
+ )
+ step_start = step_end
+
+ answer = value["answer"]
+ if answer.query_sources is None:
+ answer.query_sources = {}
+ answer.query_sources["agent_steps"] = agent_steps
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
- return value["answer"]
+ return answer
except Exception as e:
metrics.llm_query_error_total.labels(self.model_name).inc()
LogWriter.error(f"request_id={req_id_cv.get()} FAILURE question_for_agent")
From 2b337628e88a9d52d653f0da56a96cda47b9a2ca Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 17:02:19 +0530
Subject: [PATCH 26/70] fix: avoid intermediate variable in agent.py return
---
graphrag/app/agent/agent.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 735565c..ddb6a2e 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -146,13 +146,12 @@ def question_for_agent(
)
step_start = step_end
- answer = value["answer"]
- if answer.query_sources is None:
- answer.query_sources = {}
- answer.query_sources["agent_steps"] = agent_steps
+ if value["answer"].query_sources is None:
+ value["answer"].query_sources = {}
+ value["answer"].query_sources["agent_steps"] = agent_steps
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
- return answer
+ return value["answer"]
except Exception as e:
metrics.llm_query_error_total.labels(self.model_name).inc()
LogWriter.error(f"request_id={req_id_cv.get()} FAILURE question_for_agent")
From 3e260c776e811ef54b6dea36b4f00fd6827e389e Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 18:23:08 +0530
Subject: [PATCH 27/70] feat: capture node input/output in agent_steps and
display in Trace Logs
---
graphrag-ui/src/pages/TraceLogs.tsx | 11 ++++-------
graphrag/app/agent/agent.py | 12 ++++++++++++
2 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 033a726..bc4a597 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -102,26 +102,23 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const query = userQuery || message?.originalQuery || message?.query || "N/A";
const qs = message?.query_sources || {};
- const responseType = message?.response_type || "";
const totalResponseTime = message?.response_time || 0;
const ts = now.toLocaleTimeString();
// ── Tool Calls ──────────────────────────────────────────────────────────
- // query_sources.agent_steps = array of { node, duration_s } from the
- // actual LangGraph agent execution (collected in agent.py)
const toolCalls: ToolCallEntry[] = [];
- const agentSteps: { node: string; duration_s: number }[] =
+ const agentSteps: { node: string; duration_s: number; input?: string; output?: string }[] =
qs.agent_steps || [];
if (agentSteps.length > 0) {
- agentSteps.forEach((step: { node: string; duration_s: number }, i: number) => {
+ agentSteps.forEach((step, i: number) => {
toolCalls.push({
id: i + 1,
name: step.node,
timestamp: ts,
durationMs: Math.round(step.duration_s * 1000),
- input: "",
- output: "",
+ input: safeJson(step.input),
+ output: safeJson(step.output),
});
});
}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index ddb6a2e..313861a 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -131,16 +131,28 @@ def question_for_agent(
agent_steps = []
step_start = time.time()
+ prev_output = input_data["input"]
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
+
+ def _safe_serialize(obj, max_len=3000):
+ try:
+ s = json.dumps(obj, default=str)
+ except Exception:
+ s = str(obj)
+ return s[:max_len] if len(s) > max_len else s
+
agent_steps.append({
"node": key,
"duration_s": step_duration,
+ "input": _safe_serialize(prev_output),
+ "output": _safe_serialize(value),
})
+ prev_output = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
)
From 200895dddb5406b6fa99d4668ea7fdb204d8a8f9 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 14 Apr 2026 19:08:50 +0530
Subject: [PATCH 28/70] fix: move _safe_serialize above loop, remove unused
LuClock import
---
graphrag-ui/src/pages/TraceLogs.tsx | 1 -
graphrag/app/agent/agent.py | 14 +++++++-------
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index bc4a597..a3ffe34 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -7,7 +7,6 @@ import {
LuChevronUp,
LuCopy,
LuDownload,
- LuClock,
LuWrench,
LuBookOpen,
LuActivity,
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 313861a..c8a67a1 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,6 +129,13 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
+ def _safe_serialize(obj, max_len=3000):
+ try:
+ s = json.dumps(obj, default=str)
+ except Exception:
+ s = str(obj)
+ return s[:max_len] if len(s) > max_len else s
+
agent_steps = []
step_start = time.time()
prev_output = input_data["input"]
@@ -139,13 +146,6 @@ def question_for_agent(
step_end = time.time()
step_duration = round(step_end - step_start, 3)
- def _safe_serialize(obj, max_len=3000):
- try:
- s = json.dumps(obj, default=str)
- except Exception:
- s = str(obj)
- return s[:max_len] if len(s) > max_len else s
-
agent_steps.append({
"node": key,
"duration_s": step_duration,
From e6c98ab62acdca09948d5e6959ac8905b465bf7d Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 11:39:36 +0530
Subject: [PATCH 29/70] feat: role-gated View Trace, remove truncation, fix
routing labels
---
.../src/components/CustomChatMessage.tsx | 17 +-
graphrag-ui/src/components/Interact.tsx | 31 +++-
graphrag-ui/src/pages/TraceLogs.tsx | 169 ++++++++++--------
graphrag/app/agent/agent.py | 64 ++++++-
4 files changed, 180 insertions(+), 101 deletions(-)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 285ac8f..1830018 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
-import { LuActivity } from "react-icons/lu";
import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
@@ -193,18 +192,12 @@ export const CustomChatMessage: FC = ({
showExplain={handleShowExplain}
showTable={handleShowTable}
showGraph={handleShowGraph}
+ onViewTrace={() => {
+ navigate(`/trace/${message.messageId || message.message_id || ""}`, {
+ state: { message, userQuery: message.userQuery || "" },
+ });
+ }}
/>
- {(message.response_type !== "progress" && (message.query_sources?.result || message.query_sources?.reasoning)) && (
- {
- navigate("/trace", { state: { message, userQuery: message.userQuery || "" } });
- }}
- >
-
- View Trace
-
- )}
{showGraphVis ? (
diff --git a/graphrag-ui/src/components/Interact.tsx b/graphrag-ui/src/components/Interact.tsx
index e1426f0..9f259f4 100644
--- a/graphrag-ui/src/components/Interact.tsx
+++ b/graphrag-ui/src/components/Interact.tsx
@@ -10,7 +10,8 @@ import { PiArrowsCounterClockwiseFill } from "react-icons/pi";
import { Feedback, Message } from "@/actions/ActionProvider";
import { PiGraph } from "react-icons/pi";
import { FaTable } from "react-icons/fa";
-import { LuInfo } from "react-icons/lu";
+import { LuInfo, LuActivity } from "react-icons/lu";
+import { useRoles } from "@/hooks/useRoles";
const GRAPHRAG_URL = "";
interface Interactions {
@@ -18,6 +19,7 @@ interface Interactions {
showExplain: () => boolean;
showTable: () => boolean;
showGraph: () => boolean;
+ onViewTrace?: () => void;
}
export const Interactions: FC = ({
@@ -25,8 +27,11 @@ export const Interactions: FC = ({
showExplain,
showTable,
showGraph,
+ onViewTrace,
}: Interactions) => {
const [feedback, setFeedback] = useState(Feedback.NoFeedback);
+ const { isSuperuser, isGlobalDesigner, isGraphAdmin } = useRoles();
+ const canViewTrace = isSuperuser || isGlobalDesigner || isGraphAdmin;
const sendFeedback = async (action: Feedback, message: Message) => {
const creds = sessionStorage.getItem("creds");
@@ -90,13 +95,23 @@ export const Interactions: FC = ({
*/}
- showExplain()}
- >
-
- Explain
-
+ {canViewTrace ? (
+ onViewTrace?.()}
+ >
+
+ View Trace
+
+ ) : (
+ showExplain()}
+ >
+
+ Explain
+
+ )}
maxLen ? pretty.slice(0, maxLen) + "\n…truncated" : pretty;
+ return JSON.stringify(JSON.parse(obj), null, 2);
} catch {
- return obj.length > maxLen ? obj.slice(0, maxLen) + "\n…truncated" : obj;
+ return obj;
}
}
try {
- const s = JSON.stringify(obj, null, 2);
- return s.length > maxLen ? s.slice(0, maxLen) + "\n…truncated" : s;
+ return JSON.stringify(obj, null, 2);
} catch {
return String(obj);
}
}
+const NODE_LABELS: Record = {
+ entry: "Entry",
+ supportai: "SupportAI",
+ map_question_to_schema: "Map Question to Schema",
+ generate_function: "Generate Function",
+ generate_cypher: "Generate Cypher",
+ generate_answer: "Generate Answer",
+ lookup_history: "Lookup History",
+ merge_history_context: "Merge History Context",
+ rewrite_question: "Rewrite Question",
+ apologize: "Apologize",
+ greet: "Greet",
+};
+
function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const now = new Date();
const sessionTs = now.toISOString().replace(/[-:T]/g, "").slice(0, 15);
@@ -113,7 +124,7 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
agentSteps.forEach((step, i: number) => {
toolCalls.push({
id: i + 1,
- name: step.node,
+ name: NODE_LABELS[step.node] || step.node,
timestamp: ts,
durationMs: Math.round(step.duration_s * 1000),
input: safeJson(step.input),
@@ -160,14 +171,15 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
id: logId++,
type: "tool_call",
timestamp: tc.timestamp,
- label: `${tc.name} - Input`,
+ label: `${tc.name} — Input`,
content: tc.input,
+ durationMs: tc.durationMs,
});
logs.push({
id: logId++,
- type: "tool_result",
+ type: "citation",
timestamp: tc.timestamp,
- label: `${tc.name} - Result`,
+ label: `${tc.name} — Output`,
content: tc.output,
});
});
@@ -283,7 +295,7 @@ const ExpandableRow: FC<{
{open && content && (
)}
@@ -294,24 +306,19 @@ const ExpandableRow: FC<{
const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
const [collapsed, setCollapsed] = useState(false);
- const toolCount = trace.logs.filter(
- (l) => l.type === "tool_call" || l.type === "tool_result"
- ).length;
- const citationCount = trace.citations.length;
- const totalEvents = trace.logs.length;
return (
- {trace.logs.length} log entries ({totalEvents} events)
+ {trace.logs.length} agent steps
- Tools ({toolCount})
+ Nodes ({trace.toolCalls.length})
- Citations ({citationCount})
+ Citations ({trace.citations.length})
= ({ trace }) => {
- {trace.logs.map((log) => {
- const isToolCall = log.type === "tool_call";
- const isToolResult = log.type === "tool_result";
-
- const dotColor = isToolResult
- ? "bg-emerald-500"
- : "bg-blue-500";
- const iconBg = isToolResult
- ? "text-emerald-600 dark:text-emerald-400"
- : "text-blue-600 dark:text-blue-400";
- const typeLabel = isToolCall
- ? "Tool Call"
- : "Tool Result";
-
- return (
-
-
-
-
-
- {isToolCall && }
- {isToolResult && "✓ "}
- {typeLabel}
-
-
- {log.timestamp}
-
-
- {log.label}
+ {trace.logs.map((log) => (
+
+
+
+
+
+
+ Node
+
+
+ {log.timestamp}
+
+
+ {log.label}
+
+ {log.durationMs != null && log.durationMs > 0 && (
+
+ ({log.durationMs}ms)
- {log.durationMs && (
-
- ({log.durationMs}ms)
-
- )}
-
-
+ )}
+
- );
- })}
+
+ ))}
);
@@ -408,15 +399,15 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
Input
-
+
{tc.input || "N/A"}
- Result
+ Output
-
+
{tc.output || "N/A"}
@@ -512,8 +503,34 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
const TraceLogs: FC = () => {
const location = useLocation();
const navigate = useNavigate();
- const message = location.state?.message;
- const userQuery = location.state?.userQuery;
+ const { messageId } = useParams<{ messageId: string }>();
+
+ const stateMessage = location.state?.message;
+ const stateUserQuery = location.state?.userQuery;
+
+ const [apiData, setApiData] = useState(null);
+ const [loading, setLoading] = useState(!stateMessage);
+
+ useEffect(() => {
+ if (stateMessage || !messageId) return;
+ setLoading(true);
+ fetch(`/ui/trace/${messageId}`)
+ .then((res) => {
+ if (!res.ok) throw new Error("Not found");
+ return res.json();
+ })
+ .then((data) => setApiData(data))
+ .catch(() => setApiData(null))
+ .finally(() => setLoading(false));
+ }, [messageId, stateMessage]);
+
+ const message = stateMessage || (apiData ? {
+ content: apiData.natural_language_response,
+ response_time: apiData.response_time,
+ response_type: apiData.response_type,
+ query_sources: apiData.query_sources,
+ } : null);
+ const userQuery = stateUserQuery || apiData?.user_query;
const trace = useMemo(
() => buildTraceFromMessage(message, userQuery),
@@ -536,6 +553,14 @@ const TraceLogs: FC = () => {
URL.revokeObjectURL(url);
};
+ if (loading) {
+ return (
+
+
Loading trace data...
+
+ );
+ }
+
return (
{/* Header */}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index c8a67a1..e64b389 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -129,35 +129,81 @@ def question_for_agent(
logger.error(f"Failed to serialize input_data to JSON: {e}")
raise ValueError("Invalid input data format. Unable to convert to JSON.")
- def _safe_serialize(obj, max_len=3000):
+ def _safe(obj):
try:
- s = json.dumps(obj, default=str)
+ return json.dumps(obj, default=str)
except Exception:
- s = str(obj)
- return s[:max_len] if len(s) > max_len else s
+ return str(obj)
+
+ def _node_output(node, state):
+ """Extract the meaningful output that this node produced."""
+ _LOOKUP_LABELS = {"inquiryai": "db_search", "supportai": "vector_search"}
+ lookup = state.get("lookup_source", "")
+ lookup = _LOOKUP_LABELS.get(lookup, lookup)
+
+ if node == "entry":
+ return ""
+ elif node == "map_question_to_schema":
+ return _safe({"schema_mapping": str(state.get("schema_mapping", ""))})
+ elif node == "generate_function":
+ ctx = state.get("context", {})
+ return _safe({
+ "context": ctx if isinstance(ctx, dict) else str(ctx),
+ "lookup_source": lookup,
+ })
+ elif node == "generate_cypher":
+ ctx = state.get("context", {})
+ return _safe({
+ "cypher": ctx.get("cypher", "") if isinstance(ctx, dict) else "",
+ "reasoning": ctx.get("reasoning", "") if isinstance(ctx, dict) else "",
+ "result": ctx.get("result", "") if isinstance(ctx, dict) else "",
+ "lookup_source": lookup,
+ })
+ elif node == "supportai":
+ ctx = state.get("context", {})
+ return _safe({
+ "context": ctx if isinstance(ctx, dict) else str(ctx),
+ "lookup_source": lookup,
+ })
+ elif node == "generate_answer":
+ ans = state.get("answer")
+ return _safe({
+ "natural_language_response": getattr(ans, "natural_language_response", "") if ans else "",
+ "answered_question": getattr(ans, "answered_question", False) if ans else False,
+ "response_type": getattr(ans, "response_type", "") if ans else "",
+ })
+ elif node in ("greet", "apologize"):
+ ans = state.get("answer")
+ return getattr(ans, "natural_language_response", "") if ans else ""
+ return _safe(state)
agent_steps = []
step_start = time.time()
- prev_output = input_data["input"]
+ prev_state = {"question": input_data["input"], "conversation": input_data["conversation"]}
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
-
agent_steps.append({
"node": key,
"duration_s": step_duration,
- "input": _safe_serialize(prev_output),
- "output": _safe_serialize(value),
+ "input": _safe(prev_state),
+ "output": _node_output(key, value),
})
- prev_output = value
+ prev_state = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
)
step_start = step_end
+ # Backfill entry with routing decision
+ if len(agent_steps) >= 2 and agent_steps[0]["node"] == "entry":
+ next_node = agent_steps[1]["node"]
+ _ROUTE_LABELS = {"supportai": "vector_search", "map_question_to_schema": "db_search", "lookup_history": "history_lookup"}
+ agent_steps[0]["output"] = _safe({"routing_decision": _ROUTE_LABELS.get(next_node, next_node)})
+
if value["answer"].query_sources is None:
value["answer"].query_sources = {}
value["answer"].query_sources["agent_steps"] = agent_steps
From 722d11584dd638fffbea9bbcb6054106a2071237 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 14:51:52 +0530
Subject: [PATCH 30/70] feat: add trace_logs volume mount in docker-compose
---
docker-compose.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/docker-compose.yml b/docker-compose.yml
index 97a0952..449939b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,7 @@ services:
USE_CYPHER: "true"
volumes:
- ./configs/:/code/configs
+ - ./trace_logs/:/code/trace_logs
graphrag-ecc:
image: tigergraph/graphrag-ecc:latest
From 7250dddf04a8c8fde1ec87d39c840430e05395b4 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 17 Apr 2026 23:44:48 +0530
Subject: [PATCH 31/70] feat: add trace log save/fetch endpoints in ui.py and
nginx /trace route
---
docs/tutorials/configs/nginx.conf | 8 +++++
graphrag/app/routers/ui.py | 58 +++++++++++++++++++++++++++++++
2 files changed, 66 insertions(+)
diff --git a/docs/tutorials/configs/nginx.conf b/docs/tutorials/configs/nginx.conf
index 975d8a0..121922c 100644
--- a/docs/tutorials/configs/nginx.conf
+++ b/docs/tutorials/configs/nginx.conf
@@ -29,6 +29,14 @@ server {
proxy_pass http://graphrag-ui:3000/;
}
+ location /trace {
+ proxy_pass http://graphrag-ui:3000;
+ }
+
+ location /trace/ {
+ proxy_pass http://graphrag-ui:3000;
+ }
+
location ~^/ui/.*/chat$ {
proxy_pass http://graphrag:8000;
proxy_http_version 1.1;
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 400435d..432eb3a 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -70,6 +70,28 @@
logger = logging.getLogger(__name__)
+TRACE_LOGS_DIR = os.environ.get("TRACE_LOGS_DIR", "/code/trace_logs")
+
+def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
+ try:
+ os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+ trace_data = {
+ "message_id": message_id,
+ "conversation_id": conversation_id,
+ "user_query": user_query,
+ "response_time": elapsed,
+ "response_type": resp.response_type,
+ "answered_question": resp.answered_question,
+ "query_sources": resp.query_sources,
+ "natural_language_response": resp.natural_language_response,
+ "timestamp": time.time(),
+ }
+ filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
+ with open(filepath, "w") as f:
+ json.dump(trace_data, f, default=str)
+ except Exception:
+ logger.warning(f"Failed to save trace log for message {message_id}", exc_info=True)
+
# Validated graph name path parameter — rejects path traversal characters
ValidGraphName = Annotated[str, Path(pattern=r"^[A-Za-z_][A-Za-z0-9_]*$")]
@@ -338,6 +360,40 @@ def add_feedback(
return {"message": "feedback saved", "message_id": message.message_id}
+@router.get(route_prefix + "/trace/{message_id}")
+def get_trace_log(message_id: str):
+ filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
+ if not os.path.exists(filepath):
+ raise HTTPException(status_code=404, detail="Trace log not found")
+ with open(filepath, "r") as f:
+ return json.load(f)
+
+
+@router.get(route_prefix + "/traces/{conversation_id}")
+def list_trace_logs(conversation_id: str):
+ if not os.path.isdir(TRACE_LOGS_DIR):
+ return []
+ traces = []
+ for filename in os.listdir(TRACE_LOGS_DIR):
+ if not filename.endswith(".json"):
+ continue
+ filepath = os.path.join(TRACE_LOGS_DIR, filename)
+ try:
+ with open(filepath, "r") as f:
+ data = json.load(f)
+ if data.get("conversation_id") == conversation_id:
+ traces.append({
+ "message_id": data.get("message_id"),
+ "user_query": data.get("user_query"),
+ "response_time": data.get("response_time"),
+ "timestamp": data.get("timestamp"),
+ })
+ except Exception:
+ continue
+ traces.sort(key=lambda t: t.get("timestamp", 0))
+ return traces
+
+
@router.post(route_prefix + "/{graphname}/create_graph")
def create_graph(
graphname: ValidGraphName,
@@ -1074,6 +1130,7 @@ async def graph_query(
query_sources=resp.query_sources,
)
await write_message_to_history(message, auth)
+ _save_trace_log(message.message_id, convo_id, data, resp, elapsed)
prev_id = message.message_id
# reply
@@ -1200,6 +1257,7 @@ async def chat(
query_sources=resp.query_sources,
)
await write_message_to_history(message, usr_auth)
+ _save_trace_log(message.message_id, convo_id, data, resp, elapsed)
prev_id = message.message_id
# reply
From e359f98e89d97723ef6ae2f44cd9fa06236bdf0b Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Sat, 18 Apr 2026 00:23:50 +0530
Subject: [PATCH 32/70] fix: update trace route to /trace/:messageId in
main.tsx
---
graphrag-ui/src/main.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 2f6c599..79def9b 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -58,7 +58,7 @@ const router = createBrowserRouter([
element: ,
},
{
- path: "/trace",
+ path: "/trace/:messageId",
element: ,
},
{
From 39c126355c9aad0a4ed03b0ad5505b6cbc9f592e Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 21 Apr 2026 15:59:05 +0530
Subject: [PATCH 33/70] fix: show all durations in seconds in TraceLogs
---
graphrag-ui/src/pages/TraceLogs.tsx | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 2dfc851..4b43936 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -71,7 +71,6 @@ interface TraceData {
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDuration(seconds: number): string {
- if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`;
return `${seconds.toFixed(2)}s`;
}
@@ -353,7 +352,7 @@ const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => {
{log.durationMs != null && log.durationMs > 0 && (
- ({log.durationMs}ms)
+ ({formatDuration(log.durationMs / 1000)})
)}
@@ -383,7 +382,7 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{tc.durationMs > 0 && (
- {tc.durationMs}ms
+ {formatDuration(tc.durationMs / 1000)}
)}
{open ? (
@@ -489,7 +488,7 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
- {item.durationMs}ms
+ {formatDuration(item.durationMs / 1000)}
{item.name}
From ef0dae0d5979d34b6b287f7ebbf136b6892c0d9f Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Mon, 27 Apr 2026 20:06:48 +0530
Subject: [PATCH 34/70] feat: add LLM token usage tracking per node and Token
Overview tab
---
common/llm_services/base_llm.py | 28 ++
.../src/components/CustomChatMessage.tsx | 8 +-
graphrag-ui/src/main.tsx | 1 +
graphrag-ui/src/pages/TraceLogs.tsx | 253 +++++++++++++++++-
graphrag/app/agent/agent.py | 41 ++-
5 files changed, 321 insertions(+), 10 deletions(-)
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index ba1c770..005fab1 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -23,6 +23,32 @@
logger = logging.getLogger(__name__)
+# Per-request collector for LLM usage so callers (e.g. agent trace logs) can
+# aggregate token usage without breaking the existing return signatures.
+# It's a context-local list the agent resets before each node executes.
+import contextvars as _contextvars
+
+_usage_collector: _contextvars.ContextVar = _contextvars.ContextVar(
+ "llm_usage_collector", default=None
+)
+
+
+def start_usage_collection():
+ """Begin collecting LLM usage for the current context (per node)."""
+ _usage_collector.set([])
+
+
+def get_collected_usage():
+ """Return the usage entries collected since the last start (or None)."""
+ return _usage_collector.get()
+
+
+def _record_usage(caller_name: str, usage_data: dict):
+ bucket = _usage_collector.get()
+ if bucket is not None:
+ bucket.append({"caller_name": caller_name, **usage_data})
+
+
class LLM_Model:
"""Base LLM_Model Class
@@ -95,6 +121,7 @@ def invoke_with_parser(
usage_data["total_tokens"] = cb.total_tokens
usage_data["cost"] = cb.total_cost
logger.info(f"{caller_name} usage: {usage_data}")
+ _record_usage(caller_name, usage_data)
raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output)
@@ -131,6 +158,7 @@ async def ainvoke_with_parser(
usage_data["total_tokens"] = cb.total_tokens
usage_data["cost"] = cb.total_cost
logger.info(f"{caller_name} usage: {usage_data}")
+ _record_usage(caller_name, usage_data)
raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 1830018..14ccf87 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dialog"
import { ImEnlarge2 } from "react-icons/im";
import { IoIosCloseCircleOutline } from "react-icons/io";
-import { useNavigate } from "react-router-dom";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
@@ -128,7 +127,6 @@ const AuthenticatedImage: FC<{ src: string; alt: string }> = ({ src, alt }) => {
export const CustomChatMessage: FC = ({
message,
}) => {
- const navigate = useNavigate();
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
@@ -193,9 +191,9 @@ export const CustomChatMessage: FC = ({
showTable={handleShowTable}
showGraph={handleShowGraph}
onViewTrace={() => {
- navigate(`/trace/${message.messageId || message.message_id || ""}`, {
- state: { message, userQuery: message.userQuery || "" },
- });
+ const messageId = message.messageId || message.message_id || "";
+ // No noopener — browser copies sessionStorage to the new tab automatically.
+ window.open(`/trace/${messageId}`, "_blank");
}}
/>
diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx
index 79def9b..1788c05 100755
--- a/graphrag-ui/src/main.tsx
+++ b/graphrag-ui/src/main.tsx
@@ -26,6 +26,7 @@ const RequireAuth = ({ children }: { children: any }) => {
return children;
};
+
const Layout = () => {
useIdleTimeout();
return (
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 4b43936..566cc4a 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -10,6 +10,8 @@ import {
LuWrench,
LuBookOpen,
LuActivity,
+ LuCoins,
+ LuInfo,
} from "react-icons/lu";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -27,6 +29,17 @@ interface TraceLogEntry {
step?: number;
}
+interface TokenUsage {
+ input_tokens: number;
+ output_tokens: number;
+ total_tokens: number;
+ cost: number;
+}
+
+interface LlmCall extends TokenUsage {
+ caller_name: string;
+}
+
interface ToolCallEntry {
id: number;
name: string;
@@ -34,6 +47,7 @@ interface ToolCallEntry {
durationMs: number;
input?: string;
output?: string;
+ usage?: TokenUsage & { calls?: LlmCall[] };
}
interface CitationEntry {
@@ -65,12 +79,14 @@ interface TraceData {
toolCalls: ToolCallEntry[];
citations: CitationEntry[];
timeline: TimelineStep[];
+ tokenUsage: TokenUsage;
finalResponse: string;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatDuration(seconds: number): string {
+ if (seconds < 0.01) return `${Math.round(seconds * 1000)}ms`;
return `${seconds.toFixed(2)}s`;
}
@@ -116,8 +132,13 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
// ── Tool Calls ──────────────────────────────────────────────────────────
const toolCalls: ToolCallEntry[] = [];
- const agentSteps: { node: string; duration_s: number; input?: string; output?: string }[] =
- qs.agent_steps || [];
+ const agentSteps: {
+ node: string;
+ duration_s: number;
+ input?: string;
+ output?: string;
+ usage?: TokenUsage & { calls?: LlmCall[] };
+ }[] = qs.agent_steps || [];
if (agentSteps.length > 0) {
agentSteps.forEach((step, i: number) => {
@@ -128,6 +149,7 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
durationMs: Math.round(step.duration_s * 1000),
input: safeJson(step.input),
output: safeJson(step.output),
+ usage: step.usage,
});
});
}
@@ -197,6 +219,22 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const llmThinking = Math.max(0, totalResponseTime - totalToolSec);
const endTime = new Date(now.getTime() + totalResponseTime * 1000);
+ // ── Token usage totals ─────────────────────────────────────────────────
+ const serverTotal = qs.token_usage as TokenUsage | undefined;
+ const tokenUsage: TokenUsage = serverTotal || agentSteps.reduce(
+ (acc, s) => {
+ const u = s.usage;
+ if (!u) return acc;
+ return {
+ input_tokens: acc.input_tokens + (u.input_tokens || 0),
+ output_tokens: acc.output_tokens + (u.output_tokens || 0),
+ total_tokens: acc.total_tokens + (u.total_tokens || 0),
+ cost: acc.cost + (u.cost || 0),
+ };
+ },
+ { input_tokens: 0, output_tokens: 0, total_tokens: 0, cost: 0 } as TokenUsage
+ );
+
return {
originalQuery: query,
conversationContext: [`user: ${query}`],
@@ -213,10 +251,21 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
toolCalls,
citations,
timeline,
+ tokenUsage,
finalResponse: message?.content || "",
};
}
+function formatCost(cost: number): string {
+ if (!cost) return "$0.00";
+ if (cost < 0.01) return `$${cost.toFixed(6)}`;
+ return `$${cost.toFixed(4)}`;
+}
+
+function formatNumber(n: number): string {
+ return (n || 0).toLocaleString();
+}
+
// ─── Sub-components ───────────────────────────────────────────────────────────
const StatusBadge: FC<{ status: string }> = ({ status }) => {
@@ -380,6 +429,14 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{tc.timestamp}
+ {tc.usage && tc.usage.total_tokens > 0 && (
+
+ {formatNumber(tc.usage.total_tokens)} tokens
+
+ )}
{tc.durationMs > 0 && (
{formatDuration(tc.durationMs / 1000)}
@@ -394,6 +451,37 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{open && (
+ {tc.usage && tc.usage.total_tokens > 0 && (
+
+
+ LLM Usage
+
+
+
+
Input
+
{formatNumber(tc.usage.input_tokens)}
+
+
+
Output
+
{formatNumber(tc.usage.output_tokens)}
+
+
+
Total
+
{formatNumber(tc.usage.total_tokens)}
+
+
+
Cost
+
{formatCost(tc.usage.cost)}
+
+
+ {tc.usage.calls && tc.usage.calls.length > 0 && (
+
+ {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
+ {tc.usage.calls.map((c) => c.caller_name).join(", ")}
+
+ )}
+
+ )}
Input
@@ -497,6 +585,142 @@ const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => (
);
+const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => {
+ const usage = trace.tokenUsage;
+ const nodesWithUsage = trace.toolCalls.filter(
+ (tc) => tc.usage && tc.usage.total_tokens > 0
+ );
+
+ return (
+
+ {/* Totals */}
+
+
+
+ Input Tokens
+
+
+ {formatNumber(usage.input_tokens)}
+
+
+
+
+ Output Tokens
+
+
+ {formatNumber(usage.output_tokens)}
+
+
+
+
+ Total Tokens
+
+
+ {formatNumber(usage.total_tokens)}
+
+
+
+
+ Est. Cost
+
+
+
+ Cost is estimated based on the model's published per-token pricing. Actual billing may differ.
+
+
+
+
+
+ {formatCost(usage.cost)}
+
+
estimated
+
+
+
+ {/* Per-node breakdown */}
+
+
+
Usage by Node
+
+ {nodesWithUsage.length === 0 ? (
+
+ No LLM usage recorded for this trace.
+
+ ) : (
+
+
+
+
+ Node
+ Input
+ Output
+ Total
+
+
+ Est. Cost
+
+
+
+ Cost is estimated based on the model's published per-token pricing. Actual billing may differ.
+
+
+
+
+ LLM Calls
+
+
+
+ {nodesWithUsage.map((tc) => (
+
+ {tc.name}
+
+ {formatNumber(tc.usage!.input_tokens)}
+
+
+ {formatNumber(tc.usage!.output_tokens)}
+
+
+ {formatNumber(tc.usage!.total_tokens)}
+
+
+ {formatCost(tc.usage!.cost)}
+
+
+ {tc.usage!.calls && tc.usage!.calls.length > 0
+ ? tc.usage!.calls.map((c) => c.caller_name).join(", ")
+ : "—"}
+
+
+ ))}
+
+
+
+ Total
+
+ {formatNumber(usage.input_tokens)}
+
+
+ {formatNumber(usage.output_tokens)}
+
+
+ {formatNumber(usage.total_tokens)}
+
+
+
+ {formatCost(usage.cost)}
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
// ─── Main Page ────────────────────────────────────────────────────────────────
const TraceLogs: FC = () => {
@@ -537,7 +761,13 @@ const TraceLogs: FC = () => {
);
const handleBack = () => {
- navigate(-1);
+ // Trace opens in a new tab — closing it returns the user to the chat tab.
+ // If the tab cannot be closed (e.g. opened via direct link), fall back to navigate.
+ if (window.opener || window.history.length <= 1) {
+ window.close();
+ } else {
+ navigate(-1);
+ }
};
const handleDownload = () => {
@@ -571,7 +801,7 @@ const TraceLogs: FC = () => {
className="flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline mb-1"
>
- Back to Chat
+ Close & Back to Chat
Trace Logs
@@ -682,6 +912,18 @@ const TraceLogs: FC = () => {
>
Timeline
+
+
+ Token Overview
+ {trace.tokenUsage.total_tokens > 0 && (
+
+ {formatNumber(trace.tokenUsage.total_tokens)}
+
+ )}
+
@@ -696,6 +938,9 @@ const TraceLogs: FC = () => {
+
+
+
{/* Final Response */}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index e64b389..611f03c 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -11,7 +11,7 @@
from common.config import embedding_service, embedding_store, llm_config, get_completion_config, get_chat_config, get_llm_service
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.embedding_services import EmbeddingModel
-from common.llm_services.base_llm import LLM_Model
+from common.llm_services.base_llm import LLM_Model, start_usage_collection, get_collected_usage
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.metrics.prometheus_metrics import metrics
@@ -181,17 +181,47 @@ def _node_output(node, state):
step_start = time.time()
prev_state = {"question": input_data["input"], "conversation": input_data["conversation"]}
+ # Start collecting LLM usage so we can attribute tokens/cost per node.
+ start_usage_collection()
+
for output in self.agent.stream({"question": input_data["input"], "conversation": input_data["conversation"]}):
for key, value in output.items():
step_end = time.time()
step_duration = round(step_end - step_start, 3)
+
+ # Grab usage accumulated during this node and reset for next node.
+ node_usage = get_collected_usage() or []
+ input_tokens = sum(int(u.get("input_tokens", 0) or 0) for u in node_usage)
+ output_tokens = sum(int(u.get("output_tokens", 0) or 0) for u in node_usage)
+ total_tokens = sum(int(u.get("total_tokens", 0) or 0) for u in node_usage)
+ cost = sum(float(u.get("cost", 0) or 0) for u in node_usage)
+
agent_steps.append({
"node": key,
"duration_s": step_duration,
"input": _safe(prev_state),
"output": _node_output(key, value),
+ "usage": {
+ "input_tokens": input_tokens,
+ "output_tokens": output_tokens,
+ "total_tokens": total_tokens,
+ "cost": cost,
+ "calls": [
+ {
+ "caller_name": u.get("caller_name"),
+ "input_tokens": u.get("input_tokens", 0),
+ "output_tokens": u.get("output_tokens", 0),
+ "total_tokens": u.get("total_tokens", 0),
+ "cost": u.get("cost", 0),
+ }
+ for u in node_usage
+ ],
+ },
})
+ # Reset the collector for the next node.
+ start_usage_collection()
+
prev_state = value
LogWriter.info(
f"request_id={req_id_cv.get()} executed node {key} ({step_duration}s)"
@@ -208,6 +238,15 @@ def _node_output(node, state):
value["answer"].query_sources = {}
value["answer"].query_sources["agent_steps"] = agent_steps
+ # Aggregate total LLM usage across all nodes for the Token Overview UI.
+ total_usage = {
+ "input_tokens": sum(int(s.get("usage", {}).get("input_tokens", 0) or 0) for s in agent_steps),
+ "output_tokens": sum(int(s.get("usage", {}).get("output_tokens", 0) or 0) for s in agent_steps),
+ "total_tokens": sum(int(s.get("usage", {}).get("total_tokens", 0) or 0) for s in agent_steps),
+ "cost": sum(float(s.get("usage", {}).get("cost", 0) or 0) for s in agent_steps),
+ }
+ value["answer"].query_sources["token_usage"] = total_usage
+
LogWriter.info(f"request_id={req_id_cv.get()} EXIT question_for_agent")
return value["answer"]
except Exception as e:
From 269638271a9f87cba1f3bbd011168c2038ad3e63 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Mon, 27 Apr 2026 23:13:24 +0530
Subject: [PATCH 35/70] fix: deduplicate LLM caller names in Token Overview
table
---
graphrag-ui/src/pages/TraceLogs.tsx | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 566cc4a..cd82095 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -266,6 +266,17 @@ function formatNumber(n: number): string {
return (n || 0).toLocaleString();
}
+function formatCallerNames(calls: { caller_name: string }[]): string {
+ if (!calls || calls.length === 0) return "—";
+ const counts: Record = {};
+ calls.forEach((c) => {
+ counts[c.caller_name] = (counts[c.caller_name] || 0) + 1;
+ });
+ return Object.entries(counts)
+ .map(([name, count]) => (count > 1 ? `${name} ×${count}` : name))
+ .join(", ");
+}
+
// ─── Sub-components ───────────────────────────────────────────────────────────
const StatusBadge: FC<{ status: string }> = ({ status }) => {
@@ -474,12 +485,12 @@ const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => {
{formatCost(tc.usage.cost)}
- {tc.usage.calls && tc.usage.calls.length > 0 && (
-
- {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
- {tc.usage.calls.map((c) => c.caller_name).join(", ")}
-
- )}
+ {tc.usage.calls && tc.usage.calls.length > 0 && (
+
+ {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "}
+ {formatCallerNames(tc.usage.calls)}
+
+ )}
)}
@@ -687,7 +698,7 @@ const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => {
{tc.usage!.calls && tc.usage!.calls.length > 0
- ? tc.usage!.calls.map((c) => c.caller_name).join(", ")
+ ? formatCallerNames(tc.usage!.calls)
: "—"}
From ad307d9e971b054a33e3601357d4bcef84c5ee0c Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Wed, 29 Apr 2026 18:55:04 +0530
Subject: [PATCH 36/70] fix: add auth to trace endpoint, fix browser auth
dialog, async file IO, remove unused endpoint
---
.../src/components/CustomChatMessage.tsx | 4 ++-
graphrag-ui/src/pages/TraceLogs.tsx | 26 +++++++++------
graphrag/app/routers/ui.py | 33 ++++---------------
3 files changed, 25 insertions(+), 38 deletions(-)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 14ccf87..07e0d5e 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -192,7 +192,9 @@ export const CustomChatMessage: FC = ({
showGraph={handleShowGraph}
onViewTrace={() => {
const messageId = message.messageId || message.message_id || "";
- // No noopener — browser copies sessionStorage to the new tab automatically.
+ // Store message in sessionStorage so the new tab reads it directly
+ // without needing an authenticated API fetch (which triggers browser auth dialog).
+ sessionStorage.setItem(`trace_msg_${messageId}`, JSON.stringify(message));
window.open(`/trace/${messageId}`, "_blank");
}}
/>
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index cd82095..07b25ab 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -742,13 +742,23 @@ const TraceLogs: FC = () => {
const stateMessage = location.state?.message;
const stateUserQuery = location.state?.userQuery;
+ // Check sessionStorage for message stored by the opener tab before API fetch.
+ const sessionKey = messageId ? `trace_msg_${messageId}` : null;
+ const sessionRaw = sessionKey ? sessionStorage.getItem(sessionKey) : null;
+ const sessionMessage = sessionRaw ? JSON.parse(sessionRaw) : null;
+
+ const resolvedMessage = stateMessage || sessionMessage;
+
const [apiData, setApiData] = useState(null);
- const [loading, setLoading] = useState(!stateMessage);
+ const [loading, setLoading] = useState(!resolvedMessage);
useEffect(() => {
- if (stateMessage || !messageId) return;
+ if (resolvedMessage || !messageId) return;
setLoading(true);
- fetch(`/ui/trace/${messageId}`)
+ const creds = sessionStorage.getItem("creds");
+ fetch(`/ui/trace/${messageId}`, {
+ headers: { Authorization: `Basic ${creds}` },
+ })
.then((res) => {
if (!res.ok) throw new Error("Not found");
return res.json();
@@ -756,15 +766,15 @@ const TraceLogs: FC = () => {
.then((data) => setApiData(data))
.catch(() => setApiData(null))
.finally(() => setLoading(false));
- }, [messageId, stateMessage]);
+ }, [messageId, resolvedMessage]);
- const message = stateMessage || (apiData ? {
+ const message = resolvedMessage || (apiData ? {
content: apiData.natural_language_response,
response_time: apiData.response_time,
response_type: apiData.response_type,
query_sources: apiData.query_sources,
} : null);
- const userQuery = stateUserQuery || apiData?.user_query;
+ const userQuery = stateUserQuery || sessionMessage?.userQuery || apiData?.user_query;
const trace = useMemo(
() => buildTraceFromMessage(message, userQuery),
@@ -817,10 +827,6 @@ const TraceLogs: FC = () => {
Trace Logs
-
-
- Session: {trace.sessionId}
-
Date: Mon, 4 May 2026 19:23:59 +0530
Subject: [PATCH 37/70] fix: remove chunk text from citations in trace logs UI
and backend JSON
---
graphrag-ui/src/pages/TraceLogs.tsx | 68 ++++++++---------------------
graphrag/app/routers/ui.py | 11 ++++-
2 files changed, 28 insertions(+), 51 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 07b25ab..bddf861 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -156,10 +156,6 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
// ── Citations ───────────────────────────────────────────────────────────
const rawReasoning = qs.reasoning;
- const finalRetrieval =
- typeof qs.result === "object" && qs.result?.final_retrieval
- ? qs.result.final_retrieval
- : null;
const citations: CitationEntry[] = [];
if (rawReasoning && Array.isArray(rawReasoning)) {
@@ -169,17 +165,11 @@ function buildTraceFromMessage(message: any, userQuery?: string): TraceData {
const cited = raw.startsWith("* ");
const chunkName = raw.replace(/^\*\s*/, "");
- let chunkText = "";
- if (finalRetrieval && finalRetrieval[chunkName]) {
- const val = finalRetrieval[chunkName];
- chunkText = Array.isArray(val) ? val.join("\n\n") : String(val);
- }
-
citations.push({
id: i + 1,
source: chunkName,
cited,
- text: chunkText,
+ text: "",
});
});
}
@@ -524,47 +514,25 @@ const ToolCallsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
);
-const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => {
- const [open, setOpen] = useState(false);
- return (
-
-
setOpen((p) => !p)}
- >
-
-
-
- [{c.source}]
-
- {c.cited && (
-
- Cited
-
- )}
-
-
- {open ? (
-
- ) : (
-
- )}
-
-
- {open && (
-
- {c.text || "No content retrieved for this chunk."}
-
+const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => (
+
+
+
+ [{c.source}]
+ {c.cited && (
+
+ Cited
+
)}
- );
-};
+
+);
const CitationsPanel: FC<{ trace: TraceData }> = ({ trace }) => (
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 5ddfb17..79dff2a 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -75,6 +75,15 @@
def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
try:
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+
+ # Strip chunk text from query_sources to keep trace files small.
+ # final_retrieval contains the full text of every retrieved chunk.
+ query_sources = dict(resp.query_sources) if resp.query_sources else {}
+ result = query_sources.get("result")
+ if isinstance(result, dict) and "final_retrieval" in result:
+ result = {k: v for k, v in result.items() if k != "final_retrieval"}
+ query_sources = {**query_sources, "result": result}
+
trace_data = {
"message_id": message_id,
"conversation_id": conversation_id,
@@ -82,7 +91,7 @@ def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp
"response_time": elapsed,
"response_type": resp.response_type,
"answered_question": resp.answered_question,
- "query_sources": resp.query_sources,
+ "query_sources": query_sources,
"natural_language_response": resp.natural_language_response,
"timestamp": time.time(),
}
From 51bbb65b3a39d6fa382fc600b57e9705b7f70369 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Wed, 6 May 2026 20:20:36 +0530
Subject: [PATCH 38/70] feat: move Citations tab first, add 30-day trace log
cleanup
---
graphrag-ui/src/pages/TraceLogs.tsx | 26 +++++++++++++-------------
graphrag/app/routers/ui.py | 15 +++++++++++++++
2 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index bddf861..1438fa8 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -864,8 +864,17 @@ const TraceLogs: FC = () => {
{/* Tabs */}
-
+
+
+ Citations
+
+ {trace.citations.length}
+
+
{
{trace.toolCalls.length}
-
- Citations
-
- {trace.citations.length}
-
-
{
+
+
+
-
-
-
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 79dff2a..b5265a7 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -72,9 +72,24 @@
TRACE_LOGS_DIR = os.environ.get("TRACE_LOGS_DIR", "/code/trace_logs")
+def _cleanup_old_traces(max_age_days: int = 30):
+ """Delete trace log files older than max_age_days."""
+ try:
+ cutoff = time.time() - (max_age_days * 86400)
+ for filename in os.listdir(TRACE_LOGS_DIR):
+ if not filename.endswith(".json"):
+ continue
+ filepath = os.path.join(TRACE_LOGS_DIR, filename)
+ if os.path.getmtime(filepath) < cutoff:
+ os.remove(filepath)
+ except Exception:
+ logger.warning("Failed to clean up old trace logs", exc_info=True)
+
+
def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
try:
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+ _cleanup_old_traces()
# Strip chunk text from query_sources to keep trace files small.
# final_retrieval contains the full text of every retrieved chunk.
From ade9289e3e5ede6bc02e5ca062d726fc84201ffa Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:11:52 +0530
Subject: [PATCH 39/70] feat(GML-2086): add Excel and CSV extraction support
with UI warning
---
common/requirements.txt | 2 ++
common/utils/text_extractors.py | 22 +++++++++++++++++----
graphrag-ui/src/pages/setup/IngestGraph.tsx | 10 ++++++++++
3 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/common/requirements.txt b/common/requirements.txt
index 0a7c34f..f4d5ac6 100644
--- a/common/requirements.txt
+++ b/common/requirements.txt
@@ -105,6 +105,8 @@ nest-asyncio==1.6.0
nltk==3.9.1
numpy>=1, <2
openai==1.92.2
+openpyxl>=3.1.0
+xlrd>=2.0.1
ordered-set==4.1.0
orjson==3.10.18
packaging==24.2
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 449ace5..4ba4c0f 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -137,8 +137,6 @@ def __init__(self):
'.xml': 'application/xml',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
- '.png': 'image/png',
- '.gif': 'image/gif',
'.jsonl': 'application/x-jsonlines'
}
@@ -290,7 +288,7 @@ async def process_with_semaphore(file_path):
'error': result.get('error', 'Unknown error')
})
- logger.info(f"Prepared {len(processed_files_info)} files ({len(jsonl_files_copied)} JSONL copied, {len(files_to_process)} converted), {total_docs} total documents")
+ logger.info(f"Processed {len(processed_files_info)} files, extracted {total_docs} total documents")
logger.info(f"Created {len([f for f in processed_files_info if f.get('status') == 'success'])} JSONL files in {temp_folder}")
return {
@@ -624,6 +622,22 @@ def extract_text_from_file(file_path, graphname=None):
import docx
doc = docx.Document(file_path)
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
+ elif extension in ['.xlsx', '.xls']:
+ import pandas as pd
+ engine = 'openpyxl' if extension == '.xlsx' else 'xlrd'
+ try:
+ xl = pd.ExcelFile(file_path, engine=engine)
+ except Exception:
+ xl = pd.ExcelFile(file_path)
+ sheet_texts = []
+ for sheet_name in xl.sheet_names:
+ df = xl.parse(sheet_name)
+ if df.empty:
+ continue
+ df = df.fillna('')
+ sheet_md = df.to_markdown(index=False)
+ sheet_texts.append(f"## Sheet: {sheet_name}\n\n{sheet_md}")
+ return "\n\n".join(sheet_texts) if sheet_texts else "[Excel file is empty or contains no data]"
elif extension == '.xml':
import xml.etree.ElementTree as ET
tree = ET.parse(file_path)
@@ -663,7 +677,7 @@ def get_doc_type_from_extension(extension):
def get_supported_extensions():
"""Get list of supported file extensions."""
- return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif'}
+ return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls'}
def is_supported_file(file_path):
"""Check if a file is supported for text extraction."""
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index db9677a..d920b87 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -1006,6 +1006,16 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
? `Upload destination: uploads/${ingestGraphName}/`
: ""}
+ {selectedFiles && Array.from(selectedFiles).some((f) =>
+ [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase())
+ ) && (
+
+
ℹ️
+
+ CSV and Excel files will be treated as unstructured text documents.
+
+
+ )}
From 698724b6946e1b3ac08b548c63dae0d77c771136 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:21:14 +0530
Subject: [PATCH 40/70] fix(GML-2086): preserve all rows for headerless Excel
sheets
---
common/utils/text_extractors.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 4ba4c0f..891acdf 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -631,10 +631,20 @@ def extract_text_from_file(file_path, graphname=None):
xl = pd.ExcelFile(file_path)
sheet_texts = []
for sheet_name in xl.sheet_names:
- df = xl.parse(sheet_name)
+ # Always read with header=None so no data row is silently
+ # consumed as column names for headerless spreadsheets.
+ df = xl.parse(sheet_name, header=None)
if df.empty:
continue
df = df.fillna('')
+ # Detect header row: first row is all non-empty strings with
+ # no purely numeric values → treat as column names.
+ first_row = df.iloc[0]
+ if all(isinstance(v, str) and v.strip() for v in first_row):
+ df.columns = first_row.tolist()
+ df = df.iloc[1:].reset_index(drop=True)
+ else:
+ df.columns = [f"Column {i + 1}" for i in range(len(df.columns))]
sheet_md = df.to_markdown(index=False)
sheet_texts.append(f"## Sheet: {sheet_name}\n\n{sheet_md}")
return "\n\n".join(sheet_texts) if sheet_texts else "[Excel file is empty or contains no data]"
From 62ea52e231538ebb2811519ff25cdbbbc6e4b452 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:33:39 +0530
Subject: [PATCH 41/70] fix(GML-2086): align supported_extensions dict with
get_supported_extensions
---
common/utils/text_extractors.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 891acdf..957387d 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -137,6 +137,8 @@ def __init__(self):
'.xml': 'application/xml',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
+ '.png': 'image/png',
+ '.gif': 'image/gif',
'.jsonl': 'application/x-jsonlines'
}
@@ -687,7 +689,7 @@ def get_doc_type_from_extension(extension):
def get_supported_extensions():
"""Get list of supported file extensions."""
- return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls'}
+ return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.doc', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls', '.jsonl'}
def is_supported_file(file_path):
"""Check if a file is supported for text extraction."""
From f8ccdd6f5eac62db67d34afb9cd883a3e057360c Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 30 Apr 2026 16:37:26 +0530
Subject: [PATCH 42/70] fix(GML-2086): handle non-UTF-8 encodings in CSV
extraction
---
common/utils/text_extractors.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 957387d..82442ba 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -613,9 +613,22 @@ def extract_text_from_file(file_path, graphname=None):
if extension in ['.txt', '.md']:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
- elif extension in ['.html', '.htm', '.csv']:
+ elif extension in ['.html', '.htm']:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
+ elif extension == '.csv':
+ raw = file_path.read_bytes()
+ # utf-8-sig handles UTF-8 with BOM (common Excel CSV export)
+ try:
+ return raw.decode('utf-8-sig').strip()
+ except UnicodeDecodeError:
+ pass
+ # Fall back to chardet detection
+ import chardet
+ detected = chardet.detect(raw)
+ encoding = detected.get('encoding') if detected.get('confidence', 0) >= 0.5 else None
+ # latin-1 as final fallback — never raises DecodeError
+ return raw.decode(encoding or 'latin-1').strip()
elif extension == '.json':
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
From 6a2bda49a8c23614508cc7bf63a408ff1e790651 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Tue, 5 May 2026 23:09:47 +0530
Subject: [PATCH 43/70] feat(GML-2086): show unsupported file type warning in
upload UI
---
graphrag-ui/src/pages/setup/IngestGraph.tsx | 36 +++++++++++++++------
1 file changed, 26 insertions(+), 10 deletions(-)
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index d920b87..37a0d7a 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -1006,16 +1006,32 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
? `Upload destination: uploads/${ingestGraphName}/`
: ""}
- {selectedFiles && Array.from(selectedFiles).some((f) =>
- [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase())
- ) && (
-
-
ℹ️
-
- CSV and Excel files will be treated as unstructured text documents.
-
-
- )}
+ {selectedFiles && (() => {
+ const SUPPORTED_EXTENSIONS = new Set([".txt", ".md", ".pdf", ".docx", ".doc", ".html", ".htm", ".json", ".csv", ".xlsx", ".xls", ".xml", ".jpeg", ".jpg", ".png", ".gif", ".jsonl"]);
+ const files = Array.from(selectedFiles);
+ const unsupported = files.filter((f) => !SUPPORTED_EXTENSIONS.has(f.name.slice(f.name.lastIndexOf(".")).toLowerCase()));
+ const hasCsvExcel = files.some((f) => [".csv", ".xlsx", ".xls"].includes(f.name.slice(f.name.lastIndexOf(".")).toLowerCase()));
+ return (
+ <>
+ {unsupported.length > 0 && (
+
+
⚠️
+
+ Unsupported file type{unsupported.length > 1 ? "s" : ""}: {unsupported.map((f) => f.name).join(", ")} . These files will be skipped during ingestion.
+
+
+ )}
+ {hasCsvExcel && (
+
+
ℹ️
+
+ CSV and Excel files will be treated as unstructured text documents.
+
+
+ )}
+ >
+ );
+ })()}
From 934edd75e8d9efe2d15fc6da462c2f3c2f6f17aa Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Wed, 6 May 2026 13:23:45 -0700
Subject: [PATCH 44/70] feat(GML-2076): add Auto retrieval method selection
(Phase 1)
Adds an "Auto" mode that picks among the four search methods per
question using deterministic rules first, falling back to an LLM call.
Manual method selection still works as a transitional override.
---
common/llm_services/base_llm.py | 33 +++
graphrag-ui/src/components/Bot.tsx | 9 +-
graphrag/app/agent/agent.py | 4 +-
graphrag/app/agent/agent_graph.py | 72 +++++-
graphrag/app/agent/method_selector.py | 251 +++++++++++++++++++++
graphrag/app/routers/ui.py | 8 +-
graphrag/tests/test_method_selector.py | 292 +++++++++++++++++++++++++
7 files changed, 648 insertions(+), 21 deletions(-)
create mode 100644 graphrag/app/agent/method_selector.py
create mode 100644 graphrag/tests/test_method_selector.py
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index 005fab1..c587c76 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -286,6 +286,39 @@ def route_response_prompt(self):
Format: {format_instructions}\
"""
+ @property
+ def select_retriever_prompt(self):
+ """Property to get the prompt for the auto-select retriever (RetrieverSelector Stage B).
+
+ Returns the user-facing prompt template; the parser injects format_instructions.
+ """
+ result = self._read_prompt_file(self.prompt_path + "select_retriever.txt")
+ if result is not None:
+ return result
+ return """\
+You are choosing the best retrieval strategy for a knowledge-graph question.
+Pick exactly one of: similarity, contextual, hybrid, community.
+
+Methods:
+- similarity: a single fact / definition / quote; the answer lives in one passage. Cheapest. Pick this for short factoid questions about a single entity.
+- contextual: needs surrounding narrative (a process, a sequence, cause-and-effect). Returns matching chunks plus their lookback/lookahead siblings.
+- hybrid: needs relationships between named entities or multi-hop reasoning. Returns matching chunks plus graph-expansion to nearby entities.
+- community: global, thematic, or aggregate questions over the whole corpus ("main themes", "what topics are covered", "summarize the documents"). Returns community summaries instead of chunks.
+
+Important constraints:
+- similarity returns a strict subset of contextual and hybrid (same vector hits, no expansion). Do NOT pick similarity if the question needs context or relationships — pick contextual or hybrid instead.
+- community is the only method that operates on community summaries. Pick it ONLY for global/thematic questions; do not pick it for questions about specific named entities.
+
+Schema context — the knowledge graph contains these entity types: {v_types}
+And these relationship types: {e_types}
+
+Question: {question}
+Conversation history (last 2 turns, may be empty): {conversation}
+
+Return JSON: {{"method": "", "reason": "<≤20 words explaining the pick>"}}
+
+Format: {format_instructions}"""
+
@property
def hyde_prompt(self):
"""Property to get the prompt for the HyDE tool."""
diff --git a/graphrag-ui/src/components/Bot.tsx b/graphrag-ui/src/components/Bot.tsx
index 6386dec..4f21673 100644
--- a/graphrag-ui/src/components/Bot.tsx
+++ b/graphrag-ui/src/components/Bot.tsx
@@ -52,10 +52,11 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo
}
}
- // Set default ragPattern if no value in sessionStorage
+ // Set default ragPattern if no value in sessionStorage. "Auto" lets the
+ // backend RetrieverSelector pick a method per question.
if (!sessionStorage.getItem("ragPattern")) {
- setRagPattern("Hybrid Search");
- sessionStorage.setItem("ragPattern", "Hybrid Search");
+ setRagPattern("Auto");
+ sessionStorage.setItem("ragPattern", "Auto");
}
const date = new Date();
@@ -119,7 +120,7 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo
Select a GraphRAG Pattern
- {["Similarity Search", "Contextual Search", "Hybrid Search", "Community Search"].map((f, i) => (
+ {["Auto", "Similarity Search", "Contextual Search", "Hybrid Search", "Community Search"].map((f, i) => (
handleSelectRag(f)}>
{/* */}
{f}
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 611f03c..55e03a6 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -44,7 +44,7 @@ def __init__(
embedding_store: EmbeddingStore,
use_cypher: bool = False,
ws=None,
- supportai_retriever="hybridsearch"
+ supportai_retriever="auto"
):
self.conn = db_connection
@@ -265,7 +265,7 @@ def _node_output(node, state):
)
-def make_agent(graphname, conn, use_cypher, ws: WebSocket = None, supportai_retriever="hybridsearch") -> TigerGraphAgent:
+def make_agent(graphname, conn, use_cypher, ws: WebSocket = None, supportai_retriever="auto") -> TigerGraphAgent:
llm_provider = get_llm_service(get_chat_config(graphname))
chat_config = llm_provider.config
diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py
index 5341cf0..8529cb6 100644
--- a/graphrag/app/agent/agent_graph.py
+++ b/graphrag/app/agent/agent_graph.py
@@ -24,6 +24,7 @@
from agent.agent_rewrite import TigerGraphAgentRewriter
from agent.agent_router import TigerGraphAgentRouter
from agent.agent_usefulness_check import TigerGraphAgentUsefulnessCheck
+from agent.method_selector import RetrieverSelector
from agent.Q import DONE, Q
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
@@ -57,6 +58,12 @@ class GraphState(TypedDict):
schema_mapping: Optional[MapQuestionToSchemaResponse]
error_history: list[dict] = []
question_retry_count: int = 0
+ # Auto-selection (populated when supportai_retriever == "auto"; also written
+ # for manual mode so the UI can render which retriever ran). The "source"
+ # field distinguishes "rules"/"llm"/"fallback" (auto) from "manual".
+ chosen_retriever: Optional[str]
+ chosen_retriever_reason: Optional[str]
+ chosen_retriever_source: Optional[str]
class TigerGraphAgentGraph:
@@ -71,7 +78,7 @@ def __init__(
cypher_gen_tool=None,
enable_human_in_loop=False,
q: Q = None,
- supportai_retriever="hybridsearch",
+ supportai_retriever="auto",
):
self.workflow = StateGraph(GraphState)
self.llm_provider = llm_provider
@@ -455,20 +462,63 @@ def community_search(self, state):
state["lookup_source"] = "supportai"
return state
+ # User-friendly labels for the four retrieval methods. Used in progress
+ # events and UI badges; keep in sync with method_selector.METHOD_* constants.
+ _METHOD_DISPLAY_NAMES = {
+ "similaritysearch": "Similarity",
+ "contextualsearch": "Contextual",
+ "hybridsearch": "Hybrid",
+ "communitysearch": "Community",
+ }
+
def supportai_search(self, state):
"""
Run the agent supportai search.
- """
- if self.supportai_retriever == "hybridsearch":
- return self.hybrid_search(state)
- elif self.supportai_retriever == "similaritysearch":
- return self.similarity_search(state)
- elif self.supportai_retriever == "contextualsearch":
- return self.sibling_search(state)
- elif self.supportai_retriever == "communitysearch":
- return self.community_search(state)
+
+ When `self.supportai_retriever == "auto"`, picks a method via
+ `RetrieverSelector` (rules first, LLM fallback). Otherwise dispatches
+ directly to the configured retriever. Either way, populates
+ `state["chosen_retriever*"]` and surfaces the choice on the context
+ dict so it flows through `generate_answer` into `query_sources`.
+ """
+ method = self.supportai_retriever
+ chosen_reason = "user-selected"
+ chosen_source = "manual"
+
+ if method == "auto":
+ selector = RetrieverSelector(self.llm_provider, self.db_connection)
+ choice = selector.choose(state["question"], state.get("conversation"))
+ method = choice.method
+ chosen_reason = choice.reason
+ chosen_source = choice.source
+ label = self._METHOD_DISPLAY_NAMES.get(method, method)
+ self.emit_progress(f"Auto-selected {label} search")
+
+ state["chosen_retriever"] = method
+ state["chosen_retriever_reason"] = chosen_reason
+ state["chosen_retriever_source"] = chosen_source
+
+ if method == "hybridsearch":
+ result_state = self.hybrid_search(state)
+ elif method == "similaritysearch":
+ result_state = self.similarity_search(state)
+ elif method == "contextualsearch":
+ result_state = self.sibling_search(state)
+ elif method == "communitysearch":
+ result_state = self.community_search(state)
else:
- raise ValueError(f"Invalid supportai retriever: {self.supportai_retriever}")
+ raise ValueError(f"Invalid supportai retriever: {method}")
+
+ # Mirror the choice onto the context dict so it lands on
+ # GraphRAGResponse.query_sources without further plumbing.
+ ctx = result_state.get("context") or {}
+ if isinstance(ctx, dict):
+ ctx["chosen_retriever"] = method
+ ctx["chosen_retriever_reason"] = chosen_reason
+ ctx["chosen_retriever_source"] = chosen_source
+ result_state["context"] = ctx
+
+ return result_state
def generate_answer(self, state):
"""
diff --git a/graphrag/app/agent/method_selector.py b/graphrag/app/agent/method_selector.py
new file mode 100644
index 0000000..4dc1296
--- /dev/null
+++ b/graphrag/app/agent/method_selector.py
@@ -0,0 +1,251 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Auto-selection of GraphRAG retrieval method.
+
+Two stages:
+- Stage A: deterministic rules over the question.
+- Stage B: LLM fallback when rules are inconclusive.
+
+Phase 1 returns a single method. Top-K cascade, subset-constraint validation,
+and the diagnostician for retry routing land in later phases.
+"""
+
+import re
+import logging
+from typing import Literal, Optional
+
+from langchain.prompts import PromptTemplate
+from langchain_core.output_parsers import PydanticOutputParser
+from pydantic import BaseModel, Field
+from pyTigerGraph.pyTigerGraph import TigerGraphConnection
+
+from common.logs.log import req_id_cv
+from common.logs.logwriter import LogWriter
+
+logger = logging.getLogger(__name__)
+
+
+# Canonical method strings — match the dispatcher in agent_graph.supportai_search.
+METHOD_SIMILARITY = "similaritysearch"
+METHOD_CONTEXTUAL = "contextualsearch"
+METHOD_HYBRID = "hybridsearch"
+METHOD_COMMUNITY = "communitysearch"
+ALL_METHODS = (METHOD_SIMILARITY, METHOD_CONTEXTUAL, METHOD_HYBRID, METHOD_COMMUNITY)
+
+
+# Default fallback when the LLM stage can't produce a usable answer. Hybrid is the
+# pre-existing system default and the safest superset retriever.
+FALLBACK_METHOD = METHOD_HYBRID
+
+
+class RetrieverChoice(BaseModel):
+ """Public selector result. `source` records how the choice was made — useful
+ for telemetry and for the upcoming top-K / diagnostician phases."""
+
+ method: str # one of ALL_METHODS
+ reason: str # short human-readable justification
+ source: str # "rules" | "llm" | "fallback"
+
+
+class _LLMRetrieverChoice(BaseModel):
+ """Schema returned by the LLM. Uses friendly labels (no `search` suffix); we
+ normalise them to canonical method strings before returning."""
+
+ method: Literal["similarity", "contextual", "hybrid", "community"]
+ reason: str = Field(default="", description="<= 20 words explaining the pick")
+
+
+_LLM_LABEL_TO_METHOD = {
+ "similarity": METHOD_SIMILARITY,
+ "contextual": METHOD_CONTEXTUAL,
+ "hybrid": METHOD_HYBRID,
+ "community": METHOD_COMMUNITY,
+}
+
+
+# ---------- Stage A: deterministic rules ----------
+#
+# Order matters: the first pattern family that fires wins. We check community
+# first (clearest semantic signal — global/thematic language), then contextual
+# (process/narrative), then hybrid (relational), then similarity (short factoid).
+# That ordering reflects increasing ambiguity — community language is hardest to
+# confuse with the others, similarity is easiest.
+
+_COMMUNITY_PATTERNS = (
+ re.compile(r"\b(summari[sz]e|summary)\b", re.IGNORECASE),
+ re.compile(r"\b(main|key|central|important)\s+(themes?|topics?|ideas?|points?)\b", re.IGNORECASE),
+ re.compile(r"\bwhat\s+(is|are)\s+(this|the|these)\s+(corpus|dataset|documents?)\s+about\b", re.IGNORECASE),
+ re.compile(r"\bacross\s+(the|all)\s+documents?\b", re.IGNORECASE),
+ re.compile(r"\boverview\s+of\b", re.IGNORECASE),
+ re.compile(r"\b(what|which)\s+(topics?|themes?)\b", re.IGNORECASE),
+)
+
+_CONTEXTUAL_PATTERNS = (
+ re.compile(r"\bwalk\s+me\s+through\b", re.IGNORECASE),
+ re.compile(r"\bstep[- ]by[- ]step\b", re.IGNORECASE),
+ re.compile(r"\bwhat\s+happens\s+(after|before|next|when)\b", re.IGNORECASE),
+ re.compile(r"\bexplain\s+the\s+process\b", re.IGNORECASE),
+ re.compile(r"\bhow\s+does\s+(it|this|that)\s+work\b", re.IGNORECASE),
+)
+
+_HYBRID_PATTERNS = (
+ re.compile(r"\bhow\s+(is|are|does)\s+.+?\s+(related|connect|relate)\b", re.IGNORECASE),
+ re.compile(r"\b(relationship|connection)\s+between\b", re.IGNORECASE),
+ re.compile(r"\b(work\s+with|report\s+to|depend\s+on|interact\s+with)\b", re.IGNORECASE),
+)
+
+_SIMILARITY_PATTERNS = (
+ re.compile(r"^\s*(what|who)\s+(is|are|was|were)\b", re.IGNORECASE),
+ re.compile(r"^\s*define\b", re.IGNORECASE),
+ re.compile(r"^\s*when\s+(did|was|were)\b", re.IGNORECASE),
+ re.compile(r"^\s*where\s+(is|are|was|were)\b", re.IGNORECASE),
+)
+
+_SIMILARITY_MAX_TOKENS = 12
+
+
+def rules_choose(question: str) -> Optional[RetrieverChoice]:
+ """Stage A: deterministic rules. Returns None if no rule fires with confidence."""
+ if not question or not question.strip():
+ return None
+ q = question.strip()
+
+ for p in _COMMUNITY_PATTERNS:
+ if p.search(q):
+ return RetrieverChoice(
+ method=METHOD_COMMUNITY,
+ reason=f"global/thematic phrasing matched /{p.pattern}/",
+ source="rules",
+ )
+
+ for p in _CONTEXTUAL_PATTERNS:
+ if p.search(q):
+ return RetrieverChoice(
+ method=METHOD_CONTEXTUAL,
+ reason=f"process/narrative phrasing matched /{p.pattern}/",
+ source="rules",
+ )
+
+ for p in _HYBRID_PATTERNS:
+ if p.search(q):
+ return RetrieverChoice(
+ method=METHOD_HYBRID,
+ reason=f"relational phrasing matched /{p.pattern}/",
+ source="rules",
+ )
+
+ token_count = len(q.split())
+ if token_count <= _SIMILARITY_MAX_TOKENS:
+ for p in _SIMILARITY_PATTERNS:
+ if p.match(q):
+ return RetrieverChoice(
+ method=METHOD_SIMILARITY,
+ reason=f"short factoid (<= {_SIMILARITY_MAX_TOKENS} tokens) matched /{p.pattern}/",
+ source="rules",
+ )
+
+ return None
+
+
+# ---------- Stage B: LLM fallback ----------
+
+
+class RetrieverSelector:
+ """Picks the best retrieval method for a question.
+
+ Construction mirrors `TigerGraphAgentRouter` so it slots into the existing
+ LLM-call plumbing (PydanticOutputParser + invoke_with_parser).
+ """
+
+ def __init__(self, llm_model, db_conn: TigerGraphConnection):
+ self.llm = llm_model
+ self.db_conn = db_conn
+
+ def choose(
+ self,
+ question: str,
+ conversation: Optional[list[dict[str, str]]] = None,
+ ) -> RetrieverChoice:
+ """Return the best retrieval method for `question`.
+
+ Tries Stage A rules first; on miss, calls the LLM (Stage B). Always
+ returns a `RetrieverChoice` — on any unrecoverable error, falls back to
+ `FALLBACK_METHOD` rather than raising.
+ """
+ LogWriter.info(
+ f"request_id={req_id_cv.get()} ENTRY RetrieverSelector.choose: {question!r}"
+ )
+
+ # Stage A — pure-Python, no external calls
+ rule_choice = rules_choose(question)
+ if rule_choice is not None:
+ LogWriter.info(
+ f"request_id={req_id_cv.get()} EXIT RetrieverSelector.choose "
+ f"(rules) method={rule_choice.method} reason={rule_choice.reason!r}"
+ )
+ return rule_choice
+
+ # Stage B — LLM. Schema types are passed in to anchor the prompt.
+ try:
+ v_types = self.db_conn.getVertexTypes()
+ e_types = self.db_conn.getEdgeTypes()
+ except Exception as e: # noqa: BLE001 - schema lookup is best-effort
+ logger.warning(
+ f"request_id={req_id_cv.get()} schema lookup failed in selector: {e}"
+ )
+ v_types, e_types = [], []
+
+ try:
+ parser = PydanticOutputParser[_LLMRetrieverChoice](
+ pydantic_object=_LLMRetrieverChoice
+ )
+ prompt = PromptTemplate(
+ template=self.llm.select_retriever_prompt,
+ input_variables=["question", "v_types", "e_types", "conversation"],
+ partial_variables={
+ "format_instructions": parser.get_format_instructions()
+ },
+ )
+ res: _LLMRetrieverChoice = self.llm.invoke_with_parser(
+ prompt,
+ parser,
+ {
+ "question": question,
+ "v_types": v_types,
+ "e_types": e_types,
+ "conversation": conversation or [],
+ },
+ caller_name="select_retriever",
+ )
+ method = _LLM_LABEL_TO_METHOD.get(res.method.lower())
+ if method is None:
+ raise ValueError(f"LLM returned unknown method label: {res.method!r}")
+ choice = RetrieverChoice(method=method, reason=res.reason or "", source="llm")
+ except Exception as e: # noqa: BLE001 - selector must always return something
+ logger.warning(
+ f"request_id={req_id_cv.get()} RetrieverSelector LLM stage failed: {e}; "
+ f"falling back to {FALLBACK_METHOD}"
+ )
+ choice = RetrieverChoice(
+ method=FALLBACK_METHOD,
+ reason=f"selector fallback ({type(e).__name__})",
+ source="fallback",
+ )
+
+ LogWriter.info(
+ f"request_id={req_id_cv.get()} EXIT RetrieverSelector.choose "
+ f"({choice.source}) method={choice.method} reason={choice.reason!r}"
+ )
+ return choice
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index b5265a7..12f1490 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -1092,8 +1092,8 @@ async def graph_query(
LogWriter.info(f"Continuing conversation with ID: {convo_id}")
# create agent
- # get retrieval pattern to use
- rag_pattern = rag_pattern or "hybridsearch"
+ # get retrieval pattern to use; default "auto" lets RetrieverSelector pick.
+ rag_pattern = rag_pattern or "auto"
agent = make_agent(graphname, conn, use_cypher, supportai_retriever=rag_pattern)
prev_id = None
@@ -1190,8 +1190,8 @@ async def chat(
pass
return
- # Get RAG pattern
- rag_pattern = rag_pattern or "hybridsearch"
+ # Get RAG pattern; default "auto" lets RetrieverSelector pick.
+ rag_pattern = rag_pattern or "auto"
# Get conversation ID
try:
diff --git a/graphrag/tests/test_method_selector.py b/graphrag/tests/test_method_selector.py
new file mode 100644
index 0000000..fa19e95
--- /dev/null
+++ b/graphrag/tests/test_method_selector.py
@@ -0,0 +1,292 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib.util
+import os
+import sys
+import unittest
+from unittest.mock import MagicMock
+
+# Load `method_selector.py` directly rather than via `app.agent.method_selector`
+# because the `app.agent` package's __init__ pulls in agent_graph.py, which
+# transitively imports boto3 and other heavy runtime dependencies the selector
+# itself does not need. Importing the file in isolation keeps these tests
+# tightly scoped to the module under test.
+_HERE = os.path.dirname(os.path.abspath(__file__))
+_MS_PATH = os.path.normpath(
+ os.path.join(_HERE, "..", "app", "agent", "method_selector.py")
+)
+_spec = importlib.util.spec_from_file_location("method_selector", _MS_PATH)
+method_selector = importlib.util.module_from_spec(_spec)
+sys.modules["method_selector"] = method_selector
+_spec.loader.exec_module(method_selector)
+
+METHOD_COMMUNITY = method_selector.METHOD_COMMUNITY
+METHOD_CONTEXTUAL = method_selector.METHOD_CONTEXTUAL
+METHOD_HYBRID = method_selector.METHOD_HYBRID
+METHOD_SIMILARITY = method_selector.METHOD_SIMILARITY
+FALLBACK_METHOD = method_selector.FALLBACK_METHOD
+RetrieverChoice = method_selector.RetrieverChoice
+RetrieverSelector = method_selector.RetrieverSelector
+_LLMRetrieverChoice = method_selector._LLMRetrieverChoice
+rules_choose = method_selector.rules_choose
+
+
+# ---------- Stage A: rules_choose ----------
+
+
+class TestRulesChooseCommunity(unittest.TestCase):
+ """Global / thematic phrasing → community."""
+
+ def test_summarize(self):
+ self.assertEqual(rules_choose("Summarize the documents").method, METHOD_COMMUNITY)
+
+ def test_main_themes(self):
+ self.assertEqual(
+ rules_choose("What are the main themes in this corpus?").method,
+ METHOD_COMMUNITY,
+ )
+
+ def test_what_topics(self):
+ self.assertEqual(
+ rules_choose("Which topics are covered?").method, METHOD_COMMUNITY
+ )
+
+ def test_corpus_about(self):
+ self.assertEqual(
+ rules_choose("What are these documents about?").method, METHOD_COMMUNITY
+ )
+
+ def test_overview_of(self):
+ self.assertEqual(
+ rules_choose("Give me an overview of the dataset").method, METHOD_COMMUNITY
+ )
+
+
+class TestRulesChooseContextual(unittest.TestCase):
+ """Process / narrative phrasing → contextual."""
+
+ def test_walk_me_through(self):
+ self.assertEqual(
+ rules_choose("Walk me through the deployment process").method,
+ METHOD_CONTEXTUAL,
+ )
+
+ def test_step_by_step(self):
+ self.assertEqual(
+ rules_choose("Show me step-by-step how onboarding works").method,
+ METHOD_CONTEXTUAL,
+ )
+
+ def test_what_happens_after(self):
+ self.assertEqual(
+ rules_choose("What happens after the user logs in?").method,
+ METHOD_CONTEXTUAL,
+ )
+
+ def test_explain_the_process(self):
+ self.assertEqual(
+ rules_choose("Explain the process of approval").method, METHOD_CONTEXTUAL
+ )
+
+ def test_how_does_it_work(self):
+ self.assertEqual(
+ rules_choose("How does it work?").method, METHOD_CONTEXTUAL
+ )
+
+
+class TestRulesChooseHybrid(unittest.TestCase):
+ """Relational phrasing → hybrid."""
+
+ def test_how_is_x_related_to_y(self):
+ self.assertEqual(
+ rules_choose("How is Acme related to Globex?").method, METHOD_HYBRID
+ )
+
+ def test_relationship_between(self):
+ self.assertEqual(
+ rules_choose("What is the relationship between Bob and Alice?").method,
+ METHOD_HYBRID,
+ )
+
+ def test_connection_between(self):
+ self.assertEqual(
+ rules_choose("Show the connection between fraud and accounts").method,
+ METHOD_HYBRID,
+ )
+
+ def test_report_to(self):
+ self.assertEqual(
+ rules_choose("Who does Alice report to?").method, METHOD_HYBRID
+ )
+
+
+class TestRulesChooseSimilarity(unittest.TestCase):
+ """Short factoid / lookup → similarity."""
+
+ def test_what_is_short(self):
+ choice = rules_choose("What is GraphRAG?")
+ self.assertIsNotNone(choice)
+ self.assertEqual(choice.method, METHOD_SIMILARITY)
+
+ def test_who_is(self):
+ self.assertEqual(rules_choose("Who is the CEO?").method, METHOD_SIMILARITY)
+
+ def test_define(self):
+ self.assertEqual(rules_choose("Define embedding").method, METHOD_SIMILARITY)
+
+ def test_long_factoid_falls_through(self):
+ # Over the 12-token cap → similarity rule does not fire; nothing else
+ # matches → falls through to the LLM stage (rules_choose returns None).
+ long_q = (
+ "What is the deeper conceptual significance of vector similarity "
+ "search in modern enterprise knowledge management systems?"
+ )
+ self.assertIsNone(rules_choose(long_q))
+
+
+class TestRulesChooseEdgeCases(unittest.TestCase):
+ def test_empty(self):
+ self.assertIsNone(rules_choose(""))
+
+ def test_whitespace(self):
+ self.assertIsNone(rules_choose(" \n "))
+
+ def test_unmatched_question(self):
+ # No pattern matches "tell me about" — falls through to the LLM.
+ self.assertIsNone(rules_choose("Tell me about distributed systems"))
+
+ def test_priority_community_over_factoid(self):
+ """Community language wins over a factoid `what is` opener."""
+ self.assertEqual(
+ rules_choose("What is the main theme of these documents?").method,
+ METHOD_COMMUNITY,
+ )
+
+
+# ---------- Stage B: RetrieverSelector.choose ----------
+
+
+def _make_llm_mock():
+ """Mock that satisfies what RetrieverSelector reads on the LLM."""
+ llm = MagicMock()
+ # PromptTemplate validates the template, so use a real string with the
+ # placeholders the selector wires in.
+ llm.select_retriever_prompt = (
+ "Question: {question}\n"
+ "Schema: {v_types} {e_types}\n"
+ "History: {conversation}\n"
+ "{format_instructions}"
+ )
+ return llm
+
+
+def _make_db_mock(v_types=None, e_types=None):
+ db = MagicMock()
+ db.getVertexTypes.return_value = v_types or ["Entity", "Document"]
+ db.getEdgeTypes.return_value = e_types or ["RELATIONSHIP"]
+ return db
+
+
+class TestRetrieverSelectorRulesPath(unittest.TestCase):
+ """When rules fire, the LLM stage must NOT be invoked."""
+
+ def test_rules_short_circuit_skips_llm(self):
+ llm = _make_llm_mock()
+ db = _make_db_mock()
+ selector = RetrieverSelector(llm, db)
+
+ choice = selector.choose("Summarize the corpus")
+ self.assertEqual(choice.method, METHOD_COMMUNITY)
+ self.assertEqual(choice.source, "rules")
+ # LLM call must not have happened
+ llm.invoke_with_parser.assert_not_called()
+
+
+class TestRetrieverSelectorLLMPath(unittest.TestCase):
+ def test_llm_returns_method_label_normalized(self):
+ llm = _make_llm_mock()
+ db = _make_db_mock()
+ # LLM returns the user-facing label "hybrid"; selector must canonicalize
+ # to the dispatcher string "hybridsearch".
+ llm.invoke_with_parser.return_value = _LLMRetrieverChoice(
+ method="hybrid", reason="needs to relate two entities"
+ )
+ selector = RetrieverSelector(llm, db)
+
+ choice = selector.choose("Tell me about Alice and Bob's collaboration")
+ self.assertEqual(choice.method, METHOD_HYBRID)
+ self.assertEqual(choice.source, "llm")
+ self.assertEqual(choice.reason, "needs to relate two entities")
+ llm.invoke_with_parser.assert_called_once()
+
+ def test_llm_returns_each_label(self):
+ for label, method in [
+ ("similarity", METHOD_SIMILARITY),
+ ("contextual", METHOD_CONTEXTUAL),
+ ("hybrid", METHOD_HYBRID),
+ ("community", METHOD_COMMUNITY),
+ ]:
+ with self.subTest(label=label):
+ llm = _make_llm_mock()
+ db = _make_db_mock()
+ llm.invoke_with_parser.return_value = _LLMRetrieverChoice(
+ method=label, reason="reason"
+ )
+ selector = RetrieverSelector(llm, db)
+ choice = selector.choose("Tell me about distributed consensus")
+ self.assertEqual(choice.method, method)
+
+
+class TestRetrieverSelectorFallback(unittest.TestCase):
+ def test_llm_raises_falls_back_to_hybrid(self):
+ llm = _make_llm_mock()
+ db = _make_db_mock()
+ llm.invoke_with_parser.side_effect = RuntimeError("LLM unavailable")
+ selector = RetrieverSelector(llm, db)
+
+ choice = selector.choose("Tell me about distributed systems")
+ self.assertEqual(choice.method, FALLBACK_METHOD)
+ self.assertEqual(choice.source, "fallback")
+ self.assertIn("RuntimeError", choice.reason)
+
+ def test_schema_lookup_failure_does_not_break_selector(self):
+ """If the DB schema fetch fails, the LLM stage should still proceed
+ with empty type lists rather than aborting the whole selection."""
+ llm = _make_llm_mock()
+ db = MagicMock()
+ db.getVertexTypes.side_effect = RuntimeError("db down")
+ db.getEdgeTypes.side_effect = RuntimeError("db down")
+ llm.invoke_with_parser.return_value = _LLMRetrieverChoice(
+ method="hybrid", reason="default"
+ )
+ selector = RetrieverSelector(llm, db)
+
+ choice = selector.choose("Tell me about distributed systems")
+ self.assertEqual(choice.method, METHOD_HYBRID)
+ self.assertEqual(choice.source, "llm")
+
+
+class TestRetrieverChoice(unittest.TestCase):
+ """The public choice model is a Pydantic BaseModel — verify its shape."""
+
+ def test_fields(self):
+ c = RetrieverChoice(method="hybridsearch", reason="r", source="rules")
+ self.assertEqual(c.method, "hybridsearch")
+ self.assertEqual(c.reason, "r")
+ self.assertEqual(c.source, "rules")
+
+
+if __name__ == "__main__":
+ unittest.main()
From 3051fc3b3ce3fbd3d51744c519a6cca3d1e95f9a Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Wed, 6 May 2026 13:24:54 -0700
Subject: [PATCH 45/70] feat(GML-2076): observability + out-of-corpus
short-circuit (Phase 1.5)
Surfaces the auto-selection in the UI, adds Prometheus telemetry for
the selector's distribution, and short-circuits to an honest "no info"
answer when the chosen retriever returns nothing.
---
CHANGELOG.md | 10 ++++
common/metrics/prometheus_metrics.py | 5 ++
.../src/components/CustomChatMessage.tsx | 45 ++++++++++++++++
graphrag/app/agent/agent_graph.py | 52 +++++++++++++++++++
4 files changed, 112 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b34570..080e34c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## [1.4.0]
+
+### Added
+- **Auto retrieval method selection** — new "Auto" option in the chat dropdown picks among Similarity / Contextual / Hybrid / Community per question
+ - Two-stage selector: deterministic regex rules cover common cases; LLM fallback handles the rest with a subset-aware prompt
+ - Selection visible via a chip below each bot reply (method, reason, auto/manual)
+ - Manual method selection still works as override during the transition
+- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels
+- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
+
## [1.3.1]
### Changed
diff --git a/common/metrics/prometheus_metrics.py b/common/metrics/prometheus_metrics.py
index 0662872..ee671be 100644
--- a/common/metrics/prometheus_metrics.py
+++ b/common/metrics/prometheus_metrics.py
@@ -72,6 +72,11 @@ def __init__(self):
"Number of LLM responses that yielded an error result",
["llm_model"],
)
+ self.llm_method_selection_total = Counter(
+ "llm_method_selection_total",
+ "Number of times each retrieval method was selected (auto + manual)",
+ ["selected_method", "selection_source"],
+ )
# collect metrics for TigerGraph
self.tigergraph_active_connections = Gauge(
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 07e0d5e..e87f1b3 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -28,6 +28,50 @@ interface IChatbotMessageProps {
}
const urlRegex = /https?:\/\//
+
+// Phase 1.5 — render a subtle chip showing which retrieval method ran.
+// Reads the auto-selection metadata that supportai_search mirrors into
+// query_sources (chosen_retriever / chosen_retriever_reason / chosen_retriever_source).
+const METHOD_LABELS: Record = {
+ similaritysearch: "Similarity",
+ contextualsearch: "Contextual",
+ hybridsearch: "Hybrid",
+ communitysearch: "Community",
+};
+
+const RetrieverBadge: FC<{ message: any }> = ({ message }) => {
+ const qs = message?.query_sources;
+ if (!qs || typeof qs !== "object") return null;
+ const method = qs.chosen_retriever as string | undefined;
+ if (!method) return null;
+ // Suppress for greetings / errors / progress events — those don't run a retriever.
+ if (
+ message.response_type === "progress" ||
+ message.response_type === "greeting" ||
+ message.response_type === "error"
+ ) {
+ return null;
+ }
+ const label = METHOD_LABELS[method] || method;
+ const reason = (qs.chosen_retriever_reason as string | undefined) || "";
+ const source = (qs.chosen_retriever_source as string | undefined) || "";
+ // For source, show "auto" for any of rules/llm/fallback; "manual" stays as-is.
+ const sourceLabel = source === "manual" ? "manual" : "auto";
+ return (
+
+ 🔎
+ {label}
+ {reason ? (
+ · {reason}
+ ) : null}
+ · {sourceLabel}
+
+ );
+};
+
const getReasoning = (msg) => {
if(msg.query_sources.reasoning instanceof Array) {
@@ -185,6 +229,7 @@ export const CustomChatMessage: FC = ({
) : (
{message.content}
)}
+
Date: Wed, 6 May 2026 21:11:38 -0700
Subject: [PATCH 46/70] refactor(GML-2076): simplify retriever badge to icon +
label
The full reason text and auto/manual source labels were operator-side
detail surfaced to all users; moves them into a hover tooltip so the
inline chip stays glanceable.
---
CHANGELOG.md | 2 +-
graphrag-ui/src/components/CustomChatMessage.tsx | 12 +++++-------
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 080e34c..db27e68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Added
- **Auto retrieval method selection** — new "Auto" option in the chat dropdown picks among Similarity / Contextual / Hybrid / Community per question
- Two-stage selector: deterministic regex rules cover common cases; LLM fallback handles the rest with a subset-aware prompt
- - Selection visible via a chip below each bot reply (method, reason, auto/manual)
+ - Selection visible via a chip below each bot reply (method icon + label; reason and source in hover tooltip)
- Manual method selection still works as override during the transition
- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels
- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index e87f1b3..4330609 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -55,19 +55,17 @@ const RetrieverBadge: FC<{ message: any }> = ({ message }) => {
const label = METHOD_LABELS[method] || method;
const reason = (qs.chosen_retriever_reason as string | undefined) || "";
const source = (qs.chosen_retriever_source as string | undefined) || "";
- // For source, show "auto" for any of rules/llm/fallback; "manual" stays as-is.
const sourceLabel = source === "manual" ? "manual" : "auto";
+ // Reason + source live in the hover tooltip so the inline chip stays
+ // glanceable; users who want the detail can hover.
+ const tooltip = reason ? `${sourceLabel}: ${reason}` : sourceLabel;
return (
🔎
{label}
- {reason ? (
- · {reason}
- ) : null}
- · {sourceLabel}
);
};
From fa9b5b1bb2aaad64a9bebab840216c7ca686ddcf Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 7 May 2026 17:14:37 +0530
Subject: [PATCH 47/70] fix: trace log path traversal + superuser auth, usage
collector reset, excel header detection
---
common/llm_services/base_llm.py | 10 ++++++++++
common/utils/text_extractors.py | 13 +++++++++----
graphrag/app/agent/agent.py | 6 +++++-
graphrag/app/routers/ui.py | 25 +++++++++++++++++++++++--
4 files changed, 47 insertions(+), 7 deletions(-)
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index c587c76..b8a3adf 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -43,6 +43,16 @@ def get_collected_usage():
return _usage_collector.get()
+def reset_usage_collection():
+ """Drop any accumulated usage and disable collection for this context.
+
+ Must be called at the end of a request (success or failure) so stale
+ usage data doesn't bleed into the next request that runs on the same
+ thread (sync FastAPI handlers re-use worker threads from a pool).
+ """
+ _usage_collector.set(None)
+
+
def _record_usage(caller_name: str, usage_data: dict):
bucket = _usage_collector.get()
if bucket is not None:
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 82442ba..02eb5e1 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -652,11 +652,16 @@ def extract_text_from_file(file_path, graphname=None):
if df.empty:
continue
df = df.fillna('')
- # Detect header row: first row is all non-empty strings with
- # no purely numeric values → treat as column names.
first_row = df.iloc[0]
- if all(isinstance(v, str) and v.strip() for v in first_row):
- df.columns = first_row.tolist()
+ first_row_values = [str(v).strip() for v in first_row]
+ looks_like_header = (
+ len(df) > 1
+ and all(first_row_values)
+ and len(set(first_row_values)) == len(first_row_values)
+ and not any(v.isdigit() for v in first_row_values)
+ )
+ if looks_like_header:
+ df.columns = first_row_values
df = df.iloc[1:].reset_index(drop=True)
else:
df.columns = [f"Column {i + 1}" for i in range(len(df.columns))]
diff --git a/graphrag/app/agent/agent.py b/graphrag/app/agent/agent.py
index 55e03a6..82d5f75 100644
--- a/graphrag/app/agent/agent.py
+++ b/graphrag/app/agent/agent.py
@@ -11,7 +11,7 @@
from common.config import embedding_service, embedding_store, llm_config, get_completion_config, get_chat_config, get_llm_service
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.embedding_services import EmbeddingModel
-from common.llm_services.base_llm import LLM_Model, start_usage_collection, get_collected_usage
+from common.llm_services.base_llm import LLM_Model, start_usage_collection, get_collected_usage, reset_usage_collection
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.metrics.prometheus_metrics import metrics
@@ -257,6 +257,10 @@ def _node_output(node, state):
traceback.print_exc()
raise e
finally:
+ # Clear the per-request LLM usage bucket so it can't leak into the
+ # next request that runs on the same worker thread (sync FastAPI
+ # handlers re-use threads from a pool, where ContextVars persist).
+ reset_usage_collection()
metrics.llm_request_total.labels(self.model_name).inc()
metrics.llm_inprogress_requests.labels(self.model_name).dec()
duration = time.time() - start_time
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 12f1490..fdc54f8 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -72,6 +72,7 @@
TRACE_LOGS_DIR = os.environ.get("TRACE_LOGS_DIR", "/code/trace_logs")
+
def _cleanup_old_traces(max_age_days: int = 30):
"""Delete trace log files older than max_age_days."""
try:
@@ -88,7 +89,17 @@ def _cleanup_old_traces(max_age_days: int = 30):
def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
try:
+ if not isinstance(message_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", message_id):
+ logger.warning("Refusing to save trace log: invalid message_id %r", message_id)
+ return
+
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
+ base_dir = os.path.abspath(TRACE_LOGS_DIR)
+ filepath = os.path.abspath(os.path.join(base_dir, f"{message_id}.json"))
+ if os.path.commonpath([base_dir, filepath]) != base_dir:
+ logger.warning("Refusing to save trace log: path escapes TRACE_LOGS_DIR for %r", message_id)
+ return
+
_cleanup_old_traces()
# Strip chunk text from query_sources to keep trace files small.
@@ -110,7 +121,6 @@ def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp
"natural_language_response": resp.natural_language_response,
"timestamp": time.time(),
}
- filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
with open(filepath, "w") as f:
json.dump(trace_data, f, default=str)
except Exception:
@@ -389,7 +399,18 @@ def get_trace_log(
message_id: str,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
):
- filepath = os.path.join(TRACE_LOGS_DIR, f"{message_id}.json")
+ # Trace logs contain user queries (potentially PII), full LLM responses,
+ # internal cypher, schema mappings, and per-call cost. Any authenticated
+ # user could otherwise read another user's trace by guessing or learning
+ # the message_id. Restrict to superusers to prevent cross-user disclosure.
+ _require_roles(creds[1], {"superuser"})
+
+ if not re.fullmatch(r"[A-Za-z0-9_-]+", message_id):
+ raise HTTPException(status_code=400, detail="Invalid message_id")
+ base_dir = os.path.abspath(TRACE_LOGS_DIR)
+ filepath = os.path.abspath(os.path.join(base_dir, f"{message_id}.json"))
+ if os.path.commonpath([base_dir, filepath]) != base_dir:
+ raise HTTPException(status_code=400, detail="Invalid message_id")
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Trace log not found")
with open(filepath, "r") as f:
From 2a570a89e41548c9d69bdf4c422f708bdf5d5a9f Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Thu, 7 May 2026 20:14:31 +0530
Subject: [PATCH 48/70] fix(TraceLogs): show not-found state when trace data
missing
---
graphrag-ui/src/pages/TraceLogs.tsx | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 1438fa8..858fd57 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -745,7 +745,7 @@ const TraceLogs: FC = () => {
const userQuery = stateUserQuery || sessionMessage?.userQuery || apiData?.user_query;
const trace = useMemo(
- () => buildTraceFromMessage(message, userQuery),
+ () => (message ? buildTraceFromMessage(message, userQuery) : null),
[message, userQuery]
);
@@ -760,6 +760,7 @@ const TraceLogs: FC = () => {
};
const handleDownload = () => {
+ if (!trace) return;
const blob = new Blob([JSON.stringify(trace, null, 2)], {
type: "application/json",
});
@@ -779,6 +780,14 @@ const TraceLogs: FC = () => {
);
}
+ if (!trace) {
+ return (
+
+
Trace data not found.
+
+ );
+ }
+
return (
{/* Header */}
From 681be2c41533bb38e8b1b8ec68ffa423f3f3f19e Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 7 May 2026 16:06:22 -0700
Subject: [PATCH 49/70] Route schema-aware community and hybrid retrieval
through domain vertex types
- Multi-pair IN_COMMUNITY and a post-Louvain mirror so retrievers
reach community memberships through domain VTs without traversing
the Entity scaffolding.
- Pre-Louvain Community-cascade clears prior partitions for
idempotent re-detection across re-ingest cycles.
- Renderer in common/db/retriever_render rewrites the four primary
retrievers against the live schema; apply_proposal installs the
rendered bodies.
- retrieval_include_entity auto-resolves (False with a schema, True
without); transitional-graph detection keeps Entity visible when
layering a schema onto a graph with existing data.
- Pin graphrag-ui to pnpm 9 so the Docker build doesn't trip the
pnpm-10 strict-approval policy.
---
CHANGELOG.md | 13 +-
common/config.py | 2 +
common/db/retriever_render.py | 204 ++++++++++++++++
common/db/schema_utils.py | 149 +++++++++++-
.../graphrag_delete_all_communities.gsql | 21 ++
.../graphrag/graphrag_stream_all_ids.gsql | 17 ++
...raphrag_stream_entity_community_pairs.gsql | 21 ++
.../louvain/graphrag_louvain_communities.gsql | 3 +-
.../louvain/graphrag_louvain_init.gsql | 9 +-
common/gsql/graphrag/louvain/modularity.gsql | 3 +-
.../graphrag/louvain/stream_community.gsql | 3 +-
.../retrievers/Content_Similarity_Search.gsql | 12 +-
.../Content_Similarity_Vector_Search.gsql | 22 +-
ecc/app/graphrag/graph_rag.py | 39 +++
ecc/app/graphrag/util.py | 82 +++++++
graphrag-ui/.npmrc | 2 +
graphrag-ui/package.json | 7 +
.../retrievers/CommunityRetriever.py | 2 +-
.../retrievers/SimilarityRetriever.py | 4 +-
graphrag/tests/test_retriever_render.py | 177 ++++++++++++++
graphrag/tests/test_schema_utils.py | 230 ++++++++++++++++--
21 files changed, 963 insertions(+), 59 deletions(-)
create mode 100644 common/db/retriever_render.py
create mode 100644 common/gsql/graphrag/graphrag_delete_all_communities.gsql
create mode 100644 common/gsql/graphrag/graphrag_stream_all_ids.gsql
create mode 100644 common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql
create mode 100644 graphrag-ui/.npmrc
create mode 100644 graphrag/tests/test_retriever_render.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d89e53..777d721 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,12 +23,23 @@
- **`check_embedding_store_status()`** in the inquiryai / supportai routers raises HTTP 503 instead of swallowing the exception.
- **Bedrock `max_tokens` is auto-defaulted** per model family (Claude 3.x = 4096, Sonnet 3.5+ / 4.x = 8192, Titan / Cohere / Llama at their published caps), so schema extraction and other large-output prompts no longer truncate at the langchain-aws built-in 1024 default. Explicit `model_kwargs.max_tokens` and the existing `token_limit` config field both override the auto-default.
- **Hybrid / similarity retrievers surface domain vertex types** in the LLM context with a `: ` label, so type-aware questions (e.g. "which companies …") receive properly grounded answers.
+- **Community / hybrid retrievers walk domain edges and domain VTs directly** when a schema exists. The `Entity` layer becomes scaffolding for Louvain; community memberships are mirrored from `Entity` onto matching domain-VT instances after community detection so retrievers reach community context without traversing the legacy layer. New `graphrag_config.retrieval_include_entity` flag controls whether `Entity` stays visible to the chat agent — when unset, defaults to `false` for graphs with a domain schema (typed-purist) and `true` otherwise (no-op fallback).
+- **`apply_proposal` re-installs retriever queries** against the live domain schema, idempotently. Identical bodies are TG no-ops; new domain types or a changed `retrieval_include_entity` value re-render the affected queries on the next apply call.
+- **Transitional-graph detection at schema apply**: when a domain schema is added to a graph that already has Entity-layer data (typical v1.3.x → v1.4.0 upgrade applying a schema for the first time), `apply_proposal` forces `retrieval_include_entity=True` for the rendered queries so existing Entity rows stay reachable. The result payload carries a `transitional` block (`entity_count`, `new_domain_vts`, `recommendation`) for the init-graph dialog to surface a "your existing entities won't be auto-typed — re-ingest for full schema awareness" prompt. Once the user clears derived data and re-ingests (planned v1.5 admin endpoint), the auto-default flips back to typed-purist on the next apply call.
+
+> **Upgrading from a pre-release v1.4.0 build**: graphs that already
+> have domain vertex types but were created before the multi-pair
+> `IN_COMMUNITY` schema landed will see a "skipping community mirror
+> for [...]: IN_COMMUNITY pair not on schema" warning during
+> community detection. Re-run `/apply_proposal` with the existing
+> schema once to backfill the missing pairs. v1.3.x graphs (no domain
+> types) are unaffected — the mirror block is skipped entirely.
### Removed
- **`RELATIONSHIP_TYPE` edge** between `EntityType` vertices — superseded by `IS_HEAD_OF` + `HAS_TAIL` through `RelationshipType`.
### Configuration
-- New `graphrag_config` keys: `schema_max_sample_files` (default 5), `schema_max_total_mb` (default 50), `strict_mode` (default false).
+- New `graphrag_config` keys: `schema_max_sample_files` (default 5), `schema_max_total_mb` (default 50), `strict_mode` (default false), `retrieval_include_entity` (auto: false when domain schema present, true otherwise).
- New env var: `PDF_IMAGE_CONCURRENCY` (default 8).
> Implementation-level details for v1.4.0 (parser internals, endpoint contracts, dialog state machine, prompt-resolution chain, schema-aware ECC worker logic, etc.) live in `dev/plans/graphrag/v1.4.0_implementation_notes.md`.
diff --git a/common/config.py b/common/config.py
index 3216c60..e7f8c69 100644
--- a/common/config.py
+++ b/common/config.py
@@ -434,6 +434,8 @@ def get_graphrag_config(graphname=None):
graphrag_config["chunker"] = "semantic"
if "extractor" not in graphrag_config:
graphrag_config["extractor"] = "llm"
+# ``retrieval_include_entity`` is resolved at install time
+# (see ``common.db.retriever_render.resolve_include_entity``).
reuse_embedding = graphrag_config.get("reuse_embedding", True)
doc_process_switch = graphrag_config.get("doc_process_switch", True)
diff --git a/common/db/retriever_render.py b/common/db/retriever_render.py
new file mode 100644
index 0000000..a9fb28f
--- /dev/null
+++ b/common/db/retriever_render.py
@@ -0,0 +1,204 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Render and install retrieval queries against the live domain schema."""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+from typing import Iterable, Optional
+
+logger = logging.getLogger(__name__)
+
+
+_RETRIEVER_DIR = "common/gsql/supportai/retrievers"
+
+
+TEMPLATED_RETRIEVERS: tuple = (
+ "GraphRAG_Hybrid_Search",
+ "GraphRAG_Hybrid_Vector_Search",
+ "GraphRAG_Community_Search",
+ "GraphRAG_Community_Vector_Search",
+)
+
+
+def _hop_edge_pattern(body: str, domain_edges: Iterable[str]) -> str:
+ """Append directed domain edges to the hybrid-walk hop pattern."""
+ edges = sorted(set(e for e in domain_edges if e))
+ if not edges:
+ return body
+ needle = "IS_AFTER>):e"
+ if needle not in body:
+ return body
+ addition = "|" + "|".join(f"{e}>" for e in edges)
+ return body.replace(needle, f"IS_AFTER>{addition}):e")
+
+
+def _community_member_pattern(
+ body: str,
+ domain_vts: Iterable[str],
+ include_entity: bool,
+) -> str:
+ """Expand the community-walk start type to include domain VTs."""
+ vts = sorted(set(v for v in domain_vts if v))
+ if not vts:
+ return body
+ types = (["Entity"] + vts) if include_entity else vts
+ member = types[0] if len(types) == 1 else "(" + "|".join(types) + ")"
+ needle = "CONTAINS_ENTITY>)- Entity:v -(IN_COMMUNITY>"
+ if needle not in body:
+ return body
+ return body.replace(
+ needle,
+ f"CONTAINS_ENTITY>)- {member}:v -(IN_COMMUNITY>",
+ )
+
+
+def resolve_include_entity(
+ graphrag_config_get,
+ has_domain_schema: bool,
+) -> bool:
+ """Resolve effective ``retrieval_include_entity``. Default: ``False``
+ when a schema exists, ``True`` otherwise. Explicit config wins.
+ """
+ configured = graphrag_config_get("retrieval_include_entity")
+ if configured is None:
+ return not has_domain_schema
+ return bool(configured)
+
+
+def render_retriever_body(
+ template_text: str,
+ *,
+ domain_vts: Iterable[str],
+ domain_edges: Iterable[str],
+ include_entity: bool,
+) -> str:
+ """Apply every schema-aware substitution to one retriever body."""
+ body = template_text
+ body = _hop_edge_pattern(body, domain_edges)
+ body = _community_member_pattern(body, domain_vts, include_entity=include_entity)
+ return body
+
+
+def load_template(query_name: str, retriever_dir: str = _RETRIEVER_DIR) -> str:
+ return (Path(retriever_dir) / f"{query_name}.gsql").read_text()
+
+
+def render_retrievers(
+ domain_vts: Iterable[str],
+ domain_edges: Iterable[str],
+ include_entity: bool,
+ retriever_dir: str = _RETRIEVER_DIR,
+) -> dict:
+ """Return ``{query_name: rendered_body}`` for every templated retriever."""
+ rendered: dict = {}
+ for q in TEMPLATED_RETRIEVERS:
+ try:
+ text = load_template(q, retriever_dir)
+ except FileNotFoundError:
+ logger.warning(f"render_retrievers: template not found for {q}, skipped")
+ continue
+ rendered[q] = render_retriever_body(
+ text,
+ domain_vts=domain_vts,
+ domain_edges=domain_edges,
+ include_entity=include_entity,
+ )
+ return rendered
+
+
+def _install_block(graphname: str, query_name: str, body: str) -> str:
+ return (
+ f"USE GRAPH {graphname}\n"
+ f"{body}\n"
+ f"INSTALL QUERY {query_name}\n"
+ )
+
+
+def _summarize(out) -> str:
+ s = str(out)
+ s = s.replace("\n", " | ")
+ return s[:200]
+
+
+def install_retrievers(
+ conn,
+ graphname: str,
+ domain_vts: Iterable[str],
+ domain_edges: Iterable[str],
+ include_entity: bool,
+ retriever_dir: str = _RETRIEVER_DIR,
+) -> dict:
+ """Render and install every templated retriever (sync)."""
+ rendered = render_retrievers(
+ domain_vts, domain_edges, include_entity, retriever_dir
+ )
+ logger.info(
+ f"install_retrievers: graph={graphname} include_entity={include_entity} "
+ f"vts={len(list(domain_vts))} edges={len(list(domain_edges))} "
+ f"rendered={list(rendered.keys())}"
+ )
+ results: dict = {}
+ for query_name, body in rendered.items():
+ block = _install_block(graphname, query_name, body)
+ try:
+ out = conn.gsql(block)
+ results[query_name] = out
+ lower = out.lower() if isinstance(out, str) else ""
+ if "error" in lower or "failed" in lower:
+ logger.warning(
+ f"install_retrievers: {query_name} install reported "
+ f"errors: {_summarize(out)}"
+ )
+ else:
+ logger.info(
+ f"install_retrievers: {query_name} OK: {_summarize(out)}"
+ )
+ except Exception as e:
+ logger.error(f"install_retrievers: {query_name} install raised: {e}")
+ results[query_name] = f"ERROR: {e}"
+ return results
+
+
+async def install_retrievers_async(
+ conn,
+ graphname: str,
+ domain_vts: Iterable[str],
+ domain_edges: Iterable[str],
+ include_entity: bool,
+ retriever_dir: str = _RETRIEVER_DIR,
+ sem: Optional["object"] = None,
+) -> dict:
+ """Render and install every templated retriever (async)."""
+ rendered = render_retrievers(
+ domain_vts, domain_edges, include_entity, retriever_dir
+ )
+ results: dict = {}
+ for query_name, body in rendered.items():
+ block = _install_block(graphname, query_name, body)
+ try:
+ if sem is not None:
+ async with sem:
+ out = await conn.gsql(block)
+ else:
+ out = await conn.gsql(block)
+ results[query_name] = out
+ lower = out.lower() if isinstance(out, str) else ""
+ if "error" in lower or "failed" in lower:
+ logger.warning(
+ f"install_retrievers_async: {query_name} install "
+ f"reported errors: {str(out)[:300]}"
+ )
+ except Exception as e:
+ logger.error(
+ f"install_retrievers_async: {query_name} install raised: {e}"
+ )
+ results[query_name] = f"ERROR: {e}"
+ return results
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index ce4a782..39d4272 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -703,13 +703,15 @@ def parse_gsql_schema(text: str) -> SchemaProposal:
class ExistingSchema:
"""Snapshot of what's already on the graph, used by the diff emitter.
- ``vertex_types`` is the set of vertex-type names currently on the
- graph (case-sensitive). ``edge_pairs`` maps an edge type name to the
- set of ``(FROM, TO)`` pairs already declared for that edge.
+ ``vertex_types`` — vertex-type names currently on the graph.
+ ``edge_pairs`` — edge-type name → set of ``(FROM, TO)`` pairs.
+ ``directed_edges`` — subset of edge-type names with
+ ``IsDirected=True`` (consumed by the retriever renderer).
"""
vertex_types: Set[str] = field(default_factory=set)
edge_pairs: dict = field(default_factory=dict)
+ directed_edges: Set[str] = field(default_factory=set)
def has_vertex(self, name: str) -> bool:
folded = name.casefold()
@@ -819,6 +821,10 @@ def emit_structural_link_alters(
structural edges:
* ``CONTAINS_ENTITY`` — ``Document`` / ``DocumentChunk`` → domain vertex
+ * ``IN_COMMUNITY`` — domain vertex → ``Community`` (so the
+ post-Louvain mirror step can attach domain-VT instances to the
+ community their twin Entity belongs to, and retrievers walking
+ domain VTs can reach community memberships directly)
The typed-relationship pattern (``IS_HEAD_OF`` / ``HAS_TAIL``) lives
at the meta-schema layer (``EntityType`` ↔ ``RelationshipType``) and
@@ -840,6 +846,7 @@ def emit_structural_link_alters(
# bare-graph fixtures may not have them.
has_doc = existing.has_vertex("Document")
has_chunk = existing.has_vertex("DocumentChunk")
+ has_community = existing.has_vertex("Community")
stmts: List[str] = []
for v in proposal.vertices:
@@ -852,6 +859,11 @@ def emit_structural_link_alters(
stmts.append(
f"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO {v.name})"
)
+ # IN_COMMUNITY: → Community
+ if has_community and not existing.has_edge_pair("IN_COMMUNITY", v.name, "Community"):
+ stmts.append(
+ f"ALTER EDGE IN_COMMUNITY ADD PAIR (FROM {v.name}, TO Community)"
+ )
return stmts
@@ -955,6 +967,8 @@ def read_existing_schema(conn) -> ExistingSchema:
if pairs:
snapshot.edge_pairs[et_name] = pairs
+ if meta.get("IsDirected"):
+ snapshot.directed_edges.add(et_name)
return snapshot
@@ -1067,6 +1081,8 @@ async def read_existing_schema_async(conn) -> "ExistingSchema":
pairs.add((f, t))
if pairs:
snapshot.edge_pairs[et_name] = pairs
+ if meta.get("IsDirected"):
+ snapshot.directed_edges.add(et_name)
return snapshot
@@ -1202,6 +1218,9 @@ def apply_proposal(
# Even on no-op, refresh metadata so descriptions edited in the
# review panel land on EntityType / RelationshipType vertices.
metadata = upsert_type_metadata(conn, proposal)
+ retrievers = _install_retrievers_after_apply(
+ conn, graphname, proposal=proposal, pre_apply_existing=existing
+ )
return {
"status": "no-op",
"statements": [],
@@ -1209,6 +1228,7 @@ def apply_proposal(
"gsql_output": "",
"summary": summary,
"metadata": metadata,
+ "retrievers": retrievers,
}
block, job_name = build_schema_change_job(graphname, statements, job_name)
@@ -1230,6 +1250,9 @@ def apply_proposal(
"metadata": {"entity_types": [], "relationship_types": []},
}
metadata = upsert_type_metadata(conn, proposal)
+ retrievers = _install_retrievers_after_apply(
+ conn, graphname, proposal=proposal, pre_apply_existing=existing
+ )
return {
"status": "applied",
"statements": statements,
@@ -1237,7 +1260,127 @@ def apply_proposal(
"gsql_output": output,
"summary": summary,
"metadata": metadata,
+ "retrievers": retrievers,
+ }
+
+
+def _detect_transitional_state(
+ conn,
+ proposal: SchemaProposal,
+ pre_apply_existing: ExistingSchema,
+) -> Optional[dict]:
+ """Return a payload when a domain schema is being added to a graph
+ that already has Entity-layer data; ``None`` otherwise.
+ """
+ new_vts = [
+ v.name for v in proposal.vertices if not pre_apply_existing.has_vertex(v.name)
+ ]
+ if not new_vts:
+ return None
+ try:
+ entity_count = conn.getVertexCount("Entity") or 0
+ except Exception:
+ entity_count = 0
+ if entity_count <= 0:
+ return None
+ return {
+ "entity_count": int(entity_count),
+ "new_domain_vts": sorted(new_vts),
+ "recommendation": (
+ "Existing Entity-layer data won't be auto-promoted to the "
+ "newly declared domain types. Retrievers will keep walking "
+ "the Entity layer (retrieval_include_entity forced to "
+ "True) so chat answers stay grounded. For full typed "
+ "retrieval, clear derived data (Entity / RELATIONSHIP / "
+ "Community) and re-run ECC against the existing chunks."
+ ),
+ }
+
+
+def _install_retrievers_after_apply(
+ conn,
+ graphname: str,
+ proposal: Optional[SchemaProposal] = None,
+ pre_apply_existing: Optional[ExistingSchema] = None,
+) -> dict:
+ """Re-render and install the templated retrievers against the live
+ domain schema. No-op when no domain types are on the graph.
+ """
+ try:
+ snapshot = read_existing_schema(conn)
+ except Exception as exc:
+ return {"status": "error", "error": f"read live schema: {exc}"}
+
+ # Union the live-schema view with the proposal so a stale cache
+ # right after SCHEMA_CHANGE JOB doesn't miss new types.
+ domain_vt_set: Set[str] = {
+ v for v in snapshot.vertex_types if not is_structural_type(v)
+ }
+ domain_edge_set: Set[str] = {
+ e for e in snapshot.edge_pairs
+ if not is_structural_type(e) and e in snapshot.directed_edges
+ }
+ if proposal is not None:
+ for v in proposal.vertices:
+ if not is_structural_type(v.name):
+ domain_vt_set.add(v.name)
+ for e in proposal.edges:
+ if not is_structural_type(e.name) and e.directed:
+ domain_edge_set.add(e.name)
+ domain_vts = sorted(domain_vt_set)
+ domain_edges = sorted(domain_edge_set)
+
+ import logging as _logging
+ _logger = _logging.getLogger(__name__)
+ _logger.info(
+ f"_install_retrievers_after_apply: graph={graphname} "
+ f"domain_vts={len(domain_vts)} directed_domain_edges={len(domain_edges)} "
+ f"snapshot_edge_pairs={len(snapshot.edge_pairs)}"
+ )
+
+ if not domain_vts and not domain_edges:
+ return {"status": "skipped", "reason": "no domain types on graph"}
+
+ transitional: Optional[dict] = None
+ if proposal is not None and pre_apply_existing is not None:
+ transitional = _detect_transitional_state(
+ conn, proposal, pre_apply_existing
+ )
+
+ try:
+ from common.db.retriever_render import (
+ install_retrievers,
+ resolve_include_entity,
+ )
+ except Exception as exc:
+ return {"status": "error", "error": f"import renderer: {exc}"}
+
+ try:
+ from common.config import graphrag_config
+ include_entity = resolve_include_entity(
+ graphrag_config.get,
+ has_domain_schema=bool(domain_vts),
+ )
+ except Exception:
+ include_entity = False if domain_vts else True
+
+ if transitional:
+ include_entity = True
+
+ result: dict = {
+ "status": "installed",
+ "include_entity": include_entity,
+ "results": install_retrievers(
+ conn,
+ graphname,
+ domain_vts=domain_vts,
+ domain_edges=domain_edges,
+ include_entity=include_entity,
+ ),
}
+ if transitional:
+ result["transitional"] = transitional
+ return result
# -----------------------------------------------------------------------------
diff --git a/common/gsql/graphrag/graphrag_delete_all_communities.gsql b/common/gsql/graphrag/graphrag_delete_all_communities.gsql
new file mode 100644
index 0000000..ceb9f80
--- /dev/null
+++ b/common/gsql/graphrag/graphrag_delete_all_communities.gsql
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2024-2026 TigerGraph, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+*/
+
+CREATE OR REPLACE DISTRIBUTED QUERY graphrag_delete_all_communities() {
+ SumAccum @@deleted;
+
+ comms = {Community.*};
+ res = SELECT c FROM comms:c
+ ACCUM
+ DELETE(c),
+ @@deleted += 1;
+
+ PRINT @@deleted AS deleted;
+}
diff --git a/common/gsql/graphrag/graphrag_stream_all_ids.gsql b/common/gsql/graphrag/graphrag_stream_all_ids.gsql
new file mode 100644
index 0000000..8f16441
--- /dev/null
+++ b/common/gsql/graphrag/graphrag_stream_all_ids.gsql
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2024-2026 TigerGraph, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+*/
+
+CREATE OR REPLACE QUERY graphrag_stream_all_ids(STRING v_type) {
+ ListAccum @@ids;
+ Verts = {v_type};
+ Verts = SELECT v FROM Verts:v
+ ACCUM @@ids += v.id;
+ PRINT @@ids;
+}
diff --git a/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql b/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql
new file mode 100644
index 0000000..8d20117
--- /dev/null
+++ b/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2024-2026 TigerGraph, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+*/
+
+CREATE OR REPLACE DISTRIBUTED QUERY graphrag_stream_entity_community_pairs() {
+ TYPEDEF TUPLE Pair;
+ ListAccum @@pairs;
+
+ ents = {Entity.*};
+ res = SELECT e
+ FROM ents:e -(IN_COMMUNITY>:m)- Community:c
+ ACCUM @@pairs += Pair(e.id, c.id);
+
+ PRINT @@pairs AS pairs;
+}
diff --git a/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql b/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql
index 5e6dcda..91e6cf2 100644
--- a/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql
+++ b/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql
@@ -1,4 +1,5 @@
-CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_communities(UINT iteration=1, UINT max_hop = 10, UINT n_batches = 1) SYNTAX V2{
+// Non-distributed form intentional; see v1.5 plan to revisit.
+CREATE OR REPLACE QUERY graphrag_louvain_communities(UINT iteration=1, UINT max_hop = 10, UINT n_batches = 1) SYNTAX V2{
/*
* This is the same query as tg_louvain, just that Paper-related schema
* are changed to Community-related schema
diff --git a/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql b/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql
index dd85d6d..f582dc3 100644
--- a/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql
+++ b/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql
@@ -1,4 +1,5 @@
-CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UINT n_batches = 1) {
+// Non-distributed form intentional; see v1.5 plan to revisit.
+CREATE OR REPLACE QUERY graphrag_louvain_init(UINT max_hop = 10, UINT n_batches = 1) {
/*
* Initialize GraphRAG's hierarchical communities.
*/
@@ -43,9 +44,6 @@ CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UIN
ACCUM s.@k += wt,
@@m += 1;
- PRINT z.size();
- PRINT z;
-
// Local moving
INT hop = 0;
Candidates = AllNodes;
@@ -183,7 +181,4 @@ CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UIN
ACCUM
DOUBLE w = @@source_target_k_in_map.get(s.@community_vid).get(t.@community_vid),
INSERT INTO LINKS_TO VALUES (s.@community_vid+"_1", t.@community_vid+"_1", w);
-
-
- PRINT @@source_target_k_in_map;
}
diff --git a/common/gsql/graphrag/louvain/modularity.gsql b/common/gsql/graphrag/louvain/modularity.gsql
index cab6255..6f53c96 100644
--- a/common/gsql/graphrag/louvain/modularity.gsql
+++ b/common/gsql/graphrag/louvain/modularity.gsql
@@ -1,4 +1,5 @@
-CREATE OR REPLACE DISTRIBUTED QUERY modularity(UINT iteration=1) SYNTAX V2 {
+// Non-distributed form intentional; see v1.5 plan to revisit.
+CREATE OR REPLACE QUERY modularity(UINT iteration=1) SYNTAX V2 {
SumAccum @@sum_weight; // the sum of the weights of all the links in the network
MinAccum @community_id; // the community ID of the node
MapAccum> @@community_total_weight_map; // community ID C -> the sum of the weights of the links incident to nodes in C
diff --git a/common/gsql/graphrag/louvain/stream_community.gsql b/common/gsql/graphrag/louvain/stream_community.gsql
index e04f7fc..d54fd19 100644
--- a/common/gsql/graphrag/louvain/stream_community.gsql
+++ b/common/gsql/graphrag/louvain/stream_community.gsql
@@ -1,4 +1,5 @@
-CREATE OR REPLACE DISTRIBUTED QUERY stream_community(UINT iter) {
+// Non-distributed form intentional; see v1.5 plan to revisit.
+CREATE OR REPLACE QUERY stream_community(UINT iter) {
Comms = {Community.*};
Comms = SELECT s FROM Comms:s
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
index 6877c0a..e82f538 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
@@ -40,21 +40,15 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Search(STRING json_list_v
@@final_retrieval += (s.id -> tgt.text)
END
POST-ACCUM
- IF s.type == "RelationshipType" OR s.type == "Entity" THEN
+ IF s.type == "Relationship" OR s.type == "Entity" OR s.type == "Concept" THEN
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
- ELSE IF s.type != "DocumentChunk" AND s.type != "Document"
- AND s.type != "Content" AND s.type != "Image"
- AND s.type != "EntityType" THEN
- // Domain vertex type — surface ": " so the
- // LLM sees the schema-aware label.
- @@final_retrieval += (s.id -> s.type + ": " + replace(s.id, "_", " "))
END;
-
+
@@verbose_info += ("start_set" -> @@start_set_type);
- PRINT @@final_retrieval as final_retrieval;
+ PRINT @@final_retrieval as final_retrieval;
IF verbose THEN
PRINT @@verbose_info as verbose;
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
index 5d58f89..fe4a4ca 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_type, LIST query_vector, INT top_k=5, BOOL verbose = False) {
+CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_type, LIST query_vector, INT top_k=5, BOOL verbose = False) {
TYPEDEF tuple Similarity_Results;
TYPEDEF TUPLE VertexTypes;
SetAccum @@start_set_type;
@@ -22,37 +22,31 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_ty
HeapAccum(top_k, score DESC) @@topk_set;
SetAccum @@start_set;
MapAccum @@final_retrieval;
-
+
vset = {v_type};
- result = SELECT v FROM vset:v WHERE v.embedding.size() > 0 POST-ACCUM @@topk_set += Similarity_Results(v, 1 - gds.vector.distance(query_vector, v.embedding, "COSINE"));
+ result = SELECT v FROM vset:v POST-ACCUM @@topk_set += Similarity_Results(v, 1 - gds.vector.distance(query_vector, v.embedding, "COSINE"));
FOREACH item IN @@topk_set DO
@@start_set += item.v;
END;
-
+
start = {@@start_set};
-
+
res = SELECT s FROM start:s -(:e)- :tgt WHERE s.type == v_type
ACCUM @@start_set_type += VertexTypes(s, s.type),
IF (s.type == "DocumentChunk" OR s.type == "Document") AND tgt.type == "Content" THEN
@@final_retrieval += (s.id -> tgt.text)
END
POST-ACCUM
- IF s.type == "RelationshipType" OR s.type == "Entity" THEN
+ IF s.type == "Relationship" OR s.type == "Entity" OR s.type == "Concept" THEN
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
- ELSE IF s.type != "DocumentChunk" AND s.type != "Document"
- AND s.type != "Content" AND s.type != "Image"
- AND s.type != "EntityType" THEN
- // Domain vertex type — surface ": " so the
- // LLM sees the schema-aware label.
- @@final_retrieval += (s.id -> s.type + ": " + replace(s.id, "_", " "))
END;
-
+
@@verbose_info += ("start_set" -> @@start_set_type);
- PRINT @@final_retrieval as final_retrieval;
+ PRINT @@final_retrieval as final_retrieval;
IF verbose THEN
PRINT @@verbose_info as verbose;
diff --git a/ecc/app/graphrag/graph_rag.py b/ecc/app/graphrag/graph_rag.py
index 3435916..29d226d 100644
--- a/ecc/app/graphrag/graph_rag.py
+++ b/ecc/app/graphrag/graph_rag.py
@@ -31,10 +31,12 @@
load_q,
loading_event,
make_headers,
+ graphrag_mirror_communities,
stream_ids,
tg_sem,
upsert_batch,
)
+from common.db.schema_utils import is_structural_type, read_existing_schema_async
from pyTigerGraph import AsyncTigerGraphConnection
from common.config import embedding_service, entity_extraction_switch, community_detection_switch, doc_process_switch, get_graphrag_config
@@ -519,6 +521,19 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
if community_detection_switch:
await install_queries(COMMUNITY_QUERIES, conn)
logger.info("Community Processing Start")
+
+ # Clear pre-existing communities so re-detection is idempotent.
+ try:
+ async with tg_sem:
+ res = await conn.runInstalledQuery(
+ "graphrag_delete_all_communities"
+ )
+ deleted = (res[0] if res else {}).get("deleted", 0)
+ if deleted:
+ logger.info(f"Cleared {deleted} pre-existing Community vertex(es)")
+ except Exception as e:
+ logger.warning(f"graphrag_delete_all_communities failed: {e}")
+
comm_process_chan = Channel()
upsert_chan = Channel()
embed_chan = Channel()
@@ -540,6 +555,30 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
await embed_chan.join()
logger.info("Join upsert_chan")
await upsert_chan.join()
+
+ # Mirror Entity → Community memberships onto domain-VT instances
+ # that share the same id.
+ try:
+ existing_schema = await read_existing_schema_async(conn)
+ domain_vts = sorted(
+ v for v in existing_schema.vertex_types if not is_structural_type(v)
+ )
+ except Exception as e:
+ logger.warning(f"read live schema for community mirror failed: {e}")
+ domain_vts = []
+ if domain_vts:
+ mirrorable = [
+ vt for vt in domain_vts
+ if existing_schema.has_edge_pair("IN_COMMUNITY", vt, "Community")
+ ]
+ skipped = sorted(set(domain_vts) - set(mirrorable))
+ if skipped:
+ logger.warning(
+ f"skipping community mirror for {skipped}: "
+ f"IN_COMMUNITY pair missing on schema"
+ )
+ if mirrorable:
+ await graphrag_mirror_communities(conn, mirrorable)
community_end = time.perf_counter()
logger.info("Community Processing End")
diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py
index 7973107..549d49c 100644
--- a/ecc/app/graphrag/util.py
+++ b/ecc/app/graphrag/util.py
@@ -58,6 +58,9 @@
"common/gsql/graphrag/louvain/stream_community",
"common/gsql/graphrag/get_community_children",
"common/gsql/graphrag/communities_have_desc",
+ "common/gsql/graphrag/graphrag_delete_all_communities",
+ "common/gsql/graphrag/graphrag_stream_entity_community_pairs",
+ "common/gsql/graphrag/graphrag_stream_all_ids",
]
REQUIRED_QUERIES = [
@@ -464,3 +467,82 @@ async def check_embedding_rebuilt(conn, v_type: str):
logger.info(resp)
return res
+
+
+async def graphrag_mirror_communities(
+ conn: AsyncTigerGraphConnection,
+ domain_vts: list[str],
+) -> int:
+ """Mirror Entity → Community memberships onto domain-VT instances
+ that share the same id. Returns the number of mirror edges written.
+ """
+ if not domain_vts:
+ return 0
+
+ async with tg_sem:
+ try:
+ res = await conn.runInstalledQuery(
+ "graphrag_stream_entity_community_pairs",
+ params={},
+ sizeLimit=1000000000,
+ )
+ except Exception as e:
+ logger.error(f"stream entity-community pairs failed: {e}")
+ return 0
+
+ pairs = (res[0] if res else {}).get("pairs", []) or []
+ if not pairs:
+ return 0
+
+ valid_ids_by_vt: dict[str, set[str]] = {}
+ for vt in domain_vts:
+ try:
+ async with tg_sem:
+ r = await conn.runInstalledQuery(
+ "graphrag_stream_all_ids",
+ params={"v_type": vt},
+ sizeLimit=1000000000,
+ )
+ except Exception as e:
+ logger.warning(f"stream_all_ids({vt}) failed: {e}")
+ valid_ids_by_vt[vt] = set()
+ continue
+ ids = set((r[0] if r else {}).get("@@ids", []) or [])
+ valid_ids_by_vt[vt] = ids
+
+ written = 0
+ chunk_size = 5000
+ for vt, valid_ids in valid_ids_by_vt.items():
+ if not valid_ids:
+ continue
+ edges = [
+ (p["entity_id"], p["community_id"])
+ for p in pairs
+ if isinstance(p, dict)
+ and p.get("entity_id") in valid_ids
+ and p.get("community_id")
+ ]
+ if not edges:
+ continue
+ for i in range(0, len(edges), chunk_size):
+ chunk = edges[i:i + chunk_size]
+ async with tg_sem:
+ try:
+ await conn.upsertEdges(
+ sourceVertexType=vt,
+ edgeType="IN_COMMUNITY",
+ targetVertexType="Community",
+ edges=chunk,
+ )
+ written += len(chunk)
+ except Exception as e:
+ logger.error(
+ f"upsertEdges IN_COMMUNITY for {vt} (chunk size "
+ f"{len(chunk)}) failed: {e}"
+ )
+
+ logger.info(
+ f"graphrag_mirror_communities: wrote {written} mirror "
+ f"IN_COMMUNITY edges across {len(domain_vts)} domain VT(s)"
+ )
+ return written
diff --git a/graphrag-ui/.npmrc b/graphrag-ui/.npmrc
new file mode 100644
index 0000000..87e25fb
--- /dev/null
+++ b/graphrag-ui/.npmrc
@@ -0,0 +1,2 @@
+only-built-dependencies[]=@swc/core
+only-built-dependencies[]=esbuild
diff --git a/graphrag-ui/package.json b/graphrag-ui/package.json
index 12cca39..c20d3dd 100755
--- a/graphrag-ui/package.json
+++ b/graphrag-ui/package.json
@@ -3,6 +3,7 @@
"private": true,
"version": "0.0.5",
"type": "module",
+ "packageManager": "pnpm@9.15.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -57,5 +58,11 @@
"tailwindcss": "^3.4.18",
"typescript": "^5.2.2",
"vite": "^5.2.0"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "@swc/core",
+ "esbuild"
+ ]
}
}
diff --git a/graphrag/app/supportai/retrievers/CommunityRetriever.py b/graphrag/app/supportai/retrievers/CommunityRetriever.py
index 466cf4d..2724312 100644
--- a/graphrag/app/supportai/retrievers/CommunityRetriever.py
+++ b/graphrag/app/supportai/retrievers/CommunityRetriever.py
@@ -49,8 +49,8 @@ def search(self, question, community_level: int, top_k: int = 5, similarity_thre
resp = self.conn.runInstalledQuery(
"Content_Similarity_Search",
params = {
- "json_list_vts": str(start_set),
"v_type": "DocumentChunk",
+ "json_list_vts": str(start_set),
"verbose": verbose,
},
usePost=True
diff --git a/graphrag/app/supportai/retrievers/SimilarityRetriever.py b/graphrag/app/supportai/retrievers/SimilarityRetriever.py
index e5f948c..24fa62e 100644
--- a/graphrag/app/supportai/retrievers/SimilarityRetriever.py
+++ b/graphrag/app/supportai/retrievers/SimilarityRetriever.py
@@ -25,8 +25,8 @@ def search(self, question, index, top_k=1, withHyDE=False, expand=False, verbose
res = self.conn.runInstalledQuery(
"Content_Similarity_Search",
params = {
- "json_list_vts": str(start_set),
"v_type": index,
+ "json_list_vts": str(start_set),
"verbose": verbose,
},
usePost=True
@@ -41,8 +41,8 @@ def search(self, question, index, top_k=1, withHyDE=False, expand=False, verbose
res = self.conn.runInstalledQuery(
"Content_Similarity_Vector_Search",
params = {
- "v_type": index,
"query_vector": query_vector,
+ "v_type": index,
"top_k": top_k,
"verbose": verbose,
},
diff --git a/graphrag/tests/test_retriever_render.py b/graphrag/tests/test_retriever_render.py
new file mode 100644
index 0000000..62dceb9
--- /dev/null
+++ b/graphrag/tests/test_retriever_render.py
@@ -0,0 +1,177 @@
+# Copyright (c) 2024-2026 TigerGraph, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Unit tests for ``common.db.retriever_render``.
+
+The renderer rewrites stable string anchors in the retriever GSQL
+files so domain-VT instances are reachable via their own edges and
+walked alongside Entity in the community queries. Tests assert the
+substitutions land where expected and that empty domain sets pass the
+body through unchanged.
+"""
+
+from __future__ import annotations
+
+from common.db.retriever_render import (
+ TEMPLATED_RETRIEVERS,
+ render_retriever_body,
+ render_retrievers,
+ resolve_include_entity,
+)
+
+
+_HYBRID_HOP_BODY = """
+start = SELECT t FROM start:s -((RELATIONSHIP>|
+ CONTAINS_ENTITY>|
+ reverse_CONTAINS_ENTITY>|
+ IS_AFTER>):e)- :t
+"""
+
+
+_COMMUNITY_WALK_BODY = """
+related_chunks = SELECT c FROM Content:c -()- Entity:v -(IN_COMMUNITY>)- selected_comms:m
+"""
+
+
+def test_render_no_domain_types_passes_through():
+ body = _HYBRID_HOP_BODY + _COMMUNITY_WALK_BODY
+ out = render_retriever_body(
+ body, domain_vts=[], domain_edges=[], include_entity=True
+ )
+ assert out == body
+
+
+def test_render_appends_domain_edges_to_hop_pattern():
+ body = _HYBRID_HOP_BODY
+ out = render_retriever_body(
+ body,
+ domain_vts=["Company"],
+ domain_edges=["PUBLISHES", "INVESTS_IN"],
+ include_entity=True,
+ )
+ # Domain edges land between IS_AFTER> and ):e, sorted alphabetically.
+ assert "IS_AFTER>|INVESTS_IN>|PUBLISHES>):e" in out
+ # Existing structural edges preserved.
+ assert "RELATIONSHIP>" in out
+ assert "CONTAINS_ENTITY>" in out
+
+
+def test_render_expands_community_member_with_entity():
+ body = _COMMUNITY_WALK_BODY
+ out = render_retriever_body(
+ body,
+ domain_vts=["Company", "InvestmentFund"],
+ domain_edges=[],
+ include_entity=True,
+ )
+ # Member set sorts alphabetically with Entity prepended.
+ assert "(Entity|Company|InvestmentFund):v -(IN_COMMUNITY>" in out
+
+
+def test_render_excludes_entity_when_flag_false():
+ body = _COMMUNITY_WALK_BODY
+ out = render_retriever_body(
+ body,
+ domain_vts=["Company", "Report"],
+ domain_edges=[],
+ include_entity=False,
+ )
+ assert "(Company|Report):v -(IN_COMMUNITY>" in out
+ # Entity must not appear in the substituted member set.
+ assert "Entity|" not in out
+ assert "|Entity)" not in out
+
+
+def test_render_single_domain_vt_drops_parens():
+ body = _COMMUNITY_WALK_BODY
+ out = render_retriever_body(
+ body,
+ domain_vts=["Company"],
+ domain_edges=[],
+ include_entity=False,
+ )
+ # Single VT — no surrounding parens needed.
+ assert "Company:v -(IN_COMMUNITY>" in out
+ assert "(Company)" not in out
+
+
+def test_render_empty_edges_no_change_to_hop():
+ body = _HYBRID_HOP_BODY
+ out = render_retriever_body(
+ body, domain_vts=["Company"], domain_edges=[], include_entity=True
+ )
+ assert out == body
+
+
+def test_render_retrievers_targets_only_known_set(tmp_path):
+ """The renderer ships with a curated retriever list — graphs without
+ the file on disk must skip rather than blocking the pipeline.
+ """
+ (tmp_path / "GraphRAG_Hybrid_Search.gsql").write_text(_HYBRID_HOP_BODY)
+ (tmp_path / "GraphRAG_Community_Search.gsql").write_text(_COMMUNITY_WALK_BODY)
+ # Hybrid_Vector_Search and Community_Vector_Search intentionally
+ # missing — the renderer logs and keeps going.
+ rendered = render_retrievers(
+ domain_vts=["Company"],
+ domain_edges=["PUBLISHES"],
+ include_entity=True,
+ retriever_dir=str(tmp_path),
+ )
+ assert "GraphRAG_Hybrid_Search" in rendered
+ assert "GraphRAG_Community_Search" in rendered
+ # Substitutions reached the rendered output:
+ assert "IS_AFTER>|PUBLISHES>):e" in rendered["GraphRAG_Hybrid_Search"]
+ assert "(Entity|Company):v -(IN_COMMUNITY>" in rendered[
+ "GraphRAG_Community_Search"
+ ]
+ # Missing files are simply skipped, not raised.
+ assert "GraphRAG_Hybrid_Vector_Search" not in rendered
+
+
+def test_resolve_include_entity_auto_default_off_with_schema():
+ """Unset config + domain schema present → auto-default to False
+ (typed-purist retrieval). Users who declared a schema get strict
+ behaviour without having to flip a flag.
+ """
+ cfg = {}.get # nothing configured
+ assert resolve_include_entity(cfg, has_domain_schema=True) is False
+
+
+def test_resolve_include_entity_auto_default_on_without_schema():
+ """Unset config + no domain schema → True (moot — Entity is the
+ only path the retrievers can walk).
+ """
+ cfg = {}.get
+ assert resolve_include_entity(cfg, has_domain_schema=False) is True
+
+
+def test_resolve_include_entity_explicit_true_overrides_auto():
+ """Even with a domain schema, explicit `True` keeps Entity in the
+ traversal — for lenient deployments where unmatched extractions
+ still carry useful context.
+ """
+ cfg = {"retrieval_include_entity": True}.get
+ assert resolve_include_entity(cfg, has_domain_schema=True) is True
+
+
+def test_resolve_include_entity_explicit_false_overrides_auto():
+ cfg = {"retrieval_include_entity": False}.get
+ assert resolve_include_entity(cfg, has_domain_schema=False) is False
+
+
+def test_templated_retrievers_list_is_stable():
+ """The curated retriever list is part of the public contract for
+ schema-apply / post-Louvain trigger hooks. Lock it down so silent
+ drift triggers a test failure.
+ """
+ assert TEMPLATED_RETRIEVERS == (
+ "GraphRAG_Hybrid_Search",
+ "GraphRAG_Hybrid_Vector_Search",
+ "GraphRAG_Community_Search",
+ "GraphRAG_Community_Vector_Search",
+ )
diff --git a/graphrag/tests/test_schema_utils.py b/graphrag/tests/test_schema_utils.py
index 5ed6e2a..a8eca91 100644
--- a/graphrag/tests/test_schema_utils.py
+++ b/graphrag/tests/test_schema_utils.py
@@ -37,10 +37,17 @@
class _FakeConn:
"""Minimal pyTigerGraph-shaped connection for read_existing_schema tests."""
- def __init__(self, vertex_types, edge_metadata, gsql_response="OK"):
+ def __init__(
+ self,
+ vertex_types,
+ edge_metadata,
+ gsql_response="OK",
+ vertex_counts=None,
+ ):
self._vertex_types = list(vertex_types)
self._edge_metadata = dict(edge_metadata)
self._gsql_response = gsql_response
+ self._vertex_counts = dict(vertex_counts or {})
self.gsql_calls = []
self.upsert_calls = []
@@ -55,11 +62,28 @@ def getEdgeType(self, name):
def gsql(self, command):
self.gsql_calls.append(command)
+ # Minimal schema-change simulation so post-apply reads see the
+ # newly-added vertex / edge types. Just enough for tests that
+ # exercise the retriever-install hook downstream of
+ # apply_proposal.
+ import re
+ for m in re.finditer(r"\bADD VERTEX (\w+)", command):
+ vt = m.group(1)
+ if vt not in self._vertex_types:
+ self._vertex_types.append(vt)
+ for m in re.finditer(
+ r"\bADD (?:DIRECTED|UNDIRECTED) EDGE (\w+)", command
+ ):
+ et = m.group(1)
+ self._edge_metadata.setdefault(et, {"EdgePairs": []})
return self._gsql_response
def upsertVertex(self, vertex_type, vertex_id, attributes=None):
self.upsert_calls.append((vertex_type, vertex_id, dict(attributes or {})))
+ def getVertexCount(self, vertex_type):
+ return int(self._vertex_counts.get(vertex_type, 0))
+
class _FakeConnWithVertices(_FakeConn):
"""Extends _FakeConn with a getVertices() that returns canned rows."""
@@ -612,7 +636,9 @@ def test_build_schema_change_job_empty_statements_raises():
def test_apply_proposal_no_op_when_diff_is_empty():
"""If the existing graph already has every type in the proposal, the
- helper must not call gsql at all and must report status='no-op'.
+ helper must not run a SCHEMA_CHANGE JOB and must report
+ status='no-op'. Retriever re-installs are still expected because
+ they're keyed off the live schema, not the proposal diff.
"""
conn = _FakeConn(
vertex_types=["Company", "Report"],
@@ -634,7 +660,8 @@ def test_apply_proposal_no_op_when_diff_is_empty():
assert result["statements"] == []
assert result["job_name"] is None
assert result["gsql_output"] == ""
- assert conn.gsql_calls == []
+ # No SCHEMA_CHANGE JOB block — but retriever installs may run.
+ assert not any("SCHEMA_CHANGE JOB" in c for c in conn.gsql_calls)
assert result["summary"]["vertex_count"] == 2
@@ -656,8 +683,9 @@ def test_apply_proposal_runs_single_gsql_call_with_diff():
result = apply_proposal(conn, "MyGraph", proposal)
assert result["status"] == "applied"
- assert len(conn.gsql_calls) == 1
- cmd = conn.gsql_calls[0]
+ schema_calls = [c for c in conn.gsql_calls if "SCHEMA_CHANGE JOB" in c]
+ assert len(schema_calls) == 1
+ cmd = schema_calls[0]
assert "USE GRAPH MyGraph" in cmd
assert "ADD VERTEX Filing" in cmd
assert "ADD DIRECTED EDGE OWNS" in cmd
@@ -709,8 +737,9 @@ def test_apply_proposal_end_to_end_from_pasted_gsql():
result = apply_proposal(conn, "FreshGraph", proposal)
assert result["status"] == "applied"
- assert len(conn.gsql_calls) == 1
- cmd = conn.gsql_calls[0]
+ schema_calls = [c for c in conn.gsql_calls if "SCHEMA_CHANGE JOB" in c]
+ assert len(schema_calls) == 1
+ cmd = schema_calls[0]
assert "USE GRAPH FreshGraph" in cmd
assert "ADD VERTEX Company" in cmd
assert "ADD VERTEX Filing" in cmd
@@ -720,6 +749,102 @@ def test_apply_proposal_end_to_end_from_pasted_gsql():
assert result["summary"]["edge_count"] == 1
+# ---------------------------------------------------------------------------
+# Transitional graph (pre-existing Entity data + new domain schema)
+# ---------------------------------------------------------------------------
+
+
+def test_apply_proposal_transitional_forces_include_entity():
+ """Upgrade case: existing graph has Entity-layer data, user declares
+ a domain schema for the first time. The retriever installer must
+ flip ``include_entity`` to True regardless of config so existing
+ Entity rows stay reachable until re-ingest. The result surfaces a
+ transitional payload the dialog can render.
+ """
+ conn = _FakeConn(
+ vertex_types=[
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
+ ],
+ edge_metadata={},
+ # Existing Entity-layer data — 1742 entities, no domain VTs yet.
+ vertex_counts={"Entity": 1742},
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+ proposal.add_vertex("Report")
+
+ result = apply_proposal(conn, "g", proposal)
+
+ assert result["status"] == "applied"
+ retrievers = result["retrievers"]
+ # Forced True regardless of config default.
+ assert retrievers["include_entity"] is True
+ # Transitional payload surfaced for the caller / UI.
+ transitional = retrievers.get("transitional")
+ assert transitional is not None
+ assert transitional["entity_count"] == 1742
+ assert transitional["new_domain_vts"] == ["Company", "Report"]
+ assert "re-ingest" in transitional["recommendation"].lower() or \
+ "re-run" in transitional["recommendation"].lower()
+
+
+def test_apply_proposal_no_entity_data_keeps_typed_purist_default():
+ """Fresh graph (no Entity rows yet) gets the normal auto-default —
+ typed-purist when domain schema exists.
+ """
+ conn = _FakeConn(
+ vertex_types=[
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
+ ],
+ edge_metadata={},
+ vertex_counts={"Entity": 0},
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ result = apply_proposal(conn, "g", proposal)
+
+ retrievers = result["retrievers"]
+ # No Entity data → auto-default fires, typed-purist.
+ assert retrievers["include_entity"] is False
+ assert "transitional" not in retrievers
+
+
+def test_apply_proposal_no_new_domain_vts_skips_transitional_check():
+ """Re-applying an unchanged proposal against a graph that already
+ has the domain VTs is not transitional — no new VTs introduced. The
+ Entity count is irrelevant; auto-default behaviour applies.
+ """
+ conn = _FakeConn(
+ vertex_types=[
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
+ "Company", # already on the graph
+ ],
+ edge_metadata={
+ "CONTAINS_ENTITY": {
+ "FromVertexTypeName": "Document",
+ "ToVertexTypeName": "Company",
+ },
+ "IN_COMMUNITY": {
+ "FromVertexTypeName": "Company",
+ "ToVertexTypeName": "Community",
+ },
+ },
+ vertex_counts={"Entity": 9000, "Company": 100},
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company") # already on graph — no new VT
+
+ result = apply_proposal(conn, "g", proposal)
+
+ retrievers = result["retrievers"]
+ # No new VTs in proposal → not a transitional apply, normal default.
+ assert "transitional" not in retrievers
+
+
# ---------------------------------------------------------------------------
# upsert_type_metadata
# ---------------------------------------------------------------------------
@@ -866,10 +991,10 @@ def test_read_type_metadata_returns_empty_on_missing_method():
def test_emit_structural_links_for_new_domain_vertices():
"""Each new domain vertex must get CONTAINS_ENTITY pairs from
- Document and DocumentChunk. IS_HEAD_OF / HAS_TAIL are NOT added
- per-domain-vertex — they live at the EntityType ↔ RelationshipType
- meta-schema layer and the original schema declaration covers the
- only pair we ever traverse.
+ Document and DocumentChunk, plus an IN_COMMUNITY pair to Community.
+ IS_HEAD_OF / HAS_TAIL are NOT added per-domain-vertex — they live
+ at the EntityType ↔ RelationshipType meta-schema layer and the
+ original schema declaration covers the only pair we ever traverse.
"""
proposal = SchemaProposal()
proposal.add_vertex("Company")
@@ -878,11 +1003,12 @@ def test_emit_structural_links_for_new_domain_vertices():
existing = ExistingSchema(
vertex_types={
"Document", "DocumentChunk", "Entity",
- "EntityType", "RelationshipType",
+ "EntityType", "RelationshipType", "Community",
"Company", "Report",
},
edge_pairs={
"CONTAINS_ENTITY": {("Document", "Entity"), ("DocumentChunk", "Entity")},
+ "IN_COMMUNITY": {("Entity", "Community")},
"IS_HEAD_OF": {("EntityType", "RelationshipType")},
"HAS_TAIL": {("RelationshipType", "EntityType")},
},
@@ -890,18 +1016,53 @@ def test_emit_structural_links_for_new_domain_vertices():
stmts = emit_structural_link_alters(proposal, existing)
- # Each vertex gets two CONTAINS_ENTITY pair-additions: 2*2 = 4.
+ # Each vertex gets two CONTAINS_ENTITY pair-additions plus one
+ # IN_COMMUNITY pair-addition: 2*(2+1) = 6.
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in stmts
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in stmts
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Report)" in stmts
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Report)" in stmts
+ assert "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Company, TO Community)" in stmts
+ assert "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Report, TO Community)" in stmts
# No per-domain-vertex IS_HEAD_OF / HAS_TAIL emitted.
assert not any("IS_HEAD_OF" in s for s in stmts)
assert not any("HAS_TAIL" in s for s in stmts)
- assert len(stmts) == 4
+ assert len(stmts) == 6
-def test_emit_structural_links_skips_already_present_pairs():
+def test_emit_structural_links_skips_in_community_when_already_present():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ existing = ExistingSchema(
+ vertex_types={
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
+ "Company",
+ },
+ edge_pairs={
+ "CONTAINS_ENTITY": {
+ ("Document", "Entity"), ("DocumentChunk", "Entity"),
+ ("Document", "Company"), ("DocumentChunk", "Company"),
+ },
+ "IN_COMMUNITY": {
+ ("Entity", "Community"),
+ ("Company", "Community"), # already there
+ },
+ },
+ )
+
+ stmts = emit_structural_link_alters(proposal, existing)
+ # CONTAINS_ENTITY pairs already present, IN_COMMUNITY pair already
+ # present — nothing left to emit.
+ assert stmts == []
+
+
+def test_emit_structural_links_skips_in_community_when_community_missing():
+ """Bare-graph defensive case: if Community isn't on the graph yet,
+ don't emit IN_COMMUNITY pairs that would reference an undeclared
+ endpoint and fail at schema-change time.
+ """
proposal = SchemaProposal()
proposal.add_vertex("Company")
@@ -909,6 +1070,29 @@ def test_emit_structural_links_skips_already_present_pairs():
vertex_types={
"Document", "DocumentChunk", "Entity",
"EntityType", "RelationshipType",
+ # Community deliberately omitted.
+ "Company",
+ },
+ edge_pairs={
+ "CONTAINS_ENTITY": {("Document", "Entity"), ("DocumentChunk", "Entity")},
+ },
+ )
+
+ stmts = emit_structural_link_alters(proposal, existing)
+ assert not any("IN_COMMUNITY" in s for s in stmts)
+ # CONTAINS_ENTITY pairs still emitted.
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in stmts
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in stmts
+
+
+def test_emit_structural_links_skips_already_present_pairs():
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ existing = ExistingSchema(
+ vertex_types={
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
"Company",
},
edge_pairs={
@@ -917,15 +1101,18 @@ def test_emit_structural_links_skips_already_present_pairs():
("DocumentChunk", "Entity"),
("Document", "Company"), # already there
},
+ "IN_COMMUNITY": {("Entity", "Community")},
"IS_HEAD_OF": {("EntityType", "RelationshipType")},
"HAS_TAIL": {("RelationshipType", "EntityType")},
},
)
stmts = emit_structural_link_alters(proposal, existing)
- # Only the missing CONTAINS_ENTITY pair (DocumentChunk → Company).
+ # Missing CONTAINS_ENTITY pair (DocumentChunk → Company) and the
+ # missing IN_COMMUNITY pair (Company → Community).
assert stmts == [
"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)",
+ "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Company, TO Community)",
]
@@ -938,7 +1125,7 @@ def test_apply_proposal_emits_structural_links_alongside_domain_adds():
conn = _FakeConn(
vertex_types=[
"Document", "DocumentChunk", "Entity",
- "EntityType", "RelationshipType",
+ "EntityType", "RelationshipType", "Community",
],
edge_metadata={},
)
@@ -947,13 +1134,17 @@ def test_apply_proposal_emits_structural_links_alongside_domain_adds():
result = apply_proposal(conn, "g", proposal)
assert result["status"] == "applied"
- assert len(conn.gsql_calls) == 1
- cmd = conn.gsql_calls[0]
+ schema_calls = [c for c in conn.gsql_calls if "SCHEMA_CHANGE JOB" in c]
+ assert len(schema_calls) == 1
+ cmd = schema_calls[0]
# Domain ADD VERTEX is in there.
assert "ADD VERTEX Company" in cmd
# CONTAINS_ENTITY pair-additions for Company are in the same job.
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in cmd
assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in cmd
+ # IN_COMMUNITY pair-addition for Company is in the same job —
+ # community retrievers walking domain VTs need this edge present.
+ assert "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Company, TO Community)" in cmd
# No per-domain-vertex IS_HEAD_OF / HAS_TAIL — those live at
# EntityType ↔ RelationshipType in the structural schema.
assert "IS_HEAD_OF ADD PAIR" not in cmd
@@ -972,6 +1163,7 @@ def test_apply_proposal_skips_structural_links_when_core_types_missing():
result = apply_proposal(conn, "g", proposal)
cmd = conn.gsql_calls[0] if conn.gsql_calls else ""
assert "ALTER EDGE CONTAINS_ENTITY" not in cmd
+ assert "ALTER EDGE IN_COMMUNITY" not in cmd
assert "ALTER EDGE IS_HEAD_OF" not in cmd
assert "ALTER EDGE HAS_TAIL" not in cmd
From c6e38c531ca90ecc9ef8564106ada2b6f3a1889f Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 7 May 2026 16:26:58 -0700
Subject: [PATCH 50/70] feat(GML-2076): cross-lane and in-lane retrieval
fallbacks
Two new recovery paths that reduce "I'm sorry" responses while staying
honest about failure: structured-data retries now fall through to vector
search, and a chunk-based retriever that returns fewer than top_k
results auto-tries a different method.
---
CHANGELOG.md | 2 +
graphrag/app/agent/agent_graph.py | 163 +++++++++++++++++++------
graphrag/app/agent/method_selector.py | 49 ++++++++
graphrag/tests/test_method_selector.py | 95 ++++++++++++++
4 files changed, 274 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db27e68..4dabb51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@
- Manual method selection still works as override during the transition
- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels
- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
+- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search.
+- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path.
## [1.3.1]
diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py
index 3643d28..37abb73 100644
--- a/graphrag/app/agent/agent_graph.py
+++ b/graphrag/app/agent/agent_graph.py
@@ -24,7 +24,12 @@
from agent.agent_rewrite import TigerGraphAgentRewriter
from agent.agent_router import TigerGraphAgentRouter
from agent.agent_usefulness_check import TigerGraphAgentUsefulnessCheck
-from agent.method_selector import RetrieverSelector
+from agent.method_selector import (
+ CHUNK_BASED_METHODS,
+ INLANE_FALLBACK_TABLE,
+ RetrieverSelector,
+ has_insufficient_context,
+)
from agent.Q import DONE, Q
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
@@ -65,6 +70,15 @@ class GraphState(TypedDict):
chosen_retriever: Optional[str]
chosen_retriever_reason: Optional[str]
chosen_retriever_source: Optional[str]
+ # Cross-lane fallback: set by route_question after generate_function/cypher
+ # retries are exhausted, so supportai_search knows to flip the source label
+ # to "router_fallback" and force auto-selection.
+ router_fallback_attempted: Optional[bool]
+ # In-lane fallback: set by supportai_search when the first chunk-based method
+ # returned fewer than top_k chunks and we ran a second method via
+ # INLANE_FALLBACK_TABLE. The "_from" field records the original method.
+ inlane_fallback_attempted: Optional[bool]
+ inlane_fallback_from: Optional[str]
class TigerGraphAgentGraph:
@@ -145,8 +159,18 @@ def greet(self, state):
def route_question(self, state):
"""
Run the agent router.
+
+ When generate_function / generate_cypher have failed to produce a usable
+ answer through 3 rewrite cycles (`question_retry_count > 2`), instead of
+ going straight to `apologize`, fall back to vector search if available.
+ `supportai_search` reads `state["router_fallback_attempted"]` to flip its
+ source label and re-run auto-selection regardless of configured method.
"""
if state["question_retry_count"] > 2:
+ if self.supportai_enabled and not state.get("router_fallback_attempted"):
+ state["router_fallback_attempted"] = True
+ self.emit_progress("Trying a different approach…")
+ return "supportai_lookup"
return "apologize"
if self._is_greeting(state["question"]):
return "greeting"
@@ -472,66 +496,135 @@ def community_search(self, state):
"communitysearch": "Community",
}
+ def _dispatch_retriever(self, method, state):
+ """Run the retriever named by `method` and return the updated state.
+
+ Centralises the if/elif chain so it can be called twice (for in-lane
+ fallback) without duplication.
+ """
+ if method == "hybridsearch":
+ return self.hybrid_search(state)
+ elif method == "similaritysearch":
+ return self.similarity_search(state)
+ elif method == "contextualsearch":
+ return self.sibling_search(state)
+ elif method == "communitysearch":
+ return self.community_search(state)
+ raise ValueError(f"Invalid supportai retriever: {method}")
+
+ def _record_selection_metric(self, method, source):
+ """Increment the selection counter; never lets a metric error break the request."""
+ try:
+ pmetrics.llm_method_selection_total.labels(
+ selected_method=method, selection_source=source
+ ).inc()
+ except Exception: # noqa: BLE001
+ pass
+
def supportai_search(self, state):
"""
Run the agent supportai search.
- When `self.supportai_retriever == "auto"`, picks a method via
- `RetrieverSelector` (rules first, LLM fallback). Otherwise dispatches
- directly to the configured retriever. Either way, populates
- `state["chosen_retriever*"]` and surfaces the choice on the context
- dict so it flows through `generate_answer` into `query_sources`.
+ Three layers of behavior:
+
+ 1. **Method selection.** When `self.supportai_retriever == "auto"` (the
+ default), picks a method via `RetrieverSelector`. When configured to
+ a specific method, uses that. **Exception:** when reached via
+ cross-lane fallback (`state["router_fallback_attempted"]`), forces
+ auto-selection regardless of configuration — manual users still get
+ the best vector method when the structured-data path has exhausted
+ its retries.
+ 2. **In-lane fallback.** After the first chunk-based retriever runs, if
+ it returned fewer than `top_k` chunks (signal: insufficient context),
+ runs a second method per `INLANE_FALLBACK_TABLE` and uses its context
+ for downstream generation. Single retry only; skipped for manual
+ mode and community search.
+ 3. **Out-of-corpus short-circuit.** If after all retrieval attempts the
+ result is still empty, marks the context so `generate_answer`
+ returns an honest "couldn't find" message instead of letting the
+ LLM hallucinate from empty context.
+
+ State written: `chosen_retriever{,_reason,_source}` populated for the
+ UI/telemetry; mirrored into `state["context"]` so it lands on
+ `GraphRAGResponse.query_sources` without further plumbing.
"""
+ is_router_fallback = bool(state.get("router_fallback_attempted"))
+
method = self.supportai_retriever
chosen_reason = "user-selected"
chosen_source = "manual"
- if method == "auto":
+ # In router_fallback mode we always auto-select, even for manual users —
+ # they need the best vector method now that structured-data is dead.
+ if is_router_fallback or method == "auto":
selector = RetrieverSelector(self.llm_provider, self.db_connection)
choice = selector.choose(state["question"], state.get("conversation"))
method = choice.method
chosen_reason = choice.reason
chosen_source = choice.source
+
+ if is_router_fallback:
+ chosen_source = "router_fallback"
+ chosen_reason = f"{chosen_reason} (after structured-data retries exhausted)"
+ label = self._METHOD_DISPLAY_NAMES.get(method, method)
+ self.emit_progress(f"Trying a different approach: {label} search")
+ elif chosen_source != "manual":
label = self._METHOD_DISPLAY_NAMES.get(method, method)
self.emit_progress(f"Auto-selected {label} search")
state["chosen_retriever"] = method
state["chosen_retriever_reason"] = chosen_reason
state["chosen_retriever_source"] = chosen_source
-
- # Phase 1.5 — telemetry: count selection by method + source so operators
- # can see the auto-vs-manual distribution and rules-vs-llm hit rate.
- try:
- pmetrics.llm_method_selection_total.labels(
- selected_method=method, selection_source=chosen_source
- ).inc()
- except Exception: # noqa: BLE001 - metrics must never break the request path
- pass
-
- if method == "hybridsearch":
- result_state = self.hybrid_search(state)
- elif method == "similaritysearch":
- result_state = self.similarity_search(state)
- elif method == "contextualsearch":
- result_state = self.sibling_search(state)
- elif method == "communitysearch":
- result_state = self.community_search(state)
- else:
- raise ValueError(f"Invalid supportai retriever: {method}")
-
- # Mirror the choice onto the context dict so it lands on
+ self._record_selection_metric(method, chosen_source)
+
+ # First retrieval attempt
+ result_state = self._dispatch_retriever(method, state)
+
+ # In-lane fallback (Feature 2) — chunk-based methods only, single retry,
+ # skipped for manual users so we don't second-guess their pick.
+ ctx = result_state.get("context") if isinstance(result_state.get("context"), dict) else {}
+ result = ctx.get("result") if isinstance(ctx.get("result"), dict) else {}
+ final_retrieval = result.get("final_retrieval") if isinstance(result, dict) else None
+ top_k = self._graphrag_cfg.get("top_k", 5)
+ can_inlane_fallback = (
+ chosen_source != "manual"
+ and method in CHUNK_BASED_METHODS
+ and not result_state.get("inlane_fallback_attempted")
+ and has_insufficient_context(final_retrieval, method, top_k)
+ )
+ if can_inlane_fallback:
+ fallback_method = INLANE_FALLBACK_TABLE.get(method)
+ if fallback_method:
+ label_old = self._METHOD_DISPLAY_NAMES.get(method, method)
+ label_new = self._METHOD_DISPLAY_NAMES.get(fallback_method, fallback_method)
+ self.emit_progress(
+ f"Insufficient context from {label_old} search, trying {label_new} search"
+ )
+ result_state["inlane_fallback_attempted"] = True
+ result_state["inlane_fallback_from"] = method
+ # Update the active method/source for the second pass.
+ method = fallback_method
+ chosen_source = "inlane_fallback"
+ chosen_reason = f"fallback from {label_old} (returned fewer than top_k chunks)"
+ self._record_selection_metric(method, chosen_source)
+ result_state = self._dispatch_retriever(method, result_state)
+
+ # Mirror the (final) choice onto the context dict so it lands on
# GraphRAGResponse.query_sources without further plumbing.
ctx = result_state.get("context") or {}
if isinstance(ctx, dict):
ctx["chosen_retriever"] = method
ctx["chosen_retriever_reason"] = chosen_reason
ctx["chosen_retriever_source"] = chosen_source
-
- # Phase 1.5 — out-of-corpus short-circuit (single-method partial).
- # If the chosen retriever returned no usable results, mark the
- # context so generate_answer skips the LLM call and returns an
- # honest "couldn't find relevant info" message instead of letting
- # the model hallucinate from empty/off-topic context.
+ if result_state.get("inlane_fallback_attempted"):
+ ctx["inlane_fallback_from"] = result_state.get("inlane_fallback_from")
+ if is_router_fallback:
+ ctx["router_fallback"] = True
+
+ # Out-of-corpus short-circuit — applies after all retrieval attempts.
+ # If the chosen retriever (or its fallback) returned nothing, mark
+ # the context so generate_answer returns an honest "couldn't find"
+ # message instead of hallucinating from empty context.
result = ctx.get("result") if isinstance(ctx.get("result"), dict) else {}
final_retrieval = result.get("final_retrieval") if isinstance(result, dict) else None
if not final_retrieval:
diff --git a/graphrag/app/agent/method_selector.py b/graphrag/app/agent/method_selector.py
index 4dc1296..15d46ad 100644
--- a/graphrag/app/agent/method_selector.py
+++ b/graphrag/app/agent/method_selector.py
@@ -44,12 +44,61 @@
METHOD_COMMUNITY = "communitysearch"
ALL_METHODS = (METHOD_SIMILARITY, METHOD_CONTEXTUAL, METHOD_HYBRID, METHOD_COMMUNITY)
+# Methods that retrieve raw chunks and respect a `top_k` cap on the chunk count.
+# Used by `has_insufficient_context` and the in-lane fallback trigger; community
+# is excluded because its top_k counts community summaries, not chunks.
+CHUNK_BASED_METHODS = frozenset({METHOD_SIMILARITY, METHOD_CONTEXTUAL, METHOD_HYBRID})
+
# Default fallback when the LLM stage can't produce a usable answer. Hybrid is the
# pre-existing system default and the safest superset retriever.
FALLBACK_METHOD = METHOD_HYBRID
+# In-lane fallback table: when a chunk-based method returns insufficient context,
+# try this method instead. Subset-aware — never falls back to a method whose
+# results are a strict subset of the failing method's seeds (e.g., similarity is
+# a subset of contextual/hybrid, so we don't fall back to it from those).
+#
+# The table fires once per question. Community is the terminal step from hybrid
+# because its retrieval surface (community summaries) is fundamentally different
+# from chunk retrieval — when chunk-based search finds little, thematic
+# summaries may still cover the question.
+INLANE_FALLBACK_TABLE = {
+ METHOD_SIMILARITY: METHOD_HYBRID, # point lookup → graph-hop expansion
+ METHOD_CONTEXTUAL: METHOD_HYBRID, # sibling expansion thin → try graph hops
+ METHOD_HYBRID: METHOD_COMMUNITY, # entity-driven thin → try thematic summaries
+ # No fallback FROM community — its top-k semantics differ; the in-lane
+ # trigger doesn't apply, and falling back to a chunk method when community
+ # missed is a different problem (handled by router_fallback / out-of-corpus).
+}
+
+
+def has_insufficient_context(retrieval_dict, method: str, top_k: int) -> bool:
+ """Decide whether a chunk-based retriever returned fewer items than asked.
+
+ Args:
+ retrieval_dict: the `final_retrieval` dict from the retriever output, or None.
+ method: canonical method string (one of ALL_METHODS).
+ top_k: the requested number of chunks for this retrieval.
+
+ Returns:
+ True if the result is "insufficient" — i.e., the method is chunk-based and
+ the retrieved count is strictly below `top_k`. Empty results count as
+ insufficient. Returns False for community search (different semantics) and
+ for any non-dict input.
+
+ Note: this is the trigger for the in-lane fallback in supportai_search.
+ Community search is excluded because its top_k caps community summaries, not
+ chunks, and a small number of returned summaries doesn't mean "no context."
+ """
+ if method not in CHUNK_BASED_METHODS:
+ return False
+ if not isinstance(retrieval_dict, dict):
+ return True # empty / malformed → insufficient
+ return len(retrieval_dict) < top_k
+
+
class RetrieverChoice(BaseModel):
"""Public selector result. `source` records how the choice was made — useful
for telemetry and for the upcoming top-K / diagnostician phases."""
diff --git a/graphrag/tests/test_method_selector.py b/graphrag/tests/test_method_selector.py
index fa19e95..6f6f975 100644
--- a/graphrag/tests/test_method_selector.py
+++ b/graphrag/tests/test_method_selector.py
@@ -37,10 +37,13 @@
METHOD_HYBRID = method_selector.METHOD_HYBRID
METHOD_SIMILARITY = method_selector.METHOD_SIMILARITY
FALLBACK_METHOD = method_selector.FALLBACK_METHOD
+CHUNK_BASED_METHODS = method_selector.CHUNK_BASED_METHODS
+INLANE_FALLBACK_TABLE = method_selector.INLANE_FALLBACK_TABLE
RetrieverChoice = method_selector.RetrieverChoice
RetrieverSelector = method_selector.RetrieverSelector
_LLMRetrieverChoice = method_selector._LLMRetrieverChoice
rules_choose = method_selector.rules_choose
+has_insufficient_context = method_selector.has_insufficient_context
# ---------- Stage A: rules_choose ----------
@@ -288,5 +291,97 @@ def test_fields(self):
self.assertEqual(c.source, "rules")
+# ---------- In-lane fallback table + has_insufficient_context ----------
+
+
+class TestChunkBasedMethods(unittest.TestCase):
+ """The CHUNK_BASED_METHODS set governs both the insufficient-context check
+ and the in-lane fallback trigger; community must be excluded."""
+
+ def test_chunk_methods_membership(self):
+ self.assertIn(METHOD_SIMILARITY, CHUNK_BASED_METHODS)
+ self.assertIn(METHOD_CONTEXTUAL, CHUNK_BASED_METHODS)
+ self.assertIn(METHOD_HYBRID, CHUNK_BASED_METHODS)
+
+ def test_community_excluded(self):
+ self.assertNotIn(METHOD_COMMUNITY, CHUNK_BASED_METHODS)
+
+
+class TestInlaneFallbackTable(unittest.TestCase):
+ """Subset-aware: a fallback method must NOT be a strict subset of the
+ method it's falling back from. Specifically, similarity is a subset of
+ contextual and hybrid, so neither can fall back to similarity."""
+
+ def test_similarity_falls_back_to_hybrid(self):
+ self.assertEqual(INLANE_FALLBACK_TABLE[METHOD_SIMILARITY], METHOD_HYBRID)
+
+ def test_contextual_does_not_fall_back_to_similarity(self):
+ self.assertNotEqual(INLANE_FALLBACK_TABLE[METHOD_CONTEXTUAL], METHOD_SIMILARITY)
+
+ def test_hybrid_does_not_fall_back_to_similarity(self):
+ self.assertNotEqual(INLANE_FALLBACK_TABLE[METHOD_HYBRID], METHOD_SIMILARITY)
+
+ def test_contextual_falls_back_to_hybrid(self):
+ # Different expansion shape (graph hops vs siblings); not a subset.
+ self.assertEqual(INLANE_FALLBACK_TABLE[METHOD_CONTEXTUAL], METHOD_HYBRID)
+
+ def test_hybrid_falls_back_to_community(self):
+ # Different retrieval surface (community summaries vs chunks).
+ self.assertEqual(INLANE_FALLBACK_TABLE[METHOD_HYBRID], METHOD_COMMUNITY)
+
+ def test_community_has_no_fallback(self):
+ # Community's top-k semantics differ; the in-lane trigger doesn't fire
+ # for it, so a fallback entry would be unused.
+ self.assertNotIn(METHOD_COMMUNITY, INLANE_FALLBACK_TABLE)
+
+ def test_no_self_fallback(self):
+ # A method should never fall back to itself.
+ for method, fallback in INLANE_FALLBACK_TABLE.items():
+ self.assertNotEqual(method, fallback, f"{method} falls back to itself")
+
+
+class TestHasInsufficientContext(unittest.TestCase):
+ """`has_insufficient_context` decides whether to trigger in-lane fallback.
+ Only chunk-based methods qualify; community is excluded."""
+
+ def test_empty_chunk_method_is_insufficient(self):
+ self.assertTrue(has_insufficient_context({}, METHOD_HYBRID, top_k=5))
+ self.assertTrue(has_insufficient_context({}, METHOD_SIMILARITY, top_k=5))
+ self.assertTrue(has_insufficient_context({}, METHOD_CONTEXTUAL, top_k=5))
+
+ def test_none_chunk_method_is_insufficient(self):
+ # Treats malformed/missing input as insufficient.
+ self.assertTrue(has_insufficient_context(None, METHOD_HYBRID, top_k=5))
+
+ def test_partial_below_top_k_is_insufficient(self):
+ partial = {f"chunk{i}": "text" for i in range(3)}
+ self.assertTrue(has_insufficient_context(partial, METHOD_HYBRID, top_k=5))
+
+ def test_full_top_k_is_sufficient(self):
+ full = {f"chunk{i}": "text" for i in range(5)}
+ self.assertFalse(has_insufficient_context(full, METHOD_HYBRID, top_k=5))
+
+ def test_above_top_k_is_sufficient(self):
+ above = {f"chunk{i}": "text" for i in range(7)}
+ self.assertFalse(has_insufficient_context(above, METHOD_HYBRID, top_k=5))
+
+ def test_community_always_returns_false(self):
+ """Community has different top_k semantics (community summaries, not
+ chunks). It should never trigger the insufficient-context path."""
+ self.assertFalse(has_insufficient_context({}, METHOD_COMMUNITY, top_k=5))
+ self.assertFalse(has_insufficient_context(None, METHOD_COMMUNITY, top_k=5))
+ partial = {f"comm{i}": "summary" for i in range(2)}
+ self.assertFalse(has_insufficient_context(partial, METHOD_COMMUNITY, top_k=5))
+
+ def test_unknown_method_returns_false(self):
+ # An unknown method is not chunk-based; conservative: don't trigger.
+ self.assertFalse(has_insufficient_context({}, "somethingelse", top_k=5))
+
+ def test_top_k_one_edge_case(self):
+ # With top_k=1, a single chunk is sufficient.
+ self.assertFalse(has_insufficient_context({"a": "x"}, METHOD_HYBRID, top_k=1))
+ self.assertTrue(has_insufficient_context({}, METHOD_HYBRID, top_k=1))
+
+
if __name__ == "__main__":
unittest.main()
From 7bebe253d177888027f4f3bb1bcacf62d80ae4d8 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 7 May 2026 16:37:11 -0700
Subject: [PATCH 51/70] fix(GML-2076): treat empty generate_function result as
a retry trigger
generate_function previously passed empty results straight to answer
generation, which risked hallucinated narratives. Mirrors the existing
generate_cypher contract: empty result is a generation failure that
triggers rewrite-retry, and after retries are exhausted the cross-lane
vector fallback takes over.
---
CHANGELOG.md | 3 +++
graphrag/app/agent/agent_graph.py | 23 ++++++++++++++++++++++-
2 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dabb51..b732455 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@
- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search.
- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path.
+### Changed
+- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness.
+
## [1.3.1]
### Changed
diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py
index 37abb73..8713f79 100644
--- a/graphrag/app/agent/agent_graph.py
+++ b/graphrag/app/agent/agent_graph.py
@@ -310,6 +310,13 @@ def map_question_to_schema(self, state):
def generate_function(self, state):
"""
Run the agent function generator.
+
+ Empty results are treated as a generation failure (symmetric with
+ `generate_cypher`) so the rewrite-and-retry loop can re-attempt with a
+ reformulated question. After 3 retries, `route_question` falls through
+ to the cross-lane vector-search fallback. Without this symmetry, an
+ empty function result would pass straight to `generate_answer` and
+ risk a hallucinated answer from empty context.
"""
self.emit_progress("Generating the code to answer your question")
try:
@@ -322,7 +329,21 @@ def generate_function(self, state):
state["schema_mapping"].target_edge_attributes,
)
logger.info(f"generate_function: {step}")
- state["context"] = step
+ result = step.get("result") if isinstance(step, dict) else None
+ if result is None or self.is_query_result_empty(result):
+ state["context"] = (
+ {**step, "error": True} if isinstance(step, dict) else {"error": True}
+ )
+ if "error_history" not in state or state["error_history"] is None:
+ state["error_history"] = []
+ state["error_history"].append(
+ {
+ "error_message": "Function returned empty result",
+ "error_step": "generate_function",
+ }
+ )
+ else:
+ state["context"] = step
except Exception as e:
state["context"] = {"error": True}
if "error_history" not in state or state["error_history"] is None:
From 659f04e2dfb38ec209a1d9a53a800363cb4647a9 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 7 May 2026 16:57:25 -0700
Subject: [PATCH 52/70] feat(GML-2076): add enable_router_fallback config
toggle
Cross-lane fallback is gated by graphrag_config.enable_router_fallback
(default true). Operators can opt out per-graph for deployments that
prefer the strict apology path over a vector-search recovery attempt.
---
CHANGELOG.md | 2 +-
graphrag/app/agent/agent_graph.py | 8 +++++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b732455..7f4a15a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@
- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels
- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search.
-- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path.
+- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path. Toggleable per-graph via `graphrag_config.enable_router_fallback` (default `true`).
### Changed
- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness.
diff --git a/graphrag/app/agent/agent_graph.py b/graphrag/app/agent/agent_graph.py
index 8713f79..921fa8e 100644
--- a/graphrag/app/agent/agent_graph.py
+++ b/graphrag/app/agent/agent_graph.py
@@ -167,7 +167,13 @@ def route_question(self, state):
source label and re-run auto-selection regardless of configured method.
"""
if state["question_retry_count"] > 2:
- if self.supportai_enabled and not state.get("router_fallback_attempted"):
+ # Cross-lane fallback can be disabled per-graph via the
+ # `enable_router_fallback` graphrag_config key (default True).
+ if (
+ self.supportai_enabled
+ and self._graphrag_cfg.get("enable_router_fallback", True)
+ and not state.get("router_fallback_attempted")
+ ):
state["router_fallback_attempted"] = True
self.emit_progress("Trying a different approach…")
return "supportai_lookup"
From 04dfe3d32369e6ce21b22cac423cb9ec9bf68b78 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 7 May 2026 17:11:23 -0700
Subject: [PATCH 53/70] feat(GML-2076): expose enable_router_fallback in
GraphRAG config UI
Surfaces the cross-lane fallback toggle as a checkbox on the GraphRAG
config admin page so operators can edit it without hand-editing JSON.
---
CHANGELOG.md | 2 +-
.../src/pages/setup/GraphRAGConfig.tsx | 22 +++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f4a15a..2c73e29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@
- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels
- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search.
-- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path. Toggleable per-graph via `graphrag_config.enable_router_fallback` (default `true`).
+- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path. Toggleable per-graph via `graphrag_config.enable_router_fallback` (default `true`); also editable from the GraphRAG config page in the admin UI.
### Changed
- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness.
diff --git a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
index dc33689..26b0d8a 100644
--- a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
+++ b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
@@ -27,6 +27,7 @@ const GraphRAGConfig = () => {
const [numSeenMin, setNumSeenMin] = useState("2");
const [communityLevel, setCommunityLevel] = useState("2");
const [docOnly, setDocOnly] = useState(false);
+ const [enableRouterFallback, setEnableRouterFallback] = useState(true);
// Advanced ingestion settings
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -72,6 +73,7 @@ const GraphRAGConfig = () => {
setNumSeenMin(String(graphragConfig.num_seen_min ?? 2));
setCommunityLevel(String(graphragConfig.community_level ?? 2));
setDocOnly(graphragConfig.doc_only ?? false);
+ setEnableRouterFallback(graphragConfig.enable_router_fallback ?? true);
setLoadBatchSize(String(graphragConfig.load_batch_size ?? 500));
setUpsertDelay(String(graphragConfig.upsert_delay ?? 0));
setMaxConcurrency(String(graphragConfig.default_concurrency ?? 10));
@@ -154,6 +156,7 @@ const GraphRAGConfig = () => {
num_seen_min: parseInt(numSeenMin),
community_level: parseInt(communityLevel),
doc_only: docOnly,
+ enable_router_fallback: enableRouterFallback,
load_batch_size: parseInt(loadBatchSize),
upsert_delay: parseInt(upsertDelay),
default_concurrency: parseInt(maxConcurrency),
@@ -170,6 +173,7 @@ const GraphRAGConfig = () => {
num_seen_min: 2,
community_level: 2,
doc_only: false,
+ enable_router_fallback: true,
load_batch_size: 500,
upsert_delay: 0,
default_concurrency: 10,
@@ -455,6 +459,24 @@ const GraphRAGConfig = () => {
Retrieve original documents instead of document chunks in results
+
+
+
+ setEnableRouterFallback(e.target.checked)}
+ />
+
+ Fallback to Vector Search on Structured-Data Failure
+
+
+
+ When function or cypher generation fails after 3 retries, automatically try vector search instead of giving up.
+
+
From cec41539a4a493688e27e659824641388d92a376 Mon Sep 17 00:00:00 2001
From: Prins Kumar
Date: Fri, 8 May 2026 15:16:39 +0530
Subject: [PATCH 54/70] fix(trace): per-user ownership check on
/ui/trace/{message_id}
---
graphrag/app/routers/ui.py | 43 +++++++++++++++++++++++++++++++-------
1 file changed, 35 insertions(+), 8 deletions(-)
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index fdc54f8..562d56e 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -87,11 +87,15 @@ def _cleanup_old_traces(max_age_days: int = 30):
logger.warning("Failed to clean up old trace logs", exc_info=True)
-def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float):
+def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp: GraphRAGResponse, elapsed: float, username: str):
try:
if not isinstance(message_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]+", message_id):
logger.warning("Refusing to save trace log: invalid message_id %r", message_id)
return
+ if not isinstance(username, str) or not username:
+ # Without an owner we cannot enforce per-user access on read, so refuse to save.
+ logger.warning("Refusing to save trace log for %r: missing username", message_id)
+ return
os.makedirs(TRACE_LOGS_DIR, exist_ok=True)
base_dir = os.path.abspath(TRACE_LOGS_DIR)
@@ -113,6 +117,7 @@ def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp
trace_data = {
"message_id": message_id,
"conversation_id": conversation_id,
+ "username": username,
"user_query": user_query,
"response_time": elapsed,
"response_type": resp.response_type,
@@ -400,9 +405,11 @@ def get_trace_log(
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
):
# Trace logs contain user queries (potentially PII), full LLM responses,
- # internal cypher, schema mappings, and per-call cost. Any authenticated
- # user could otherwise read another user's trace by guessing or learning
- # the message_id. Restrict to superusers to prevent cross-user disclosure.
+ # internal cypher, schema mappings, and per-call cost.
+ # Two layers of access control:
+ # 1. Role: must be a superuser.
+ # 2. Ownership: must be the user who originated the trace.
+ # This prevents cross-user disclosure even between superusers.
_require_roles(creds[1], {"superuser"})
if not re.fullmatch(r"[A-Za-z0-9_-]+", message_id):
@@ -413,8 +420,26 @@ def get_trace_log(
raise HTTPException(status_code=400, detail="Invalid message_id")
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Trace log not found")
- with open(filepath, "r") as f:
- return json.load(f)
+
+ try:
+ with open(filepath, "r") as f:
+ data = json.load(f)
+ except (OSError, json.JSONDecodeError):
+ logger.warning("Failed to read trace log %r", message_id, exc_info=True)
+ raise HTTPException(status_code=404, detail="Trace log not found")
+
+ # Per-user segregation. Legacy files (saved before this fix) have no
+ # "username" field and therefore can't pass this check — they will 404
+ # for everyone and age out via the existing 30-day cleanup.
+ owner = data.get("username")
+ if owner != creds[1].username:
+ logger.warning(
+ "User %r attempted to read trace owned by %r (message_id=%s)",
+ creds[1].username, owner, message_id,
+ )
+ raise HTTPException(status_code=404, detail="Trace log not found")
+
+ return data
@@ -1154,7 +1179,7 @@ async def graph_query(
query_sources=resp.query_sources,
)
await write_message_to_history(message, auth)
- await asyncio.to_thread(_save_trace_log, message.message_id, convo_id, data, resp, elapsed)
+ await asyncio.to_thread(_save_trace_log, message.message_id, convo_id, data, resp, elapsed, creds.username)
prev_id = message.message_id
# reply
@@ -1195,6 +1220,8 @@ async def chat(
usr_auth = await asyncio.wait_for(websocket.receive_text(), timeout=10.0)
logger.info(f"Received authentication data, length: {len(usr_auth)}")
_, conn = ws_basic_auth(usr_auth, graphname)
+ # Extract the authenticated username for trace-log ownership tracking.
+ ws_username = base64.b64decode(usr_auth.encode()).decode().split(":", 1)[0]
logger.info("Authentication successful")
except asyncio.TimeoutError:
logger.error("WebSocket authentication timeout - no credentials received")
@@ -1281,7 +1308,7 @@ async def chat(
query_sources=resp.query_sources,
)
await write_message_to_history(message, usr_auth)
- await asyncio.to_thread(_save_trace_log, message.message_id, convo_id, data, resp, elapsed)
+ await asyncio.to_thread(_save_trace_log, message.message_id, convo_id, data, resp, elapsed, ws_username)
prev_id = message.message_id
# reply
From 819072b6c178ffe5ce810880f772b1ba5b5bcd00 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Fri, 8 May 2026 17:08:40 -0700
Subject: [PATCH 55/70] Address review feedback on schema-aware ingest and
config reload
- apply_proposal validates graph and job identifiers, cleans up the
schema-change job on every failure path, and returns a uniform
result shape on the error branch.
- Permissive GSQL parser early-rejects edge pairs whose endpoints
are structural or reserved.
- reload_llm_config and reload_db_config reset the embedding store so
callers don't keep stale credentials or model bindings after a
config reload.
- ECC worker resolves endpoint-pair lookups via the canonical-resolved
edge name.
- convert_sample_files rejects duplicate-basename uploads;
extract_schema_from_jsonl fails fast when any requested file's JSONL
is missing.
- E2E schema-aware test sources the TG host from TG_HOST or
SERVER_CONFIG.db_config.hostname; no baked-in default.
- Similarity retrievers' Python param order matches their GSQL
signatures.
---
common/config.py | 24 +++++++
common/db/schema_utils.py | 42 ++++++++++--
ecc/app/graphrag/workers.py | 7 +-
graphrag/app/routers/ui.py | 20 ++++++
.../retrievers/CommunityRetriever.py | 2 +-
.../retrievers/SimilarityRetriever.py | 4 +-
.../tests/test_e2e_schema_aware_ingest.py | 40 ++++++++----
graphrag/tests/test_schema_utils.py | 65 +++++++++++++++++++
8 files changed, 182 insertions(+), 22 deletions(-)
diff --git a/common/config.py b/common/config.py
index e7f8c69..0fe3b2c 100644
--- a/common/config.py
+++ b/common/config.py
@@ -570,6 +570,24 @@ def get_embedding_store(timeout: float = 0):
return embedding_store
+def reset_embedding_store() -> None:
+ """Drop the in-memory store and re-run ``_init_embedding_store`` so
+ a config reload picks up the new ``embedding_service`` and
+ ``db_config``. Callers should swap the inputs before calling.
+ No-op when ``INIT_EMBED_STORE`` is disabled (e.g. ECC).
+ """
+ global embedding_store
+ if os.getenv("INIT_EMBED_STORE", "true") != "true":
+ return
+ embedding_store = None
+ _embedding_store_ready.clear()
+ service_status["embedding_store"] = {
+ "status": "initializing",
+ "error": "Embedding store is still initializing",
+ }
+ threading.Thread(target=_init_embedding_store, daemon=True).start()
+
+
if os.getenv("INIT_EMBED_STORE", "true") == "true":
threading.Thread(target=_init_embedding_store, daemon=True).start()
@@ -682,6 +700,9 @@ def reload_llm_config(new_llm_config: dict = None):
else:
raise Exception("Embedding service not implemented")
+ # Re-init so the store binds to the freshly-built embedding_service.
+ reset_embedding_store()
+
return {
"status": "success",
"message": "LLM configuration reloaded successfully"
@@ -732,6 +753,9 @@ def reload_db_config(new_db_config: dict = None):
del db_config[k]
db_config.update(new_db_config)
+ # Re-init so the store binds to the freshly-updated db_config.
+ reset_embedding_store()
+
return {
"status": "success",
"message": "DB configuration reloaded successfully"
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index 39d4272..affbe34 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -94,6 +94,12 @@
})
+# TigerGraph identifier pattern (graphs, jobs, vertex/edge types). Must
+# match the route-level ``ValidGraphName`` regex so direct callers of
+# the helpers below get the same protection the API layer enforces.
+_GSQL_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+
+
_GSQL_RESERVED_CACHE: Optional[frozenset] = None
@@ -675,9 +681,13 @@ def parse_gsql_schema(text: str) -> SchemaProposal:
for pm in _EDGE_PAIR_RE.finditer(body):
from_vt = pm.group("from")
to_vt = pm.group("to")
- if is_structural_type(from_vt) and is_structural_type(to_vt):
- # Both endpoints are structural — definitely not a domain
- # edge pair the user is trying to add. Drop it.
+ if is_structural_type(from_vt) or is_structural_type(to_vt):
+ # Either endpoint is a structural type, a reverse_*
+ # auto-generated companion, or a GSQL reserved word —
+ # the pair would be invalid as a user-declared domain
+ # edge. ``drop_dangling_pairs`` would catch it later
+ # anyway; rejecting here keeps the proposal free of
+ # transient invalid state.
continue
proposal.add_edge_pair(
name=name,
@@ -996,8 +1006,12 @@ def build_schema_change_job(
"""
if not statements:
raise ValueError("build_schema_change_job: statements is empty")
+ if not _GSQL_IDENT_RE.fullmatch(graphname):
+ raise ValueError(f"Invalid graph name: {graphname!r}")
if job_name is None:
job_name = f"add_domain_schema_{uuid.uuid4().hex[:8]}"
+ elif not _GSQL_IDENT_RE.fullmatch(job_name):
+ raise ValueError(f"Invalid job name: {job_name!r}")
body = ";\n ".join(s.rstrip(";") for s in statements) + ";"
block = (
@@ -1232,14 +1246,31 @@ def apply_proposal(
}
block, job_name = build_schema_change_job(graphname, statements, job_name)
- output = conn.gsql(block)
+ try:
+ output = conn.gsql(block)
+ except Exception:
+ # If gsql() raised mid-block, the trailing DROP JOB may not have
+ # executed. Best-effort cleanup so leaked jobs don't accumulate.
+ try:
+ conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {job_name}")
+ except Exception:
+ pass
+ raise
err = gsql_output_error(output)
if err:
+ # Server-reported failure (no exception). RUN may have aborted
+ # before DROP — try to clean up; ignore the "not found" path.
+ try:
+ conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {job_name}")
+ except Exception:
+ pass
# pyTigerGraph's gsql() returned a failure response without
# raising — surface it explicitly so the caller doesn't
# falsely report "applied". Skip metadata upsert (the schema
# change didn't land, so writing EntityType vertices for
- # types that don't exist would also fail).
+ # types that don't exist would also fail). Include a
+ # ``retrievers`` placeholder so the result shape is uniform
+ # with the no-op / applied paths.
return {
"status": "error",
"statements": statements,
@@ -1248,6 +1279,7 @@ def apply_proposal(
"error": err,
"summary": summary,
"metadata": {"entity_types": [], "relationship_types": []},
+ "retrievers": {"status": "skipped", "reason": "schema apply failed"},
}
metadata = upsert_type_metadata(conn, proposal)
retrievers = _install_retrievers_after_apply(
diff --git a/ecc/app/graphrag/workers.py b/ecc/app/graphrag/workers.py
index bc5819f..cd099ea 100644
--- a/ecc/app/graphrag/workers.py
+++ b/ecc/app/graphrag/workers.py
@@ -482,10 +482,15 @@ async def extract(
rel_type_lower = (edge.type or "").casefold()
canonical_rel = domain_edge_canonical.get(rel_type_lower)
+ # Use the canonical-resolved name as the key for the
+ # endpoint-pair lookup so the check stays correct even
+ # if ``domain_edge_canonical`` later admits alias →
+ # canonical mappings.
+ canonical_rel_key = canonical_rel.casefold() if canonical_rel else ""
valid_pair = (
canonical_rel is not None
and (src_type.casefold(), tgt_type.casefold())
- in edge_endpoint_pairs.get(rel_type_lower, set())
+ in edge_endpoint_pairs.get(canonical_rel_key, set())
)
# Strict mode: only write the typed pattern. Legacy
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 7690e6c..df956f8 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -640,6 +640,14 @@ async def convert_sample_files(
),
)
safe_name = os.path.basename(f.filename or "sample")
+ if safe_name in saved_basenames:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"Duplicate filename '{safe_name}' in upload set. "
+ "Rename one of the files and try again."
+ ),
+ )
target = os.path.join(upload_dir, safe_name)
with open(target, "wb") as out:
out.write(data)
@@ -704,11 +712,23 @@ def extract_schema_from_jsonl(
if requested:
jsonl_paths = []
+ missing_jsonls = []
for name in requested:
stem = os.path.splitext(os.path.basename(name))[0]
p = os.path.join(temp_folder, f"{stem}.jsonl")
if os.path.exists(p):
jsonl_paths.append(p)
+ else:
+ missing_jsonls.append(name)
+ if missing_jsonls:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ "Converted JSONL not found for: "
+ + ", ".join(missing_jsonls)
+ + ". Run convert_sample_files first for those files."
+ ),
+ )
else:
jsonl_paths = [
os.path.join(temp_folder, fn)
diff --git a/graphrag/app/supportai/retrievers/CommunityRetriever.py b/graphrag/app/supportai/retrievers/CommunityRetriever.py
index 2724312..466cf4d 100644
--- a/graphrag/app/supportai/retrievers/CommunityRetriever.py
+++ b/graphrag/app/supportai/retrievers/CommunityRetriever.py
@@ -49,8 +49,8 @@ def search(self, question, community_level: int, top_k: int = 5, similarity_thre
resp = self.conn.runInstalledQuery(
"Content_Similarity_Search",
params = {
- "v_type": "DocumentChunk",
"json_list_vts": str(start_set),
+ "v_type": "DocumentChunk",
"verbose": verbose,
},
usePost=True
diff --git a/graphrag/app/supportai/retrievers/SimilarityRetriever.py b/graphrag/app/supportai/retrievers/SimilarityRetriever.py
index 24fa62e..e5f948c 100644
--- a/graphrag/app/supportai/retrievers/SimilarityRetriever.py
+++ b/graphrag/app/supportai/retrievers/SimilarityRetriever.py
@@ -25,8 +25,8 @@ def search(self, question, index, top_k=1, withHyDE=False, expand=False, verbose
res = self.conn.runInstalledQuery(
"Content_Similarity_Search",
params = {
- "v_type": index,
"json_list_vts": str(start_set),
+ "v_type": index,
"verbose": verbose,
},
usePost=True
@@ -41,8 +41,8 @@ def search(self, question, index, top_k=1, withHyDE=False, expand=False, verbose
res = self.conn.runInstalledQuery(
"Content_Similarity_Vector_Search",
params = {
- "query_vector": query_vector,
"v_type": index,
+ "query_vector": query_vector,
"top_k": top_k,
"verbose": verbose,
},
diff --git a/graphrag/tests/test_e2e_schema_aware_ingest.py b/graphrag/tests/test_e2e_schema_aware_ingest.py
index ebf31c5..7e76896 100644
--- a/graphrag/tests/test_e2e_schema_aware_ingest.py
+++ b/graphrag/tests/test_e2e_schema_aware_ingest.py
@@ -38,8 +38,12 @@
Environment variables:
GRAPHRAG_URL Base URL of running GraphRAG service (required to run)
- DB_CONFIG Path to db_config.json (default: ./configs/db_config.json)
- TG_USERNAME / TG_PASSWORD Fallbacks if DB_CONFIG is missing
+ TG_HOST TigerGraph host URL (e.g. http://host:14240). Required
+ unless ``db_config.hostname`` is set in SERVER_CONFIG.
+ SERVER_CONFIG Path to server_config.json (default:
+ ./configs/server_config.json). Read for the
+ ``db_config`` block (hostname / username / password).
+ TG_USERNAME / TG_PASSWORD Fallbacks if SERVER_CONFIG is missing or partial.
TEST_GRAPH Graph name (default: SchemaAwareE2E_)
TEST_FILES Comma-separated file paths (default: BarclaysDocs PDFs)
REBUILD_TIMEOUT Max seconds to wait for rebuild (default: 7200)
@@ -64,15 +68,27 @@
GRAPHRAG_URL = os.getenv("GRAPHRAG_URL", "http://localhost:80")
-_db_config_path = os.getenv("DB_CONFIG", "./configs/db_config.json")
+_server_config_path = os.getenv("SERVER_CONFIG", "./configs/server_config.json")
+_db: dict = {}
try:
- with open(_db_config_path) as _f:
- _db = json.load(_f)
- USERNAME = _db.get("username", "tigergraph")
- PASSWORD = _db.get("password", "tigergraph")
+ with open(_server_config_path) as _f:
+ _db = (json.load(_f) or {}).get("db_config") or {}
except Exception:
- USERNAME = os.getenv("TG_USERNAME", "tigergraph")
- PASSWORD = os.getenv("TG_PASSWORD", "tigergraph")
+ _db = {}
+
+USERNAME = _db.get("username") or os.getenv("TG_USERNAME", "tigergraph")
+PASSWORD = _db.get("password") or os.getenv("TG_PASSWORD", "tigergraph")
+# Resolution order: TG_HOST env override → ``db_config.hostname`` in
+# server_config.json → fail fast. No baked-in default — local
+# environments differ, and a wrong fallback can silently point the
+# test at the wrong cluster.
+TG_HOST = os.getenv("TG_HOST") or _db.get("hostname")
+if not TG_HOST:
+ raise RuntimeError(
+ "TG_HOST is not set. Export it in the shell or set "
+ f"'db_config.hostname' in {_server_config_path} before running "
+ "the e2e test."
+ )
REBUILD_TIMEOUT = int(os.getenv("REBUILD_TIMEOUT", "7200"))
SCHEMA_EXTRACT_TIMEOUT = int(os.getenv("SCHEMA_EXTRACT_TIMEOUT", "300"))
@@ -265,9 +281,8 @@ def test_05_validate_live_schema():
from pyTigerGraph import TigerGraphConnection
- tg_host = os.getenv("TG_HOST", "http://192.168.11.11")
conn = TigerGraphConnection(
- host=tg_host,
+ host=TG_HOST,
graphname=GRAPH_NAME,
username=USERNAME,
password=PASSWORD,
@@ -433,9 +448,8 @@ def test_09_validate_final_graph():
from pyTigerGraph import TigerGraphConnection
- tg_host = os.getenv("TG_HOST", "http://192.168.11.11")
conn = TigerGraphConnection(
- host=tg_host,
+ host=TG_HOST,
graphname=GRAPH_NAME,
username=USERNAME,
password=PASSWORD,
diff --git a/graphrag/tests/test_schema_utils.py b/graphrag/tests/test_schema_utils.py
index a8eca91..99fe3d8 100644
--- a/graphrag/tests/test_schema_utils.py
+++ b/graphrag/tests/test_schema_utils.py
@@ -629,6 +629,40 @@ def test_build_schema_change_job_empty_statements_raises():
build_schema_change_job("g", [])
+@pytest.mark.parametrize(
+ "bad_name",
+ [
+ "g raph", # whitespace
+ 'g"x', # quote — closes a STRING literal
+ "g; DROP GRAPH x", # statement separator + injection attempt
+ "g\n", # newline
+ "1graph", # leading digit
+ "", # empty
+ ],
+)
+def test_build_schema_change_job_rejects_invalid_graphname(bad_name):
+ with pytest.raises(ValueError, match="Invalid graph name"):
+ build_schema_change_job(bad_name, ["ADD VERTEX X(PRIMARY_ID id STRING)"])
+
+
+@pytest.mark.parametrize(
+ "bad_job",
+ [
+ "job name",
+ 'job"x',
+ "job; DROP JOB other",
+ "1job",
+ ],
+)
+def test_build_schema_change_job_rejects_invalid_job_name(bad_job):
+ with pytest.raises(ValueError, match="Invalid job name"):
+ build_schema_change_job(
+ "g",
+ ["ADD VERTEX X(PRIMARY_ID id STRING)"],
+ job_name=bad_job,
+ )
+
+
# ---------------------------------------------------------------------------
# apply_proposal
# ---------------------------------------------------------------------------
@@ -845,6 +879,37 @@ def test_apply_proposal_no_new_domain_vts_skips_transitional_check():
assert "transitional" not in retrievers
+def test_apply_proposal_error_path_returns_uniform_shape():
+ """When ``conn.gsql`` reports a server-side failure, the result
+ payload must carry the same keys as the success / no-op paths so
+ callers can read ``status / statements / retrievers / metadata``
+ uniformly without a per-status branch.
+ """
+ conn = _FakeConn(
+ vertex_types=[
+ "Document", "DocumentChunk", "Entity",
+ "EntityType", "RelationshipType", "Community",
+ ],
+ edge_metadata={},
+ gsql_response="SEMANTIC ERROR: simulated failure",
+ )
+ proposal = SchemaProposal()
+ proposal.add_vertex("Company")
+
+ result = apply_proposal(conn, "g", proposal)
+
+ assert result["status"] == "error"
+ # Full uniform shape — same key set as success / no-op.
+ assert set(result.keys()) == {
+ "status", "statements", "job_name", "gsql_output",
+ "error", "summary", "metadata", "retrievers",
+ }
+ assert result["retrievers"] == {
+ "status": "skipped",
+ "reason": "schema apply failed",
+ }
+
+
# ---------------------------------------------------------------------------
# upsert_type_metadata
# ---------------------------------------------------------------------------
From c071ad79dc68742629dc826c46d8ef5895c3ea8e Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Fri, 8 May 2026 18:36:26 -0700
Subject: [PATCH 56/70] Prepare 1.4.0: CHANGELOG backfill, per-graph embedding
store, pnpm pin
- Backfill the 1.4.0 CHANGELOG with Trace Logs, Excel / CSV ingestion,
the default chat method flip (hybridsearch -> auto), and a
Configuration section listing enable_router_fallback and the pnpm
pin.
- Cache one embedding store per graphname so concurrent chat across
different graphs doesn't race over a shared TG connection.
reload_llm_config and reload_db_config reset the cache.
- BaseRetriever resolves the per-graph store via get_embedding_store
instead of mutating a shared singleton.
- Pin graphrag-ui to pnpm 9 via packageManager and an .npmrc
allow-list so image builds don't trip pnpm 10's strict approval
policy.
- Drop the trace_logs host volume from docker-compose; traces live
for the container's lifetime.
---
CHANGELOG.md | 9 ++
common/config.py | 115 ++++++++++++------
docker-compose.yml | 1 -
.../app/supportai/retrievers/BaseRetriever.py | 12 +-
4 files changed, 98 insertions(+), 39 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43f7d48..cdcf311 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,13 @@
- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context
- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search.
- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path. Toggleable per-graph via `graphrag_config.enable_router_fallback` (default `true`); also editable from the GraphRAG config page in the admin UI.
+- **Trace Logs UI** — new admin page that captures and displays the full agent execution trace for each chat turn (per-node inputs/outputs, durations in seconds, citations, token usage by node)
+ - Citations tab (now shown first), Token Overview tab, and a per-node detail view
+ - Role-gated "View Trace" entry from the chat reply; superuser-only access on the trace endpoint
+ - Per-user ownership check on `/ui/trace/{message_id}` and 30-day automatic cleanup of stored traces
+ - Routed through nginx at `/trace`
+- **Excel and CSV ingestion** — `.xlsx` / `.xls` / `.csv` accepted in document ingestion; the upload UI shows a clear warning when an unsupported file type is selected
+ - Headerless Excel sheets preserve all rows; CSV extraction handles non-UTF-8 encodings without dropping content
### Changed
- **All customizable prompts now ship as in-code defaults**, packaged inside the LLM service. Provider prompt directories are kept (empty) for backward compatibility; per-graph and global overrides still win when present.
@@ -35,6 +42,7 @@
- **`apply_proposal` re-installs retriever queries** against the live domain schema, idempotently. Identical bodies are TG no-ops; new domain types or a changed `retrieval_include_entity` value re-render the affected queries on the next apply call.
- **Transitional-graph detection at schema apply**: when a domain schema is added to a graph that already has Entity-layer data (typical v1.3.x → v1.4.0 upgrade applying a schema for the first time), `apply_proposal` forces `retrieval_include_entity=True` for the rendered queries so existing Entity rows stay reachable. The result payload carries a `transitional` block (`entity_count`, `new_domain_vts`, `recommendation`) for the init-graph dialog to surface a "your existing entities won't be auto-typed — re-ingest for full schema awareness" prompt. Once the user clears derived data and re-ingests (planned v1.5 admin endpoint), the auto-default flips back to typed-purist on the next apply call.
- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness.
+- **Default chat retrieval method is now `auto`** instead of `hybridsearch`. Existing graphs that did not configure a method explicitly will route through the new selector after upgrade. Manual mode (and any explicitly-selected method in the chat dropdown) overrides the default unchanged.
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
@@ -50,6 +58,7 @@
### Configuration
- New `graphrag_config` keys: `schema_max_sample_files` (default 5), `schema_max_total_mb` (default 50), `strict_mode` (default false), `retrieval_include_entity` (auto: false when domain schema present, true otherwise), `enable_router_fallback` (default true).
- New env var: `PDF_IMAGE_CONCURRENCY` (default 8).
+- `graphrag-ui` build now pins pnpm via `packageManager: "pnpm@9.15.0"` and ships an `.npmrc` allow-list for `@swc/core` / `esbuild` so the Docker image build does not trip pnpm 10's strict `[ERR_PNPM_IGNORED_BUILDS]` policy.
> Implementation-level details for v1.4.0 (parser internals, endpoint contracts, dialog state machine, prompt-resolution chain, schema-aware ECC worker logic, etc.) live in `dev/plans/graphrag/v1.4.0_implementation_notes.md`.
diff --git a/common/config.py b/common/config.py
index 0fe3b2c..dd59ad1 100644
--- a/common/config.py
+++ b/common/config.py
@@ -505,43 +505,64 @@ def get_llm_service(service_config: dict) -> LLM_Model:
raise Exception(f"LLM service '{service_name}' not supported")
-# Module-level ``embedding_store`` is kept for back-compat with direct
-# importers (`from common.config import embedding_store`); it's
-# populated by the background-init thread below. New callers should
-# prefer the ``get_embedding_store(timeout)`` getter so they fail
-# fast (or wait briefly) instead of seeing a ``None`` mid-startup.
+# Module-level ``embedding_store`` is the back-compat default for
+# direct importers (``from common.config import embedding_store``).
+# It's populated by the background init thread below.
+#
+# ``_embedding_stores`` is the per-graph cache used by chatbot
+# retrievers via ``get_embedding_store(graphname=...)``. Each entry
+# has its own ``TigerGraphConnection`` bound to that graphname for
+# its lifetime — no in-place ``set_graphname`` mutation — so
+# concurrent chat across different graphs can't race over a shared
+# connection.
embedding_store = None
_embedding_store_ready = threading.Event()
+_embedding_stores: dict = {}
+_embedding_stores_lock = threading.Lock()
service_status["embedding_store"] = {
"status": "initializing",
"error": "Embedding store is still initializing",
}
+def _build_embedding_store(graphname: str = "") -> TigerGraphEmbeddingStore:
+ """Construct a fresh ``TigerGraphEmbeddingStore`` bound to *graphname*.
+
+ Uses the live globals (``db_config`` for the connection and
+ ``embedding_service`` for the model) so the result reflects the
+ current config.
+ """
+ conn = TigerGraphConnection(
+ host=db_config.get("hostname", "http://tigergraph"),
+ username=db_config.get("username", "tigergraph"),
+ password=db_config.get("password", "tigergraph"),
+ gsPort=db_config.get("gsPort", "14240"),
+ restppPort=db_config.get("restppPort", "9000"),
+ graphname=graphname or db_config.get("graphname", ""),
+ apiToken=db_config.get("apiToken", ""),
+ )
+ if not db_config.get("apiToken") and db_config.get("getToken"):
+ conn.getToken()
+
+ store = TigerGraphEmbeddingStore(
+ conn,
+ embedding_service,
+ support_ai_instance=True,
+ )
+ if graphname:
+ # Runs the GDS check and per-graph vector-query install.
+ store.set_graphname(graphname)
+ return store
+
+
def _init_embedding_store():
- """Background thread target. Builds the embedding store without
- blocking module import — TigerGraph may be slow on first connect,
- and we don't want app startup to wait on it.
+ """Background thread target. Builds the default embedding store
+ without blocking module import — TigerGraph may be slow on first
+ connect, and we don't want app startup to wait on it.
"""
global embedding_store
try:
- conn = TigerGraphConnection(
- host=db_config.get("hostname", "http://tigergraph"),
- username=db_config.get("username", "tigergraph"),
- password=db_config.get("password", "tigergraph"),
- gsPort=db_config.get("gsPort", "14240"),
- restppPort=db_config.get("restppPort", "9000"),
- graphname=db_config.get("graphname", ""),
- apiToken=db_config.get("apiToken", ""),
- )
- if not db_config.get("apiToken") and db_config.get("getToken"):
- conn.getToken()
-
- embedding_store = TigerGraphEmbeddingStore(
- conn,
- embedding_service,
- support_ai_instance=True,
- )
+ embedding_store = _build_embedding_store()
service_status["embedding_store"] = {"status": "ok", "error": None}
except Exception as e:
service_status["embedding_store"] = {"status": "error", "error": str(e)}
@@ -550,16 +571,35 @@ def _init_embedding_store():
_embedding_store_ready.set()
-def get_embedding_store(timeout: float = 0):
- """Return the embedding store if ready.
+def get_embedding_store(graphname: str | None = None, timeout: float = 0):
+ """Return an embedding store.
Args:
- timeout: Seconds to wait for initialization. Default 0
- (non-blocking — raises immediately if still initializing).
+ graphname: When supplied, returns a per-graph instance built
+ and cached on first request (each cache entry has its own
+ connection bound to *graphname* for its lifetime).
+ timeout: Seconds to wait for the default-store init when
+ *graphname* is not supplied. Default 0 (non-blocking —
+ raises immediately if still initializing).
Raises:
RuntimeError: if not yet ready, timed out, or initialization failed.
"""
+ if graphname:
+ with _embedding_stores_lock:
+ cached = _embedding_stores.get(graphname)
+ if cached is not None:
+ return cached
+ # Build outside the lock so first-time setup for one graph
+ # doesn't serialize first-time setup for another.
+ store = _build_embedding_store(graphname)
+ with _embedding_stores_lock:
+ existing = _embedding_stores.get(graphname)
+ if existing is not None:
+ return existing # racing thread won
+ _embedding_stores[graphname] = store
+ return store
+
if not _embedding_store_ready.wait(timeout=timeout):
raise RuntimeError(
"Embedding store is still initializing. Please try again shortly."
@@ -571,14 +611,17 @@ def get_embedding_store(timeout: float = 0):
def reset_embedding_store() -> None:
- """Drop the in-memory store and re-run ``_init_embedding_store`` so
- a config reload picks up the new ``embedding_service`` and
- ``db_config``. Callers should swap the inputs before calling.
- No-op when ``INIT_EMBED_STORE`` is disabled (e.g. ECC).
+ """Drop the per-graph cache and the default store, then re-run the
+ background init so a config reload picks up the new
+ ``embedding_service`` and ``db_config``. Callers should swap the
+ inputs before calling. No-op when ``INIT_EMBED_STORE`` is disabled
+ (e.g. ECC).
"""
global embedding_store
if os.getenv("INIT_EMBED_STORE", "true") != "true":
return
+ with _embedding_stores_lock:
+ _embedding_stores.clear()
embedding_store = None
_embedding_store_ready.clear()
service_status["embedding_store"] = {
@@ -700,7 +743,8 @@ def reload_llm_config(new_llm_config: dict = None):
else:
raise Exception("Embedding service not implemented")
- # Re-init so the store binds to the freshly-built embedding_service.
+ # Clear per-graph cache + rebuild the default so callers don't
+ # keep references to the old embedding service.
reset_embedding_store()
return {
@@ -753,7 +797,8 @@ def reload_db_config(new_db_config: dict = None):
del db_config[k]
db_config.update(new_db_config)
- # Re-init so the store binds to the freshly-updated db_config.
+ # Clear per-graph cache + rebuild the default so callers don't
+ # keep connections bound to the old credentials.
reset_embedding_store()
return {
diff --git a/docker-compose.yml b/docker-compose.yml
index 449939b..97a0952 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,7 +18,6 @@ services:
USE_CYPHER: "true"
volumes:
- ./configs/:/code/configs
- - ./trace_logs/:/code/trace_logs
graphrag-ecc:
image: tigergraph/graphrag-ecc:latest
diff --git a/graphrag/app/supportai/retrievers/BaseRetriever.py b/graphrag/app/supportai/retrievers/BaseRetriever.py
index 271e34c..3c3d107 100644
--- a/graphrag/app/supportai/retrievers/BaseRetriever.py
+++ b/graphrag/app/supportai/retrievers/BaseRetriever.py
@@ -4,7 +4,7 @@
from common.llm_services.base_llm import LLM_Model
from common.py_schemas import CandidateScore, CandidateGenerator, GraphRAGAnswerOutput, CommunityAnswer
from common.utils.token_calculator import get_token_calculator
-from common.config import get_chat_config, get_graphrag_config
+from common.config import get_chat_config, get_embedding_store, get_graphrag_config
from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
@@ -24,8 +24,14 @@ def __init__(
self.emb_service = embedding_service
self.llm_service = llm_service
self.conn = connection
- self.embedding_store = embedding_store
- self.embedding_store.set_graphname(connection.graphname)
+ # Resolve the per-graph store; the ``embedding_store`` arg is
+ # accepted for back-compat but ignored when a graphname is
+ # available so concurrent chat across different graphs can't
+ # race over a shared connection.
+ if connection and getattr(connection, "graphname", None):
+ self.embedding_store = get_embedding_store(connection.graphname)
+ else:
+ self.embedding_store = embedding_store
self.logger = logging.getLogger(__name__)
# Use llm_service's own config when available (chatbot path);
# fall back to get_chat_config() (direct supportai API path).
From 92507bea7c7d9d77f3762b8bfa61ad6a70054b2e Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Fri, 8 May 2026 23:21:23 -0700
Subject: [PATCH 57/70] Harden schema apply against TG validator and
reserved-word collisions
- Drop attribute names that collide with GSQL reserved words at parse
time so LLM-extracted schemas no longer crash schema-change with
"Encountered ',' ..." on names like ``count`` / ``min`` / ``max``.
- Run apply_proposal as two phases (ADD VERTEX/EDGE, then ALTER EDGE
ADD PAIR) so TG's upfront job validator never sees an ALTER that
references a vertex type created in the same job. Result payload
exposes both phase names via a new ``job_names`` list; ``job_name``
remains the first phase that ran.
---
CHANGELOG.md | 2 +
common/db/schema_utils.py | 96 ++++++++++++++++++-----------
graphrag/tests/test_schema_utils.py | 41 +++++++-----
3 files changed, 87 insertions(+), 52 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdcf311..fe27d69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,8 @@
- **Transitional-graph detection at schema apply**: when a domain schema is added to a graph that already has Entity-layer data (typical v1.3.x → v1.4.0 upgrade applying a schema for the first time), `apply_proposal` forces `retrieval_include_entity=True` for the rendered queries so existing Entity rows stay reachable. The result payload carries a `transitional` block (`entity_count`, `new_domain_vts`, `recommendation`) for the init-graph dialog to surface a "your existing entities won't be auto-typed — re-ingest for full schema awareness" prompt. Once the user clears derived data and re-ingests (planned v1.5 admin endpoint), the auto-default flips back to typed-purist on the next apply call.
- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness.
- **Default chat retrieval method is now `auto`** instead of `hybridsearch`. Existing graphs that did not configure a method explicitly will route through the new selector after upgrade. Manual mode (and any explicitly-selected method in the chat dropdown) overrides the default unchanged.
+- **Schema parser drops attribute names that collide with GSQL reserved words** (e.g. `count`, `min`, `max`). LLM-extracted schemas previously failed schema-change with `Encountered "," at line N` when an attribute named after a keyword reached TG; the offending attribute is now silently skipped at parse time.
+- **`apply_proposal` runs schema changes in two phases** — phase 1 issues every `ADD VERTEX` / `ADD EDGE`, phase 2 issues every `ALTER EDGE ADD PAIR`. TG validates an entire `SCHEMA_CHANGE JOB` upfront, so an `ALTER` referencing a vertex type created in the same job aborted with a parser error. Splitting the job means phase 2 runs against a graph where the new types already exist. The result payload now exposes both phase names via a new `job_names` list; the legacy `job_name` key remains the first phase that ran.
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index affbe34..a468e1a 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -566,6 +566,12 @@ def _extract_attributes(body: str, *, is_edge_body: bool) -> List[Tuple[str, str
# stripped, but be defensive against other reserved tokens).
if folded in {"from", "to", "primary_id"}:
continue
+ # Drop attribute names that collide with GSQL reserved words
+ # (e.g. ``count``, ``min``, ``max``). The schema-change job
+ # would otherwise fail with "Encountered ',' ..." when TG's
+ # parser reads the keyword in attribute position.
+ if is_reserved_word(name):
+ continue
seen.add(folded)
out.append((name, type_str))
return out
@@ -1239,48 +1245,65 @@ def apply_proposal(
"status": "no-op",
"statements": [],
"job_name": None,
+ "job_names": [],
"gsql_output": "",
"summary": summary,
"metadata": metadata,
"retrievers": retrievers,
}
- block, job_name = build_schema_change_job(graphname, statements, job_name)
- try:
- output = conn.gsql(block)
- except Exception:
- # If gsql() raised mid-block, the trailing DROP JOB may not have
- # executed. Best-effort cleanup so leaked jobs don't accumulate.
- try:
- conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {job_name}")
- except Exception:
- pass
- raise
- err = gsql_output_error(output)
- if err:
- # Server-reported failure (no exception). RUN may have aborted
- # before DROP — try to clean up; ignore the "not found" path.
+ # Split into two phases so TG's job-validator never sees an ALTER
+ # statement that references a vertex/edge type created elsewhere in
+ # the same job. Phase 1 ADDs new types; phase 2 ALTERs (e.g. ADD
+ # PAIR on existing edges) runs only after phase 1 commits.
+ add_stmts = [s for s in statements if s.lstrip().upper().startswith("ADD ")]
+ alter_stmts = [s for s in statements if s.lstrip().upper().startswith("ALTER ")]
+
+ def _run_phase(phase_stmts: List[str], phase_job: Optional[str]) -> Tuple[str, str]:
+ block, name = build_schema_change_job(graphname, phase_stmts, phase_job)
try:
- conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {job_name}")
+ out = conn.gsql(block)
except Exception:
- pass
- # pyTigerGraph's gsql() returned a failure response without
- # raising — surface it explicitly so the caller doesn't
- # falsely report "applied". Skip metadata upsert (the schema
- # change didn't land, so writing EntityType vertices for
- # types that don't exist would also fail). Include a
- # ``retrievers`` placeholder so the result shape is uniform
- # with the no-op / applied paths.
- return {
- "status": "error",
- "statements": statements,
- "job_name": job_name,
- "gsql_output": output,
- "error": err,
- "summary": summary,
- "metadata": {"entity_types": [], "relationship_types": []},
- "retrievers": {"status": "skipped", "reason": "schema apply failed"},
- }
+ try:
+ conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {name}")
+ except Exception:
+ pass
+ raise
+ return out, name
+
+ phase_outputs: List[str] = []
+ phase_jobs: List[str] = []
+ first_job_name: Optional[str] = None
+ for phase_stmts in (add_stmts, alter_stmts):
+ if not phase_stmts:
+ continue
+ # Only honor the caller-supplied job_name on the first phase that
+ # actually runs; subsequent phases get auto-generated names so
+ # they don't collide.
+ phase_job = job_name if first_job_name is None else None
+ output, ran_name = _run_phase(phase_stmts, phase_job)
+ phase_outputs.append(output)
+ phase_jobs.append(ran_name)
+ if first_job_name is None:
+ first_job_name = ran_name
+ err = gsql_output_error(output)
+ if err:
+ try:
+ conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {ran_name}")
+ except Exception:
+ pass
+ return {
+ "status": "error",
+ "statements": statements,
+ "job_name": first_job_name,
+ "job_names": phase_jobs,
+ "gsql_output": "\n".join(phase_outputs),
+ "error": err,
+ "summary": summary,
+ "metadata": {"entity_types": [], "relationship_types": []},
+ "retrievers": {"status": "skipped", "reason": "schema apply failed"},
+ }
+
metadata = upsert_type_metadata(conn, proposal)
retrievers = _install_retrievers_after_apply(
conn, graphname, proposal=proposal, pre_apply_existing=existing
@@ -1288,8 +1311,9 @@ def apply_proposal(
return {
"status": "applied",
"statements": statements,
- "job_name": job_name,
- "gsql_output": output,
+ "job_name": first_job_name,
+ "job_names": phase_jobs,
+ "gsql_output": "\n".join(phase_outputs),
"summary": summary,
"metadata": metadata,
"retrievers": retrievers,
diff --git a/graphrag/tests/test_schema_utils.py b/graphrag/tests/test_schema_utils.py
index 99fe3d8..6907ac0 100644
--- a/graphrag/tests/test_schema_utils.py
+++ b/graphrag/tests/test_schema_utils.py
@@ -901,7 +901,7 @@ def test_apply_proposal_error_path_returns_uniform_shape():
assert result["status"] == "error"
# Full uniform shape — same key set as success / no-op.
assert set(result.keys()) == {
- "status", "statements", "job_name", "gsql_output",
+ "status", "statements", "job_name", "job_names", "gsql_output",
"error", "summary", "metadata", "retrievers",
}
assert result["retrievers"] == {
@@ -1182,10 +1182,12 @@ def test_emit_structural_links_skips_already_present_pairs():
def test_apply_proposal_emits_structural_links_alongside_domain_adds():
- """End-to-end: apply_proposal runs both emit_add_statements and
- emit_structural_link_alters in a single schema-change job. The
- fake graph has the GraphRAG structural types in place (production
- invariant — init_supportai runs before apply_proposal).
+ """End-to-end: apply_proposal runs emit_add_statements (phase 1)
+ and emit_structural_link_alters (phase 2) as two separate
+ schema-change jobs so TG's job validator never sees an ALTER that
+ references a vertex type created in the same job. The fake graph
+ has the GraphRAG structural types in place (production invariant —
+ init_supportai runs before apply_proposal).
"""
conn = _FakeConn(
vertex_types=[
@@ -1200,20 +1202,27 @@ def test_apply_proposal_emits_structural_links_alongside_domain_adds():
result = apply_proposal(conn, "g", proposal)
assert result["status"] == "applied"
schema_calls = [c for c in conn.gsql_calls if "SCHEMA_CHANGE JOB" in c]
- assert len(schema_calls) == 1
- cmd = schema_calls[0]
- # Domain ADD VERTEX is in there.
- assert "ADD VERTEX Company" in cmd
- # CONTAINS_ENTITY pair-additions for Company are in the same job.
- assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in cmd
- assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in cmd
- # IN_COMMUNITY pair-addition for Company is in the same job —
+ # Two phases: phase 1 = ADD VERTEX, phase 2 = ALTER EDGE ADD PAIR.
+ assert len(schema_calls) == 2
+ add_cmd, alter_cmd = schema_calls
+ # Phase 1 carries the domain ADD VERTEX.
+ assert "ADD VERTEX Company" in add_cmd
+ assert "ALTER EDGE" not in add_cmd
+ # Phase 2 carries every structural-link ALTER, no ADD VERTEX.
+ assert "ADD VERTEX" not in alter_cmd
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO Company)" in alter_cmd
+ assert "ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO Company)" in alter_cmd
+ # IN_COMMUNITY pair-addition for Company —
# community retrievers walking domain VTs need this edge present.
- assert "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Company, TO Community)" in cmd
+ assert "ALTER EDGE IN_COMMUNITY ADD PAIR (FROM Company, TO Community)" in alter_cmd
# No per-domain-vertex IS_HEAD_OF / HAS_TAIL — those live at
# EntityType ↔ RelationshipType in the structural schema.
- assert "IS_HEAD_OF ADD PAIR" not in cmd
- assert "HAS_TAIL ADD PAIR" not in cmd
+ assert "IS_HEAD_OF ADD PAIR" not in alter_cmd
+ assert "HAS_TAIL ADD PAIR" not in alter_cmd
+ # Result surfaces both phase job names; the legacy ``job_name`` key
+ # remains the first phase for callers that haven't migrated.
+ assert len(result["job_names"]) == 2
+ assert result["job_name"] == result["job_names"][0]
def test_apply_proposal_skips_structural_links_when_core_types_missing():
From 93d8e5d06a796970eb9d9695a724ea286774ffe5 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Sat, 9 May 2026 00:11:00 -0700
Subject: [PATCH 58/70] Resolve schema-extraction sample budget from LLM
context window
- Distribute the budget across uploaded files via equal-share with
rollover so every file contributes head sample, instead of greedy
first-fit dropping later files when the first is large.
- Drive the budget from llm_config.token_limit when configured, and
otherwise from a curated per-model context-window table (Claude
family 200K / Opus 4.7 1M, GPT-4o 128K, Gemini 1.5 1M, etc.).
Unknown models fall back to a similar family default with a warning;
no-family-match emits a louder warning at 128K.
- Expose the budget as ``max_tokens`` on ``extract_schema_gsql`` and
``concatenate_samples``; characters-per-token conversion is local
to the truncation step.
---
CHANGELOG.md | 1 +
common/db/schema_extraction.py | 170 +++++++++++++++++++++--
graphrag/tests/test_schema_extraction.py | 116 +++++++++++++++-
3 files changed, 269 insertions(+), 18 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe27d69..d35b4b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@
- **Default chat retrieval method is now `auto`** instead of `hybridsearch`. Existing graphs that did not configure a method explicitly will route through the new selector after upgrade. Manual mode (and any explicitly-selected method in the chat dropdown) overrides the default unchanged.
- **Schema parser drops attribute names that collide with GSQL reserved words** (e.g. `count`, `min`, `max`). LLM-extracted schemas previously failed schema-change with `Encountered "," at line N` when an attribute named after a keyword reached TG; the offending attribute is now silently skipped at parse time.
- **`apply_proposal` runs schema changes in two phases** — phase 1 issues every `ADD VERTEX` / `ADD EDGE`, phase 2 issues every `ALTER EDGE ADD PAIR`. TG validates an entire `SCHEMA_CHANGE JOB` upfront, so an `ALTER` referencing a vertex type created in the same job aborted with a parser error. Splitting the job means phase 2 runs against a graph where the new types already exist. The result payload now exposes both phase names via a new `job_names` list; the legacy `job_name` key remains the first phase that ran.
+- **Schema-extraction sample budget now scales with the configured LLM**. Previously hardcoded at 200 KB (~50K tokens), causing later files in multi-file uploads to be silently truncated. The budget is now resolved from `llm_config.token_limit` if set, otherwise from a per-model context-window table (Claude family 200K / Opus 4.7 1M, GPT-4o 128K, Gemini 1.5 1M, etc.). Unknown models fall back to a similar family default with a warning. Within the resolved budget, characters are distributed across uploaded files using equal-share-with-rollover so every file contributes — the first file no longer crowds out the rest.
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/db/schema_extraction.py b/common/db/schema_extraction.py
index b71794e..529da11 100644
--- a/common/db/schema_extraction.py
+++ b/common/db/schema_extraction.py
@@ -25,7 +25,7 @@
from __future__ import annotations
import logging
-from typing import Iterable, List
+from typing import Iterable, List, Optional
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
@@ -39,6 +39,118 @@
logger = logging.getLogger(__name__)
+# Specific known model builds → context window in tokens. Matched by
+# longest-prefix substring against the lowercased ``llm_model`` value.
+# When a configured model hits this table, no warning is logged.
+_MODEL_CONTEXT_TOKENS = {
+ # Anthropic Claude — Opus 4.7 1M is keyed first so its longer prefix
+ # wins over the 200K Opus 4.x default.
+ "claude-opus-4-7": 1_000_000,
+ "claude-opus-4": 200_000,
+ "claude-sonnet-4": 200_000,
+ "claude-haiku-4": 200_000,
+ "claude-3-5-sonnet": 200_000,
+ "claude-3-5-haiku": 200_000,
+ "claude-3-opus": 200_000,
+ "claude-3-sonnet": 200_000,
+ "claude-3-haiku": 200_000,
+ # OpenAI GPT-4
+ "gpt-4o": 128_000,
+ "gpt-4-turbo": 128_000,
+ "gpt-4-1106": 128_000,
+ "gpt-4-0125": 128_000,
+ "gpt-4-32k": 32_000,
+ "gpt-4": 8_000,
+ # OpenAI GPT-3.5
+ "gpt-3.5-turbo-16k": 16_000,
+ "gpt-3.5-turbo": 16_000,
+ "gpt-3.5": 4_000,
+ # Google Gemini
+ "gemini-1.5-pro": 1_000_000,
+ "gemini-1.5-flash": 1_000_000,
+ "gemini-1.0-pro": 32_000,
+ # Meta Llama
+ "llama-3.1": 128_000,
+ "llama-3": 8_000,
+ "llama-2": 4_000,
+}
+# Family-level fallbacks for unknown variants. When the specific-build
+# table misses, the first matching family is used and a warning is
+# logged so the operator knows the value is a guess.
+_FAMILY_FALLBACK_TOKENS = [
+ ("claude", 200_000),
+ ("gpt-4", 128_000),
+ ("gpt-3.5", 16_000),
+ ("gpt", 128_000), # unknown gpt-* — assume modern
+ ("gemini", 1_000_000),
+ ("llama", 128_000), # unknown llama — assume modern
+ ("mistral", 32_000),
+ ("mixtral", 32_000),
+ ("deepseek", 128_000),
+ ("qwen", 32_000),
+ ("titan", 32_000),
+ ("cohere", 128_000),
+ ("nova", 128_000),
+]
+_DEFAULT_CONTEXT_TOKENS_FALLBACK = 128_000
+# Tokens reserved for the prompt template, structural-types list, the
+# reserved-words list, and the LLM's output. The remaining budget is
+# spent on sample content.
+_PROMPT_OVERHEAD_TOKENS = 4_000
+# Lower bound so unknown / tiny-context models still get *something*.
+_MIN_SAMPLE_TOKENS = 1_000
+# Approximation used to convert the resolved token budget into the
+# character budget that ``concatenate_samples`` consumes. English
+# markdown averages ~4 chars/token.
+_CHARS_PER_TOKEN = 4
+
+
+def _default_context_tokens(model_name: Optional[str]) -> int:
+ if not model_name:
+ logger.warning(
+ "schema_extraction: no llm_model configured; defaulting to %d tokens",
+ _DEFAULT_CONTEXT_TOKENS_FALLBACK,
+ )
+ return _DEFAULT_CONTEXT_TOKENS_FALLBACK
+ name = model_name.lower()
+ # Longest-prefix substring match against the specific-build table.
+ for prefix in sorted(_MODEL_CONTEXT_TOKENS, key=len, reverse=True):
+ if prefix in name:
+ return _MODEL_CONTEXT_TOKENS[prefix]
+ # Specific table missed — pick a similar family and warn so the
+ # operator knows the value was guessed.
+ for family, tokens in _FAMILY_FALLBACK_TOKENS:
+ if family in name:
+ logger.warning(
+ "schema_extraction: model %r not in known-build table; "
+ "using %s-family default of %d tokens. Add it to "
+ "_MODEL_CONTEXT_TOKENS for an exact value.",
+ model_name, family, tokens,
+ )
+ return tokens
+ logger.warning(
+ "schema_extraction: model %r unknown; using fallback default of %d tokens",
+ model_name, _DEFAULT_CONTEXT_TOKENS_FALLBACK,
+ )
+ return _DEFAULT_CONTEXT_TOKENS_FALLBACK
+
+
+def _resolve_sample_token_budget(llm_service) -> int:
+ """Pick the sample-text token budget from the LLM's configured
+ ``token_limit``, falling back to the model's default context window
+ when ``token_limit`` is not set.
+
+ Reserves ``_PROMPT_OVERHEAD_TOKENS`` for the prompt scaffolding and
+ LLM output. Returns tokens — callers convert to characters at the
+ truncation boundary.
+ """
+ cfg = getattr(llm_service, "config", None) or {}
+ token_budget = int(cfg.get("token_limit") or 0)
+ if token_budget <= 0:
+ token_budget = _default_context_tokens(cfg.get("llm_model"))
+ return max(token_budget - _PROMPT_OVERHEAD_TOKENS, _MIN_SAMPLE_TOKENS)
+
+
def _build_prompt(llm_service) -> PromptTemplate:
"""Wrap *llm_service*'s ``schema_extraction_prompt`` text in a
``PromptTemplate`` with the three required input variables.
@@ -52,36 +164,59 @@ def _build_prompt(llm_service) -> PromptTemplate:
def concatenate_samples(
samples: Iterable[dict],
- max_chars: int,
+ max_tokens: int,
) -> str:
"""Concatenate sample-doc markdown into a single blob, with each
- document preceded by an ``# `` heading. Truncates at
- *max_chars* total characters; truncation is logged.
+ document preceded by an ``# `` heading.
+
+ The budget is expressed in *tokens*; this function converts to
+ characters internally at ~4 chars/token for ``len()``-based
+ truncation. The budget is distributed across files so every
+ uploaded sample contributes — files are not silently dropped when
+ the first file is large. Each file gets
+ ``remaining_budget // remaining_files`` characters of head sample;
+ if a file uses less, the leftover rolls forward to subsequent files.
*samples* is an iterable of ``{"doc_id": str, "content": str}``
dicts (the same shape ``extract_text_from_file_with_images_as_docs``
returns).
"""
+ samples_list = list(samples)
+ n = len(samples_list)
+ if n == 0:
+ return ""
+
+ max_chars = max_tokens * _CHARS_PER_TOKEN
parts: List[str] = []
- total = 0
- for s in samples:
+ remaining_budget = max_chars
+ remaining_files = n
+ truncated_any = False
+ for s in samples_list:
doc_id = s.get("doc_id", "doc")
content = s.get("content", "") or ""
header = f"\n\n# {doc_id}\n\n"
- budget = max_chars - total
- if budget <= 0:
- logger.warning("Sample doc budget exhausted; later files truncated.")
- break
- chunk = (header + content)[:budget]
+ per_file = remaining_budget // max(remaining_files, 1)
+ full = header + content
+ if len(full) > per_file:
+ truncated_any = True
+ chunk = full[:per_file]
parts.append(chunk)
- total += len(chunk)
+ remaining_budget -= len(chunk)
+ remaining_files -= 1
+
+ if truncated_any:
+ logger.warning(
+ "Schema-extraction samples truncated to fit %d-token budget across %d files",
+ max_tokens,
+ n,
+ )
return "".join(parts).lstrip()
def extract_schema_gsql(
llm_service,
samples: Iterable[dict],
- max_chars: int = 200_000,
+ max_tokens: Optional[int] = None,
) -> str:
"""Run the schema-extraction prompt against *llm_service*. Returns
the raw GSQL string the model produced (caller passes it to
@@ -92,9 +227,16 @@ def extract_schema_gsql(
``invoke_with_parser(prompt, parser, inputs, caller_name)`` entry
point. Per-graph prompt overrides are picked up automatically by
``schema_extraction_prompt``'s resolution chain.
+
+ When *max_tokens* is ``None`` (the production path), the sample
+ budget is resolved from ``llm_service.config.token_limit`` if set,
+ otherwise from the model's default context window. Tests can pass
+ an explicit *max_tokens* to pin behavior independently of config.
"""
+ if max_tokens is None:
+ max_tokens = _resolve_sample_token_budget(llm_service)
prompt = _build_prompt(llm_service)
- samples_blob = concatenate_samples(samples, max_chars=max_chars)
+ samples_blob = concatenate_samples(samples, max_tokens=max_tokens)
structural_types = ", ".join(
sorted(GRAPHRAG_STRUCTURAL_VERTEX_TYPES | GRAPHRAG_STRUCTURAL_EDGE_TYPES)
)
diff --git a/graphrag/tests/test_schema_extraction.py b/graphrag/tests/test_schema_extraction.py
index 5b5d383..092235a 100644
--- a/graphrag/tests/test_schema_extraction.py
+++ b/graphrag/tests/test_schema_extraction.py
@@ -47,28 +47,136 @@ def test_concatenate_samples_joins_doc_id_headers():
{"doc_id": "report1", "content": "Hello world."},
{"doc_id": "report2", "content": "Second body."},
]
- blob = schema_extraction.concatenate_samples(samples, max_chars=10_000)
+ blob = schema_extraction.concatenate_samples(samples, max_tokens=2_500)
assert "# report1" in blob
assert "# report2" in blob
assert "Hello world." in blob
assert "Second body." in blob
-def test_concatenate_samples_truncates_at_max_chars():
+def test_concatenate_samples_truncates_at_max_budget():
samples = [
{"doc_id": "a", "content": "x" * 1_000},
{"doc_id": "b", "content": "y" * 1_000},
]
- blob = schema_extraction.concatenate_samples(samples, max_chars=300)
+ # 75 tokens × 4 chars/token = 300 char cap.
+ blob = schema_extraction.concatenate_samples(samples, max_tokens=75)
assert len(blob) <= 300
def test_concatenate_samples_handles_empty_content():
samples = [{"doc_id": "empty", "content": ""}]
- blob = schema_extraction.concatenate_samples(samples, max_chars=1_000)
+ blob = schema_extraction.concatenate_samples(samples, max_tokens=250)
assert "# empty" in blob
+def test_concatenate_samples_distributes_budget_across_files():
+ """Every uploaded file must contribute to the LLM blob even when
+ the first file is large — proportional/equal-share split prevents
+ later files from being silently dropped.
+ """
+ samples = [
+ {"doc_id": f"f{i}", "content": "x" * 5_000} for i in range(4)
+ ]
+ # 100 tokens × 4 = 400 char cap; ~100 chars per file.
+ blob = schema_extraction.concatenate_samples(samples, max_tokens=100)
+ # All four headers must appear; greedy first-fit would only emit f0 / f1.
+ for i in range(4):
+ assert f"# f{i}" in blob
+ assert len(blob) <= 400
+
+
+def test_concatenate_samples_rolls_unused_budget_forward():
+ """A small first file leaves room for later files. The leftover
+ budget must flow forward, not be discarded.
+ """
+ samples = [
+ {"doc_id": "small", "content": "tiny"},
+ {"doc_id": "big", "content": "y" * 10_000},
+ ]
+ # 250 tokens × 4 = 1_000 char cap.
+ blob = schema_extraction.concatenate_samples(samples, max_tokens=250)
+ # Big file should consume most of the leftover from small.
+ assert blob.count("y") > 700 # ≥ 700 chars of big-file body
+ assert "tiny" in blob
+
+
+def test_resolve_sample_token_budget_uses_token_limit_when_configured():
+ """When llm_service.config.token_limit is set, it drives the
+ sample budget — model name is irrelevant.
+ """
+
+ class _LLM:
+ config = {"token_limit": 50_000, "llm_model": "anything"}
+
+ tokens = schema_extraction._resolve_sample_token_budget(_LLM())
+ # 50_000 - 4_000 reserved = 46_000 sample tokens.
+ assert tokens == 46_000
+
+
+def test_resolve_sample_token_budget_uses_known_model_default():
+ """Falls back to the per-model default context window when
+ token_limit is not configured. Known build → no warning.
+ """
+
+ class _LLM:
+ config = {"llm_model": "claude-3-5-sonnet-20241022"}
+
+ tokens = schema_extraction._resolve_sample_token_budget(_LLM())
+ # claude-3-5-sonnet → 200_000 tokens default - 4_000 reserved.
+ assert tokens == 200_000 - 4_000
+
+
+def test_resolve_sample_token_budget_warns_for_unknown_family_member(caplog):
+ """An unrecognized but family-matchable model picks the family's
+ default and emits a warning so the operator can update the table.
+ """
+ import logging
+
+ class _LLM:
+ config = {"llm_model": "claude-7-future-2030"}
+
+ caplog.set_level(logging.WARNING, logger=schema_extraction.logger.name)
+ tokens = schema_extraction._resolve_sample_token_budget(_LLM())
+ # Family fallback for "claude" → 200_000 tokens.
+ assert tokens == 200_000 - 4_000
+ assert any(
+ "claude-7-future-2030" in rec.message and "claude-family default" in rec.message
+ for rec in caplog.records
+ )
+
+
+def test_resolve_sample_token_budget_warns_for_completely_unknown_model(caplog):
+ """A model that doesn't match any family substring still gets a
+ sane budget, but the warning is louder so the operator notices.
+ """
+ import logging
+
+ class _LLM:
+ config = {"llm_model": "homegrown-frobnicator-v3"}
+
+ caplog.set_level(logging.WARNING, logger=schema_extraction.logger.name)
+ tokens = schema_extraction._resolve_sample_token_budget(_LLM())
+ assert tokens == 128_000 - 4_000
+ assert any(
+ "unknown" in rec.message and "homegrown-frobnicator" in rec.message
+ for rec in caplog.records
+ )
+
+
+def test_resolve_sample_token_budget_handles_missing_config():
+ """LLM service without a ``config`` attribute (e.g., test stubs)
+ still produces a usable budget via the fallback path.
+ """
+
+ class _LLM:
+ pass
+
+ tokens = schema_extraction._resolve_sample_token_budget(_LLM())
+ # No config → no token_limit, no model_name → fallback 128_000.
+ assert tokens == 128_000 - 4_000
+
+
def test_extract_schema_gsql_passes_structural_and_keyword_lists_to_llm():
llm = _CapturingLLM(response="// A company.\nADD VERTEX Company();")
samples = [{"doc_id": "x", "content": "Acme Corp issues bonds."}]
From 3e6bdee5ef20e2c43a1c94aa88a1ea2043097b04 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Sat, 9 May 2026 17:53:25 -0700
Subject: [PATCH 59/70] Make graph initialization async-job + harden init UX
- Convert /initialize_graph to submit-then-poll so long inits no
longer race the browser's idle-response cutoff.
- Refresh the UI's available-graphs list from the live backend so
a graph created mid-session isn't lost when the client drops the
success signal.
- Pause the UI idle timer during long backend calls.
- Fix two retriever queries that referenced stale vertex types,
and stop flagging successful query installs as errors.
---
CHANGELOG.md | 5 +
common/db/retriever_render.py | 10 +-
.../retrievers/Content_Similarity_Search.gsql | 2 +-
.../Content_Similarity_Vector_Search.gsql | 2 +-
graphrag-ui/src/pages/Setup.tsx | 31 ++-
graphrag-ui/src/pages/setup/IngestGraph.tsx | 25 +-
graphrag-ui/src/pages/setup/KGAdmin.tsx | 96 +++++++-
graphrag/app/routers/ui.py | 225 +++++++++++++-----
.../tests/test_e2e_schema_aware_ingest.py | 52 +++-
9 files changed, 360 insertions(+), 88 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d35b4b8..e523d32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,11 @@
- **Schema parser drops attribute names that collide with GSQL reserved words** (e.g. `count`, `min`, `max`). LLM-extracted schemas previously failed schema-change with `Encountered "," at line N` when an attribute named after a keyword reached TG; the offending attribute is now silently skipped at parse time.
- **`apply_proposal` runs schema changes in two phases** — phase 1 issues every `ADD VERTEX` / `ADD EDGE`, phase 2 issues every `ALTER EDGE ADD PAIR`. TG validates an entire `SCHEMA_CHANGE JOB` upfront, so an `ALTER` referencing a vertex type created in the same job aborted with a parser error. Splitting the job means phase 2 runs against a graph where the new types already exist. The result payload now exposes both phase names via a new `job_names` list; the legacy `job_name` key remains the first phase that ran.
- **Schema-extraction sample budget now scales with the configured LLM**. Previously hardcoded at 200 KB (~50K tokens), causing later files in multi-file uploads to be silently truncated. The budget is now resolved from `llm_config.token_limit` if set, otherwise from a per-model context-window table (Claude family 200K / Opus 4.7 1M, GPT-4o 128K, Gemini 1.5 1M, etc.). Unknown models fall back to a similar family default with a warning. Within the resolved budget, characters are distributed across uploaded files using equal-share-with-rollover so every file contributes — the first file no longer crowds out the rest.
+- **`/initialize_graph` is now an async-job endpoint**. POST returns 202-style `{"status": "submitted"}` immediately and the long-running work (structural schema, optional domain schema apply, retriever installs) runs in a `BackgroundTask`. Clients poll `GET /ui/{graphname}/initialize_status` for `state` (`queued` / `running` / `completed` / `error`) and the final result. Previously the endpoint was fully synchronous; long inits (TG schema-change + retriever installs ≥ 5 minutes) tripped the browser's idle-response cutoff with `net::ERR_TIMED_OUT` even when the backend completed successfully.
+- **New `GET /ui/list_graphs`**. Returns the live list of graphs the authenticated user has access to. UI clients (`KGAdmin`, `IngestGraph`, `Setup`) now seed `availableGraphs` from `sessionStorage` for instant render and then refresh from the live list, so a graph created mid-session is visible without a re-login.
+- **Init / extract dialogs pause the idle timer for the duration of the long-running call**. The dialog used to log the user out after 60 minutes of "no activity" even while a backend init or schema extraction was in flight; the existing `pauseIdleTimer()` / `resumeIdleTimer()` pattern is now wired into `handleExtractSchema` and `handleInitializeGraph`.
+- **Removed two dead vertex-type references from the retriever queries**. `Content_Similarity_Search` and `Content_Similarity_Vector_Search` referenced `Relationship` (never a vertex type — it's the `RELATIONSHIP` edge) and `Concept` (removed in an earlier release); both queries now save as draft with TYP-152 errors against any v1.4.0 graph. The IF-branch is reduced to `s.type == "Entity"` and the existing `Community` branch.
+- **Retriever-install error detection no longer false-positives on TG's normal output**. `install_retrievers` and `install_retrievers_async` were doing a substring `"error" in output.lower()` check, which trips on every successful install (TG output contains literals like `0 errors`, `no warnings`). Both now delegate to the existing `gsql_output_error()` helper that matches actual error markers (`SEMANTIC ERROR`, `Failed to create`, transport-level failures).
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/db/retriever_render.py b/common/db/retriever_render.py
index a9fb28f..3956815 100644
--- a/common/db/retriever_render.py
+++ b/common/db/retriever_render.py
@@ -14,6 +14,8 @@
from pathlib import Path
from typing import Iterable, Optional
+from common.db.schema_utils import gsql_output_error
+
logger = logging.getLogger(__name__)
@@ -151,8 +153,8 @@ def install_retrievers(
try:
out = conn.gsql(block)
results[query_name] = out
- lower = out.lower() if isinstance(out, str) else ""
- if "error" in lower or "failed" in lower:
+ err = gsql_output_error(out) if isinstance(out, str) else None
+ if err:
logger.warning(
f"install_retrievers: {query_name} install reported "
f"errors: {_summarize(out)}"
@@ -190,8 +192,8 @@ async def install_retrievers_async(
else:
out = await conn.gsql(block)
results[query_name] = out
- lower = out.lower() if isinstance(out, str) else ""
- if "error" in lower or "failed" in lower:
+ err = gsql_output_error(out) if isinstance(out, str) else None
+ if err:
logger.warning(
f"install_retrievers_async: {query_name} install "
f"reported errors: {str(out)[:300]}"
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
index e82f538..2801164 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql
@@ -40,7 +40,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Search(STRING json_list_v
@@final_retrieval += (s.id -> tgt.text)
END
POST-ACCUM
- IF s.type == "Relationship" OR s.type == "Entity" OR s.type == "Concept" THEN
+ IF s.type == "Entity" THEN
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
index fe4a4ca..e711208 100644
--- a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
+++ b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql
@@ -38,7 +38,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_ty
@@final_retrieval += (s.id -> tgt.text)
END
POST-ACCUM
- IF s.type == "Relationship" OR s.type == "Entity" OR s.type == "Concept" THEN
+ IF s.type == "Entity" THEN
@@final_retrieval += (s.id -> s.definition)
ELSE IF s.type == "Community" THEN
@@final_retrieval += (s.id -> s.description)
diff --git a/graphrag-ui/src/pages/Setup.tsx b/graphrag-ui/src/pages/Setup.tsx
index 168ac65..f1a8d79 100644
--- a/graphrag-ui/src/pages/Setup.tsx
+++ b/graphrag-ui/src/pages/Setup.tsx
@@ -1001,20 +1001,45 @@ const [activeTab, setActiveTab] = useState("upload");
}
}, [refreshOpen, refreshGraphName]);
- // Load available graphs from sessionStorage on mount
+ // Load available graphs. Seed from sessionStorage, then refresh from
+ // /ui/list_graphs so a graph created mid-session is visible without
+ // re-login (the post-init success path that updates sessionStorage
+ // can be skipped if the init fetch times out client-side even though
+ // the backend completed).
useEffect(() => {
const store = JSON.parse(sessionStorage.getItem("site") || "{}");
if (store.graphs && Array.isArray(store.graphs)) {
setAvailableGraphs(store.graphs);
- // Auto-select first graph if available
if (store.graphs.length > 0 && !ingestGraphName) {
setIngestGraphName(store.graphs[0]);
}
- // Auto-select first graph for refresh as well
if (store.graphs.length > 0 && !refreshGraphName) {
setRefreshGraphName(store.graphs[0]);
}
}
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) return;
+ fetch("/ui/list_graphs", {
+ headers: { Authorization: `Basic ${creds}` },
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ if (!data || !Array.isArray(data.graphs)) return;
+ const graphs: string[] = data.graphs;
+ setAvailableGraphs(graphs);
+ const cached = JSON.parse(sessionStorage.getItem("site") || "{}");
+ cached.graphs = graphs;
+ sessionStorage.setItem("site", JSON.stringify(cached));
+ if (graphs.length > 0 && !ingestGraphName) {
+ setIngestGraphName(graphs[0]);
+ }
+ if (graphs.length > 0 && !refreshGraphName) {
+ setRefreshGraphName(graphs[0]);
+ }
+ })
+ .catch(() => {
+ /* keep cached value; not fatal */
+ });
}, []);
// Load files when ingest dialog opens or graph name changes
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index 37a0d7a..c36a9de 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -870,16 +870,37 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
}
}, [isIngesting]);
- // Load available graphs from sessionStorage on mount
+ // Load available graphs. Seed from sessionStorage for instant render,
+ // then refresh from /ui/list_graphs so newly-initialized graphs show
+ // up without a re-login.
useEffect(() => {
const store = JSON.parse(sessionStorage.getItem("site") || "{}");
if (store.graphs && Array.isArray(store.graphs)) {
setAvailableGraphs(store.graphs);
- // Auto-select first graph if available
if (store.graphs.length > 0 && !ingestGraphName) {
setIngestGraphName(store.graphs[0]);
}
}
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) return;
+ fetch("/ui/list_graphs", {
+ headers: { Authorization: `Basic ${creds}` },
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ if (!data || !Array.isArray(data.graphs)) return;
+ const graphs: string[] = data.graphs;
+ setAvailableGraphs(graphs);
+ const cached = JSON.parse(sessionStorage.getItem("site") || "{}");
+ cached.graphs = graphs;
+ sessionStorage.setItem("site", JSON.stringify(cached));
+ if (graphs.length > 0 && !ingestGraphName) {
+ setIngestGraphName(graphs[0]);
+ }
+ })
+ .catch(() => {
+ /* keep cached value; not fatal */
+ });
}, []);
// Load files when graph name changes
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index 572c9fa..089efcb 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -192,7 +192,11 @@ const KGAdmin = () => {
const [isCheckingStatus, setIsCheckingStatus] = useState(false);
const [pollingActive, setPollingActive] = useState(false);
- // Load available graphs
+ // Load available graphs. First seed from sessionStorage so the
+ // dropdown shows something immediately, then refresh from
+ // /ui/list_graphs so a graph created/initialized after login (or
+ // during a session where the init request failed client-side but
+ // succeeded server-side) is still visible without re-login.
useEffect(() => {
const store = JSON.parse(sessionStorage.getItem("site") || "{}");
if (store.graphs && Array.isArray(store.graphs)) {
@@ -201,6 +205,26 @@ const KGAdmin = () => {
setRefreshGraphName(store.graphs[0]);
}
}
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) return;
+ fetch("/ui/list_graphs", {
+ headers: { Authorization: `Basic ${creds}` },
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ if (!data || !Array.isArray(data.graphs)) return;
+ const graphs: string[] = data.graphs;
+ setAvailableGraphs(graphs);
+ const cached = JSON.parse(sessionStorage.getItem("site") || "{}");
+ cached.graphs = graphs;
+ sessionStorage.setItem("site", JSON.stringify(cached));
+ if (graphs.length > 0 && !refreshGraphName) {
+ setRefreshGraphName(graphs[0]);
+ }
+ })
+ .catch(() => {
+ /* keep cached value; not fatal */
+ });
}, []);
// Pull schema-init caps from /ui/config when the Initialize dialog opens.
@@ -274,6 +298,9 @@ const KGAdmin = () => {
`Step 1/2: Converting ${sampleFiles.length} uploaded file${sampleFiles.length === 1 ? "" : "s"} to text…`
);
setStatusType("");
+ // The LLM call can take minutes; pause the idle timer so the
+ // user isn't logged out mid-extraction.
+ pauseIdleTimer();
try {
const creds = sessionStorage.getItem("creds");
if (!creds) throw new Error("Not authenticated. Please login first.");
@@ -359,6 +386,7 @@ const KGAdmin = () => {
setStatusMessage(`❌ ${error.message}`);
setStatusType("error");
} finally {
+ resumeIdleTimer();
setIsExtractingSchema(false);
}
};
@@ -380,6 +408,9 @@ const KGAdmin = () => {
setIsInitializing(true);
setStatusMessage("Creating graph and initializing GraphRAG schema...");
setStatusType("");
+ // Schema-change job + retriever installs can take minutes; pause
+ // the idle timer so the user isn't logged out mid-init.
+ pauseIdleTimer();
try {
const creds = sessionStorage.getItem("creds");
@@ -423,7 +454,7 @@ const KGAdmin = () => {
}
}
- setStatusMessage("Step 2/2: Initializing GraphRAG schema...");
+ setStatusMessage("Step 2/2: Submitting GraphRAG schema initialization...");
const initBody: { schema_gsql?: string } = {};
if (schemaSource === "gsql" && pasteGsql.trim()) {
initBody.schema_gsql = pasteGsql;
@@ -431,6 +462,10 @@ const KGAdmin = () => {
const gsql = draftProposalToGsql(draftProposal).trim();
if (gsql) initBody.schema_gsql = gsql;
}
+ // Submit the init job. The backend kicks off a BackgroundTask
+ // and returns 202 immediately so the browser doesn't drop the
+ // request mid-flight on long inits (TG schema-change + retriever
+ // installs can take 10+ minutes).
const initResponse = await fetch(`/ui/${graphName}/initialize_graph`, {
method: "POST",
headers: {
@@ -444,20 +479,60 @@ const KGAdmin = () => {
if (!initResponse.ok) {
throw new Error(
- initData.detail || `Failed to initialize graph: ${initResponse.statusText}`
+ initData.detail || `Failed to submit init: ${initResponse.statusText}`
);
}
- if (initData.status !== "success") {
- setStatusMessage(
- initData.message || `Failed to initialize graph: ${initData.details}`
+ if (initData.status !== "submitted") {
+ throw new Error(
+ initData.message || `Init submission failed: ${JSON.stringify(initData)}`
);
- setStatusType("error");
- setIsInitializing(false);
- return;
}
- const domain = initData.domain_schema_status;
+ // Poll for completion. The bg task updates per-graph state on
+ // the server; we read it every few seconds and surface progress.
+ setStatusMessage("Step 2/2: Initializing GraphRAG schema (this can take several minutes)...");
+ const pollIntervalMs = 5000;
+ const maxWaitMs = 30 * 60 * 1000; // 30 minutes hard cap
+ const start = Date.now();
+ let finalState: any = null;
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (Date.now() - start > maxWaitMs) {
+ throw new Error(
+ "Init still running after 30 minutes; check server logs."
+ );
+ }
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
+ let statusResp: Response;
+ try {
+ statusResp = await fetch(
+ `/ui/${graphName}/initialize_status`,
+ { headers: { Authorization: `Basic ${creds}` } }
+ );
+ } catch {
+ // Transient network blip — retry on the next tick rather
+ // than aborting; the bg task is still working server-side.
+ continue;
+ }
+ if (!statusResp.ok) continue;
+ const statusData = await statusResp.json();
+ if (statusData.message) {
+ setStatusMessage(`Step 2/2: ${statusData.message}`);
+ }
+ if (statusData.state === "completed") {
+ finalState = statusData;
+ break;
+ }
+ if (statusData.state === "error") {
+ throw new Error(
+ statusData.error || statusData.message || "Init failed"
+ );
+ }
+ }
+
+ const result = finalState?.result || {};
+ const domain = result.domain_schema_status;
let domainNote = "";
if (domain && domain.status === "applied") {
const stmts = domain.statements?.length ?? 0;
@@ -490,6 +565,7 @@ const KGAdmin = () => {
setStatusMessage(`❌ Error: ${error.message}`);
setStatusType("error");
} finally {
+ resumeIdleTimer();
setIsInitializing(false);
}
};
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index df956f8..f6d3798 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -378,6 +378,17 @@ def login(auth: Annotated[list[str], Depends(ui_basic_auth)]):
return {"graphs": graphs, "roles": global_roles, "graph_roles": graph_roles}
+@router.get(f"{route_prefix}/list_graphs")
+def list_graphs(auth: Annotated[list[str], Depends(ui_basic_auth)]):
+ """Return the live list of graphs the authenticated user has access
+ to. UI clients call this on mount to refresh their cached graph
+ list, so a graph created or initialized after login (or during a
+ session where the init request failed client-side but succeeded
+ server-side) becomes visible without re-login.
+ """
+ return {"graphs": auth[0]}
+
+
@router.post(f"{route_prefix}/feedback")
def add_feedback(
message: Message,
@@ -491,14 +502,43 @@ def create_graph(
}
+# Per-graph init state store. Init runs as a BackgroundTask so the HTTP
+# request returns immediately; the UI polls /initialize_status for
+# progress/completion. The browser used to drop the request after ~5
+# minutes of silent response on long inits (TG schema-change + retriever
+# installs can run for 10+ minutes), even though the backend completed
+# successfully — see ``ERR_TIMED_OUT`` reports during v1.4.0 schema-aware
+# init.
+_init_state: dict[str, dict] = {}
+_init_state_lock = threading.Lock()
+
+
+def _set_init_state(graphname: str, **fields) -> None:
+ with _init_state_lock:
+ cur = _init_state.get(graphname, {})
+ cur.update(fields)
+ _init_state[graphname] = cur
+
+
+def _get_init_state(graphname: str) -> dict:
+ with _init_state_lock:
+ return dict(_init_state.get(graphname, {"state": "unknown"}))
+
+
@router.post(route_prefix + "/{graphname}/initialize_graph")
def init_graph(
graphname: ValidGraphName,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+ bg_tasks: BackgroundTasks,
payload: Annotated[dict | None, Body()] = None,
):
"""
- Initialize a TigerGraph knowledge graph with GraphRAG schema.
+ Submit a TigerGraph knowledge-graph initialization job.
+
+ Returns 202 immediately with ``{"status": "submitted", "graphname": ...}``;
+ the long-running work (structural schema, optional domain schema apply,
+ retriever installs) runs in a BackgroundTask. UI clients poll
+ ``GET /ui/{graphname}/initialize_status`` for state and final result.
The structural GraphRAG schema (Document, DocumentChunk, Entity,
EntityType, RelationshipType, Content, Community, Image and their
@@ -506,79 +546,136 @@ def init_graph(
Optionally accepts a JSON body with a domain-schema proposal:
- {"schema_gsql": "ADD VERTEX Company(...); ADD DIRECTED EDGE PUBLISHES(FROM Company, TO Report);"}
+ {"schema_gsql": "ADD VERTEX Company(...); ..."}
When ``schema_gsql`` is provided, the pasted text is parsed
- permissively (``ADD`` form *or* ``gsql ls`` output), structural-type
- collisions and dangling pairs are silently dropped, the diff against
- the current graph is computed, and the additive delta is applied as a
- single atomic ``SCHEMA_CHANGE JOB``. Existing types are never dropped.
+ permissively, structural-type collisions and dangling pairs are
+ dropped, the diff against the current graph is computed, and the
+ additive delta is applied. Existing types are never dropped.
"""
- try:
- # Extract credentials from the dependency (same pattern as other endpoints)
- creds = creds[1]
- auth = base64.b64encode(f"{creds.username}:{creds.password}".encode()).decode()
- _, conn = ws_basic_auth(auth, graphname)
+ cur = _get_init_state(graphname)
+ if cur.get("state") in {"queued", "running"}:
+ raise HTTPException(
+ status_code=409,
+ detail=f"Initialization already in progress for graph '{graphname}'",
+ )
- # Initialize the graph with GraphRAG schema
- LogWriter.info(f"Initializing graph: {graphname}")
- resp = supportai.init_supportai(conn, graphname)
- schema_res, index_res, query_res = resp[0], resp[1], resp[2]
-
- domain_schema_status: dict | None = None
- schema_gsql = (payload or {}).get("schema_gsql") if isinstance(payload, dict) else None
- if isinstance(schema_gsql, str) and schema_gsql.strip():
- LogWriter.info(f"Applying domain schema proposal for graph: {graphname}")
- proposal = schema_utils_mod.parse_gsql_schema(schema_gsql)
- proposal.drop_dangling_pairs()
- domain_schema_status = schema_utils_mod.apply_proposal(
- conn, graphname, proposal
- )
- LogWriter.info(
- f"Domain schema status for {graphname}: "
- f"{domain_schema_status['status']} "
- f"({len(domain_schema_status['statements'])} stmts)"
+ schema_gsql = (
+ (payload or {}).get("schema_gsql") if isinstance(payload, dict) else None
+ )
+ cred_obj = creds[1]
+ auth_b64 = base64.b64encode(
+ f"{cred_obj.username}:{cred_obj.password}".encode()
+ ).decode()
+
+ _set_init_state(
+ graphname,
+ state="queued",
+ message="Initialization queued",
+ started_at=time.time(),
+ completed_at=None,
+ result=None,
+ error=None,
+ )
+
+ def _run_init():
+ try:
+ _set_init_state(
+ graphname, state="running", message="Connecting to TigerGraph"
)
- # apply_proposal returns status=error when the gsql output
- # contains a known failure marker. Surface it as a 5xx so
- # the caller doesn't falsely think the schema landed.
- if domain_schema_status.get("status") == "error":
- LogWriter.error(
- f"Domain schema apply failed for {graphname}: "
- f"{domain_schema_status.get('error')}"
+ _, conn = ws_basic_auth(auth_b64, graphname)
+
+ _set_init_state(graphname, message="Initializing structural schema")
+ LogWriter.info(f"Initializing graph: {graphname}")
+ resp = supportai.init_supportai(conn, graphname)
+ schema_res, index_res, query_res = resp[0], resp[1], resp[2]
+
+ domain_schema_status: dict | None = None
+ if isinstance(schema_gsql, str) and schema_gsql.strip():
+ _set_init_state(graphname, message="Applying domain schema")
+ LogWriter.info(
+ f"Applying domain schema proposal for graph: {graphname}"
)
- raise HTTPException(
- status_code=500,
- detail={
- "message": "Domain schema apply failed",
- "error": domain_schema_status.get("error"),
- "gsql_output": domain_schema_status.get("gsql_output", "")[:1000],
- "statements": domain_schema_status.get("statements", []),
- },
+ proposal = schema_utils_mod.parse_gsql_schema(schema_gsql)
+ proposal.drop_dangling_pairs()
+ domain_schema_status = schema_utils_mod.apply_proposal(
+ conn, graphname, proposal
)
+ LogWriter.info(
+ f"Domain schema status for {graphname}: "
+ f"{domain_schema_status['status']} "
+ f"({len(domain_schema_status['statements'])} stmts)"
+ )
+ if domain_schema_status.get("status") == "error":
+ LogWriter.error(
+ f"Domain schema apply failed for {graphname}: "
+ f"{domain_schema_status.get('error')}"
+ )
+ _set_init_state(
+ graphname,
+ state="error",
+ message="Domain schema apply failed",
+ error=domain_schema_status.get("error"),
+ completed_at=time.time(),
+ result={"domain_schema_status": domain_schema_status},
+ )
+ return
- LogWriter.info(f"Graph initialization completed for: {graphname}")
+ LogWriter.info(f"Graph initialization completed for: {graphname}")
- result = {
- "status": "success",
- "message": f"Graph '{graphname}' initialized successfully",
- "graphname": graphname,
- "host_name": conn._tg_connection.host,
- "schema_creation_status": json.dumps(schema_res),
- "index_creation_status": json.dumps(index_res),
- "query_creation_status": json.dumps(query_res),
- }
- if domain_schema_status is not None:
- result["domain_schema_status"] = domain_schema_status
- return result
+ result = {
+ "status": "success",
+ "message": f"Graph '{graphname}' initialized successfully",
+ "graphname": graphname,
+ "host_name": conn._tg_connection.host,
+ "schema_creation_status": json.dumps(schema_res),
+ "index_creation_status": json.dumps(index_res),
+ "query_creation_status": json.dumps(query_res),
+ }
+ if domain_schema_status is not None:
+ result["domain_schema_status"] = domain_schema_status
+
+ _set_init_state(
+ graphname,
+ state="completed",
+ message="Initialization completed successfully",
+ completed_at=time.time(),
+ result=result,
+ )
+ except Exception as e:
+ LogWriter.error(f"Error initializing graph {graphname}: {str(e)}")
+ _set_init_state(
+ graphname,
+ state="error",
+ message=f"Initialization failed: {e}",
+ error=str(e),
+ completed_at=time.time(),
+ )
- except Exception as e:
- LogWriter.error(f"Error initializing graph {graphname}: {str(e)}")
- return {
- "status": "error",
- "message": f"Failed to initialize graph '{graphname}': {str(e)}",
- "details": str(e)
- }
+ bg_tasks.add_task(_run_init)
+ return {
+ "status": "submitted",
+ "graphname": graphname,
+ "message": "Initialization started; poll initialize_status for progress.",
+ }
+
+
+@router.get(route_prefix + "/{graphname}/initialize_status")
+def get_initialize_status(
+ graphname: ValidGraphName,
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+):
+ """Return the current init state for *graphname*.
+
+ States:
+ * ``unknown`` — no init has ever been submitted for this graph
+ (or the worker restarted, dropping in-memory state).
+ * ``queued`` — submitted, background task not yet running.
+ * ``running`` — backend is doing work; ``message`` describes the phase.
+ * ``completed``— done; ``result`` carries the final init payload.
+ * ``error`` — failed; ``error`` carries the failure reason.
+ """
+ return _get_init_state(graphname)
@router.post(route_prefix + "/{graphname}/convert_sample_files")
diff --git a/graphrag/tests/test_e2e_schema_aware_ingest.py b/graphrag/tests/test_e2e_schema_aware_ingest.py
index 7e76896..7d99a9a 100644
--- a/graphrag/tests/test_e2e_schema_aware_ingest.py
+++ b/graphrag/tests/test_e2e_schema_aware_ingest.py
@@ -244,6 +244,12 @@ def test_04_initialize_graph_with_schema():
The endpoint creates the structural GraphRAG schema first, then
applies the domain types in a single atomic schema-change job.
+
+ The endpoint is async-job: POST returns ``{status: "submitted"}``
+ immediately and we poll ``/initialize_status`` until the
+ background task finishes. This avoids the browser/proxy
+ timeout that happens when retriever installs run for many
+ minutes inside one HTTP call.
"""
_require_stage("schema_gsql")
print(f"\n--- Stage 4: Initializing graph with extracted schema ---")
@@ -251,12 +257,52 @@ def test_04_initialize_graph_with_schema():
f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/initialize_graph",
json={"schema_gsql": _state["schema_gsql"]},
auth=AUTH,
- timeout=(60, None),
+ timeout=(60, 60),
)
assert resp.status_code == 200, resp.text
body = resp.json()
- assert body["status"] == "success", body.get("message")
- domain_status = body.get("domain_schema_status") or {}
+ assert body["status"] == "submitted", body
+ print(f"Init job submitted: {body.get('message')}")
+
+ # Poll initialize_status until terminal.
+ init_timeout = int(os.getenv("INIT_TIMEOUT", "1800")) # 30 min
+ poll_interval = 5
+ start = time.time()
+ last_message: str | None = None
+ final_state: dict | None = None
+ while time.time() - start < init_timeout:
+ time.sleep(poll_interval)
+ try:
+ sresp = requests.get(
+ f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/initialize_status",
+ auth=AUTH,
+ timeout=(30, 30),
+ )
+ except requests.RequestException as e:
+ print(f" status poll transient error: {e}; retrying")
+ continue
+ if sresp.status_code != 200:
+ print(f" status poll {sresp.status_code}: {sresp.text[:200]}")
+ continue
+ sdata = sresp.json()
+ msg = sdata.get("message")
+ if msg and msg != last_message:
+ print(f" state={sdata.get('state')} message={msg}")
+ last_message = msg
+ if sdata.get("state") == "completed":
+ final_state = sdata
+ break
+ if sdata.get("state") == "error":
+ pytest.fail(
+ f"Init failed: {sdata.get('error') or sdata.get('message')}"
+ )
+ assert final_state is not None, (
+ f"Init did not reach 'completed' within {init_timeout}s"
+ )
+
+ result = final_state.get("result") or {}
+ assert result.get("status") == "success", result.get("message")
+ domain_status = result.get("domain_schema_status") or {}
print(f"Domain schema status: {domain_status.get('status')}")
print(f"Statements applied: {len(domain_status.get('statements', []))}")
if domain_status.get("metadata"):
From 6bc3847689db25c44d11b5fabad6cc059422ddc5 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Mon, 11 May 2026 10:24:19 -0700
Subject: [PATCH 60/70] Structured hints for schema extraction and surrounding
UX
- Add suggested vertex and edge type chip inputs to the Initialize
Graph dialog, with optional edge endpoint pinning. Hints guide
the schema extractor and persist as the per-graph prompt after a
successful initialization.
- Reject suggested type names that collide with reserved or
structural names inline, with a clear reason.
- Expose the schema-extraction prompt on the Customize Prompts page.
- Add per-card collapse to the draft-schema review form.
- Distinguish image-description calls in the log.
- Recover when ingest is run without a cached job by seeding the
configuration first.
---
CHANGELOG.md | 6 +
common/db/schema_extraction.py | 102 ++++-
common/utils/image_data_extractor.py | 24 +-
graphrag-ui/src/components/ui/tag-input.tsx | 186 +++++++++
.../src/pages/setup/CustomizePrompts.tsx | 9 +-
graphrag-ui/src/pages/setup/IngestGraph.tsx | 50 ++-
graphrag-ui/src/pages/setup/KGAdmin.tsx | 355 +++++++++++++++---
graphrag/app/routers/ui.py | 48 ++-
graphrag/tests/test_schema_extraction.py | 94 ++++-
9 files changed, 794 insertions(+), 80 deletions(-)
create mode 100644 graphrag-ui/src/components/ui/tag-input.tsx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e523d32..25bd4f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,12 @@
- **Init / extract dialogs pause the idle timer for the duration of the long-running call**. The dialog used to log the user out after 60 minutes of "no activity" even while a backend init or schema extraction was in flight; the existing `pauseIdleTimer()` / `resumeIdleTimer()` pattern is now wired into `handleExtractSchema` and `handleInitializeGraph`.
- **Removed two dead vertex-type references from the retriever queries**. `Content_Similarity_Search` and `Content_Similarity_Vector_Search` referenced `Relationship` (never a vertex type — it's the `RELATIONSHIP` edge) and `Concept` (removed in an earlier release); both queries now save as draft with TYP-152 errors against any v1.4.0 graph. The IF-branch is reduced to `s.type == "Entity"` and the existing `Community` branch.
- **Retriever-install error detection no longer false-positives on TG's normal output**. `install_retrievers` and `install_retrievers_async` were doing a substring `"error" in output.lower()` check, which trips on every successful install (TG output contains literals like `0 errors`, `no warnings`). Both now delegate to the existing `gsql_output_error()` helper that matches actual error markers (`SEMANTIC ERROR`, `Failed to create`, transport-level failures).
+- **Suggested types in the Generate-from-samples dialog**. Two chip inputs ("Suggested Vertex Types", "Suggested Edge Types") let the user guide the schema extractor with structured hints. Vertex chips use `Name` or `Name: description`; edge chips additionally accept `Name (From -> To)` to pin direction. Hints render into a `## Suggested types` block injected before the prompt's `## Inputs` section, and the fully-rendered prompt is auto-saved as the graph's per-graph `schema_extraction.txt` override after a successful init — future re-extractions for the same graph reuse the same guidance.
+- **Inline rejection of reserved names in suggested type chips**. New `GET /ui/schema_reserved_names` returns GSQL reserved words and GraphRAG structural type names; the dialog rejects suggestions that would collide with either, before the call is made. Previously such names were silently dropped by the downstream parser, leaving the user wondering why a suggested type didn't appear in the draft.
+- **Schema Extraction prompt is now editable on the Customize Prompts page**, alongside the existing four prompts. Global and per-graph scope both work — the per-graph file is what the auto-save from the suggested-types flow writes.
+- **Per-card collapse in the draft-schema review form**. Each vertex / edge card has a chevron toggle that hides everything except its name row; section headers expose "Collapse all vertex types" / "Collapse all edge types" buttons. Keeps a 30+ type proposal readable without losing edit access.
+- **Multimodal image-description LLM calls are now distinguishable in the log**. `describe_image_with_llm` emits `multimodal_describe: image= model=` before each call and a paired `done` line after, so the per-image vision calls can be filtered out of the chat-completions stream (a single PDF can produce hundreds of them).
+- **Ingest no longer fails with `Data path not found: None`** when the "Ingest Documents into Knowledge Graph" button is clicked without first running the two-step ingest flow. The UI handler now calls `/create_ingest` first when the cached job state is empty, so the backend always receives a well-shaped configuration with the resolved JSONL temp folder.
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/db/schema_extraction.py b/common/db/schema_extraction.py
index 529da11..c1fe07c 100644
--- a/common/db/schema_extraction.py
+++ b/common/db/schema_extraction.py
@@ -25,6 +25,7 @@
from __future__ import annotations
import logging
+import re
from typing import Iterable, List, Optional
from langchain.prompts import PromptTemplate
@@ -213,14 +214,96 @@ def concatenate_samples(
return "".join(parts).lstrip()
+def render_type_hints_block(
+ vertex_hints: Optional[List[dict]] = None,
+ edge_hints: Optional[List[dict]] = None,
+) -> str:
+ """Render structured type hints into a markdown block the LLM
+ can read. Empty inputs return an empty string so the prompt is
+ untouched when the user provides no hints.
+
+ Each hint is a ``{"name": str, "description": str}`` dict.
+ Edge hints may additionally carry ``"fromType"`` and ``"toType"``
+ when the user pinned a direction; the renderer emits
+ ``Name (From → To)`` in that case.
+ """
+ def _row(h: dict, with_endpoints: bool) -> str:
+ name = (h.get("name") or "").strip()
+ if not name:
+ return ""
+ from_type = (h.get("fromType") or "").strip() if with_endpoints else ""
+ to_type = (h.get("toType") or "").strip() if with_endpoints else ""
+ desc = (h.get("description") or "").strip()
+ head = name
+ if from_type and to_type:
+ head = f"{name} ({from_type} → {to_type})"
+ return f"- {head}: {desc}" if desc else f"- {head}"
+
+ def _block(items, label, action, with_endpoints):
+ rows = [r for r in (_row(h, with_endpoints) for h in items or []) if r]
+ if not rows:
+ return ""
+ return f"{label} {action}:\n" + "\n".join(rows)
+
+ blocks = []
+ v_block = _block(
+ vertex_hints, "Vertex types",
+ "to include if their instances appear in the documents", False,
+ )
+ if v_block:
+ blocks.append(v_block)
+ e_block = _block(
+ edge_hints, "Edge types",
+ "to include if supported by the documents", True,
+ )
+ if e_block:
+ blocks.append(e_block)
+ if not blocks:
+ return ""
+ return "## Suggested types\n\n" + "\n\n".join(blocks)
+
+
+def _build_prompt_with_hints(
+ llm_service, hints_block: str
+) -> tuple[PromptTemplate, str]:
+ """Build the prompt template, injecting *hints_block* before the
+ ``## Inputs`` section when non-empty. Falls back to appending if
+ no Inputs marker is found (defensive — the shipped default has it).
+
+ Returns ``(prompt_template, full_template_text)`` so the caller
+ can persist the rendered text as a per-graph override after a
+ successful init.
+ """
+ base = llm_service.schema_extraction_prompt
+ if hints_block:
+ m = re.search(r"^##\s*Inputs\b", base, re.MULTILINE)
+ if m:
+ template_str = base[: m.start()].rstrip() + "\n\n" + hints_block + "\n\n" + base[m.start():]
+ else:
+ template_str = base.rstrip() + "\n\n" + hints_block + "\n"
+ else:
+ template_str = base
+ return (
+ PromptTemplate(
+ template=template_str,
+ input_variables=["samples", "structural_types", "tg_keywords"],
+ ),
+ template_str,
+ )
+
+
def extract_schema_gsql(
llm_service,
samples: Iterable[dict],
max_tokens: Optional[int] = None,
-) -> str:
+ vertex_hints: Optional[List[dict]] = None,
+ edge_hints: Optional[List[dict]] = None,
+) -> tuple[str, str]:
"""Run the schema-extraction prompt against *llm_service*. Returns
- the raw GSQL string the model produced (caller passes it to
- ``schema_utils.parse_gsql_schema``).
+ ``(gsql_text, rendered_prompt)``: the raw GSQL the model produced
+ (caller passes it to ``schema_utils.parse_gsql_schema``) and the
+ fully-rendered prompt template (so the caller can persist it as a
+ per-graph override after a successful init).
*llm_service* must expose ``schema_extraction_prompt`` (from
:class:`common.llm_services.base_llm.LLM_Model`) and the standard
@@ -232,10 +315,16 @@ def extract_schema_gsql(
budget is resolved from ``llm_service.config.token_limit`` if set,
otherwise from the model's default context window. Tests can pass
an explicit *max_tokens* to pin behavior independently of config.
+
+ *vertex_hints* / *edge_hints* are optional ``[{name, description}]``
+ lists from the UI's TagInputs. When non-empty, a "Suggested types"
+ block is injected before the ``## Inputs`` section of the resolved
+ prompt so the LLM treats them as must-include candidates.
"""
if max_tokens is None:
max_tokens = _resolve_sample_token_budget(llm_service)
- prompt = _build_prompt(llm_service)
+ hints_block = render_type_hints_block(vertex_hints, edge_hints)
+ prompt, rendered_template = _build_prompt_with_hints(llm_service, hints_block)
samples_blob = concatenate_samples(samples, max_tokens=max_tokens)
structural_types = ", ".join(
sorted(GRAPHRAG_STRUCTURAL_VERTEX_TYPES | GRAPHRAG_STRUCTURAL_EDGE_TYPES)
@@ -252,6 +341,5 @@ def extract_schema_gsql(
},
caller_name="schema_extraction",
)
- if isinstance(raw, str):
- return raw.strip()
- return str(raw).strip()
+ gsql_text = raw.strip() if isinstance(raw, str) else str(raw).strip()
+ return gsql_text, rendered_template
diff --git a/common/utils/image_data_extractor.py b/common/utils/image_data_extractor.py
index f925e9d..59c6178 100644
--- a/common/utils/image_data_extractor.py
+++ b/common/utils/image_data_extractor.py
@@ -38,7 +38,9 @@ def describe_image_with_llm(file_path):
"""
try:
from PIL import Image as PILImage
-
+ import os
+ import time
+
client = _get_client()
if not client:
return "Image: Failed to create multimodal LLM client"
@@ -70,7 +72,27 @@ def describe_image_with_llm(file_path):
]
langchain_client = client.llm
+ # Tag the upcoming chat completion as a multimodal image
+ # describe so it's distinguishable from text-only completions
+ # in the log stream (e.g. schema extraction, retriever LLM
+ # calls). Image-describe runs are typically dozens-to-hundreds
+ # per PDF, while text completions are one-shot.
+ image_basename = os.path.basename(str(file_path))
+ model_name = (
+ getattr(_multimodal_client, "config", {}).get("llm_model")
+ if _multimodal_client else None
+ ) or "?"
+ logger.info(
+ f"multimodal_describe: image={image_basename} "
+ f"model={model_name} provider={_multimodal_provider}"
+ )
+ t0 = time.monotonic()
response = langchain_client.invoke(messages)
+ elapsed = time.monotonic() - t0
+ logger.info(
+ f"multimodal_describe done: image={image_basename} "
+ f"elapsed={elapsed:.2f}s"
+ )
return response.content if hasattr(response, "content") else str(response)
except Exception as e:
error_str = str(e).lower()
diff --git a/graphrag-ui/src/components/ui/tag-input.tsx b/graphrag-ui/src/components/ui/tag-input.tsx
new file mode 100644
index 0000000..80311ee
--- /dev/null
+++ b/graphrag-ui/src/components/ui/tag-input.tsx
@@ -0,0 +1,186 @@
+import React, { useState, KeyboardEvent } from "react";
+
+export interface TypeHint {
+ name: string;
+ description: string;
+ // Edge variants only: ``Name (From -> To)`` captures the endpoint
+ // pair the user has in mind. Vertex chips leave these undefined.
+ fromType?: string;
+ toType?: string;
+}
+
+interface TagInputProps {
+ values: TypeHint[];
+ onChange: (values: TypeHint[]) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ ariaLabel?: string;
+ // When true, the parser accepts ``Name (From -> To)`` (and the
+ // unicode ``→``) before the optional ``: description``. Used on the
+ // Suggested Edge Types row only — vertex chips never carry
+ // endpoints.
+ acceptsEndpoints?: boolean;
+ // Map of lowercased names to a human-readable reason for rejection.
+ // Lets the parent reject GSQL reserved words, GraphRAG structural
+ // types, etc., with a clear message instead of a silent drop later.
+ forbiddenNames?: Record;
+}
+
+// Vertex format: ``Name`` or ``Name: description``.
+const VERTEX_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*(.*))?\s*$/;
+// Edge format: ``Name``, ``Name (From -> To)``, ``Name: description``,
+// or ``Name (From -> To): description``. ``->`` and unicode ``→`` both
+// accepted as the arrow.
+const EDGE_RE =
+ /^([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:->|→)\s*([A-Za-z_][A-Za-z0-9_]*)\s*\))?\s*(?::\s*(.*))?\s*$/;
+
+const MAX_DESC_DISPLAY = 32;
+
+const formatChip = (hint: TypeHint): string => {
+ let label = hint.name;
+ if (hint.fromType && hint.toType) {
+ label += ` (${hint.fromType} → ${hint.toType})`;
+ }
+ if (!hint.description) return label;
+ const desc =
+ hint.description.length > MAX_DESC_DISPLAY
+ ? hint.description.slice(0, MAX_DESC_DISPLAY - 1) + "…"
+ : hint.description;
+ return `${label}: ${desc}`;
+};
+
+export const TagInput: React.FC = ({
+ values,
+ onChange,
+ placeholder,
+ disabled,
+ ariaLabel,
+ acceptsEndpoints = false,
+ forbiddenNames,
+}) => {
+ const [draft, setDraft] = useState("");
+ const [error, setError] = useState(null);
+
+ const reasonFor = (name: string): string | null => {
+ if (!forbiddenNames) return null;
+ return forbiddenNames[name.toLowerCase()] || null;
+ };
+
+ const commit = () => {
+ const text = draft.trim();
+ if (!text) return;
+ const re = acceptsEndpoints ? EDGE_RE : VERTEX_RE;
+ const m = re.exec(text);
+ if (!m) {
+ setError(
+ acceptsEndpoints
+ ? `"${text}" is not valid. Use \`Name\`, \`Name: description\`, \`Name (From -> To)\`, or \`Name (From -> To): description\`.`
+ : `"${text}" is not valid. Use \`Name\` or \`Name: description\` (names must start with a letter or underscore).`
+ );
+ return;
+ }
+ const name = m[1];
+ const fromType = acceptsEndpoints ? (m[2] || "").trim() : "";
+ const toType = acceptsEndpoints ? (m[3] || "").trim() : "";
+ const descriptionIdx = acceptsEndpoints ? 4 : 2;
+ const description = (m[descriptionIdx] || "").trim();
+
+ // Reject reserved/structural names — for every name reference
+ // (the type name itself + the optional endpoint vertex types).
+ for (const candidate of [name, fromType, toType].filter(Boolean)) {
+ const reason = reasonFor(candidate);
+ if (reason) {
+ setError(`"${candidate}" cannot be used: ${reason}`);
+ return;
+ }
+ }
+
+ if (values.some((v) => v.name.toLowerCase() === name.toLowerCase())) {
+ setError(`"${name}" is already in the list.`);
+ return;
+ }
+
+ onChange([
+ ...values,
+ {
+ name,
+ description,
+ ...(fromType ? { fromType } : {}),
+ ...(toType ? { toType } : {}),
+ },
+ ]);
+ setDraft("");
+ setError(null);
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter" || e.key === ",") {
+ e.preventDefault();
+ commit();
+ } else if (e.key === "Backspace" && draft === "" && values.length > 0) {
+ // Remove last chip on backspace from empty input.
+ onChange(values.slice(0, -1));
+ }
+ };
+
+ const remove = (idx: number) => {
+ onChange(values.filter((_, i) => i !== idx));
+ };
+
+ return (
+
+
+ {values.map((v, i) => (
+
+ {formatChip(v)}
+ {!disabled && (
+ remove(i)}
+ className="text-blue-700 dark:text-blue-200 hover:text-red-600"
+ aria-label={`Remove ${v.name}`}
+ >
+ ×
+
+ )}
+
+ ))}
+ {
+ setDraft(e.target.value);
+ if (error) setError(null);
+ }}
+ onKeyDown={handleKeyDown}
+ onBlur={commit}
+ placeholder={values.length === 0 ? placeholder : ""}
+ disabled={disabled}
+ className="flex-1 min-w-[120px] bg-transparent outline-none text-sm py-0.5"
+ />
+
+ {error && (
+
{error}
+ )}
+
+ );
+};
+
+export default TagInput;
diff --git a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
index d16fe59..b22c702 100644
--- a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
+++ b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
@@ -11,6 +11,7 @@ const ALL_PROMPT_TYPES = [
{ id: "entity_relationship", name: "Entity Relationships", description: "Configure entity and relationship extraction from document chunks" },
{ id: "community_summarization", name: "Community Summarization", description: "Define how community summaries are generated" },
{ id: "query_generation", name: "Schema Instructions", description: "Configure instructions for schema filtering and schema generation" },
+ { id: "schema_extraction", name: "Schema Extraction", description: "Define the rules the LLM follows when proposing a domain schema from sample documents" },
];
const CustomizePrompts = () => {
@@ -29,14 +30,16 @@ const CustomizePrompts = () => {
entity_relationship: "",
community_summarization: "",
query_generation: "",
+ schema_extraction: "",
});
-
+
// Template variables that should not be edited (stored separately)
const [promptTemplates, setPromptTemplates] = useState({
chatbot_response: "",
entity_relationship: "",
community_summarization: "",
query_generation: "",
+ schema_extraction: "",
});
// Only render prompt types the backend returned for this user
@@ -126,6 +129,9 @@ const CustomizePrompts = () => {
query_generation: data.prompts.query_generation?.editable_content !== undefined
? data.prompts.query_generation.editable_content
: (typeof data.prompts.query_generation === 'string' ? data.prompts.query_generation : ""),
+ schema_extraction: data.prompts.schema_extraction?.editable_content !== undefined
+ ? data.prompts.schema_extraction.editable_content
+ : (typeof data.prompts.schema_extraction === 'string' ? data.prompts.schema_extraction : ""),
});
// Store template variables separately
@@ -134,6 +140,7 @@ const CustomizePrompts = () => {
entity_relationship: data.prompts.entity_relationship?.template_variables || "",
community_summarization: data.prompts.community_summarization?.template_variables || "",
query_generation: data.prompts.query_generation?.template_variables || "",
+ schema_extraction: data.prompts.schema_extraction?.template_variables || "",
});
// Set configured provider
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index c36a9de..81e4a17 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -488,17 +488,45 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
const creds = sessionStorage.getItem("creds");
const folderPath = sourceType === "uploaded" ? `uploads/${ingestGraphName}` : `downloaded_files_cloud/${ingestGraphName}`;
- // Use existing ingestJobData if available, otherwise construct from folder path
- const jobData = ingestJobData || {
- load_job_id: "load_documents_content_json",
- data_source_id: {
- data_source: "server",
- data_source_config: { data_path: folderPath },
- loader_config: {},
- file_format: "multi"
- },
- data_path: folderPath,
- };
+ // If no cached job from a prior create_ingest, run it now. The
+ // backend's /ingest endpoint expects the data_source_id dict
+ // shape that create_ingest emits (with the resolved JSONL temp
+ // folder at top-level ``data_path``); building a fallback in
+ // the UI loses that contract.
+ let jobData = ingestJobData;
+ if (!jobData) {
+ setIngestMessage("Step 1/2: Preparing ingest job...");
+ const createResp = await fetch(
+ `/ui/${ingestGraphName}/create_ingest`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Basic ${creds}`,
+ },
+ body: JSON.stringify({
+ data_source: "server",
+ data_source_config: { data_path: folderPath },
+ loader_config: {},
+ file_format: "multi",
+ }),
+ }
+ );
+ if (!createResp.ok) {
+ const err = await createResp.json();
+ throw new Error(
+ err.detail || `Failed to create ingest job: ${createResp.statusText}`
+ );
+ }
+ const createData = await createResp.json();
+ jobData = {
+ load_job_id: createData.load_job_id,
+ data_source_id: createData.data_source_id,
+ data_path: createData.data_path || folderPath,
+ };
+ setIngestJobData(jobData);
+ setIngestMessage("Step 2/2: Loading documents into knowledge graph...");
+ }
const ingestResponse = await fetch(`/ui/${ingestGraphName}/ingest`, {
method: "POST",
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index 089efcb..c356f67 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { TagInput, TypeHint } from "@/components/ui/tag-input";
import { Database, Loader2, RefreshCw, Upload } from "lucide-react";
import { pauseIdleTimer, resumeIdleTimer } from "@/hooks/useIdleTimeout";
import {
@@ -56,6 +57,11 @@ const KGAdmin = () => {
setSampleFiles([]);
setExtractedFingerprint(null);
setAttributesCollapsed(false);
+ setCollapsedVertices(new Set());
+ setCollapsedEdges(new Set());
+ setVertexHints([]);
+ setEdgeHints([]);
+ setRenderedSchemaPrompt("");
setIsInitComplete(false);
};
@@ -109,6 +115,24 @@ const KGAdmin = () => {
const [maxSampleFiles, setMaxSampleFiles] = useState(5);
const [maxTotalMb, setMaxTotalMb] = useState(50);
const [isExtractingSchema, setIsExtractingSchema] = useState(false);
+ // Optional structured guidance for the schema-extraction LLM.
+ // Each chip is a ``{name, description}`` pair entered as
+ // ``Name`` or ``Name: description`` in the TagInput. Backend
+ // injects these as a "Suggested types" block in the resolved
+ // prompt; on init success the rendered prompt is persisted as
+ // the per-graph override so future re-extractions reuse it.
+ const [vertexHints, setVertexHints] = useState([]);
+ const [edgeHints, setEdgeHints] = useState([]);
+ // Captures the rendered prompt returned by /extract_schema_from_jsonl
+ // so the post-init save flow can write it as the per-graph override.
+ const [renderedSchemaPrompt, setRenderedSchemaPrompt] = useState("");
+ // Lookup of names the user CAN'T pick for suggested types: GSQL
+ // reserved words + GraphRAG structural type names. Keyed by
+ // lowercased name → reason string the TagInput surfaces inline.
+ // Same forbidden set feeds both Suggested Vertex Types and
+ // Suggested Edge Types so e.g. ``Document`` is rejected as a
+ // vertex name AND as an edge endpoint reference.
+ const [forbiddenNames, setForbiddenNames] = useState>({});
// Fingerprint of the file set used for the most recent successful
// extraction. Used to disable the *Extract draft schema* button
// when the same files are selected (no new work to do).
@@ -116,6 +140,53 @@ const KGAdmin = () => {
// True when the form-mode editor's per-card attribute lists are
// hidden, for a cleaner overview of types.
const [attributesCollapsed, setAttributesCollapsed] = useState(false);
+ // Per-card collapse state in the draft-schema review form. When a
+ // type's index is in the set, only its name + description are shown
+ // (everything else — attributes, edge endpoints — is hidden).
+ // ``allCollapsed`` drives the toggle button label and lets us flip
+ // every card at once.
+ const [collapsedVertices, setCollapsedVertices] = useState>(new Set());
+ const [collapsedEdges, setCollapsedEdges] = useState>(new Set());
+ const allVerticesCollapsed =
+ !!draftProposal &&
+ draftProposal.vertices.length > 0 &&
+ collapsedVertices.size === draftProposal.vertices.length;
+ const allEdgesCollapsed =
+ !!draftProposal &&
+ draftProposal.edges.length > 0 &&
+ collapsedEdges.size === draftProposal.edges.length;
+ const toggleVertexCollapsed = (idx: number) =>
+ setCollapsedVertices((prev) => {
+ const next = new Set(prev);
+ if (next.has(idx)) next.delete(idx);
+ else next.add(idx);
+ return next;
+ });
+ const toggleEdgeCollapsed = (idx: number) =>
+ setCollapsedEdges((prev) => {
+ const next = new Set(prev);
+ if (next.has(idx)) next.delete(idx);
+ else next.add(idx);
+ return next;
+ });
+ const toggleAllVerticesCollapsed = () => {
+ if (!draftProposal) return;
+ if (allVerticesCollapsed) {
+ setCollapsedVertices(new Set());
+ } else {
+ setCollapsedVertices(
+ new Set(draftProposal.vertices.map((_, i) => i))
+ );
+ }
+ };
+ const toggleAllEdgesCollapsed = () => {
+ if (!draftProposal) return;
+ if (allEdgesCollapsed) {
+ setCollapsedEdges(new Set());
+ } else {
+ setCollapsedEdges(new Set(draftProposal.edges.map((_, i) => i)));
+ }
+ };
const fingerprintFiles = (files: File[]): string =>
files
@@ -123,7 +194,12 @@ const KGAdmin = () => {
.sort()
.join("|");
- const sampleFingerprint = fingerprintFiles(sampleFiles);
+ // Composite fingerprint covering both the file set AND the hint
+ // chips so changing either re-enables the Extract button.
+ const sampleFingerprint =
+ fingerprintFiles(sampleFiles) +
+ "|hints:" +
+ JSON.stringify({ v: vertexHints, e: edgeHints });
const PRIMITIVE_TYPES = [
"STRING",
@@ -253,6 +329,32 @@ const KGAdmin = () => {
.catch(() => {
/* fall back to defaults */
});
+ // Pull the list of names the user can't pick for suggested types.
+ // Empty / failed response leaves ``forbiddenNames`` as ``{}`` so
+ // the TagInput falls back to format-only validation — the
+ // downstream parser would still drop reserved/structural names,
+ // just without the inline message.
+ fetch(`/ui/schema_reserved_names`, {
+ headers: { Authorization: `Basic ${creds}` },
+ })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ if (!data) return;
+ const map: Record = {};
+ for (const w of data.gsql_keywords || []) {
+ map[String(w).toLowerCase()] = "GSQL reserved word";
+ }
+ for (const t of data.structural_vertex_types || []) {
+ map[String(t).toLowerCase()] = "reserved structural vertex type";
+ }
+ for (const t of data.structural_edge_types || []) {
+ map[String(t).toLowerCase()] = "reserved structural edge type";
+ }
+ setForbiddenNames(map);
+ })
+ .catch(() => {
+ /* keep current value; not fatal */
+ });
}, [initializeDialogOpen]);
const handleSampleFileSelect = (e: React.ChangeEvent) => {
@@ -335,13 +437,22 @@ const KGAdmin = () => {
Authorization: `Basic ${creds}`,
"Content-Type": "application/json",
},
- body: JSON.stringify({ filenames: convertData.saved_files || [] }),
+ body: JSON.stringify({
+ filenames: convertData.saved_files || [],
+ vertex_hints: vertexHints,
+ edge_hints: edgeHints,
+ }),
}
);
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.detail || `Extraction failed: ${resp.statusText}`);
}
+ // Stash the rendered prompt so the post-init save can write it
+ // as the per-graph schema_extraction.txt override.
+ if (typeof data.rendered_prompt === "string") {
+ setRenderedSchemaPrompt(data.rendered_prompt);
+ }
const proposal = data.proposal;
if (
!proposal ||
@@ -375,7 +486,9 @@ const KGAdmin = () => {
})),
})),
});
- setExtractedFingerprint(fingerprintFiles(sampleFiles));
+ // Capture the composite fingerprint (files + hint chips) so the
+ // Extract button stays disabled until something changes.
+ setExtractedFingerprint(sampleFingerprint);
setStatusMessage(
`Draft schema ready (${data.summary?.vertex_count ?? "?"} vertex types, ` +
`${data.summary?.edge_count ?? "?"} edge types). Review/edit below, then click Initialize.`
@@ -546,6 +659,31 @@ const KGAdmin = () => {
setStatusType("success");
setIsInitComplete(true);
+ // If the user supplied any structured hints, persist the
+ // rendered prompt (default + suggested-types block) as the
+ // per-graph schema_extraction.txt override so future
+ // re-extractions on this graph reuse the same hints. Failure
+ // here is non-fatal — the init itself already succeeded.
+ const hintCount = vertexHints.length + edgeHints.length;
+ if (hintCount > 0 && renderedSchemaPrompt) {
+ try {
+ await fetch("/ui/prompts", {
+ method: "POST",
+ headers: {
+ Authorization: `Basic ${creds}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ graphname: graphName,
+ prompt_type: "schema_extraction",
+ editable_content: renderedSchemaPrompt,
+ }),
+ });
+ } catch (e) {
+ console.warn("Saving per-graph schema prompt failed (non-fatal):", e);
+ }
+ }
+
const newGraph = graphName;
setAvailableGraphs(prev => {
if (!prev.includes(newGraph)) {
@@ -925,6 +1063,46 @@ const KGAdmin = () => {
{sampleFiles.length > 0 &&
` (${(sampleFiles.reduce((s, f) => s + f.size, 0) / (1024 * 1024)).toFixed(1)} MB)`}
+
+
+ Suggested types (optional). Vertex format:{" "}
+ Name{" "}
+ or{" "}
+ Name: description.
+ Edge format adds an optional endpoint pair:{" "}
+ Name (From -> To){" "}
+ or{" "}
+ Name (From -> To): description.
+ Press Enter or comma to add each entry.
+
+
+
+ Suggested Vertex Types
+
+
+
+
+
+ Suggested Edge Types
+
+
+
+
{
{draftProposal && (
-
+
Review and edit the draft below. Each vertex auto-gets a primary
key id (STRING) — you don't need to add it. Click
@@ -971,31 +1149,47 @@ const KGAdmin = () => {
{/* Vertex types */}
-
+
Vertex types ({draftProposal.vertices.length})
-
- setDraftProposal((p) =>
- p
- ? {
- ...p,
- vertices: [
- ...p.vertices,
- { name: "", description: "", attributes: [] },
- ],
- }
- : p
- )
- }
- className="text-xs h-7 dark:border-[#3D3D3D]"
- >
- + Add vertex
-
+
+
+ {allVerticesCollapsed
+ ? "Expand all vertex types"
+ : "Collapse all vertex types"}
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ vertices: [
+ ...p.vertices,
+ { name: "", description: "", attributes: [] },
+ ],
+ }
+ : p
+ )
+ }
+ className="text-xs h-7 dark:border-[#3D3D3D]"
+ >
+ + Add vertex
+
+
))}
@@ -1207,36 +1420,52 @@ const KGAdmin = () => {
{/* Edge types */}
-
+
Edge types ({draftProposal.edges.length})
-
- setDraftProposal((p) =>
- p
- ? {
- ...p,
- edges: [
- ...p.edges,
- {
- name: "",
- description: "",
- pairs: [["", ""]],
- attributes: [],
- },
- ],
- }
- : p
- )
- }
- className="text-xs h-7 dark:border-[#3D3D3D]"
- >
- + Add edge
-
+
+
+ {allEdgesCollapsed
+ ? "Expand all edge types"
+ : "Collapse all edge types"}
+
+
+ setDraftProposal((p) =>
+ p
+ ? {
+ ...p,
+ edges: [
+ ...p.edges,
+ {
+ name: "",
+ description: "",
+ pairs: [["", ""]],
+ attributes: [],
+ },
+ ],
+ }
+ : p
+ )
+ }
+ className="text-xs h-7 dark:border-[#3D3D3D]"
+ >
+ + Add edge
+
+
))}
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index f6d3798..2136ba0 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -389,6 +389,35 @@ def list_graphs(auth: Annotated[list[str], Depends(ui_basic_auth)]):
return {"graphs": auth[0]}
+@router.get(f"{route_prefix}/schema_reserved_names")
+def schema_reserved_names(
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+):
+ """Return name sets the UI uses to reject suggested types up-front
+ in the Initialize Graph dialog. The downstream parser silently
+ drops these anyway, but inline rejection gives the user a clear
+ reason instead of a confusing "type didn't appear in the draft".
+
+ Returns three lists:
+ * ``gsql_keywords`` — GSQL reserved words (sourced from
+ pyTigerGraph). Naming a vertex/edge type after one would crash
+ the schema-change job.
+ * ``structural_vertex_types`` — GraphRAG always-present vertex
+ types (Document, DocumentChunk, Entity, ...).
+ * ``structural_edge_types`` — GraphRAG always-present edge
+ types (HAS_CONTENT, CONTAINS_ENTITY, ...).
+ """
+ return {
+ "gsql_keywords": sorted(schema_utils_mod.get_gsql_reserved_words()),
+ "structural_vertex_types": sorted(
+ schema_utils_mod.GRAPHRAG_STRUCTURAL_VERTEX_TYPES
+ ),
+ "structural_edge_types": sorted(
+ schema_utils_mod.GRAPHRAG_STRUCTURAL_EDGE_TYPES
+ ),
+ }
+
+
@router.post(f"{route_prefix}/feedback")
def add_feedback(
message: Message,
@@ -851,13 +880,21 @@ def extract_schema_from_jsonl(
detail="No extractable text in the converted files.",
)
+ # Optional structured hints from the UI (TagInput chips). Each
+ # hint is ``{"name": str, "description": str}``. Backend ignores
+ # malformed entries silently — names are validated client-side.
+ vertex_hints = (payload or {}).get("vertex_hints") if isinstance(payload, dict) else None
+ edge_hints = (payload or {}).get("edge_hints") if isinstance(payload, dict) else None
+
LogWriter.info(
f"Running schema extraction LLM for {graphname} "
- f"({len(jsonl_paths)} JSONLs, {len(samples)} doc parts)"
+ f"({len(jsonl_paths)} JSONLs, {len(samples)} doc parts, "
+ f"{len(vertex_hints or [])} vertex hints, {len(edge_hints or [])} edge hints)"
)
llm_service = get_llm_service(get_completion_config(graphname))
- gsql_text = schema_extraction_mod.extract_schema_gsql(
- llm_service, samples
+ gsql_text, rendered_prompt = schema_extraction_mod.extract_schema_gsql(
+ llm_service, samples,
+ vertex_hints=vertex_hints, edge_hints=edge_hints,
)
proposal = schema_utils_mod.parse_gsql_schema(gsql_text)
proposal.drop_dangling_pairs()
@@ -868,6 +905,11 @@ def extract_schema_from_jsonl(
"preview_gsql": schema_utils_mod.emit_preview_gsql(proposal),
"proposal": proposal.to_dict(),
"summary": schema_utils_mod.summarize(proposal),
+ # The fully-rendered prompt (default + suggested-types block).
+ # The UI saves this verbatim as the per-graph override after a
+ # successful initialize_graph so the addendum survives the
+ # session.
+ "rendered_prompt": rendered_prompt,
}
diff --git a/graphrag/tests/test_schema_extraction.py b/graphrag/tests/test_schema_extraction.py
index 092235a..8ef2b5d 100644
--- a/graphrag/tests/test_schema_extraction.py
+++ b/graphrag/tests/test_schema_extraction.py
@@ -180,9 +180,10 @@ class _LLM:
def test_extract_schema_gsql_passes_structural_and_keyword_lists_to_llm():
llm = _CapturingLLM(response="// A company.\nADD VERTEX Company();")
samples = [{"doc_id": "x", "content": "Acme Corp issues bonds."}]
- out = schema_extraction.extract_schema_gsql(llm, samples)
+ out, rendered = schema_extraction.extract_schema_gsql(llm, samples)
assert out.startswith("// A company.")
+ assert "Stub schema-extraction prompt" in rendered
assert len(llm.calls) == 1
inputs = llm.calls[0]["inputs"]
assert "samples" in inputs
@@ -215,7 +216,7 @@ def __str__(self): # noqa: D401
return "ADD VERTEX Foo();"
llm = _CapturingLLM(response=_ObjResp())
- out = schema_extraction.extract_schema_gsql(
+ out, _ = schema_extraction.extract_schema_gsql(
llm, [{"doc_id": "x", "content": "y"}]
)
assert "ADD VERTEX Foo" in out
@@ -244,7 +245,7 @@ def test_extract_schema_gsql_round_trips_through_parser():
"UNDIRECTED EDGE COLLEAGUE_OF(FROM Person, TO Person);\n"
)
llm = _CapturingLLM(response=response)
- gsql = schema_extraction.extract_schema_gsql(
+ gsql, _ = schema_extraction.extract_schema_gsql(
llm, [{"doc_id": "x", "content": "y"}]
)
proposal = parse_gsql_schema(gsql)
@@ -277,7 +278,7 @@ def schema_extraction_prompt(self) -> str:
)
llm = _StubLLM(response="// V.\nVERTEX V();")
- out = schema_extraction.extract_schema_gsql(
+ out, _ = schema_extraction.extract_schema_gsql(
llm, [{"doc_id": "x", "content": "y"}]
)
assert "VERTEX V" in out
@@ -289,6 +290,91 @@ def schema_extraction_prompt(self) -> str:
assert "tg_keywords" in inputs
+def test_render_type_hints_block_renders_both_categories():
+ block = schema_extraction.render_type_hints_block(
+ vertex_hints=[
+ {"name": "Company", "description": "publicly listed corporation"},
+ {"name": "Filing"},
+ ],
+ edge_hints=[
+ {"name": "PUBLISHES", "description": "Company publishes a Filing"},
+ ],
+ )
+ assert "## Suggested types" in block
+ assert "Vertex types to include" in block
+ assert "- Company: publicly listed corporation" in block
+ assert "- Filing" in block
+ assert "Edge types to include" in block
+ assert "- PUBLISHES: Company publishes a Filing" in block
+
+
+def test_render_type_hints_block_emits_endpoint_pair_for_edges():
+ """Edge hints carrying ``fromType`` / ``toType`` render with the
+ ``Name (From → To)`` form so the LLM sees the user's direction.
+ Vertex hints ignore endpoint fields even when present (defensive).
+ """
+ block = schema_extraction.render_type_hints_block(
+ vertex_hints=[
+ # Endpoint fields on a vertex hint are silently ignored.
+ {"name": "Company", "fromType": "X", "toType": "Y"},
+ ],
+ edge_hints=[
+ {
+ "name": "PUBLISHES",
+ "fromType": "Company",
+ "toType": "Filing",
+ "description": "Company publishes a Filing",
+ },
+ {"name": "OWNS", "fromType": "Company", "toType": "Asset"},
+ # No endpoints — renders as plain ``- WORKS_AT``.
+ {"name": "WORKS_AT"},
+ ],
+ )
+ # Vertex row is unchanged.
+ assert "- Company\n" in block or "- Company" in block.split("Edge types")[0]
+ # Edge rows include the endpoint arrow.
+ assert "- PUBLISHES (Company → Filing): Company publishes a Filing" in block
+ assert "- OWNS (Company → Asset)" in block
+ assert "- WORKS_AT" in block
+
+
+def test_render_type_hints_block_empty_returns_empty_string():
+ assert schema_extraction.render_type_hints_block(None, None) == ""
+ assert schema_extraction.render_type_hints_block([], []) == ""
+
+
+def test_extract_schema_gsql_injects_hints_block_into_prompt():
+ """The hints block must reach the LLM via the rendered prompt
+ template, and the rendered template returned to the caller must
+ contain the same block (so the UI can persist it as the per-graph
+ override after a successful init).
+ """
+
+ class _StubLLM(_CapturingLLM):
+ @property
+ def schema_extraction_prompt(self) -> str:
+ return (
+ "Schema extraction.\n\n"
+ "## Inputs\n"
+ "{samples}\n{structural_types}\n{tg_keywords}\n"
+ )
+
+ llm = _StubLLM(response="VERTEX V();")
+ out, rendered = schema_extraction.extract_schema_gsql(
+ llm,
+ [{"doc_id": "x", "content": "y"}],
+ vertex_hints=[{"name": "Company", "description": "a corp"}],
+ edge_hints=[{"name": "OWNS"}],
+ )
+ assert "VERTEX V" in out
+ # Rendered prompt has the hints block injected before the Inputs
+ # section so the LLM treats hints as guidance, not as content.
+ assert "## Suggested types" in rendered
+ assert "- Company: a corp" in rendered
+ assert "- OWNS" in rendered
+ assert rendered.index("## Suggested types") < rendered.index("## Inputs")
+
+
def test_extract_schema_gsql_propagates_missing_prompt_file():
"""If llm_service.schema_extraction_prompt raises FileNotFoundError,
extract_schema_gsql must propagate — no silent fallback. The
From 8efa08402e1b440a4d03f1d143dad651f4c4f82f Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Mon, 11 May 2026 15:41:09 -0700
Subject: [PATCH 61/70] Add Query Guidance prompt, image versioning, and ECC
rebuild fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add Query Guidance — a free-form, optional partial that the user
edits on Customize Prompts and that injects into the four
query-related prompts. Customize Prompts is also reordered by
graph lifecycle.
- Stamp each image with the repo-root VERSION and a build date;
expose them via /ui/version and a small footer on the Setup page.
- Stop aborting graph rebuilds on TigerGraph's normal success line.
- Center the Knowledge Graph Setup cards in the row.
- Make the prompt-customization E2E test revert on failure.
---
CHANGELOG.md | 5 +
common/llm_services/base_llm.py | 43 ++++-
common/utils/prompt_validation.py | 9 +-
docker-compose.yml | 8 +-
ecc/Dockerfile | 5 +-
ecc/app/graphrag/util.py | 5 +-
ecc/app/main.py | 21 ++
ecc/app/supportai/util.py | 5 +-
graphrag-ui/Dockerfile | 10 +-
.../src/pages/setup/CustomizePrompts.tsx | 61 ++----
graphrag-ui/src/pages/setup/KGAdmin.tsx | 2 +-
graphrag-ui/src/pages/setup/SetupLayout.tsx | 28 ++-
graphrag/Dockerfile | 10 +-
graphrag/app/routers/ui.py | 87 ++++++++-
graphrag/app/tools/generate_cypher.py | 7 +-
graphrag/app/tools/generate_function.py | 4 +-
graphrag/app/tools/generate_gsql.py | 7 +-
graphrag/app/tools/map_question_to_schema.py | 9 +-
.../tests/test_e2e_prompt_customization.py | 179 +++++++++++-------
19 files changed, 370 insertions(+), 135 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25bd4f3..c29db78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,11 @@
- **Per-card collapse in the draft-schema review form**. Each vertex / edge card has a chevron toggle that hides everything except its name row; section headers expose "Collapse all vertex types" / "Collapse all edge types" buttons. Keeps a 30+ type proposal readable without losing edit access.
- **Multimodal image-description LLM calls are now distinguishable in the log**. `describe_image_with_llm` emits `multimodal_describe: image= model=` before each call and a paired `done` line after, so the per-image vision calls can be filtered out of the chat-completions stream (a single PDF can produce hundreds of them).
- **Ingest no longer fails with `Data path not found: None`** when the "Ingest Documents into Knowledge Graph" button is clicked without first running the two-step ingest flow. The UI handler now calls `/create_ingest` first when the cached job state is empty, so the backend always receives a well-shaped configuration with the resolved JSONL temp folder.
+- **Query Guidance** — a free-form, optional partial that the user edits on the Customize Prompts page. Empty by default; when configured, the rendered block is injected after the hard rules in `map_question_to_schema`, `generate_function`, `generate_cypher`, and `generate_gsql`. Length-capped at 8000 characters and brace-escaped server-side. The page is also reordered by graph lifecycle (setup → ingest → rebuild → query) and the now-redundant "Configured LLM Provider" field is removed.
+- **Stop aborting graph rebuilds on TigerGraph's normal success line**. `ecc.app.graphrag.util.install_queries` and its supportai sibling were raising on the literal `"failed" in res.lower()` substring — TG's success line `succeeded: N, skipped: 0, failed: 0` tripped that check and rolled back the rebuild. Both now use the existing `gsql_output_error()` helper.
+- **Image version stamped at build time**. Each image now carries the repo-root `VERSION` file plus a `/code/BUILD_DATE` written at build time; `GET /ui/version` aggregates the three components for support checks, and the Setup pages show a small "Version " line at the bottom-center. Plain `docker compose build` works as-is; no env vars or helper scripts required.
+- **Prompt-customization E2E test always reverts on failure**. The schema-extraction round-trip test could leak its `[E2E TEST EDIT — schema_extraction]` marker into `configs/prompts/schema_extraction.txt` whenever a mid-flight assertion failed; both the chatbot-response and schema-extraction tests now wrap their save-then-assert in `try/finally` (or `try/except`) so the revert always runs.
+- **Knowledge Graph Setup cards centered**. The three setup cards (Initialize / Ingest / Refresh) drop from a 4-column grid at large breakpoints to a 3-column grid so they fill the row evenly instead of leaving an empty fourth column.
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index aa7729f..48897b4 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -189,7 +189,7 @@ def map_question_schema_prompt(self):
return result
return """# Map Question to Schema
-Replace entities and relationships in the question with their canonical schema names provided in the Inputs section below.
+Replace each entity in the question with its corresponding **vertex type name**, and each relationship with its corresponding **edge type name**, using the canonical schema names in the Inputs section below.
## Rules
- If an entity (e.g. "John Doe") is referred to by different names or pronouns ("Joe", "he"), use the most complete identifier ("John Doe") consistently.
@@ -200,6 +200,8 @@ def map_question_schema_prompt(self):
- Generate the **complete** rewritten question. Keep the case of schema elements unchanged.
- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question.
+{query_guidance}
+
## Inputs
- **Vertices**: {vertices}
- **Vertex attributes**: {verticesAttrs}
@@ -229,6 +231,8 @@ def generate_function_prompt(self):
- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question.
- Pick exactly **one** function to execute.
+{query_guidance}
+
## Schema
- **Vertex Types**: {vertex_types}
- **Vertex Attributes**: {vertex_attributes}
@@ -321,6 +325,8 @@ def generate_cypher_prompt(self):
- For "summarize" / "write a summary" questions, fetch all neighbour nodes and edges.
- Avoid invalid queries based on errors in the history above.
+{query_guidance}
+
## Supported
- **Clauses**: `MATCH`, `OPTIONAL MATCH`, `MANDATORY MATCH`, `WHERE`, `RETURN`, `WITH`, `ORDER BY`, `SKIP`, `LIMIT`, `DELETE`, `DETACH DELETE`
- **Operators**:
@@ -374,6 +380,8 @@ def generate_gsql_prompt(self):
- Use aliases for `ORDER BY`. Aliases / attributes used in `ORDER BY` must also be in `PRINT`. Always specify `ASC` / `DESC` based on data type.
- Avoid invalid queries based on errors in the history above.
+{query_guidance}
+
## Unsupported
- **Clauses**: `CREATE`, `DELETE`, `INSERT`, `UPDATE`, `UPSERT`
@@ -614,6 +622,39 @@ def schema_extraction_prompt(self):
{samples}
"""
+ @property
+ def query_guidance_prompt(self):
+ """User-editable Query Guidance partial. Domain-specific
+ instructions / few-shot examples the user provides on the
+ Customize Prompts page. Injected into the four query-related
+ templates (map_question_to_schema, generate_function,
+ generate_cypher, generate_gsql) *after* their hard rules so
+ the LLM treats the guidance as advisory.
+
+ Default is the empty string — the four templates render
+ unchanged from their pre-Query-Guidance form when no override
+ is configured.
+ """
+ result = self._read_prompt_file(self.prompt_path + "query_guidance.txt")
+ return (result or "").strip()
+
+ @property
+ def query_guidance_block(self):
+ """Wrap ``query_guidance_prompt`` in a markdown section so it
+ drops cleanly into a downstream template. Returns an empty
+ string when no guidance is configured — keeps the surrounding
+ prompts identical to today's behavior on the empty path.
+ """
+ text = self.query_guidance_prompt
+ if not text:
+ return ""
+ return (
+ "## Domain Hints\n"
+ "Use the following hints only when they do not conflict with the "
+ "rules above:\n\n"
+ f"{text}\n"
+ )
+
@property
def contextualize_question_prompt(self):
"""Property to get the prompt for contextualizing a follow-up question
diff --git a/common/utils/prompt_validation.py b/common/utils/prompt_validation.py
index 51c5cfd..8f4e8f5 100644
--- a/common/utils/prompt_validation.py
+++ b/common/utils/prompt_validation.py
@@ -64,6 +64,9 @@
},
# common/db/schema_extraction.py.
"schema_extraction": {"samples", "structural_types", "tg_keywords"},
+ # Free-form partial injected into the four query-related templates;
+ # no required placeholders — the user content IS the body.
+ "query_guidance": set(),
}
@@ -74,8 +77,12 @@
"chatbot_response": {"format_instructions", "query", "history"},
"entity_relationship": {"format_instructions", "input"},
"community_summarization": {"format_instructions"},
- "query_generation": {"format_instructions"},
+ # ``query_guidance`` is a partial the runtime supplies; allowing
+ # it here keeps a user-pasted ``{query_guidance}`` from being
+ # double-braced into a literal.
+ "query_generation": {"format_instructions", "query_guidance"},
"schema_extraction": set(),
+ "query_guidance": set(),
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 97a0952..1034ead 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
graphrag:
- image: tigergraph/graphrag:latest
+ image: tigergraph/graphrag:latest
platform: linux/amd64
container_name: graphrag
build:
@@ -51,12 +51,14 @@ services:
- ./configs/:/configs
graphrag-ui:
- image: tigergraph/graphrag-ui:latest
+ image: tigergraph/graphrag-ui:latest
platform: linux/amd64
- container_name: graphrag-ui
+ container_name: graphrag-ui
build:
context: graphrag-ui
dockerfile: Dockerfile
+ additional_contexts:
+ repo: .
ports:
- 3000:3000
depends_on:
diff --git a/ecc/Dockerfile b/ecc/Dockerfile
index 5b83e4a..9d9f063 100644
--- a/ecc/Dockerfile
+++ b/ecc/Dockerfile
@@ -2,13 +2,16 @@ FROM python:3.11.9-bullseye
WORKDIR /code
COPY common/requirements.txt requirements.txt
-
+
RUN apt-get update && apt-get upgrade -y
RUN pip install -r requirements.txt
COPY ecc/app /code
COPY common /code/common
+COPY VERSION /code/VERSION
+RUN date -u +%Y-%m-%dT%H:%M:%SZ > /code/BUILD_DATE
+
ENV SERVER_CONFIG="/server_config.json"
ENV LOGLEVEL="INFO"
diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py
index 549d49c..3534c9b 100644
--- a/ecc/app/graphrag/util.py
+++ b/ecc/app/graphrag/util.py
@@ -31,6 +31,7 @@
get_graphrag_config,
)
from common.db.schema_utils import (
+ gsql_output_error,
is_structural_type,
read_existing_schema_async,
read_type_metadata_async,
@@ -101,8 +102,8 @@ async def install_queries(
async with tg_sem:
res = await conn.gsql(query)
logger.info(f"INSTALL QUERY ALL returned: {str(res)[:200]}")
- res_lower = res.lower() if isinstance(res, str) else ""
- if "error" in res_lower or "does not exist" in res_lower or "failed" in res_lower:
+ err = gsql_output_error(res) if isinstance(res, str) else None
+ if err:
raise Exception(res)
max_wait = 600 # seconds
diff --git a/ecc/app/main.py b/ecc/app/main.py
index f4ba391..7d6e67f 100644
--- a/ecc/app/main.py
+++ b/ecc/app/main.py
@@ -178,6 +178,27 @@ def root():
return {"status": "ok"}
+@app.get("/version")
+def version():
+ """Return image-build version info. ``VERSION`` is the repo-root
+ file copied into the image; ``BUILD_DATE`` is stamped at build
+ time by the Dockerfile. Both fall back to ``unknown`` when the
+ files aren't present.
+ """
+ def _safe_read(path: str) -> str:
+ try:
+ with open(path) as f:
+ return f.read().strip()
+ except Exception:
+ return "unknown"
+
+ return {
+ "component": "graphrag-ecc",
+ "version": _safe_read("/code/VERSION"),
+ "build_date": _safe_read("/code/BUILD_DATE"),
+ }
+
+
@app.get("/{graphname}/{ecc_method}/rebuild_status")
def rebuild_status(
graphname: str,
diff --git a/ecc/app/supportai/util.py b/ecc/app/supportai/util.py
index e3dd36f..1b1328a 100644
--- a/ecc/app/supportai/util.py
+++ b/ecc/app/supportai/util.py
@@ -18,6 +18,7 @@
get_completion_config,
get_graphrag_config,
)
+from common.db.schema_utils import gsql_output_error
from common.embeddings.base_embedding_store import EmbeddingStore
from common.embeddings.tigergraph_embedding_store import TigerGraphEmbeddingStore
from common.extractors import GraphExtractor, LLMEntityRelationshipExtractor
@@ -64,8 +65,8 @@ async def install_queries(
async with tg_sem:
res = await conn.gsql(query)
logger.info(f"INSTALL QUERY ALL returned: {str(res)[:200]}")
- res_lower = res.lower() if isinstance(res, str) else ""
- if "error" in res_lower or "does not exist" in res_lower or "failed" in res_lower:
+ err = gsql_output_error(res) if isinstance(res, str) else None
+ if err:
raise Exception(res)
max_wait = 300 # seconds
diff --git a/graphrag-ui/Dockerfile b/graphrag-ui/Dockerfile
index aec0713..c87dbf0 100644
--- a/graphrag-ui/Dockerfile
+++ b/graphrag-ui/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:23.7-slim
+FROM node:23.7-slim
WORKDIR /app
ENV PNPM_HOME="/pnpm"
@@ -7,6 +7,14 @@ RUN corepack enable
COPY . .
+# Pull the shared VERSION file from the repo root (exposed via the
+# ``repo`` additional build context). Compose ``docker compose build``
+# resolves it automatically; no env vars or wrapper script needed.
+COPY --from=repo VERSION ./public/version
+RUN mkdir -p public && \
+ echo "{\"component\":\"graphrag-ui\",\"version\":\"$(cat public/version | tr -d '\n')\",\"build_date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > public/version.json && \
+ rm public/version
+
RUN pnpm install
RUN pnpm run build
RUN pnpm i -g serve
diff --git a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
index b22c702..cdad795 100644
--- a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
+++ b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
@@ -6,19 +6,27 @@ import ConfigScopeToggle from "@/components/ConfigScopeToggle";
import { useRoles } from "@/hooks/useRoles";
import { useLocation } from "react-router-dom";
+// Ordered to follow the lifecycle of a graph: setup → ingest → rebuild
+// → query. The Customize Prompts page lists them in the same order so
+// admins read them top-down in the order they fire.
+//
+// ``query_generation`` (map_question_to_schema) is intentionally not
+// listed here — Query Guidance now covers its only end-user-facing
+// customization need (domain hints + examples). The underlying prompt
+// is still available on disk and editable via direct API for advanced
+// use cases.
const ALL_PROMPT_TYPES = [
- { id: "chatbot_response", name: "Chatbot Responses", description: "Customize how the chatbot responds to user questions" },
- { id: "entity_relationship", name: "Entity Relationships", description: "Configure entity and relationship extraction from document chunks" },
- { id: "community_summarization", name: "Community Summarization", description: "Define how community summaries are generated" },
- { id: "query_generation", name: "Schema Instructions", description: "Configure instructions for schema filtering and schema generation" },
- { id: "schema_extraction", name: "Schema Extraction", description: "Define the rules the LLM follows when proposing a domain schema from sample documents" },
+ { id: "schema_extraction", name: "Schema Extraction", description: "Rules the LLM follows when proposing a domain schema from sample documents (Initialize Graph dialog)." },
+ { id: "entity_relationship", name: "Entity Relationships", description: "Extract entities and relationships from document chunks during ingest." },
+ { id: "community_summarization", name: "Community Summarization", description: "Summarize each community after Louvain detection during rebuild." },
+ { id: "query_guidance", name: "Query Guidance", description: "Free-form domain hints and example mappings — injected into question-to-schema, generate-function, generate-cypher, and generate-gsql prompts. Empty by default. Max 8000 characters." },
+ { id: "chatbot_response", name: "Chatbot Responses", description: "How the chatbot composes the final answer to the user from retrieved context." },
];
const CustomizePrompts = () => {
const location = useLocation();
const { isSuperuser, isGlobalDesigner } = useRoles(location.pathname);
const graphOnly = !isSuperuser && !isGlobalDesigner;
- const [configuredProvider, setConfiguredProvider] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [expandedPrompt, setExpandedPrompt] = useState(null);
// Only the prompt types returned by the backend (filtered by access level)
@@ -31,6 +39,7 @@ const CustomizePrompts = () => {
community_summarization: "",
query_generation: "",
schema_extraction: "",
+ query_guidance: "",
});
// Template variables that should not be edited (stored separately)
@@ -40,6 +49,7 @@ const CustomizePrompts = () => {
community_summarization: "",
query_generation: "",
schema_extraction: "",
+ query_guidance: "",
});
// Only render prompt types the backend returned for this user
@@ -132,6 +142,9 @@ const CustomizePrompts = () => {
schema_extraction: data.prompts.schema_extraction?.editable_content !== undefined
? data.prompts.schema_extraction.editable_content
: (typeof data.prompts.schema_extraction === 'string' ? data.prompts.schema_extraction : ""),
+ query_guidance: data.prompts.query_guidance?.editable_content !== undefined
+ ? data.prompts.query_guidance.editable_content
+ : (typeof data.prompts.query_guidance === 'string' ? data.prompts.query_guidance : ""),
});
// Store template variables separately
@@ -141,22 +154,10 @@ const CustomizePrompts = () => {
community_summarization: data.prompts.community_summarization?.template_variables || "",
query_generation: data.prompts.query_generation?.template_variables || "",
schema_extraction: data.prompts.schema_extraction?.template_variables || "",
+ query_guidance: data.prompts.query_guidance?.template_variables || "",
});
-
- // Set configured provider
- const providerMap: Record = {
- openai: "OpenAI",
- azure: "Azure OpenAI",
- genai: "Google GenAI (Gemini)",
- vertexai: "Google Vertex AI",
- bedrock: "AWS Bedrock",
- ollama: "Ollama",
- };
- const provider = data.configured_provider?.toLowerCase() || "openai";
- setConfiguredProvider(providerMap[provider] || data.configured_provider || "OpenAI");
} catch (error) {
console.error("Error loading prompts:", error);
- setConfiguredProvider("OpenAI");
} finally {
setIsLoading(false);
}
@@ -239,28 +240,6 @@ const CustomizePrompts = () => {
- {/* Configured Provider - Read Only */}
-
-
- Configured LLM Provider
-
-
-
- {isLoading && (
-
-
-
- )}
-
-
- Prompts are configured for your currently active LLM provider. Change provider in Server Configuration.
-
-
-
{/* Save Message */}
{saveMessage && (
{
{/* Card Grid */}
-
+
{/* Initialize Card */}
diff --git a/graphrag-ui/src/pages/setup/SetupLayout.tsx b/graphrag-ui/src/pages/setup/SetupLayout.tsx
index 3907609..31cf93f 100644
--- a/graphrag-ui/src/pages/setup/SetupLayout.tsx
+++ b/graphrag-ui/src/pages/setup/SetupLayout.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Database, Settings, FileText, ChevronRight } from "lucide-react";
@@ -19,6 +19,21 @@ const SetupLayout = () => {
const canAccessPrompts = canAccessSetup;
const canAccessLlmConfig = canAccessSetup;
+ // Fetched once on mount and shown in the footer for support / version
+ // checks. Falls back to empty silently if the endpoint is unreachable.
+ const [version, setVersion] = useState("");
+ useEffect(() => {
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) return;
+ fetch("/ui/version", { headers: { Authorization: `Basic ${creds}` } })
+ .then((r) => (r.ok ? r.json() : null))
+ .then((data) => {
+ const v = data?.graphrag?.version;
+ if (v && v !== "unknown") setVersion(v);
+ })
+ .catch(() => {});
+ }, []);
+
const menuItems = [
{
title: "Knowledge Graph Setup",
@@ -205,8 +220,15 @@ const SetupLayout = () => {
{/* Main Content Area */}
-
-
+
+
+
+
+ {version && (
+
+ Version {version}
+
+ )}
);
diff --git a/graphrag/Dockerfile b/graphrag/Dockerfile
index e493e7f..d46cd6d 100644
--- a/graphrag/Dockerfile
+++ b/graphrag/Dockerfile
@@ -2,13 +2,19 @@ FROM python:3.11.9
WORKDIR /code
COPY common/requirements.txt requirements.txt
-
+
RUN apt-get update && apt-get upgrade -y
RUN pip install -r requirements.txt
-
+
COPY graphrag/app /code
COPY common /code/common
+# Version baked from the repo-root VERSION file. The build date is
+# stamped at image-build time so plain ``docker compose build``
+# captures both without any args or scripts.
+COPY VERSION /code/VERSION
+RUN date -u +%Y-%m-%dT%H:%M:%SZ > /code/BUILD_DATE
+
ENV SERVER_CONFIG="/server_config.json"
ENV LOGLEVEL="INFO"
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 2136ba0..729451a 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -378,6 +378,68 @@ def login(auth: Annotated[list[str], Depends(ui_basic_auth)]):
return {"graphs": graphs, "roles": global_roles, "graph_roles": graph_roles}
+def _read_local_version(component: str) -> dict:
+ """Read the ``/code/VERSION`` (repo-root file copied into image)
+ and ``/code/BUILD_DATE`` (stamped at image build time).
+ """
+ def _safe_read(path: str) -> str:
+ try:
+ with open(path) as f:
+ return f.read().strip()
+ except Exception:
+ return "unknown"
+
+ return {
+ "component": component,
+ "version": _safe_read("/code/VERSION"),
+ "build_date": _safe_read("/code/BUILD_DATE"),
+ }
+
+
+def _unknown_version(component: str) -> dict:
+ return {"component": component, "version": "unknown", "build_date": "unknown"}
+
+
+@router.get(f"{route_prefix}/version")
+def get_version():
+ """Return image-build version info for all running components.
+
+ The graphrag container reads its own ``/code/VERSION`` directly;
+ ``ecc`` and ``graphrag-ui`` are fetched over the network so this
+ one call surfaces every component a UI client cares about.
+ Unreachable components return ``unknown`` rather than failing the
+ whole call.
+ """
+ graphrag_version = _read_local_version("graphrag")
+
+ ecc_base = graphrag_config.get("ecc", "http://graphrag-ecc:8001")
+ try:
+ ecc_resp = httpx.get(f"{ecc_base}/version", timeout=5.0)
+ ecc_version = (
+ ecc_resp.json() if ecc_resp.status_code == 200
+ else _unknown_version("graphrag-ecc")
+ )
+ except Exception:
+ ecc_version = _unknown_version("graphrag-ecc")
+
+ ui_version = _unknown_version("graphrag-ui")
+ try:
+ # ``serve`` exposes static files at port 3000 inside the
+ # compose network; fall through quietly if it isn't reachable
+ # (e.g. running graphrag in isolation).
+ ui_resp = httpx.get("http://graphrag-ui:3000/version.json", timeout=5.0)
+ if ui_resp.status_code == 200:
+ ui_version = ui_resp.json()
+ except Exception:
+ pass
+
+ return {
+ "graphrag": graphrag_version,
+ "graphrag_ecc": ecc_version,
+ "graphrag_ui": ui_version,
+ }
+
+
@router.get(f"{route_prefix}/list_graphs")
def list_graphs(auth: Annotated[list[str], Depends(ui_basic_auth)]):
"""Return the live list of graphs the authenticated user has access
@@ -3117,6 +3179,11 @@ async def get_prompts(
(completion_llm, "map_question_schema_prompt"),
"schema_extraction":
(completion_llm, "schema_extraction_prompt"),
+ # Free-form partial injected into the four query-related
+ # templates (map_question_to_schema, generate_function,
+ # generate_cypher, generate_gsql). Empty by default.
+ "query_guidance":
+ (completion_llm, "query_guidance_prompt"),
}
def _get_prompt(prompt_type: str) -> dict:
@@ -3262,11 +3329,28 @@ async def save_prompts(
"community_summarization": "community_summarization.txt",
"query_generation": "map_question_to_schema.txt",
"schema_extraction": "schema_extraction.txt",
+ "query_guidance": "query_guidance.txt",
}
if prompt_type not in prompt_type_to_file:
raise HTTPException(status_code=400, detail=f"Invalid prompt_type: {prompt_type}")
+ # Hard length cap on Query Guidance specifically. It's a
+ # free-form partial that flows into four templates; runaway
+ # content can push the surrounding prompts past the LLM's
+ # context window. 8000 chars ≈ 2K tokens is plenty for
+ # rules + a half-dozen examples while leaving room for
+ # everything else.
+ QUERY_GUIDANCE_MAX_CHARS = 8000
+ if prompt_type == "query_guidance" and len(content) > QUERY_GUIDANCE_MAX_CHARS:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ f"Query Guidance is too long ({len(content)} characters); "
+ f"keep it under {QUERY_GUIDANCE_MAX_CHARS}."
+ ),
+ )
+
# Gatekeepers — escape stray ``{token}`` occurrences (so user
# examples like ``{example}`` don't crash str.format at call
# time) and reject saves that miss a required placeholder.
@@ -3292,8 +3376,9 @@ async def save_prompts(
"chatbot_response": "Chatbot response prompt saved successfully",
"entity_relationship": "Entity relationship prompt saved successfully",
"community_summarization": "Community summarization prompt saved successfully",
- "query_generation": "Schema instructions prompt saved successfully",
+ "query_generation": "Question-to-schema mapping prompt saved successfully",
"schema_extraction": "Schema extraction prompt saved successfully",
+ "query_guidance": "Query guidance saved successfully",
}
return {"status": "success", "message": messages.get(prompt_type, "Prompt saved successfully")}
diff --git a/graphrag/app/tools/generate_cypher.py b/graphrag/app/tools/generate_cypher.py
index b749fe9..9e32a60 100644
--- a/graphrag/app/tools/generate_cypher.py
+++ b/graphrag/app/tools/generate_cypher.py
@@ -125,7 +125,12 @@ def generate_cypher(self, question: str, history: Iterable[str]) -> str:
"question",
"schema",
"history"
- ]
+ ],
+ partial_variables={
+ # Pre-bind the Query Guidance partial; empty when no
+ # override is configured.
+ "query_guidance": self.llm.query_guidance_block,
+ },
)
LogWriter.info(f"request_id={req_id_cv.get()} ENTRY generate_cypher with {question}")
diff --git a/graphrag/app/tools/generate_function.py b/graphrag/app/tools/generate_function.py
index 9801000..9b44bf3 100644
--- a/graphrag/app/tools/generate_function.py
+++ b/graphrag/app/tools/generate_function.py
@@ -150,7 +150,9 @@ def _run(
"doc8",
],
partial_variables={
- "format_instructions": func_parser.get_format_instructions()
+ "format_instructions": func_parser.get_format_instructions(),
+ # See map_question_to_schema for the rationale.
+ "query_guidance": self.llm.query_guidance_block,
},
)
diff --git a/graphrag/app/tools/generate_gsql.py b/graphrag/app/tools/generate_gsql.py
index 02675b7..6894265 100644
--- a/graphrag/app/tools/generate_gsql.py
+++ b/graphrag/app/tools/generate_gsql.py
@@ -127,7 +127,12 @@ def generate_gsql(self, question: str, history: Iterable[str]) -> str:
"question",
"schema",
"history"
- ]
+ ],
+ partial_variables={
+ # Pre-bind the Query Guidance partial; empty when no
+ # override is configured.
+ "query_guidance": self.llm.query_guidance_block,
+ },
)
LogWriter.info(f"request_id={req_id_cv.get()} ENTRY generate_gsql with {question}")
diff --git a/graphrag/app/tools/map_question_to_schema.py b/graphrag/app/tools/map_question_to_schema.py
index b9dbd90..1ce9211 100644
--- a/graphrag/app/tools/map_question_to_schema.py
+++ b/graphrag/app/tools/map_question_to_schema.py
@@ -89,7 +89,14 @@ def _run(self, query: str, conversation: List[Dict[str, str]]) -> str:
"edges",
"edgesInfo",
],
- partial_variables={"format_instructions": parser.get_format_instructions()},
+ partial_variables={
+ "format_instructions": parser.get_format_instructions(),
+ # Pre-bind the Query Guidance partial so every render
+ # picks up the current per-graph / global override.
+ # ``query_guidance_block`` is empty when no override is
+ # configured, leaving the template effectively unchanged.
+ "query_guidance": self.llm.query_guidance_block,
+ },
)
schema_ver = get_schema_ver(self.conn)
diff --git a/graphrag/tests/test_e2e_prompt_customization.py b/graphrag/tests/test_e2e_prompt_customization.py
index bffb4bb..5b2fea9 100644
--- a/graphrag/tests/test_e2e_prompt_customization.py
+++ b/graphrag/tests/test_e2e_prompt_customization.py
@@ -138,6 +138,12 @@ def test_02_save_customized_chatbot_response_round_trips():
"""Saving a customized chatbot_response prompt persists it; a
follow-up GET returns the customized text with placeholders still
hidden.
+
+ Wrapped in try/except so a mid-flight assertion failure still
+ reverts the file, instead of leaving the test-marker in
+ ``configs/prompts/chatbot_response.txt`` for every later run.
+ Stage 3 reverts again as its primary action; doing both is
+ idempotent.
"""
if "originals" not in _state:
pytest.skip("Skipped because Stage 1 did not capture originals")
@@ -147,36 +153,55 @@ def test_02_save_customized_chatbot_response_round_trips():
custom_marker = "[E2E TEST EDIT — chatbot_response]"
new_editable = f"{custom_marker}\n\n{original['editable_content']}"
- resp = requests.post(
- f"{GRAPHRAG_URL}/ui/prompts",
- json={
- "prompt_type": "chatbot_response",
- "editable_content": new_editable,
- "template_variables": original["template_variables"],
- },
- auth=AUTH,
- timeout=180,
- )
- assert resp.status_code == 200, resp.text
-
- resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
- assert resp.status_code == 200, resp.text
- after = resp.json()["prompts"]["chatbot_response"]
- assert custom_marker in after["editable_content"], (
- "Customized marker missing from chatbot_response after save+reload"
- )
- placeholders_in_editable = _placeholder_set(after["editable_content"])
- assert not placeholders_in_editable, (
- f"Placeholders leaked into editable_content after customize: "
- f"{sorted(placeholders_in_editable)}"
- )
- required = REQUIRED_PLACEHOLDERS["chatbot_response"]
- placeholders_in_tv = _placeholder_set(after["template_variables"])
- missing = required - placeholders_in_tv
- assert not missing, (
- f"Required placeholders dropped during round-trip: {sorted(missing)}"
- )
- _state["chatbot_customized"] = True
+ saved = False
+ try:
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "chatbot_response",
+ "editable_content": new_editable,
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+ saved = True
+
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ after = resp.json()["prompts"]["chatbot_response"]
+ assert custom_marker in after["editable_content"], (
+ "Customized marker missing from chatbot_response after save+reload"
+ )
+ placeholders_in_editable = _placeholder_set(after["editable_content"])
+ assert not placeholders_in_editable, (
+ f"Placeholders leaked into editable_content after customize: "
+ f"{sorted(placeholders_in_editable)}"
+ )
+ required = REQUIRED_PLACEHOLDERS["chatbot_response"]
+ placeholders_in_tv = _placeholder_set(after["template_variables"])
+ missing = required - placeholders_in_tv
+ assert not missing, (
+ f"Required placeholders dropped during round-trip: {sorted(missing)}"
+ )
+ _state["chatbot_customized"] = True
+ except BaseException:
+ if saved:
+ try:
+ requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "chatbot_response",
+ "editable_content": original["editable_content"],
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ except Exception as revert_exc:
+ print(f" chatbot_response revert failed: {revert_exc}")
+ raise
@skip_unless_graphrag
@@ -215,6 +240,11 @@ def test_04_save_customized_schema_extraction_round_trips():
"""Same round-trip flow for schema_extraction (the prompt with
the largest set of required placeholders / structural-context
template variables).
+
+ Wrapped in try/finally so a failed assertion mid-flight always
+ reverts to the original — otherwise the marker leaks into
+ ``configs/prompts/schema_extraction.txt`` and pollutes every
+ subsequent extraction call.
"""
if "originals" not in _state:
pytest.skip("Skipped because Stage 1 did not capture originals")
@@ -224,48 +254,53 @@ def test_04_save_customized_schema_extraction_round_trips():
custom_marker = "[E2E TEST EDIT — schema_extraction]"
new_editable = f"{custom_marker}\n\n{original['editable_content']}"
- resp = requests.post(
- f"{GRAPHRAG_URL}/ui/prompts",
- json={
- "prompt_type": "schema_extraction",
- "editable_content": new_editable,
- "template_variables": original["template_variables"],
- },
- auth=AUTH,
- timeout=180,
- )
- assert resp.status_code == 200, resp.text
-
- resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
- assert resp.status_code == 200, resp.text
- after = resp.json()["prompts"]["schema_extraction"]
- assert custom_marker in after["editable_content"], (
- "Customized marker missing from schema_extraction after save+reload"
- )
- placeholders_in_editable = _placeholder_set(after["editable_content"])
- assert not placeholders_in_editable, (
- f"Placeholders leaked into editable_content after customize: "
- f"{sorted(placeholders_in_editable)}"
- )
- required = REQUIRED_PLACEHOLDERS["schema_extraction"]
- placeholders_in_tv = _placeholder_set(after["template_variables"])
- missing = required - placeholders_in_tv
- assert not missing, (
- f"Required placeholders dropped during round-trip: {sorted(missing)}"
- )
-
- # Revert to keep the test idempotent.
- resp = requests.post(
- f"{GRAPHRAG_URL}/ui/prompts",
- json={
- "prompt_type": "schema_extraction",
- "editable_content": original["editable_content"],
- "template_variables": original["template_variables"],
- },
- auth=AUTH,
- timeout=180,
- )
- assert resp.status_code == 200, resp.text
+ saved = False
+ try:
+ resp = requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "schema_extraction",
+ "editable_content": new_editable,
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ assert resp.status_code == 200, resp.text
+ saved = True
+
+ resp = requests.get(f"{GRAPHRAG_URL}/ui/prompts", auth=AUTH, timeout=180)
+ assert resp.status_code == 200, resp.text
+ after = resp.json()["prompts"]["schema_extraction"]
+ assert custom_marker in after["editable_content"], (
+ "Customized marker missing from schema_extraction after save+reload"
+ )
+ placeholders_in_editable = _placeholder_set(after["editable_content"])
+ assert not placeholders_in_editable, (
+ f"Placeholders leaked into editable_content after customize: "
+ f"{sorted(placeholders_in_editable)}"
+ )
+ required = REQUIRED_PLACEHOLDERS["schema_extraction"]
+ placeholders_in_tv = _placeholder_set(after["template_variables"])
+ missing = required - placeholders_in_tv
+ assert not missing, (
+ f"Required placeholders dropped during round-trip: {sorted(missing)}"
+ )
+ finally:
+ if saved:
+ try:
+ requests.post(
+ f"{GRAPHRAG_URL}/ui/prompts",
+ json={
+ "prompt_type": "schema_extraction",
+ "editable_content": original["editable_content"],
+ "template_variables": original["template_variables"],
+ },
+ auth=AUTH,
+ timeout=180,
+ )
+ except Exception as exc:
+ print(f" schema_extraction revert failed: {exc}")
@skip_unless_graphrag
From bfd55001cff688534d74c04b74371a7ee6f4d2bc Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Tue, 12 May 2026 01:28:26 -0700
Subject: [PATCH 62/70] Tighten rebuild observability, sync graph picker,
harden chat handshake
- Stream rebuild progress per stage with heartbeats so chunking,
entity extraction, community detection, and mirror each surface
in the refresh dialog.
- Demote log lines that exposed user content (entity names, chunk
IDs, edge payloads) from INFO to DEBUG.
- Share the schema renderer across the four query tools and feed
the same per-type descriptions to all of them.
- Keep the graph picker in sync across chat, refresh, ingest, and
customize-prompts.
- Fix the chat WebSocket handshake when the embedding store is
unavailable and recover stream_chunks empty-response races.
- Stop rendering raw image markdown in chat by sanitizing alt text
on insert, and ask the image-description prompt to focus on
content (text, charts, tables) over layout.
---
CHANGELOG.md | 7 +
common/db/retriever_render.py | 34 +++-
common/db/schema_utils.py | 117 +++++++++++++-
.../embeddings/tigergraph_embedding_store.py | 3 +-
common/utils/image_data_extractor.py | 22 ++-
common/utils/text_extractors.py | 36 ++++-
ecc/app/graphrag/graph_rag.py | 147 +++++++++++++++---
ecc/app/graphrag/util.py | 4 +-
ecc/app/graphrag/workers.py | 41 ++---
ecc/app/main.py | 29 +++-
ecc/app/supportai/supportai_init.py | 8 +-
ecc/app/supportai/workers.py | 22 +--
graphrag-ui/src/components/Bot.tsx | 9 ++
.../src/pages/setup/CustomizePrompts.tsx | 20 +++
graphrag-ui/src/pages/setup/IngestGraph.tsx | 24 ++-
graphrag-ui/src/pages/setup/KGAdmin.tsx | 35 ++++-
graphrag/app/routers/ui.py | 32 +++-
graphrag/app/tools/generate_cypher.py | 58 +------
graphrag/app/tools/generate_function.py | 23 ++-
graphrag/app/tools/generate_gsql.py | 58 +------
graphrag/app/tools/map_question_to_schema.py | 23 ++-
21 files changed, 563 insertions(+), 189 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c29db78..b4c1d94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -62,6 +62,13 @@
- **Image version stamped at build time**. Each image now carries the repo-root `VERSION` file plus a `/code/BUILD_DATE` written at build time; `GET /ui/version` aggregates the three components for support checks, and the Setup pages show a small "Version " line at the bottom-center. Plain `docker compose build` works as-is; no env vars or helper scripts required.
- **Prompt-customization E2E test always reverts on failure**. The schema-extraction round-trip test could leak its `[E2E TEST EDIT — schema_extraction]` marker into `configs/prompts/schema_extraction.txt` whenever a mid-flight assertion failed; both the chatbot-response and schema-extraction tests now wrap their save-then-assert in `try/finally` (or `try/except`) so the revert always runs.
- **Knowledge Graph Setup cards centered**. The three setup cards (Initialize / Ingest / Refresh) drop from a 4-column grid at large breakpoints to a 3-column grid so they fill the row evenly instead of leaving an empty fourth column.
+- **Per-stage progress for graph rebuild.** The refresh dialog now shows individual phases — chunking, entity extraction, community detection, domain-type update — with per-stage heartbeats so a long phase never looks stalled.
+- **Reclassified data-bearing log lines from INFO to DEBUG.** Rebuild logs in steady state now carry only metadata and counts; lines that included entity names, chunk identifiers, or edge payloads drop to DEBUG. Typical INFO volume falls from a few thousand lines per rebuild to under 150.
+- **Query-generation prompts see user-supplied type descriptions.** The descriptions / definitions a user attaches to vertex and edge types via Initialize Graph or Customize Prompts now reach every query-side LLM call, not just the cypher/gsql ones.
+- **Graph picker stays in sync across dialogs.** Changing the selected graph in chat, refresh, ingest, or customize-prompts updates the others immediately — they no longer drift apart.
+- **Chat WebSocket fails gracefully when the embedding store is unavailable.** Clients receive a structured error and a Try-Again-Later close code instead of an instant disconnect.
+- **Rebuilds survive chunk-creation races.** A transient empty response when a freshly created chunk's content row hasn't flushed yet is now retried instead of aborting that chunk.
+- **Images in chat render correctly.** Multi-line LLM image captions used to break the markdown image syntax so chat showed the raw markup; alt text is now sanitized on insert and the image-description prompt asks for a single content-focused paragraph (text, charts, tables, diagrams, logos — no layout or decorative styling).
> **Upgrading from a pre-release v1.4.0 build**: graphs that already
> have domain vertex types but were created before the multi-pair
diff --git a/common/db/retriever_render.py b/common/db/retriever_render.py
index 3956815..c7567ab 100644
--- a/common/db/retriever_render.py
+++ b/common/db/retriever_render.py
@@ -12,7 +12,7 @@
import logging
from pathlib import Path
-from typing import Iterable, Optional
+from typing import Callable, Iterable, Optional
from common.db.schema_utils import gsql_output_error
@@ -137,8 +137,14 @@ def install_retrievers(
domain_edges: Iterable[str],
include_entity: bool,
retriever_dir: str = _RETRIEVER_DIR,
+ progress: Optional["Callable[[str], None]"] = None,
) -> dict:
- """Render and install every templated retriever (sync)."""
+ """Render and install every templated retriever (sync).
+
+ *progress* is an optional callback invoked once per query with a
+ short status message; lets the caller surface per-query progress
+ in a UI (init dialog poll, etc.).
+ """
rendered = render_retrievers(
domain_vts, domain_edges, include_entity, retriever_dir
)
@@ -147,8 +153,32 @@ def install_retrievers(
f"vts={len(list(domain_vts))} edges={len(list(domain_edges))} "
f"rendered={list(rendered.keys())}"
)
+ # Group the four templated retrievers into two user-facing
+ # status messages — the text/vector variants of each family
+ # install back-to-back and a per-query message flickers too
+ # fast to be useful. The mapping is exhaustive over the
+ # current ``TEMPLATED_RETRIEVERS`` set; new entries fall
+ # through to a single "Installing retriever queries" message.
+ _GROUP_MESSAGE = {
+ "GraphRAG_Hybrid_Search": ("hybrid", "Installing hybrid retriever queries"),
+ "GraphRAG_Hybrid_Vector_Search": ("hybrid", "Installing hybrid retriever queries"),
+ "GraphRAG_Community_Search": ("community", "Installing community retriever queries"),
+ "GraphRAG_Community_Vector_Search": ("community", "Installing community retriever queries"),
+ }
+
results: dict = {}
+ emitted_groups: set = set()
for query_name, body in rendered.items():
+ if progress is not None:
+ group_key, group_msg = _GROUP_MESSAGE.get(
+ query_name, ("_other", "Installing retriever queries")
+ )
+ if group_key not in emitted_groups:
+ try:
+ progress(group_msg)
+ except Exception:
+ pass
+ emitted_groups.add(group_key)
block = _install_block(graphname, query_name, body)
try:
out = conn.gsql(block)
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index a468e1a..779054d 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -49,7 +49,7 @@
import time
import uuid
from dataclasses import dataclass, field
-from typing import Iterable, List, Optional, Sequence, Set, Tuple
+from typing import Callable, Iterable, List, Optional, Sequence, Set, Tuple
# -----------------------------------------------------------------------------
@@ -1106,6 +1106,87 @@ async def read_existing_schema_async(conn) -> "ExistingSchema":
return snapshot
+def render_schema_rep(conn) -> Tuple[str, Optional[int]]:
+ """Render the live schema as a per-vertex / per-edge text block
+ suitable for inclusion in an LLM prompt (`generate_cypher`,
+ `generate_gsql`, etc.). Includes any user-supplied definitions
+ stored on the ``EntityType`` / ``RelationshipType`` meta vertices
+ via :func:`read_type_metadata`.
+
+ Returns ``(schema_text, schema_version)`` so the caller can cache
+ the text against the version and skip re-rendering on the next
+ call (schemas don't change between rebuilds in normal operation).
+ """
+ from common.db.connections import get_schema_ver as _get_schema_ver
+
+ schema_ver = _get_schema_ver(conn)
+ verts = conn.getVertexTypes()
+ edges = conn.getEdgeTypes()
+ try:
+ entity_descs, rel_defs = read_type_metadata(conn)
+ except Exception:
+ # Older / unmigrated graphs may lack the EntityType /
+ # RelationshipType meta-schema; render without definitions
+ # rather than failing.
+ entity_descs, rel_defs = {}, {}
+
+ vertex_blocks: List[str] = []
+ for vert in verts:
+ vinfo = conn.getVertexType(vert)
+ primary_id = vinfo["PrimaryId"]["AttributeName"]
+ attributes = "\n\t\t".join(
+ a["AttributeName"] + " of type " + a["AttributeType"]["Name"]
+ for a in vinfo["Attributes"]
+ ) or "No attributes"
+ defn_line = (
+ f"\n\tDefinition: {entity_descs[vert]}" if entity_descs.get(vert) else ""
+ )
+ vertex_blocks.append(
+ f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id}"
+ f"\n\tAttributes: \n\t\t{attributes}"
+ )
+
+ edge_blocks: List[str] = []
+ for edge in edges:
+ einfo = conn.getEdgeType(edge)
+ from_vertex = einfo["FromVertexTypeName"]
+ to_vertex = einfo["ToVertexTypeName"]
+ direction = "Directed" if einfo["IsDirected"] else "Undirected"
+ attributes = "\n\t\t".join(
+ a["AttributeName"] + " of type " + a["AttributeType"]["Name"]
+ for a in einfo["Attributes"]
+ ) or "No attributes"
+ defn_line = (
+ f"\n\tDefinition: {rel_defs[edge]}" if rel_defs.get(edge) else ""
+ )
+ if from_vertex == "*" or to_vertex == "*":
+ for pair in einfo.get("EdgePairs", []):
+ pair_info = (
+ f"From Vertex: {pair['From']}\n\tTo Vertex: {pair['To']}"
+ )
+ edge_blocks.append(
+ f"{edge}{defn_line}\n\t{pair_info}"
+ f"\n\tEdge direction: {direction}"
+ f"\n\tAttributes: \n\t\t{attributes}"
+ )
+ else:
+ pair_info = f"From Vertex: {from_vertex}\n\tTo Vertex: {to_vertex}"
+ edge_blocks.append(
+ f"{edge}{defn_line}\n\t{pair_info}"
+ f"\n\tEdge direction: {direction}"
+ f"\n\tAttributes: \n\t\t{attributes}"
+ )
+
+ graphname = getattr(conn, "graphname", "") or ""
+ graph_label = f" {graphname}" if graphname else ""
+ text = (
+ f"The schema of the graph{graph_label} is as follows:\n"
+ f"Vertex Types:\n{chr(10).join(vertex_blocks)}\n\n"
+ f"Edge Types:\n{chr(10).join(edge_blocks)}\n"
+ )
+ return text, schema_ver
+
+
async def read_type_metadata_async(conn) -> Tuple[dict, dict]:
"""Async counterpart to :func:`read_type_metadata` — used by the
ECC pipeline where the available connection is
@@ -1202,6 +1283,7 @@ def apply_proposal(
graphname: str,
proposal: SchemaProposal,
job_name: Optional[str] = None,
+ progress: Optional[Callable[[str], None]] = None,
) -> dict:
"""Diff *proposal* against the current schema on *conn* and apply the
additive delta as a single atomic ``SCHEMA_CHANGE JOB``.
@@ -1216,11 +1298,24 @@ def apply_proposal(
"summary": {...}, # summarize(proposal)
}
+ *progress* is an optional callback invoked at each sub-phase with a
+ short status string (e.g. ``"Creating new vertex/edge types"``,
+ ``"Installing retriever queries"``); the router uses it to drive
+ the init-dialog status line.
+
Schema introspection errors propagate; the caller decides whether the
overall init flow should be marked as failed. The structural GraphRAG
schema must already exist on the graph (so the diff sees structural
types and only emits domain-side ADDs).
"""
+ def _report(msg: str) -> None:
+ if progress is None:
+ return
+ try:
+ progress(msg)
+ except Exception:
+ pass
+
existing = read_existing_schema(conn)
domain_stmts = emit_add_statements(proposal, existing)
# Run the structural-link emitter against an *augmented* snapshot
@@ -1237,9 +1332,13 @@ def apply_proposal(
if not statements:
# Even on no-op, refresh metadata so descriptions edited in the
# review panel land on EntityType / RelationshipType vertices.
+ # The upsert is fast (<5s) so we don't surface it as its own
+ # status — the previous phase's message lingers through it.
metadata = upsert_type_metadata(conn, proposal)
retrievers = _install_retrievers_after_apply(
- conn, graphname, proposal=proposal, pre_apply_existing=existing
+ conn, graphname,
+ proposal=proposal, pre_apply_existing=existing,
+ progress=progress,
)
return {
"status": "no-op",
@@ -1271,6 +1370,13 @@ def _run_phase(phase_stmts: List[str], phase_job: Optional[str]) -> Tuple[str, s
raise
return out, name
+ # The two-phase split (ADD then ALTER) is internal mechanics; the
+ # user just sees a single "Applying domain schema" message that
+ # spans both phases plus the brief metadata upsert. The wording
+ # matches both schema-source paths (sample extraction and pasted
+ # GSQL) — "extracted" would be misleading for the paste mode.
+ _report("Applying domain schema")
+
phase_outputs: List[str] = []
phase_jobs: List[str] = []
first_job_name: Optional[str] = None
@@ -1304,9 +1410,12 @@ def _run_phase(phase_stmts: List[str], phase_job: Optional[str]) -> Tuple[str, s
"retrievers": {"status": "skipped", "reason": "schema apply failed"},
}
+ # Metadata upsert is fast (<5s); no separate status message.
metadata = upsert_type_metadata(conn, proposal)
retrievers = _install_retrievers_after_apply(
- conn, graphname, proposal=proposal, pre_apply_existing=existing
+ conn, graphname,
+ proposal=proposal, pre_apply_existing=existing,
+ progress=progress,
)
return {
"status": "applied",
@@ -1358,6 +1467,7 @@ def _install_retrievers_after_apply(
graphname: str,
proposal: Optional[SchemaProposal] = None,
pre_apply_existing: Optional[ExistingSchema] = None,
+ progress: Optional[Callable[[str], None]] = None,
) -> dict:
"""Re-render and install the templated retrievers against the live
domain schema. No-op when no domain types are on the graph.
@@ -1432,6 +1542,7 @@ def _install_retrievers_after_apply(
domain_vts=domain_vts,
domain_edges=domain_edges,
include_entity=include_entity,
+ progress=progress,
),
}
if transitional:
diff --git a/common/embeddings/tigergraph_embedding_store.py b/common/embeddings/tigergraph_embedding_store.py
index e4758b8..12d3caf 100644
--- a/common/embeddings/tigergraph_embedding_store.py
+++ b/common/embeddings/tigergraph_embedding_store.py
@@ -425,7 +425,8 @@ def has_embeddings(
"vertex_id": v_id,
}
)
- logger.info(f"Return result {res} for has_embeddings({v_ids})")
+ # v_ids carry user-content-derived identifiers; demote.
+ logger.debug(f"Return result {res} for has_embeddings({v_ids})")
found = False
if "results" in res[0]:
for v in res[0]["results"]:
diff --git a/common/utils/image_data_extractor.py b/common/utils/image_data_extractor.py
index 59c6178..575264a 100644
--- a/common/utils/image_data_extractor.py
+++ b/common/utils/image_data_extractor.py
@@ -60,10 +60,24 @@ def describe_image_with_llm(file_path):
{
"type": "text",
"text": (
- "Please describe what you see in this image and "
- "if the image has scanned text then extract all the text. "
- "If the image has any graph, chart, table, or other diagram, describe it. "
- "If the image has any logo, identify and describe the logo."
+ "Describe the substantive CONTENT of this image so it "
+ "can be retrieved alongside the surrounding document. "
+ "Prioritize, in this order: (1) any text — copy it "
+ "verbatim, including headings, labels, axis ticks, "
+ "captions, and footnotes; (2) the data and structure of "
+ "any chart, graph, or table — name the chart type, the "
+ "axes / columns, and the values or trend the chart "
+ "actually shows; (3) the entities, relationships, or "
+ "process steps in any diagram or flowchart; (4) any logo "
+ "or branding mark, identified by name. Do NOT describe "
+ "layout, background color, decorative styling, slide "
+ "templates, or generic visual impressions — those add "
+ "no retrieval value. If the image is purely decorative "
+ "(no text, no data, no diagram), reply with just "
+ "\"decorative image\" and nothing else. Respond as a "
+ "SINGLE plain-text paragraph — no markdown headings, no "
+ "bullet lists, no blank lines. The reply is used "
+ "verbatim as the alt-text inside ``."
),
},
_build_image_content_block(image_base64, "image/jpeg"),
diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py
index 5fa302b..d8df543 100644
--- a/common/utils/text_extractors.py
+++ b/common/utils/text_extractors.py
@@ -87,7 +87,7 @@ def insert_description_by_id(md_text, image_id, description):
"""
Replace the description for an image whose basename == image_id.
"""
- safe_desc = description.replace("[", "(").replace("]", ")")
+ safe_desc = _sanitize_alt_text(description)
def repl(m):
old_path = m.group(2)
@@ -100,6 +100,40 @@ def repl(m):
return _md_pattern.sub(repl, md_text)
+# Maximum characters retained from an LLM image description when
+# rendered as markdown alt text. Long alt text bloats the chat
+# rendering and offers no extra accessibility value beyond the first
+# couple of sentences.
+_ALT_TEXT_MAX_CHARS = 400
+
+
+def _sanitize_alt_text(description: str) -> str:
+ """Collapse an LLM image description into a single-line, markdown-
+ safe alt-text string. The LLM is free to respond with headings,
+ paragraph breaks and bracketed phrases; the markdown image syntax
+ ```` doesn't tolerate any of that — a newline or
+ unescaped ``]`` terminates the construct and the renderer falls
+ back to printing the raw text (the bug this guards against).
+ """
+ if not description:
+ return ""
+ text = str(description)
+ # Drop a leading markdown heading like ``# Image Description``
+ # the LLM tends to emit as a preamble.
+ text = re.sub(r"^\s*#{1,6}\s*[^\n]*\n+", "", text, count=1)
+ # Drop a literal "Image Description:" / "Description:" prefix that
+ # the LLM occasionally writes in place of (or alongside) a heading.
+ text = re.sub(r"^\s*(image\s+description|description)\s*:\s*", "", text, count=1, flags=re.IGNORECASE)
+ # Replace every newline + run of whitespace with a single space.
+ text = re.sub(r"\s+", " ", text).strip()
+ # ``]`` would close the alt-text bracket; ``[`` can also confuse
+ # some renderers. Swap both for round parens.
+ text = text.replace("[", "(").replace("]", ")")
+ if len(text) > _ALT_TEXT_MAX_CHARS:
+ text = text[: _ALT_TEXT_MAX_CHARS - 1].rstrip() + "…"
+ return text
+
+
def replace_path_with_tg_protocol(md_text, image_id, tg_reference):
"""
Replace the file path for an image whose basename == image_id with tg:// protocol reference.
diff --git a/ecc/app/graphrag/graph_rag.py b/ecc/app/graphrag/graph_rag.py
index 29d226d..d2bee46 100644
--- a/ecc/app/graphrag/graph_rag.py
+++ b/ecc/app/graphrag/graph_rag.py
@@ -51,11 +51,18 @@ async def stream_docs(
conn: AsyncTigerGraphConnection,
docs_chan: Channel,
ttl_batches: int = 10,
+ progress=None,
):
"""
- Streams the document contents into the docs_chan
+ Streams the document contents into the docs_chan.
+
+ *progress* (optional) is a callable invoked once when document
+ streaming completes — runtime hands the rebuild status forward
+ from "Chunking documents" to "Extracting entities and
+ relationships" at that boundary.
"""
- logger.info("streaming docs")
+ logger.info(f"streaming docs ({ttl_batches} batches)")
+ n_docs = 0
for i in range(ttl_batches):
doc_ids = await stream_ids(conn, "Document", i, ttl_batches)
if doc_ids["error"]:
@@ -68,19 +75,28 @@ async def stream_docs(
async with tg_sem:
res = await conn.runInstalledQuery(
"StreamDocContent",
- params={"doc": d},
+ # 1-tuple form for VERTEX params; see
+ # stream_chunks for the deprecation context.
+ params={"doc": (d,)},
)
- logger.info(f"stream_docs writes {d} to docs")
+ # Demoted from INFO — ``d`` is a user document ID.
+ logger.debug(f"stream_docs writes {d} to docs")
await docs_chan.put(res[0]["DocContent"][0])
+ n_docs += 1
except Exception as e:
exc = traceback.format_exc()
logger.error(f"Error retrieving doc: {d} --> {e}\n{exc}")
continue # try retrieving the next doc
- logger.info("stream_docs done")
+ logger.info(f"stream_docs done: {n_docs} document(s) streamed")
# close the docs chan -- this function is the only sender
logger.info("closing docs chan")
docs_chan.close()
+ if progress is not None:
+ try:
+ progress("Extracting entities and relationships")
+ except Exception:
+ pass
async def stream_chunks(
conn: AsyncTigerGraphConnection,
@@ -91,7 +107,8 @@ async def stream_chunks(
"""
Streams the chunk contents into the extract_chan and embed_chan
"""
- logger.info("streaming chunks")
+ logger.info(f"streaming chunks ({ttl_batches} batches)")
+ n_chunks = 0
for i in range(ttl_batches):
chunk_ids = await stream_ids(conn, "DocumentChunk", i, ttl_batches)
if chunk_ids["error"]:
@@ -99,24 +116,52 @@ async def stream_chunks(
for c in chunk_ids["ids"]:
try:
- async with tg_sem:
- res = await conn.runInstalledQuery(
- "StreamChunkContent",
- params={"chunk": c},
+ # Retry briefly when ChunkContent is empty — that
+ # happens when stream_ids surfaced a DocumentChunk
+ # vertex but its HAS_CONTENT edge upsert hasn't
+ # flushed yet (the loader runs in batches). Without
+ # the retry the chunk gets silently dropped and
+ # extracted only on the next ECC sweep.
+ chunk_rows = []
+ for attempt in range(3):
+ async with tg_sem:
+ res = await conn.runInstalledQuery(
+ "StreamChunkContent",
+ # 1-tuple form is the supported shape for
+ # VERTEX params in current pyTigerGraph;
+ # the plain-value form raises a deprecation
+ # warning and falls back to a slower GET.
+ params={"chunk": (c,)},
+ )
+ chunk_rows = (res[0] if res else {}).get("ChunkContent") or []
+ if chunk_rows:
+ break
+ # Back off and try again — the loader's batch
+ # interval is a few seconds.
+ await asyncio.sleep(2 * (attempt + 1))
+ if not chunk_rows:
+ logger.warning(
+ f"No content row for chunk {c} after retries; skipping"
)
- content = res[0]["ChunkContent"][0]["attributes"]["text"].encode('raw_unicode_escape').decode('unicode_escape')
- logger.info("chunk writes to extract_chan")
+ continue
+ content = chunk_rows[0]["attributes"]["text"].encode(
+ 'raw_unicode_escape'
+ ).decode('unicode_escape')
+ logger.debug("chunk writes to extract_chan")
await extract_chan.put((content, c))
# send chunks to be embedded
- logger.info("chunk writes to embed_chan")
+ logger.debug("chunk writes to embed_chan")
await embed_chan.put((c, content, "DocumentChunk"))
+ n_chunks += 1
+ if n_chunks % 100 == 0:
+ logger.info(f"streaming chunks: {n_chunks} streamed")
except Exception as e:
exc = traceback.format_exc()
logger.error(f"Error retrieving chunk: {c} --> {e}\n{exc}")
continue # try retrieving the next doc
- logger.info("stream_chunks done")
+ logger.info(f"stream_chunks done: {n_chunks} chunk(s) streamed")
logger.info("closing extract_chan")
await extract_chan.put(None)
@@ -134,10 +179,12 @@ async def chunk_docs(
"""
logger.info("Chunk Processing Start")
doc_tasks = []
+ n_docs = 0
async with asyncio.TaskGroup() as grp:
while True:
try:
content = await docs_chan.get()
+ n_docs += 1
task = grp.create_task(
workers.chunk_doc(conn, content, upsert_chan, embed_chan, extract_chan)
)
@@ -147,7 +194,7 @@ async def chunk_docs(
except Exception:
raise
- logger.info("Chunk Processing End")
+ logger.info(f"Chunk Processing End: {n_docs} document(s) processed")
logger.info("closing extract_chan")
await extract_chan.put(None)
@@ -161,20 +208,29 @@ async def upsert(upsert_chan: Channel):
"""
logger.info("Data Upserting Start")
+ n_upserts = 0
# consume task queue
async with asyncio.TaskGroup() as grp:
while True:
try:
(func, args) = await upsert_chan.get()
- logger.info(f"Upserting with {func.__name__}, {args[1:3]}")
+ # Demoted from INFO — ``args`` carries vertex IDs and
+ # payloads derived from user documents.
+ logger.debug(f"Upserting with {func.__name__}, {args[1:3]}")
# execute the task
grp.create_task(func(*args))
+ n_upserts += 1
+ # Heartbeat every 200 upserts so a long stage doesn't
+ # look stalled in the INFO log without exposing the
+ # underlying data.
+ if n_upserts % 200 == 0:
+ logger.info(f"Data Upserting: {n_upserts} dispatched")
except ChannelClosed:
break
except Exception:
raise
- logger.info("Data Upserting End")
+ logger.info(f"Data Upserting End: {n_upserts} dispatched")
logger.info("closing load_q chan")
load_q.close()
@@ -256,15 +312,19 @@ async def embed(
(v_id, content, index_name) <- q.get()
"""
logger.info("Embedding Processing Start")
+ n_embed = 0
+ n_reused = 0
async with asyncio.TaskGroup() as grp:
# consume task queue
while True:
try:
(v_id, content, index_name) = await embed_chan.get()
v_id = (v_id, index_name)
- logger.info(f"Embed to {graphname}_{index_name}: {v_id}")
+ # v_id is a per-vertex identifier derived from user content.
+ logger.debug(f"Embed to {graphname}_{index_name}: {v_id}")
if get_graphrag_config(graphname).get("reuse_embedding", True) and embedding_store.has_embeddings([v_id]):
- logger.info(f"Embeddings for {v_id} already exists, skipping to save cost")
+ logger.debug(f"Embeddings for {v_id} already exists, skipping to save cost")
+ n_reused += 1
continue
grp.create_task(
workers.embed(
@@ -274,12 +334,17 @@ async def embed(
content,
)
)
+ n_embed += 1
+ if n_embed % 100 == 0:
+ logger.info(f"Embedding Processing: {n_embed} embedded so far")
except ChannelClosed:
break
except Exception:
raise
- logger.info("Embedding Processing End")
+ logger.info(
+ f"Embedding Processing End: {n_embed} embedded, {n_reused} reused"
+ )
async def extract(
@@ -297,6 +362,7 @@ async def extract(
"""
logger.info("Entity Extration Start")
# consume task queue
+ n_chunks = 0
async with asyncio.TaskGroup() as grp:
done_count = 0
while True:
@@ -312,12 +378,17 @@ async def extract(
grp.create_task(
workers.extract(upsert_chan, extractor, conn, *item)
)
+ n_chunks += 1
+ if n_chunks % 50 == 0:
+ logger.info(
+ f"Entity Extraction: {n_chunks} chunks dispatched"
+ )
except ChannelClosed:
break
except Exception:
raise
- logger.info("Entity Extration End")
+ logger.info(f"Entity Extration End: {n_chunks} chunks extracted")
logger.info("closing extract, upsert and embed chan")
extract_chan.close()
@@ -437,12 +508,21 @@ async def summarize_communities(
upsert_chan: Channel,
embed_chan: Channel,
):
+ logger.info("Community summarization started")
+ n_comm = 0
async with asyncio.TaskGroup() as tg:
while True:
try:
c = await comm_process_chan.get()
tg.create_task(workers.process_community(conn, upsert_chan, embed_chan, *c))
logger.debug(f"Added community to process: {c}")
+ n_comm += 1
+ # Per-community summarization can take 30s; emit a
+ # heartbeat every 20 so a long run doesn't go silent.
+ if n_comm % 20 == 0:
+ logger.info(
+ f"Community summarization: {n_comm} dispatched"
+ )
except ChannelClosed:
break
except Exception:
@@ -451,10 +531,12 @@ async def summarize_communities(
logger.info("closing upsert_chan")
upsert_chan.close()
embed_chan.close()
- logger.info("summarize_communities done")
+ logger.info(
+ f"Community summarization done: {n_comm} communities dispatched"
+ )
-async def run(graphname: str, conn: AsyncTigerGraphConnection):
+async def run(graphname: str, conn: AsyncTigerGraphConnection, progress=None):
"""
Set up GraphRAG:
- Install necessary queries.
@@ -464,12 +546,27 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
- entities/relationships
- upsert everything to the graph
- Detect communities and summarize them
+
+ *progress* is an optional ``Callable[[str], None]`` invoked at
+ each user-visible sub-phase; ECC's ``run_with_tracking`` wires it
+ to the task's ``stage`` field so the UI rebuild dialog can show
+ where the job is.
"""
+ def _report(msg: str) -> None:
+ if progress is None:
+ return
+ try:
+ progress(msg)
+ except Exception:
+ pass
+
+ _report("Preparing rebuild")
extractor, embedding_store = await init(conn)
init_start = time.perf_counter()
if doc_process_switch:
+ _report("Chunking documents")
logger.info("Doc Processing Start")
docs_chan = Channel(1)
embed_chan = Channel()
@@ -479,7 +576,7 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
async with asyncio.TaskGroup() as grp:
# get docs
- grp.create_task(stream_docs(conn, docs_chan, 100))
+ grp.create_task(stream_docs(conn, docs_chan, 100, progress=progress))
# process docs
grp.create_task(
chunk_docs(conn, docs_chan, embed_chan, upsert_chan, extract_chan)
@@ -519,6 +616,7 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
# schema edits could still leave queries missing.
community_start = time.perf_counter()
if community_detection_switch:
+ _report("Detecting communities")
await install_queries(COMMUNITY_QUERIES, conn)
logger.info("Community Processing Start")
@@ -578,6 +676,7 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection):
f"IN_COMMUNITY pair missing on schema"
)
if mirrorable:
+ _report("Updating domain types")
await graphrag_mirror_communities(conn, mirrorable)
community_end = time.perf_counter()
logger.info("Community Processing End")
diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py
index 3534c9b..2f94a6d 100644
--- a/ecc/app/graphrag/util.py
+++ b/ecc/app/graphrag/util.py
@@ -410,7 +410,9 @@ async def get_commuinty_children(conn, i: int, c: str):
try:
resp = await conn.runInstalledQuery(
"get_community_children",
- params={"comm": c, "iter": i}
+ # 1-tuple form for VERTEX params; plain value
+ # is deprecated in current pyTigerGraph.
+ params={"comm": (c,), "iter": i}
)
except:
logger.error(f"Get Children err:\n{traceback.format_exc()}")
diff --git a/ecc/app/graphrag/workers.py b/ecc/app/graphrag/workers.py
index cd099ea..7d15244 100644
--- a/ecc/app/graphrag/workers.py
+++ b/ecc/app/graphrag/workers.py
@@ -94,30 +94,32 @@ async def chunk_doc(
v_id = util.process_id(doc["v_id"])
if v_id != doc["v_id"]:
- logger.info(f"""Cloning doc/content {doc["v_id"]} -> {v_id}""")
+ # v_id is a sanitized form of a user document ID — DEBUG.
+ logger.debug(f"""Cloning doc/content {doc["v_id"]} -> {v_id}""")
await upsert_chan.put((upsert_doc, (conn, v_id, chunker_type, doc["attributes"]["text"])))
-
+
# Use get_chunker for all types (including images)
# For images, get_chunker returns SingleChunker which preserves markdown image references
chunker = ecc_util.get_chunker(chunker_type, graphname=conn.graphname)
# decode the text return from tigergraph as it was encoded when written into jsonl file for uploading
chunks = chunker.chunk(doc["attributes"]["text"].encode('raw_unicode_escape').decode('unicode_escape'))
-
- logger.info(f"Chunking {v_id} into {len(chunks)} chunk(s)")
+
+ # v_id / chunk_id derive from user document content.
+ logger.debug(f"Chunking {v_id} into {len(chunks)} chunk(s)")
for i, chunk in enumerate(chunks):
chunk_id = f"{v_id}_chunk_{i}"
- logger.info(f"Processing chunk {chunk_id}")
+ logger.debug(f"Processing chunk {chunk_id}")
# send chunks to be upserted (func, args)
- logger.info("chunk writes to upsert_chan")
+ logger.debug("chunk writes to upsert_chan")
await upsert_chan.put((upsert_chunk, (conn, v_id, chunk_id, chunk)))
# send chunks to have entities extracted
- logger.info("chunk writes to extract_chan")
+ logger.debug("chunk writes to extract_chan")
await extract_chan.put((chunk, chunk_id))
# send chunks to be embedded
- logger.info("chunk writes to embed_chan")
+ logger.debug("chunk writes to embed_chan")
await embed_chan.put((chunk_id, chunk, "DocumentChunk"))
return v_id
@@ -142,7 +144,7 @@ async def upsert_doc(conn: AsyncTigerGraphConnection, doc_id, ctype, content_tex
)
async def upsert_chunk(conn: AsyncTigerGraphConnection, doc_id, chunk_id, chunk):
- logger.info(f"Upserting chunk {chunk_id}")
+ logger.debug(f"Upserting chunk {chunk_id}")
date_added = int(time.time())
await util.upsert_vertex(
conn,
@@ -198,11 +200,11 @@ async def embed(
the vertex index to write to
"""
async with embed_sem:
- logger.info(f"Embedding {v_id}")
+ logger.debug(f"Embedding {v_id}")
# if loader is running, wait until it's done
if not util.loading_event.is_set():
- logger.info("Embed worker waiting for loading event to finish")
+ logger.debug("Embed worker waiting for loading event to finish")
await util.loading_event.wait()
try:
await embed_store.aadd_embeddings([(content, [])], [{"vertex_id": v_id}])
@@ -257,7 +259,8 @@ async def extract(
async with extract_sem:
try:
extracted: list[GraphDocument] = await extractor.aextract(chunk)
- logger.info(
+ # chunk_id is user-content-derived; demote.
+ logger.debug(
f"Extracting chunk: {chunk_id} ({len(extracted)} graph docs extracted)"
)
except Exception as e:
@@ -309,7 +312,7 @@ async def extract(
node_type_by_id[pid] = n.type
for i, node in enumerate(doc.nodes):
- logger.info(f"extract writes entity vert to upsert\nNode: {node.id}")
+ logger.debug(f"extract writes entity vert to upsert\nNode: {node.id}")
v_id = util.process_id(str(node.id))
if len(v_id) == 0:
continue
@@ -364,7 +367,7 @@ async def extract(
# ``investmentfund``.
meta_type_id = domain_vt
if meta_type_id:
- logger.info("extract writes type vert to upsert")
+ logger.debug("extract writes type vert to upsert")
await upsert_chan.put(
(
util.upsert_vertex,
@@ -376,7 +379,7 @@ async def extract(
),
)
)
- logger.info("extract writes entity_has_type edge to upsert")
+ logger.debug("extract writes entity_has_type edge to upsert")
await upsert_chan.put(
(
util.upsert_edge,
@@ -393,7 +396,7 @@ async def extract(
)
# link the entity to the chunk it came from
- logger.info("extract writes contains edge to upsert")
+ logger.debug("extract writes contains edge to upsert")
await upsert_chan.put(
(
util.upsert_edge,
@@ -415,7 +418,7 @@ async def extract(
# chunk via the multi-pair CONTAINS_ENTITY pair we
# added at init time.
if domain_vt is not None:
- logger.info(
+ logger.debug(
f"extract writes domain {domain_vt} vert + CONTAINS_ENTITY pair"
)
# Domain VTs don't carry the ECC bookkeeping
@@ -466,7 +469,9 @@ async def extract(
)
for edge in doc.relationships:
- logger.info(
+ # Edge content includes entity names + relationship
+ # types pulled from user documents.
+ logger.debug(
f"extract writes relates edge to upsert:{edge.source.id} -({edge.type})-> {edge.target.id}"
)
src_id = util.process_id(edge.source.id)
diff --git a/ecc/app/main.py b/ecc/app/main.py
index 7d6e67f..9889dc0 100644
--- a/ecc/app/main.py
+++ b/ecc/app/main.py
@@ -225,6 +225,7 @@ def rebuild_status(
"method": ecc_method,
"is_running": task_info.get("status") == "running",
"status": task_info.get("status"),
+ "stage": task_info.get("stage"),
"started_at": task_info.get("started_at"),
"completed_at": task_info.get("completed_at"),
"failed_at": task_info.get("failed_at"),
@@ -239,10 +240,24 @@ def rebuild_status(
}
+def _set_stage(task_key: str, msg: str) -> None:
+ """Update the human-readable stage label for an in-flight task.
+ Pulled out so individual stage transitions don't have to know
+ about the ``running_tasks`` schema.
+ """
+ info = running_tasks.get(task_key)
+ if info is not None:
+ info["stage"] = msg
+
+
async def run_with_tracking(task_key: str, run_func, graphname: str, conn):
"""Wrapper to track running tasks"""
try:
- running_tasks[task_key] = {"status": "running", "started_at": time.time()}
+ running_tasks[task_key] = {
+ "status": "running",
+ "started_at": time.time(),
+ "stage": "Preparing rebuild",
+ }
LogWriter.info(f"Starting ECC task: {task_key}")
# Verify the graph still exists before doing any work
@@ -284,8 +299,16 @@ async def run_with_tracking(task_key: str, run_func, graphname: str, conn):
else:
LogWriter.warning(f"GraphRAG config reload had issues: {graphrag_result['message']}")
- # Now run the actual job with fresh config
- await run_func(graphname, conn)
+ # Now run the actual job with fresh config. Pass a progress
+ # callback so sub-phases can surface in the UI rebuild dialog.
+ # ``run_func`` may ignore the kwarg (the supportai legacy path
+ # does); the call falls back to the no-progress signature on
+ # ``TypeError``.
+ progress_cb = lambda msg: _set_stage(task_key, msg)
+ try:
+ await run_func(graphname, conn, progress=progress_cb)
+ except TypeError:
+ await run_func(graphname, conn)
running_tasks[task_key] = {"status": "completed", "completed_at": time.time()}
LogWriter.info(f"Completed ECC task: {task_key}")
except Exception as e:
diff --git a/ecc/app/supportai/supportai_init.py b/ecc/app/supportai/supportai_init.py
index db18ab0..d622737 100644
--- a/ecc/app/supportai/supportai_init.py
+++ b/ecc/app/supportai/supportai_init.py
@@ -59,7 +59,8 @@ async def stream_docs(
async with tg_sem:
res = await conn.runInstalledQuery(
"StreamDocContent",
- params={"doc": d},
+ # 1-tuple form for VERTEX params.
+ params={"doc": (d,)},
)
logger.info("stream_docs writes to docs")
await docs_chan.put(res[0]["DocContent"][0])
@@ -90,7 +91,7 @@ async def chunk_docs(
# v_id = content["v_id"]
# txt = content["attributes"]["text"]
- logger.info("chunk writes to extract")
+ logger.debug("chunk writes to extract")
# await embed_chan.put((v_id, txt, "Document"))
task = sp.create_task(
@@ -137,7 +138,8 @@ async def embed(
async with asyncio.TaskGroup() as sp:
# consume task queue
async for v_id, content, index_name in embed_chan:
- logger.info(f"Embed to {graphname}_{index_name}: {v_id}")
+ # v_id derives from user content.
+ logger.debug(f"Embed to {graphname}_{index_name}: {v_id}")
sp.create_task(
workers.embed(
get_embedding_service(),
diff --git a/ecc/app/supportai/workers.py b/ecc/app/supportai/workers.py
index 3afc44f..ce85274 100644
--- a/ecc/app/supportai/workers.py
+++ b/ecc/app/supportai/workers.py
@@ -89,26 +89,28 @@ async def chunk_doc(
chunker = ecc_util.get_chunker(chunker_type, graphname=conn.graphname)
chunks = chunker.chunk(doc["attributes"]["text"])
- logger.info(f"Chunking {v_id} into {len(chunks)} chunk(s)")
+ # v_id / chunk_id derive from user document content; demote
+ # to DEBUG so the steady-state log doesn't carry data identifiers.
+ logger.debug(f"Chunking {v_id} into {len(chunks)} chunk(s)")
for i, chunk in enumerate(chunks):
chunk_id = f"{v_id}_chunk_{i}"
# send chunks to be upserted (func, args)
- logger.info("chunk writes to upsert_chan")
+ logger.debug("chunk writes to upsert_chan")
await upsert_chan.put((upsert_chunk, (conn, v_id, chunk_id, chunk)))
# send chunks to be embedded
- logger.info("chunk writes to embed_chan")
+ logger.debug("chunk writes to embed_chan")
await embed_chan.put((chunk_id, chunk, "DocumentChunk"))
# send chunks to have entities extracted
- logger.info("chunk writes to extract_chan")
+ logger.debug("chunk writes to extract_chan")
await extract_chan.put((chunk, chunk_id))
return doc["v_id"]
async def upsert_chunk(conn: TigerGraphConnection, doc_id, chunk_id, chunk):
- logger.info(f"Upserting chunk {chunk_id}")
+ logger.debug(f"Upserting chunk {chunk_id}")
date_added = int(time.time())
await util.upsert_vertex(
conn,
@@ -160,7 +162,7 @@ async def embed(
index_name: str
the vertex index to write to
"""
- logger.info(f"Embedding {v_id}")
+ logger.debug(f"Embedding {v_id}")
await embed_store.aadd_embeddings([(content, [])], [{"vertex_id": v_id}])
@@ -201,12 +203,12 @@ async def extract(
chunk: str,
chunk_id: str,
):
- logger.info(f"Extracting chunk: {chunk_id}")
+ logger.debug(f"Extracting chunk: {chunk_id}")
extracted: list[GraphDocument] = await extractor.aextract(chunk)
# upsert nodes and edges to the graph
for doc in extracted:
for node in doc.nodes:
- logger.info(f"extract writes entity vert to upsert\nNode: {node.id}")
+ logger.debug(f"extract writes entity vert to upsert\nNode: {node.id}")
v_id = util.process_id(str(node.id))
if len(v_id) == 0:
continue
@@ -231,7 +233,7 @@ async def extract(
)
# link the entity to the chunk it came from
- logger.info("extract writes contains edge to upsert")
+ logger.debug("extract writes contains edge to upsert")
await upsert_chan.put(
(
util.upsert_edge,
@@ -308,7 +310,7 @@ async def extract(
# Entity instance. Legacy supportai ECC paths without
# per-instance entity_type info skip the meta-layer.
# link the relationship to the chunk it came from
- logger.info("extract writes mentions edge to upsert")
+ logger.debug("extract writes mentions edge to upsert")
await upsert_chan.put(
(
util.upsert_edge,
diff --git a/graphrag-ui/src/components/Bot.tsx b/graphrag-ui/src/components/Bot.tsx
index 4f21673..283e2af 100644
--- a/graphrag-ui/src/components/Bot.tsx
+++ b/graphrag-ui/src/components/Bot.tsx
@@ -71,9 +71,18 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo
window.addEventListener('focus', handleFocus);
+ // Stay in sync when another component (Refresh dialog, Ingest
+ // dialog, Customize Prompts) changes the shared selectedGraph.
+ const handleSelectedGraph = () => {
+ const next = sessionStorage.getItem("selectedGraph") || '';
+ if (next !== selectedGraph) setSelectedGraph(next);
+ };
+ window.addEventListener('graphrag:selectedGraph', handleSelectedGraph);
+
// Cleanup
return () => {
window.removeEventListener('focus', handleFocus);
+ window.removeEventListener('graphrag:selectedGraph', handleSelectedGraph);
};
}, []);
diff --git a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
index cdad795..92142e6 100644
--- a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
+++ b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx
@@ -188,6 +188,26 @@ const CustomizePrompts = () => {
}
}, [graphOnly]);
+ // Stay in sync when another component (Bot, Refresh dialog,
+ // Ingest dialog) changes the shared selectedGraph. The prompts
+ // are scoped per graph, so a change triggers a re-fetch too.
+ useEffect(() => {
+ const handler = () => {
+ const next = sessionStorage.getItem("selectedGraph") || "";
+ if (next === selectedGraph) return;
+ setSelectedGraph(next);
+ if (next) {
+ setConfigScope("graph");
+ fetchPrompts(next);
+ } else if (!graphOnly) {
+ setConfigScope("global");
+ fetchPrompts("");
+ }
+ };
+ window.addEventListener("graphrag:selectedGraph", handler);
+ return () => window.removeEventListener("graphrag:selectedGraph", handler);
+ }, [selectedGraph, graphOnly]);
+
return (
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index 81e4a17..71efb12 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -46,7 +46,11 @@ const formatBytes = (bytes: number) => {
const IngestGraph: React.FC
= ({ isModal = false }) => {
const [confirm, confirmDialog] = useConfirm();
const [availableGraphs, setAvailableGraphs] = useState([]);
- const [ingestGraphName, setIngestGraphName] = useState("");
+ // Seed from the shared ``selectedGraph`` so the dropdown matches
+ // whatever was last picked elsewhere (KGAdmin refresh, Bot, etc.).
+ const [ingestGraphName, setIngestGraphName] = useState(
+ sessionStorage.getItem("selectedGraph") || ""
+ );
const [selectedFiles, setSelectedFiles] = useState(null);
const fileInputRef = useRef(null);
const [uploadedFiles, setUploadedFiles] = useState([]);
@@ -931,6 +935,18 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
});
}, []);
+ // Keep the Ingest dialog's graph picker in sync with the shared
+ // ``selectedGraph`` so changing the graph elsewhere (KGAdmin
+ // refresh, Bot) immediately reflects here.
+ useEffect(() => {
+ const handler = () => {
+ const next = sessionStorage.getItem("selectedGraph") || "";
+ if (next && next !== ingestGraphName) setIngestGraphName(next);
+ };
+ window.addEventListener("graphrag:selectedGraph", handler);
+ return () => window.removeEventListener("graphrag:selectedGraph", handler);
+ }, [ingestGraphName]);
+
// Load files when graph name changes
useEffect(() => {
if (ingestGraphName) {
@@ -963,7 +979,11 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
{
+ setIngestGraphName(v);
+ sessionStorage.setItem("selectedGraph", v);
+ window.dispatchEvent(new Event("graphrag:selectedGraph"));
+ }}
disabled={isIngesting}
>
{
};
// Refresh state
- const [refreshGraphName, setRefreshGraphName] = useState("");
+ // Seed from the shared ``selectedGraph`` so the dropdown
+ // matches whatever was last picked elsewhere (Bot, IngestGraph,
+ // etc.). Reacts to ``graphrag:selectedGraph`` events below.
+ const [refreshGraphName, setRefreshGraphName] = useState(
+ sessionStorage.getItem("selectedGraph") || ""
+ );
const [isRefreshing, setIsRefreshing] = useState(false);
const [refreshMessage, setRefreshMessage] = useState("");
const [isRebuildRunning, setIsRebuildRunning] = useState(false);
@@ -303,6 +308,18 @@ const KGAdmin = () => {
});
}, []);
+ // Keep the Refresh-dialog graph picker in sync with the shared
+ // ``selectedGraph`` so changing the graph elsewhere (Bot, the
+ // Ingest dialog) immediately reflects here.
+ useEffect(() => {
+ const handler = () => {
+ const next = sessionStorage.getItem("selectedGraph") || "";
+ if (next && next !== refreshGraphName) setRefreshGraphName(next);
+ };
+ window.addEventListener("graphrag:selectedGraph", handler);
+ return () => window.removeEventListener("graphrag:selectedGraph", handler);
+ }, [refreshGraphName]);
+
// Pull schema-init caps from /ui/config when the Initialize dialog opens.
// Read-only here; the values are edited on the GraphRAG Config page.
useEffect(() => {
@@ -696,6 +713,13 @@ const KGAdmin = () => {
return prev;
});
+ // Make the just-initialized graph the default selection across
+ // the app (chat picker, ingest dialog, customize prompts, etc.).
+ // Bot.tsx listens to ``graphrag:selectedGraph`` events to refresh
+ // its dropdown; other pages re-read sessionStorage on mount.
+ sessionStorage.setItem("selectedGraph", newGraph);
+ window.dispatchEvent(new Event("graphrag:selectedGraph"));
+
setRefreshGraphName(graphName);
setGraphName("");
} catch (error: any) {
@@ -740,8 +764,9 @@ const KGAdmin = () => {
const startTime = statusData.started_at
? new Date(statusData.started_at * 1000).toLocaleString()
: "unknown time";
+ const stage = statusData.stage ? ` — ${statusData.stage}` : "";
setRefreshMessage(
- `⚠️ A rebuild is already in progress for "${graphName}" (started at ${startTime}). Please wait for it to complete.`
+ `⚠️ A rebuild is already in progress for "${graphName}" (started at ${startTime})${stage}. Please wait for it to complete.`
);
} else if (wasRunning && statusData.status === "completed") {
setRefreshMessage(`✅ Rebuild completed successfully for "${graphName}".`);
@@ -1970,7 +1995,11 @@ const KGAdmin = () => {
{
+ setRefreshGraphName(v);
+ sessionStorage.setItem("selectedGraph", v);
+ window.dispatchEvent(new Event("graphrag:selectedGraph"));
+ }}
disabled={isRefreshing || isRebuildRunning || isCheckingStatus}
>
str:
+ d = desc_map.get(name)
+ return f"{name} ({d})" if d else name
+
+ vertex_types_for_llm = [_label(v, entity_descs) for v in target_vertex_types]
+ edge_types_for_llm = [_label(e, rel_defs) for e in target_edge_types]
+
inputs = {
"question": question,
- "vertex_types": target_vertex_types,
- "edge_types": target_edge_types,
+ "vertex_types": vertex_types_for_llm,
+ "edge_types": edge_types_for_llm,
"vertex_attributes": target_vertex_attributes,
"vertex_ids": target_vertex_ids,
"edge_attributes": target_edge_attributes,
diff --git a/graphrag/app/tools/generate_gsql.py b/graphrag/app/tools/generate_gsql.py
index 6894265..60c13f9 100644
--- a/graphrag/app/tools/generate_gsql.py
+++ b/graphrag/app/tools/generate_gsql.py
@@ -20,7 +20,7 @@
from langchain.llms.base import LLM
from common.metrics.tg_proxy import TigerGraphConnectionProxy
from common.db.connections import get_schema_ver
-from common.db.schema_utils import read_type_metadata
+from common.db.schema_utils import render_schema_rep
from common.logs.logwriter import LogWriter
from common.logs.log import req_id_cv
@@ -55,59 +55,15 @@ def __init__(self, conn: TigerGraphConnectionProxy, llm):
self.schema_ver = 0
def _generate_schema_rep(self):
- schema_ver = get_schema_ver(self.conn)
+ # Schema rendering is shared with generate_cypher + the
+ # question-mapping tools via ``schema_utils.render_schema_rep``;
+ # we only keep the per-instance cache here.
+ text, schema_ver = render_schema_rep(self.conn)
if self.schema_rep and self.schema_ver == schema_ver:
logger.info(f"Reusing existing schema rep for schema version {schema_ver}")
return self.schema_rep
- verts = self.conn.getVertexTypes()
- edges = self.conn.getEdgeTypes()
- try:
- entity_descs, rel_defs = read_type_metadata(self.conn)
- except Exception as exc:
- logger.warning(f"read_type_metadata failed: {exc}")
- entity_descs, rel_defs = {}, {}
- vertex_schema = []
- for vert in verts:
- primary_id = self.conn.getVertexType(vert)["PrimaryId"]["AttributeName"]
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
- for attr in self.conn.getVertexType(vert)["Attributes"]])
- if attributes == "":
- attributes = "No attributes"
- defn_line = ""
- if entity_descs.get(vert):
- defn_line = f"\n\tDefinition: {entity_descs[vert]}"
- vertex_schema.append(f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id}\n\tAttributes: \n\t\t{attributes}")
-
- edge_schema = []
- for edge in edges:
- from_vertex = self.conn.getEdgeType(edge)["FromVertexTypeName"]
- to_vertex = self.conn.getEdgeType(edge)["ToVertexTypeName"]
- direction = "Directed" if self.conn.getEdgeType(edge)["IsDirected"] else "Undirected"
- #reverse_edge = conn.getEdgeType(edge)["Config"].get("REVERSE_EDGE")
- attributes = "\n\t\t".join([attr["AttributeName"] + " of type " + attr["AttributeType"]["Name"]
- for attr in self.conn.getEdgeType(edge)["Attributes"]])
- if attributes == "":
- attributes = "No attributes"
- defn_line = ""
- if rel_defs.get(edge):
- defn_line = f"\n\tDefinition: {rel_defs[edge]}"
- if from_vertex == "*" or to_vertex == "*":
- edge_pairs = self.conn.getEdgeType(edge)["EdgePairs"]
- for an_edge in edge_pairs:
- edge_info = f"""From Vertex: {an_edge["From"]}\n\tTo Vertex: {an_edge["To"]}"""
- edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
- else:
- edge_info = f"""From Vertex: {from_vertex}\n\tTo Vertex: {to_vertex}"""
- edge_schema.append(f"""{edge}{defn_line}\n\t{edge_info}\n\tEdge direction: {direction}\n\tAttributes: \n\t\t{attributes}""")
-
- self.schema_rep = f"""The schema of the graph {self.conn.graphname} is as follows:
-Vertex Types:
-{chr(10).join(vertex_schema)}
-
-Edge Types:
-{chr(10).join(edge_schema)}
-"""
- self.schema_ver = schema_ver
+ self.schema_rep = text
+ self.schema_ver = schema_ver if schema_ver is not None else 0
return self.schema_rep
def generate_gsql(self, question: str, history: Iterable[str]) -> str:
diff --git a/graphrag/app/tools/map_question_to_schema.py b/graphrag/app/tools/map_question_to_schema.py
index 1ce9211..bde9f3d 100644
--- a/graphrag/app/tools/map_question_to_schema.py
+++ b/graphrag/app/tools/map_question_to_schema.py
@@ -27,6 +27,7 @@
from common.logs.log import req_id_cv
from common.logs.logwriter import LogWriter
from common.db.connections import get_schema_ver
+from common.db.schema_utils import read_type_metadata
logger = logging.getLogger(__name__)
@@ -119,12 +120,30 @@ def _run(self, query: str, conversation: List[Dict[str, str]]) -> str:
else:
logger.info(f"Reusing existing schema rep for schema version {schema_ver}")
+ # Pull the user-defined ``description`` / ``definition`` from
+ # the EntityType / RelationshipType meta vertices so the LLM
+ # sees the same domain hints that the cypher/gsql generators
+ # already get. Empty when no metadata is set — the surface
+ # shape stays a plain list of names for those types.
+ try:
+ entity_descs, rel_defs = read_type_metadata(self.conn)
+ except Exception as exc:
+ logger.warning(f"read_type_metadata failed in mq2s: {exc}")
+ entity_descs, rel_defs = {}, {}
+
+ def _label(name: str, desc_map: dict) -> str:
+ d = desc_map.get(name)
+ return f"{name} ({d})" if d else name
+
+ vertices_for_llm = [_label(v, entity_descs) for v in self.vertices]
+ edges_for_llm = [_label(e, rel_defs) for e in self.edges]
+
parsed_q = self.llm.invoke_with_parser(
RESTATE_QUESTION_PROMPT, parser,
{
- "vertices": self.vertices,
+ "vertices": vertices_for_llm,
"verticesAttrs": self.vertices_info,
- "edges": self.edges,
+ "edges": edges_for_llm,
"edgesInfo": self.edges_info,
"question": query,
"conversation": conversation,
From 6e9d64f0c319d24e15d6a5b97746042aca22f3b6 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 14 May 2026 12:22:11 -0700
Subject: [PATCH 63/70] Multi-edge support, anti-hallucination, init UX
hardening
- Support parallel edges on the same (src,dst) via discriminator attributes
- Strengthen extraction prompt to suppress facts and values not in the source text
- Coerce LLM-emitted attributes to declared types and drop sentinel placeholders
- Raise per-type attribute caps and remove the hard upper bound on edge type count
- Add eligibility precheck and per-type description capture to the init dialog
- Expose new schema-aware knobs (strict mode, retrieval scope, sample limits)
---
README.md | 7 +
common/db/schema_utils.py | 427 +++++++++++++---
.../LLMEntityRelationshipExtractor.py | 396 ++++++++-------
common/llm_services/base_llm.py | 11 +-
ecc/app/graphrag/util.py | 220 ++++++--
ecc/app/graphrag/workers.py | 63 ++-
graphrag-ui/src/actions/ActionProvider.tsx | 7 +-
graphrag-ui/src/components/Bot.tsx | 4 +-
.../src/components/ConfigScopeToggle.tsx | 2 +-
.../src/components/CustomChatMessage.tsx | 45 +-
graphrag-ui/src/components/Interact.tsx | 44 +-
graphrag-ui/src/components/ModeToggle.tsx | 2 +-
graphrag-ui/src/components/SideMenu.tsx | 13 +-
graphrag-ui/src/hooks/useIdleTimeout.ts | 13 +
graphrag-ui/src/index.css | 6 +-
graphrag-ui/src/pages/Chat.tsx | 107 +++-
graphrag-ui/src/pages/TraceLogs.tsx | 11 +-
.../src/pages/setup/GraphRAGConfig.tsx | 357 +++++++++----
graphrag-ui/src/pages/setup/IngestGraph.tsx | 5 +-
graphrag-ui/src/pages/setup/KGAdmin.tsx | 476 ++++++++++++++++--
graphrag-ui/src/pages/setup/LLMConfig.tsx | 4 +-
graphrag/app/routers/ui.py | 439 ++++++++++++++--
graphrag/app/tools/generate_cypher.py | 3 +-
graphrag/app/tools/generate_gsql.py | 3 +-
24 files changed, 2152 insertions(+), 513 deletions(-)
diff --git a/README.md b/README.md
index 94d4cbd..662708d 100644
--- a/README.md
+++ b/README.md
@@ -479,6 +479,11 @@ Copy the below code into `configs/server_config.json`. You shouldn’t need to c
| `chat_history_api` | string | `"http://chat-history:8002"` | URL of the chat history service. No change needed when using the provided Docker Compose file. |
| `chunker` | string | `"semantic"` | Default document chunker. Options: `semantic`, `character`, `regex`, `markdown`, `html`, `recursive`. |
| `extractor` | string | `"llm"` | Entity extraction method. Options: `llm`, `graphrag`. |
+| `strict_mode` | bool | `false` | Dynamic-schema enforcement during extraction. When `true`, entities and relationships that don't match the domain schema are dropped. When `false` (default), unmatched nodes fall back to generic `Entity` vertices. |
+| `retrieval_include_entity` | bool \| null | `null` (auto) | Whether retriever queries include the generic `Entity` vertex alongside domain types. When unset, the server uses `false` if a domain schema exists and `true` otherwise. Set explicitly to override. |
+| `schema_max_sample_files` | int | `5` | Maximum number of sample documents accepted by the *Generate from sample documents* path on the *Initialize Knowledge Graph* dialog. |
+| `schema_max_total_mb` | int | `50` | Combined upload cap (MB) across all sample files for schema extraction. Bounds the content sent to the LLM. A single file may use the full budget; no separate per-file cap. |
+| `enable_router_fallback` | bool | `true` | When the function-call or Cypher path fails after 3 retries, fall back to vector search instead of failing the query. |
| `chunker_config` | object | `{}` | Chunker-specific settings (see sub-parameters below). All settings are saved regardless of which chunker is selected as default. |
| ↳ `chunk_size` | int | `2048` | Maximum number of characters per chunk. Used by `character`, `markdown`, `html`, and `recursive` chunkers. Larger values produce fewer, bigger chunks; smaller values produce more, finer-grained chunks. |
| ↳ `overlap_size` | int | 1/8 of `chunk_size` | Number of overlapping characters between consecutive chunks. Used by `character`, `markdown`, `html`, and `recursive` chunkers. More overlap preserves cross-chunk context but increases total chunk count. Set to `0` for no overlap. |
@@ -929,6 +934,8 @@ Today's primary lever is the **entity-extraction prompt**:
If extraction quality is still poor after iterating on the prompt, the next-best option today is to clear the graph's domain types and re-ingest with the improved prompt — schema growth is currently driven entirely by what extraction produces. (A schema-aware initialization flow that lets you supply a curated schema up front is on the roadmap.)
+**Note on LLM faithfulness.** Entity, relationship, and attribute extraction is best-effort and may include occasional errors, especially for well-known entities. For high-stakes applications, validate critical extracted values against your source documents before relying on them.
+
### 4. Retrieval — match context size to the question
Three knobs interact: `top_k`, `num_hops`, `num_seen_min`. Also `chunk_only` / `doc_only` and (for community search) `community_level` / `with_chunk`.
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index 779054d..cdb3360 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -752,6 +752,64 @@ def has_edge_pair(self, name: str, from_vt: str, to_vt: str) -> bool:
return False
+@dataclass
+class AllowedSchema:
+ """Domain-schema bundle handed to the LLM entity/relationship
+ extractor. Carries one text rendering for the LLM prompt and the
+ structured maps the worker layer uses for runtime coercion and
+ endpoint validation.
+
+ All fields exclude GraphRAG structural types — only user-declared
+ domain types reach the extractor.
+
+ Fields:
+ schema_rep — rendered schema text suitable for an LLM prompt
+ (vertex types with attributes, edge types with endpoints,
+ inline definitions). Reuses the same shape that
+ ``render_schema_rep`` produces for query-side tools.
+ vertex_types / edge_types — name lists for fast allow-checks.
+ vertex_attributes / edge_attributes — ``{type: {attr: tg_type}}``
+ for typed-attribute coercion at upsert time.
+ vertex_definitions / edge_definitions — ``{type: description}``
+ from EntityType / RelationshipType meta-vertices.
+ edge_endpoints — ``{edge: [(from_vt, to_vt), ...]}`` for the
+ worker's endpoint-pair validation.
+ """
+
+ schema_rep: str = ""
+ schema_version: Optional[int] = None
+ vertex_types: List[str] = field(default_factory=list)
+ edge_types: List[str] = field(default_factory=list)
+ vertex_attributes: dict = field(default_factory=dict)
+ edge_attributes: dict = field(default_factory=dict)
+ vertex_definitions: dict = field(default_factory=dict)
+ edge_definitions: dict = field(default_factory=dict)
+ edge_endpoints: dict = field(default_factory=dict)
+
+
+# TG accepts only these types inside a DISCRIMINATOR(...) clause.
+# DOUBLE / FLOAT / BOOL are rejected at schema-change time.
+_DISCRIMINATOR_TYPES = frozenset({"INT", "UINT", "STRING", "DATETIME"})
+
+
+def _default_literal(tg_type: str) -> str:
+ """Return the GSQL literal for the per-type default — used inside
+ ``DISCRIMINATOR(... DEFAULT )`` clauses so the column is
+ non-nullable but per-instance upserts that omit the value still
+ succeed (the omitted attribute falls to the default).
+ """
+ t = (tg_type or "").upper()
+ if t in ("INT", "UINT"):
+ return "0"
+ if t in ("DOUBLE", "FLOAT"):
+ return "0.0"
+ if t == "BOOL":
+ return "false"
+ if t == "DATETIME":
+ return '"1970-01-01 00:00:00"'
+ return '""'
+
+
def emit_add_statements(
proposal: SchemaProposal,
existing: Optional[ExistingSchema] = None,
@@ -799,11 +857,20 @@ def emit_add_statements(
pairs_str = " | ".join(
f"FROM {src}, TO {tgt}" for src, tgt in e.pairs
)
- attrs_part = ""
- if e.attributes:
- attrs_part = ", " + ", ".join(
- f"{a.name} {a.type}" for a in e.attributes
- )
+ # Promote discriminator-eligible attributes (per TG: INT,
+ # UINT, STRING, DATETIME) into a ``DISCRIMINATOR(...)``
+ # clause with type defaults. Other attribute types stay as
+ # regular nullable columns outside the clause.
+ disc_attrs = [a for a in e.attributes if a.type.upper() in _DISCRIMINATOR_TYPES]
+ plain_attrs = [a for a in e.attributes if a.type.upper() not in _DISCRIMINATOR_TYPES]
+ parts: List[str] = []
+ if disc_attrs:
+ parts.append("DISCRIMINATOR(" + ", ".join(
+ f"{a.name} {a.type} DEFAULT {_default_literal(a.type)}"
+ for a in disc_attrs
+ ) + ")")
+ parts.extend(f"{a.name} {a.type}" for a in plain_attrs)
+ attrs_part = (", " + ", ".join(parts)) if parts else ""
edge_kw = "DIRECTED EDGE" if e.directed else "UNDIRECTED EDGE"
# Undirected edges have no reverse companion, so omit the
# WITH REVERSE_EDGE clause.
@@ -814,10 +881,10 @@ def emit_add_statements(
f'ADD {edge_kw} {e.name} ({pairs_str}{attrs_part}){with_clause}'
)
else:
- # Existing edge: only ALTER ADD PAIR is supported. Attributes
- # of an existing edge can't be added at the same time; that's
- # a separate ALTER ATTRIBUTE statement and is out of scope
- # for the additive Phase 1 diff emitter.
+ # Existing edge: only ALTER ADD PAIR is supported here.
+ # Adding attributes on an existing edge needs a separate
+ # ALTER ATTRIBUTE statement and is out of scope for this
+ # additive diff.
for src, tgt in e.pairs:
if existing.has_edge_pair(e.name, src, tgt):
continue
@@ -1106,22 +1173,66 @@ async def read_existing_schema_async(conn) -> "ExistingSchema":
return snapshot
-def render_schema_rep(conn) -> Tuple[str, Optional[int]]:
- """Render the live schema as a per-vertex / per-edge text block
- suitable for inclusion in an LLM prompt (`generate_cypher`,
- `generate_gsql`, etc.). Includes any user-supplied definitions
- stored on the ``EntityType`` / ``RelationshipType`` meta vertices
- via :func:`read_type_metadata`.
+def _assemble_schema_rep(
+ *,
+ graphname: str,
+ schema_ver: Optional[int],
+ vertex_blocks: List[str],
+ edge_blocks: List[str],
+ exclude_structural: bool,
+ domain_verts: List[str],
+ domain_edge_types: List[str],
+ vertex_attributes: dict,
+ edge_attributes: dict,
+ entity_descs: dict,
+ rel_defs: dict,
+ edge_endpoints: dict,
+) -> AllowedSchema:
+ """Bundle pre-computed blocks into an ``AllowedSchema``. Shared by
+ the sync and async builders so both paths produce identical output.
+ """
+ if exclude_structural and not domain_verts and not domain_edge_types:
+ return AllowedSchema(schema_version=schema_ver)
+ graph_label = f" {graphname}" if graphname else ""
+ qualifier = "domain " if exclude_structural else ""
+ text = (
+ f"The {qualifier}schema of the graph{graph_label} is as follows:\n"
+ f"Vertex Types:\n{chr(10).join(vertex_blocks) if vertex_blocks else '(none)'}"
+ f"\n\nEdge Types:\n{chr(10).join(edge_blocks) if edge_blocks else '(none)'}\n"
+ )
+ domain_entity_defs = {v: entity_descs[v] for v in domain_verts if entity_descs.get(v)}
+ domain_rel_defs = {e: rel_defs[e] for e in domain_edge_types if rel_defs.get(e)}
+ return AllowedSchema(
+ schema_rep=text,
+ schema_version=schema_ver,
+ vertex_types=domain_verts,
+ edge_types=domain_edge_types,
+ vertex_attributes=vertex_attributes,
+ edge_attributes=edge_attributes,
+ vertex_definitions=domain_entity_defs,
+ edge_definitions=domain_rel_defs,
+ edge_endpoints=edge_endpoints,
+ )
+
+
+def render_schema_rep(conn, exclude_structural: bool = False) -> AllowedSchema:
+ """Read the live schema and return a full :class:`AllowedSchema`
+ bundle (rendered text + structured maps + version).
- Returns ``(schema_text, schema_version)`` so the caller can cache
- the text against the version and skip re-rendering on the next
- call (schemas don't change between rebuilds in normal operation).
+ Used by both query-side tools (``generate_cypher`` / ``generate_gsql``
+ / ``map_question_to_schema``) and the ECC entity extractor. Pass
+ ``exclude_structural=True`` to drop GraphRAG structural types
+ (Entity, Document, Community, structural edges, etc.) — the
+ extractor uses this mode; query-side tools use the default so the
+ LLM sees the full graph including bookkeeping types.
+
+ Returns an :class:`AllowedSchema` with at least ``schema_version``
+ populated; when the graph has no types yet, the other fields stay
+ empty.
"""
from common.db.connections import get_schema_ver as _get_schema_ver
-
schema_ver = _get_schema_ver(conn)
- verts = conn.getVertexTypes()
- edges = conn.getEdgeTypes()
+
try:
entity_descs, rel_defs = read_type_metadata(conn)
except Exception:
@@ -1130,61 +1241,253 @@ def render_schema_rep(conn) -> Tuple[str, Optional[int]]:
# rather than failing.
entity_descs, rel_defs = {}, {}
+ try:
+ all_verts = conn.getVertexTypes() or []
+ except Exception:
+ all_verts = []
+ domain_verts = (
+ [v for v in all_verts if not is_structural_type(v)]
+ if exclude_structural else list(all_verts)
+ )
+
+ vertex_attributes: dict = {}
vertex_blocks: List[str] = []
- for vert in verts:
- vinfo = conn.getVertexType(vert)
- primary_id = vinfo["PrimaryId"]["AttributeName"]
- attributes = "\n\t\t".join(
- a["AttributeName"] + " of type " + a["AttributeType"]["Name"]
- for a in vinfo["Attributes"]
- ) or "No attributes"
+ for vert in sorted(domain_verts):
+ try:
+ vinfo = conn.getVertexType(vert) or {}
+ except Exception:
+ continue
+ primary_id_name = (vinfo.get("PrimaryId") or {}).get("AttributeName", "")
+ attrs_map, attr_lines = _collect_attrs(vinfo.get("Attributes"), primary_id_name)
+ vertex_attributes[vert] = attrs_map
defn_line = (
f"\n\tDefinition: {entity_descs[vert]}" if entity_descs.get(vert) else ""
)
+ attrs_block = "\n\t\t".join(attr_lines) or "No attributes"
vertex_blocks.append(
- f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id}"
- f"\n\tAttributes: \n\t\t{attributes}"
+ f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id_name}"
+ f"\n\tAttributes: \n\t\t{attrs_block}"
)
+ try:
+ all_edges = conn.getEdgeTypes() or []
+ except Exception:
+ all_edges = []
+ edge_attributes: dict = {}
+ edge_endpoints: dict = {}
edge_blocks: List[str] = []
- for edge in edges:
- einfo = conn.getEdgeType(edge)
- from_vertex = einfo["FromVertexTypeName"]
- to_vertex = einfo["ToVertexTypeName"]
- direction = "Directed" if einfo["IsDirected"] else "Undirected"
- attributes = "\n\t\t".join(
- a["AttributeName"] + " of type " + a["AttributeType"]["Name"]
- for a in einfo["Attributes"]
- ) or "No attributes"
+ domain_edge_types: List[str] = []
+ for edge in sorted(all_edges):
+ if exclude_structural and is_structural_type(edge):
+ continue
+ try:
+ einfo = conn.getEdgeType(edge) or {}
+ except Exception:
+ continue
+ pairs = _collect_edge_pairs(einfo, exclude_structural)
+ if exclude_structural and not pairs:
+ continue
+ domain_edge_types.append(edge)
+ edge_endpoints[edge] = pairs
+ attrs_map, attr_lines = _collect_attrs(einfo.get("Attributes"), "")
+ edge_attributes[edge] = attrs_map
+ direction = "Directed" if einfo.get("IsDirected") else "Undirected"
defn_line = (
f"\n\tDefinition: {rel_defs[edge]}" if rel_defs.get(edge) else ""
)
- if from_vertex == "*" or to_vertex == "*":
- for pair in einfo.get("EdgePairs", []):
- pair_info = (
- f"From Vertex: {pair['From']}\n\tTo Vertex: {pair['To']}"
- )
- edge_blocks.append(
- f"{edge}{defn_line}\n\t{pair_info}"
- f"\n\tEdge direction: {direction}"
- f"\n\tAttributes: \n\t\t{attributes}"
- )
- else:
- pair_info = f"From Vertex: {from_vertex}\n\tTo Vertex: {to_vertex}"
+ attrs_block = "\n\t\t".join(attr_lines) or "No attributes"
+ # Emit one block per (FROM, TO) pair — keeps the rendered
+ # text single-pair-per-block.
+ for src, tgt in pairs:
+ pair_info = f"From Vertex: {src}\n\tTo Vertex: {tgt}"
edge_blocks.append(
f"{edge}{defn_line}\n\t{pair_info}"
f"\n\tEdge direction: {direction}"
- f"\n\tAttributes: \n\t\t{attributes}"
+ f"\n\tAttributes: \n\t\t{attrs_block}"
)
- graphname = getattr(conn, "graphname", "") or ""
- graph_label = f" {graphname}" if graphname else ""
- text = (
- f"The schema of the graph{graph_label} is as follows:\n"
- f"Vertex Types:\n{chr(10).join(vertex_blocks)}\n\n"
- f"Edge Types:\n{chr(10).join(edge_blocks)}\n"
+ return _assemble_schema_rep(
+ graphname=getattr(conn, "graphname", "") or "",
+ schema_ver=schema_ver,
+ vertex_blocks=vertex_blocks,
+ edge_blocks=edge_blocks,
+ exclude_structural=exclude_structural,
+ domain_verts=sorted(domain_verts) if exclude_structural else list(all_verts),
+ domain_edge_types=domain_edge_types,
+ vertex_attributes=vertex_attributes,
+ edge_attributes=edge_attributes,
+ entity_descs=entity_descs,
+ rel_defs=rel_defs,
+ edge_endpoints=edge_endpoints,
+ )
+
+
+def _collect_attrs(attr_list, skip_name: str) -> Tuple[dict, List[str]]:
+ """Walk an ``Attributes`` array from ``getVertexType`` /
+ ``getEdgeType`` and return ``({attr_name: tg_type}, ["name of type
+ type", ...])``. ``skip_name`` is the primary-id attribute that
+ shouldn't appear in the user-facing schema rep.
+ """
+ attrs_map: dict = {}
+ lines: List[str] = []
+ for a in attr_list or []:
+ a_name = a.get("AttributeName")
+ a_type = ((a.get("AttributeType") or {}).get("Name")) or "STRING"
+ if not a_name or a_name == skip_name:
+ continue
+ attrs_map[a_name] = a_type
+ lines.append(f"{a_name} of type {a_type}")
+ return attrs_map, lines
+
+
+def _collect_edge_pairs(einfo: dict, exclude_structural: bool) -> List[Tuple[str, str]]:
+ """Build the (FROM, TO) pair list for an edge, filtering out pairs
+ whose endpoint is a structural type when ``exclude_structural`` is
+ set. Used by both schema-rep paths.
+ """
+ pairs: List[Tuple[str, str]] = []
+ from_v = einfo.get("FromVertexTypeName")
+ to_v = einfo.get("ToVertexTypeName")
+ if from_v and to_v and from_v != "*" and to_v != "*":
+ if not (exclude_structural and (is_structural_type(from_v) or is_structural_type(to_v))):
+ pairs.append((from_v, to_v))
+ for ep in einfo.get("EdgePairs", []) or []:
+ f, t = ep.get("From"), ep.get("To")
+ if not (f and t):
+ continue
+ if exclude_structural and (is_structural_type(f) or is_structural_type(t)):
+ continue
+ pairs.append((f, t))
+ return pairs
+
+
+# Backwards-compatible alias for callers that still want the old name.
+# ``render_schema_rep(conn, exclude_structural=True)`` is the canonical
+# spelling; keep this until call sites migrate.
+def build_allowed_schema(conn) -> AllowedSchema:
+ """Back-compat alias for ``render_schema_rep(conn, exclude_structural=True)``."""
+ return render_schema_rep(conn, exclude_structural=True)
+
+
+ domain_entity_defs = {v: entity_descs[v] for v in domain_verts if entity_descs.get(v)}
+ domain_rel_defs = {e: rel_defs[e] for e in domain_edge_types if rel_defs.get(e)}
+
+ return AllowedSchema(
+ schema_rep=text,
+ vertex_types=domain_verts,
+ edge_types=domain_edge_types,
+ vertex_attributes=vertex_attributes,
+ edge_attributes=edge_attributes,
+ vertex_definitions=domain_entity_defs,
+ edge_definitions=domain_rel_defs,
+ edge_endpoints=edge_endpoints,
)
- return text, schema_ver
+
+
+async def render_schema_rep_async(
+ conn, exclude_structural: bool = False,
+) -> AllowedSchema:
+ """Async counterpart to :func:`render_schema_rep`. Used by the ECC
+ pipeline where ``conn`` is an ``AsyncTigerGraphConnection`` (whose
+ ``getVertexType`` / ``getEdgeType`` are coroutines).
+
+ Same semantics as the sync version — see :func:`render_schema_rep`.
+ """
+ from common.db.connections import get_schema_ver as _get_schema_ver
+
+ try:
+ schema_ver = _get_schema_ver(conn)
+ except Exception:
+ schema_ver = None
+
+ try:
+ entity_descs, rel_defs = await read_type_metadata_async(conn)
+ except Exception:
+ entity_descs, rel_defs = {}, {}
+
+ try:
+ all_verts = await conn.getVertexTypes() or []
+ except Exception:
+ all_verts = []
+ domain_verts = (
+ [v for v in all_verts if not is_structural_type(v)]
+ if exclude_structural else list(all_verts)
+ )
+
+ vertex_attributes: dict = {}
+ vertex_blocks: List[str] = []
+ for vert in sorted(domain_verts):
+ try:
+ vinfo = await conn.getVertexType(vert) or {}
+ except Exception:
+ continue
+ primary_id_name = (vinfo.get("PrimaryId") or {}).get("AttributeName", "")
+ attrs_map, attr_lines = _collect_attrs(vinfo.get("Attributes"), primary_id_name)
+ vertex_attributes[vert] = attrs_map
+ defn_line = (
+ f"\n\tDefinition: {entity_descs[vert]}" if entity_descs.get(vert) else ""
+ )
+ attrs_block = "\n\t\t".join(attr_lines) or "No attributes"
+ vertex_blocks.append(
+ f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id_name}"
+ f"\n\tAttributes: \n\t\t{attrs_block}"
+ )
+
+ try:
+ all_edges = await conn.getEdgeTypes() or []
+ except Exception:
+ all_edges = []
+ edge_attributes: dict = {}
+ edge_endpoints: dict = {}
+ edge_blocks: List[str] = []
+ domain_edge_types: List[str] = []
+ for edge in sorted(all_edges):
+ if exclude_structural and is_structural_type(edge):
+ continue
+ try:
+ einfo = await conn.getEdgeType(edge) or {}
+ except Exception:
+ continue
+ pairs = _collect_edge_pairs(einfo, exclude_structural)
+ if exclude_structural and not pairs:
+ continue
+ domain_edge_types.append(edge)
+ edge_endpoints[edge] = pairs
+ attrs_map, attr_lines = _collect_attrs(einfo.get("Attributes"), "")
+ edge_attributes[edge] = attrs_map
+ direction = "Directed" if einfo.get("IsDirected") else "Undirected"
+ defn_line = (
+ f"\n\tDefinition: {rel_defs[edge]}" if rel_defs.get(edge) else ""
+ )
+ attrs_block = "\n\t\t".join(attr_lines) or "No attributes"
+ for src, tgt in pairs:
+ pair_info = f"From Vertex: {src}\n\tTo Vertex: {tgt}"
+ edge_blocks.append(
+ f"{edge}{defn_line}\n\t{pair_info}"
+ f"\n\tEdge direction: {direction}"
+ f"\n\tAttributes: \n\t\t{attrs_block}"
+ )
+
+ return _assemble_schema_rep(
+ graphname=getattr(conn, "graphname", "") or "",
+ schema_ver=schema_ver,
+ vertex_blocks=vertex_blocks,
+ edge_blocks=edge_blocks,
+ exclude_structural=exclude_structural,
+ domain_verts=sorted(domain_verts) if exclude_structural else list(all_verts),
+ domain_edge_types=domain_edge_types,
+ vertex_attributes=vertex_attributes,
+ edge_attributes=edge_attributes,
+ entity_descs=entity_descs,
+ rel_defs=rel_defs,
+ edge_endpoints=edge_endpoints,
+ )
+
+
+# Back-compat alias for the ECC pipeline.
+async def build_allowed_schema_async(conn) -> AllowedSchema:
+ """Back-compat alias for ``render_schema_rep_async(conn, exclude_structural=True)``."""
+ return await render_schema_rep_async(conn, exclude_structural=True)
async def read_type_metadata_async(conn) -> Tuple[dict, dict]:
@@ -1352,9 +1655,9 @@ def _report(msg: str) -> None:
}
# Split into two phases so TG's job-validator never sees an ALTER
- # statement that references a vertex/edge type created elsewhere in
- # the same job. Phase 1 ADDs new types; phase 2 ALTERs (e.g. ADD
- # PAIR on existing edges) runs only after phase 1 commits.
+ # referencing a vertex/edge type created elsewhere in the same
+ # job. The ADD phase runs first; the ALTER phase (e.g. ADD PAIR
+ # on existing edges) runs only after the ADD phase commits.
add_stmts = [s for s in statements if s.lstrip().upper().startswith("ADD ")]
alter_stmts = [s for s in statements if s.lstrip().upper().startswith("ALTER ")]
diff --git a/common/extractors/LLMEntityRelationshipExtractor.py b/common/extractors/LLMEntityRelationshipExtractor.py
index 877197b..3b77410 100644
--- a/common/extractors/LLMEntityRelationshipExtractor.py
+++ b/common/extractors/LLMEntityRelationshipExtractor.py
@@ -29,33 +29,57 @@ class LLMEntityRelationshipExtractor(BaseExtractor):
def __init__(
self,
llm_service: LLM_Model,
- allowed_entity_types: List[str] = None,
- allowed_relationship_types: List[str] = None,
+ allowed_schema=None,
strict_mode: bool = False,
- entity_type_definitions: dict = None,
- relationship_type_definitions: dict = None,
- domain_edge_endpoints: dict = None,
):
+ """Build an LLM-driven entity/relationship extractor.
+
+ ``allowed_schema`` is the consolidated description of the
+ domain schema the extractor must respect. It carries the
+ LLM-facing text rendering plus the structured maps the worker
+ layer uses for coercion and endpoint validation. Pass ``None``
+ for "no schema — extract anything" mode.
+
+ ``strict_mode`` (default ``False``) — when ``True`` the parser
+ drops nodes / relationships whose type isn't in the schema
+ AND the prompt tells the LLM to stay within it. Read from
+ ``graphrag_config.strict_mode`` by the ECC builder.
+ """
+ from common.db.schema_utils import AllowedSchema
self.llm_service = llm_service
- self.allowed_vertex_types = allowed_entity_types
- self.allowed_edge_types = allowed_relationship_types
- # When True the existing parser filter (drop nodes/rels whose
- # type isn't in the allowed list) is enforced AND the prompt
- # tells the LLM to stay within the schema. Read from
- # graphrag_config.strict_mode by the ECC builder.
+ self.allowed_schema = allowed_schema or AllowedSchema()
self.strict_mode = strict_mode
- self.entity_type_definitions = dict(entity_type_definitions or {})
- self.relationship_type_definitions = dict(
- relationship_type_definitions or {}
- )
- # Per-edge ``{name: [(from_vt, to_vt), ...]}`` derived from the
- # live schema. Used by the prompt to tell the LLM the valid
- # source/target pairs per relationship type, and by the ingest
- # worker to validate that an extracted relationship's endpoints
- # match a declared pair before writing IS_HEAD_OF / HAS_TAIL.
- self.domain_edge_endpoints = {
- k: list(v) for k, v in (domain_edge_endpoints or {}).items()
- }
+
+ # Thin @property accessors so the worker can read schema fields
+ # directly off the extractor without unpacking ``allowed_schema``.
+
+ @property
+ def allowed_vertex_types(self):
+ return self.allowed_schema.vertex_types or None
+
+ @property
+ def allowed_edge_types(self):
+ return self.allowed_schema.edge_types or None
+
+ @property
+ def entity_type_definitions(self):
+ return self.allowed_schema.vertex_definitions
+
+ @property
+ def relationship_type_definitions(self):
+ return self.allowed_schema.edge_definitions
+
+ @property
+ def domain_edge_endpoints(self):
+ return self.allowed_schema.edge_endpoints
+
+ @property
+ def entity_type_attributes(self):
+ return self.allowed_schema.vertex_attributes
+
+ @property
+ def relationship_type_attributes(self):
+ return self.allowed_schema.edge_attributes
def _format_definitions(self, defs: dict) -> str:
"""Render a ``{type_name: definition}`` dict as one
@@ -85,33 +109,65 @@ def _format_edge_endpoints(self) -> str:
lines.append(f"- {name}: {pair_strs}{tail}")
return "\n".join(lines)
+ @staticmethod
+ def _rel_props(rels: dict) -> dict:
+ """Pull a ``properties`` / ``attributes`` dict off an LLM-
+ emitted relationship object. Empty dict when neither key is
+ present or the value isn't a dict.
+ """
+ p = rels.get("properties") or rels.get("attributes") or {}
+ return p if isinstance(p, dict) else {}
+
+ def _format_type_attributes(self, type_attrs: dict) -> str:
+ """Render ``{type_name: {attr_name: tg_type}}`` as a nested
+ block the LLM can read::
+
+ - Filing
+ - filed_at (DATETIME)
+ - amount (DOUBLE)
+ - jurisdiction (STRING)
+ - Company
+ - founded_year (INT)
+ - industry (STRING)
+
+ Empty when no types carry attributes.
+ """
+ if not type_attrs:
+ return ""
+ lines = []
+ for name in sorted(type_attrs.keys()):
+ attrs = type_attrs.get(name) or {}
+ if not attrs:
+ continue
+ lines.append(f"- {name}")
+ for attr_name in sorted(attrs.keys()):
+ lines.append(f" - {attr_name} ({attrs[attr_name]})")
+ return "\n".join(lines)
+
def _build_schema_prompt_messages(self) -> list:
"""Return the human-message tuples that describe the domain
schema to the LLM. Used by both sync and async extraction paths.
Empty list when no schema is configured.
"""
msgs = []
- entity_def_block = self._format_definitions(self.entity_type_definitions)
- rel_def_block = self._format_definitions(self.relationship_type_definitions)
- endpoints_block = self._format_edge_endpoints()
- if not (entity_def_block or rel_def_block or endpoints_block):
+ schema_rep = (self.allowed_schema.schema_rep or "").strip()
+ if not schema_rep:
return msgs
if self.strict_mode:
msgs.append((
"human",
"STRICT SCHEMA MODE: only emit entities whose entity_type "
- "matches one of the schema entity types listed below, and "
- "only emit relationships whose relation_type matches a "
- "schema relationship type AND whose source / target "
- "entity types match a declared (FROM, TO) endpoint pair "
- "for that relationship. Drop any entity or relationship "
+ "matches one of the vertex types in the schema below, and "
+ "only emit relationships whose relation_type matches an "
+ "edge type AND whose source / target match a declared "
+ "(FROM, TO) endpoint pair. Drop any entity or relationship "
"that doesn't fit. Do NOT invent new types.",
))
else:
msgs.append((
"human",
- "When deciding the entity_type / relationship_type for an "
+ "When choosing the entity_type / relationship_type for an "
"extraction, strongly prefer the schema types listed below "
"and use their definitions to disambiguate similar types. "
"Ignore page-structure / chart / layout artifacts (axes, "
@@ -120,23 +176,20 @@ def _build_schema_prompt_messages(self) -> list:
"abstract categorical groupings. Only invent a new type "
"when nothing in the schema fits.",
))
- if entity_def_block:
- msgs.append((
- "human",
- f"Schema entity types with definitions:\n{entity_def_block}",
- ))
- if endpoints_block:
- msgs.append((
- "human",
- "Schema relationship types — each line lists the valid "
- "(source -> target) endpoint pairs for that relationship "
- "and the relationship's definition:\n" + endpoints_block,
- ))
- elif rel_def_block:
- msgs.append((
- "human",
- f"Schema relationship types with definitions:\n{rel_def_block}",
- ))
+ msgs.append(("human", schema_rep))
+ msgs.append((
+ "human",
+ "For every node and relationship, populate a `properties` map "
+ "with values you find in the text for the attributes shown in "
+ "the schema. Use the exact attribute names listed. Match the "
+ "declared type: INT / UINT as integers, DOUBLE / FLOAT as "
+ "numbers, BOOL as true/false, DATETIME as an ISO-8601 string "
+ "(e.g. \"2024-01-15\" or \"2024-01-15T09:30:00\"). Omit "
+ "attributes you can't find values for — partial coverage is "
+ "fine. Do NOT invent attribute names beyond those in the "
+ "schema. The `id` / primary-id attribute lives on the node's "
+ "`id` field — do NOT also put it in `properties`.",
+ ))
return msgs
def _parse_json_output(self, content: str) -> dict:
@@ -181,61 +234,8 @@ async def _aextract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]
try:
json_out = self._parse_json_output(out.content)
- formatted_rels = []
- for rels in json_out["rels"]:
- if isinstance(rels["source"], str) and isinstance(rels["target"], str):
- formatted_rels.append(
- {
- "source": rels["source"],
- "target": rels["target"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], dict) and isinstance(
- rels["target"], str
- ):
- formatted_rels.append(
- {
- "source": rels["source"]["id"],
- "target": rels["target"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], str) and isinstance(
- rels["target"], dict
- ):
- formatted_rels.append(
- {
- "source": rels["source"],
- "target": rels["target"]["id"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], dict) and isinstance(
- rels["target"], dict
- ):
- formatted_rels.append(
- {
- "source": rels["source"]["id"],
- "target": rels["target"]["id"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- else:
- raise Exception("Relationship parsing error")
- formatted_nodes = []
- for node in json_out["nodes"]:
- formatted_nodes.append(
- {
- "id": node["id"],
- "type": node["node_type"].replace(" ", "_").capitalize(),
- "definition": node["definition"],
- }
- )
+ formatted_rels = self._format_rels(json_out["rels"])
+ formatted_nodes = self._format_nodes(json_out["nodes"])
# filter relationships and nodes based on allowed types
if self.strict_mode:
@@ -252,19 +252,11 @@ async def _aextract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]
if rel["type"] in self.allowed_edge_types
]
- nodes = []
- for node in formatted_nodes:
- nodes.append(Node(id=node["id"],
- type=node["type"],
- properties={"description": node["definition"]}))
- relationships = []
- for rel in formatted_rels:
- relationships.append(Relationship(source=Node(id=rel["source"], type=rel["source"],
- properties={"description": rel["definition"]}),
- target=Node(id=rel["target"], type=rel["target"],
- properties={"description": rel["definition"]}), type=rel["type"]))
-
- return [GraphDocument(nodes=nodes, relationships=relationships, source=Document(page_content=doc))]
+ return [GraphDocument(
+ nodes=self._build_nodes(formatted_nodes),
+ relationships=self._build_rels(formatted_rels),
+ source=Document(page_content=doc),
+ )]
except:
return [GraphDocument(nodes=[], relationships=[], source=Document(page_content=doc))]
@@ -279,61 +271,8 @@ def _extract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]:
try:
json_out = self._parse_json_output(out.content)
- formatted_rels = []
- for rels in json_out["rels"]:
- if isinstance(rels["source"], str) and isinstance(rels["target"], str):
- formatted_rels.append(
- {
- "source": rels["source"],
- "target": rels["target"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], dict) and isinstance(
- rels["target"], str
- ):
- formatted_rels.append(
- {
- "source": rels["source"]["id"],
- "target": rels["target"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], str) and isinstance(
- rels["target"], dict
- ):
- formatted_rels.append(
- {
- "source": rels["source"],
- "target": rels["target"]["id"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- elif isinstance(rels["source"], dict) and isinstance(
- rels["target"], dict
- ):
- formatted_rels.append(
- {
- "source": rels["source"]["id"],
- "target": rels["target"]["id"],
- "type": rels["relation_type"].replace(" ", "_").upper(),
- "definition": rels["definition"],
- }
- )
- else:
- raise Exception("Relationship parsing error")
- formatted_nodes = []
- for node in json_out["nodes"]:
- formatted_nodes.append(
- {
- "id": node["id"],
- "type": node["node_type"].replace(" ", "_").capitalize(),
- "definition": node["definition"],
- }
- )
+ formatted_rels = self._format_rels(json_out["rels"])
+ formatted_nodes = self._format_nodes(json_out["nodes"])
# filter relationships and nodes based on allowed types
if self.strict_mode:
@@ -349,23 +288,106 @@ def _extract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]:
for rel in formatted_rels
if rel["type"] in self.allowed_edge_types
]
-
- nodes = []
- for node in formatted_nodes:
- nodes.append(Node(id=node["id"],
- type=node["type"],
- properties={"description": node["definition"]}))
- relationships = []
- for rel in formatted_rels:
- relationships.append(Relationship(source=Node(id=rel["source"], type=rel["source"],
- properties={"description": rel["definition"]}),
- target=Node(id=rel["target"], type=rel["target"],
- properties={"description": rel["definition"]}), type=rel["type"]))
-
- return [GraphDocument(nodes=nodes, relationships=relationships, source=Document(page_content=doc))]
+
+ return [GraphDocument(
+ nodes=self._build_nodes(formatted_nodes),
+ relationships=self._build_rels(formatted_rels),
+ source=Document(page_content=doc),
+ )]
except:
return [GraphDocument(nodes=[], relationships=[], source=Document(page_content=doc))]
+
+ # --- LLM-output normalization helpers (shared by sync + async) ----
+
+ @staticmethod
+ def _resolve_id_and_props(value):
+ """Source / target in the LLM's ``rels`` list may come as a
+ bare id string or as a dict with ``id`` + optional
+ ``properties``. Return ``(id_str, props_dict)``.
+ """
+ if isinstance(value, dict):
+ props = value.get("properties") or value.get("attributes") or {}
+ return str(value.get("id", "")), props if isinstance(props, dict) else {}
+ return str(value), {}
+
+ def _format_rels(self, rels_in: list) -> list:
+ formatted = []
+ for rels in rels_in or []:
+ try:
+ src_id, src_props = self._resolve_id_and_props(rels["source"])
+ tgt_id, tgt_props = self._resolve_id_and_props(rels["target"])
+ if not (src_id and tgt_id):
+ continue
+ # Edge-level properties (typed attrs the LLM extracted
+ # for this edge type, e.g. ``MONEY_TRANSFER.amount``)
+ # live directly under the rel object. Source/target
+ # vertex attrs are kept separately so the worker can
+ # apply each to the right row.
+ rel_props = self._rel_props(rels)
+ formatted.append({
+ "source": src_id,
+ "target": tgt_id,
+ "source_props": src_props,
+ "target_props": tgt_props,
+ "type": rels["relation_type"].replace(" ", "_").upper(),
+ "definition": rels.get("definition", ""),
+ "properties": rel_props,
+ })
+ except (KeyError, TypeError):
+ continue
+ return formatted
+
+ def _format_nodes(self, nodes_in: list) -> list:
+ formatted = []
+ for node in nodes_in or []:
+ try:
+ # ``properties`` (or ``attributes``) is optional — the
+ # LLM may omit it when nothing in the text fits the
+ # typed attribute schema we sent.
+ props = node.get("properties") or node.get("attributes") or {}
+ formatted.append({
+ "id": node["id"],
+ "type": node["node_type"].replace(" ", "_").capitalize(),
+ "definition": node.get("definition", ""),
+ "properties": props if isinstance(props, dict) else {},
+ })
+ except (KeyError, TypeError):
+ continue
+ return formatted
+
+ def _build_nodes(self, formatted_nodes: list) -> list:
+ nodes = []
+ for node in formatted_nodes:
+ # Forward LLM-emitted typed attributes alongside the
+ # description text. The worker splits ``description``
+ # (-> Entity row) from typed attributes (-> domain VT row)
+ # and coerces / filters the latter to the live schema.
+ node_props = {**(node.get("properties") or {}),
+ "description": node["definition"]}
+ nodes.append(Node(id=node["id"],
+ type=node["type"],
+ properties=node_props))
+ return nodes
+
+ def _build_rels(self, formatted_rels: list) -> list:
+ relationships = []
+ for rel in formatted_rels:
+ src_props = {**(rel.get("source_props") or {}),
+ "description": rel["definition"]}
+ tgt_props = {**(rel.get("target_props") or {}),
+ "description": rel["definition"]}
+ edge_props = {**(rel.get("properties") or {}),
+ "description": rel["definition"]}
+ relationships.append(Relationship(
+ source=Node(id=rel["source"], type=rel["source"],
+ properties=src_props),
+ target=Node(id=rel["target"], type=rel["target"],
+ properties=tgt_props),
+ type=rel["type"],
+ properties=edge_props,
+ ))
+ return relationships
async def adocument_er_extraction(self, document):
from langchain.prompts import ChatPromptTemplate
diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py
index 48897b4..bf24588 100644
--- a/common/llm_services/base_llm.py
+++ b/common/llm_services/base_llm.py
@@ -272,6 +272,12 @@ def entity_relationship_extraction_prompt(self):
You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
+## Faithfulness — Most Important Rule
+- Only emit entities, relationships, definitions, and attribute values that are **explicitly stated in the input text**.
+- Do NOT include information from your general knowledge, training data, or background context about well-known entities.
+- If a fact is not in the text, leave the corresponding field empty or omit the attribute — never guess, infer, or fill from outside knowledge.
+- A short, faithful description is always better than a long description that adds plausible-sounding facts.
+
## Goals
- **Nodes** represent entities, concepts, and properties of entities.
- Aim for simplicity and clarity so the graph is accessible to a vast audience.
@@ -284,6 +290,7 @@ def entity_relationship_extraction_prompt(self):
- Incorporate as **attributes / properties** of the respective nodes.
- Do NOT create separate nodes for dates or numerical values.
- Properties are key-value. Use properties only for dates and numbers; string properties become new nodes.
+- Only include numerical or date values that are **explicitly written in the input text** — do NOT compute, estimate, or recall from memory.
- Never use escaped single or double quotes within property values.
- Use `camelCase` for property keys (e.g. `birthDate`).
@@ -596,9 +603,9 @@ def schema_extraction_prompt(self):
2. **Skip layout**: do NOT produce types for axes, page numbers, captions, table cells, or other document-rendering artifacts.
3. **Edge naming**: use a specific action verb. Include an edge type ONLY IF the source documents contain **2+ concrete instances** of that relationship between named entities — do NOT propose merely-plausible edges. Avoid generic edges (`RELATED_TO`, `CONNECTED_TO`, `ASSOCIATED_WITH`, `HAS`, `BELONGS_TO`). Use `DIRECTED EDGE` for asymmetric verbs and `UNDIRECTED EDGE` only for genuinely symmetric peer relationships.
4. **Reserved names**: do NOT use a name (case-insensitive) matching any of the reserved structural types or GSQL keywords listed in the Inputs section. Pick a synonym or qualifier (e.g. `KeywordRecord`).
-5. **Attributes**: each `VERTEX` has **1–5** attributes; each `EDGE` has **0–3**. Primitive types only: `STRING`, `INT`, `UINT`, `DOUBLE`, `FLOAT`, `BOOL`, `DATETIME`. Do NOT include any id / primary-key field.
+5. **Attributes**: each `VERTEX` has **1–10** attributes; each `EDGE` has **0–5**. Primitive types only: `STRING`, `INT`, `UINT`, `DOUBLE`, `FLOAT`, `BOOL`, `DATETIME`. Do NOT include any id / primary-key field.
6. **Comments**: every `VERTEX` and `EDGE` MUST be preceded by exactly one `// ` line.
-7. **Size**: produce **8–25** vertex types and **8–25** edge types.
+7. **Size**: produce at least 8 vertex types. Emit every edge type that rule 3 supports — no upper bound on edge count, but every edge must earn its place via 2+ concrete instances in the source documents.
## Example Output (illustrative — pick names that fit YOUR documents)
diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py
index 2f94a6d..897853f 100644
--- a/ecc/app/graphrag/util.py
+++ b/ecc/app/graphrag/util.py
@@ -150,67 +150,28 @@ async def init(
if graph_cfg.get("extractor") == "graphrag":
extractor = GraphExtractor()
elif graph_cfg.get("extractor") == "llm":
- # Read the live schema directly (without going through the
- # proposal-flow SchemaProposal type). This intentionally
- # supports graphs whose domain types were created outside of
- # the proposal flow — admin UI, prior releases,
- # external migration scripts — as long as the domain types
- # and the EntityType / RelationshipType metadata are on the
- # graph, ECC will use them.
+ # Read the live schema and pack it into the LLM-extractor
+ # bundle. ``build_allowed_schema_async`` filters structural
+ # types, reads attribute schemas + definitions, and renders the
+ # prompt text in one pass — same shape that query-side tools
+ # consume via ``render_schema_rep``.
try:
- existing = await read_existing_schema_async(conn)
+ from common.db.schema_utils import build_allowed_schema_async, AllowedSchema
+ allowed_schema = await build_allowed_schema_async(conn)
except Exception as exc:
- logger.warning(f"Loading live schema for extractor failed: {exc}")
- from common.db.schema_utils import ExistingSchema
- existing = ExistingSchema()
- try:
- entity_descs, rel_defs = await read_type_metadata_async(conn)
- except Exception as exc:
- logger.warning(f"Loading type metadata for extractor failed: {exc}")
- entity_descs, rel_defs = {}, {}
+ logger.warning(f"Loading domain schema for extractor failed: {exc}")
+ from common.db.schema_utils import AllowedSchema
+ allowed_schema = AllowedSchema()
- # Filter to domain types (drop GraphRAG structural types and
- # any pair whose endpoint touches a structural vertex).
- domain_vertex_types = sorted(
- v for v in existing.vertex_types if not is_structural_type(v)
- )
- domain_edge_endpoints: dict = {}
- for edge_name, pairs in existing.edge_pairs.items():
- if is_structural_type(edge_name):
- continue
- domain_pairs = [
- (s, t)
- for s, t in pairs
- if not is_structural_type(s) and not is_structural_type(t)
- ]
- if domain_pairs:
- domain_edge_endpoints[edge_name] = domain_pairs
- domain_edge_types = sorted(domain_edge_endpoints.keys())
-
- # Trim the descriptions to domain types only.
- domain_entity_defs = {
- vt: entity_descs[vt]
- for vt in domain_vertex_types
- if entity_descs.get(vt)
- }
- domain_rel_defs = {
- et: rel_defs[et]
- for et in domain_edge_types
- if rel_defs.get(et)
- }
-
- # Strict-mode comes from graphrag_config; default false (legacy
- # fallback to plain Entity vertices for non-domain extractions).
+ # Strict mode (graphrag_config.strict_mode, default false):
+ # when false, entities whose type doesn't match a domain VT
+ # still land in the plain Entity vertex.
strict_mode = bool(graph_cfg.get("strict_mode", False))
extractor = LLMEntityRelationshipExtractor(
get_llm_service(get_completion_config(conn.graphname)),
- allowed_entity_types=domain_vertex_types or None,
- allowed_relationship_types=domain_edge_types or None,
+ allowed_schema=allowed_schema,
strict_mode=strict_mode,
- entity_type_definitions=domain_entity_defs,
- relationship_type_definitions=domain_rel_defs,
- domain_edge_endpoints=domain_edge_endpoints,
)
else:
raise ValueError("Invalid extractor type")
@@ -346,6 +307,159 @@ async def upsert_vertex(
await load_q.put(("vertices", (vertex_type, vertex_id, attrs)))
+def coerce_attrs_for_schema(
+ props: dict,
+ schema: dict,
+) -> dict:
+ """Coerce LLM-emitted properties to the declared TigerGraph types
+ and drop anything not in *schema*.
+
+ *props* — dict the LLM produced (values may be strings, numbers,
+ bools depending on the model and the schema instruction).
+ *schema* — ``{attr_name: tg_type}`` for the destination type
+ (vertex or edge). ``tg_type`` is one of TG's primitive type names
+ (case-insensitive): ``STRING``, ``INT``, ``UINT``, ``DOUBLE``,
+ ``FLOAT``, ``BOOL``, ``DATETIME``.
+
+ Behavior:
+ * Attribute names are matched case-insensitively; the canonical
+ schema spelling is used in the returned dict.
+ * Values that can't be coerced (e.g. a non-numeric string for
+ an INT field) are silently dropped — partial coverage is
+ fine; a single bad value shouldn't reject the whole upsert.
+ * Empty strings / ``None`` / sentinel values like ``"N/A"`` /
+ ``"unknown"`` are dropped before coercion to avoid writing
+ junk into typed columns.
+ """
+ if not props or not schema:
+ return {}
+ # Build a case-folded lookup once.
+ schema_ci = {k.casefold(): k for k in schema.keys()}
+ out: dict = {}
+ for raw_name, raw_val in props.items():
+ if not raw_name:
+ continue
+ canonical = schema_ci.get(str(raw_name).casefold())
+ if not canonical:
+ continue
+ tg_type = (schema.get(canonical) or "").upper()
+ coerced = _coerce_value(raw_val, tg_type)
+ if coerced is not None:
+ out[canonical] = coerced
+ return out
+
+
+_LLM_NULL_SENTINELS = frozenset({
+ "", "n/a", "na", "none", "null", "unknown", "not specified",
+ "not available", "not applicable", "tbd", "?",
+})
+
+
+# Primitive types accepted inside a TG DISCRIMINATOR(...) clause.
+# Discriminator attrs must be present in every upsert; the worker
+# fills missing values from ``_DISCRIMINATOR_FALLBACKS`` below.
+_DISCRIMINATOR_TYPES = frozenset({"INT", "UINT", "STRING", "DATETIME"})
+
+_DISCRIMINATOR_FALLBACKS: dict = {
+ "INT": 0,
+ "UINT": 0,
+ "STRING": "",
+ "DATETIME": "1970-01-01 00:00:00",
+}
+
+
+def coerce_edge_attrs_for_schema(
+ props: dict,
+ schema: dict,
+) -> dict:
+ """Coerce LLM-emitted properties for an edge upsert.
+
+ Same matching + coercion as the vertex helper; additionally fills
+ in default values for any discriminator-typed schema attribute
+ the LLM did not provide, since TG requires every discriminator
+ attribute to be present in each upsert.
+ """
+ if not schema:
+ return {}
+ coerced = coerce_attrs_for_schema(props or {}, schema)
+ # Fill missing discriminator-typed attributes with type defaults.
+ schema_ci = {k.casefold(): k for k in schema.keys()}
+ for ci_name, canonical in schema_ci.items():
+ if canonical in coerced:
+ continue
+ tg_type = (schema.get(canonical) or "").upper()
+ if tg_type in _DISCRIMINATOR_TYPES:
+ coerced[canonical] = _DISCRIMINATOR_FALLBACKS[tg_type]
+ return coerced
+
+
+def _coerce_value(value, tg_type: str):
+ """Convert *value* to the TG type *tg_type*. Returns ``None`` when
+ coercion would lose meaning (empty value, sentinel like ``"N/A"``,
+ or a parse failure). Caller drops attrs that come back ``None``.
+ """
+ if value is None:
+ return None
+ # Quick string-sentinel filter — applies to every type.
+ if isinstance(value, str):
+ s = value.strip()
+ if s.casefold() in _LLM_NULL_SENTINELS:
+ return None
+
+ try:
+ if tg_type in ("INT", "UINT"):
+ if isinstance(value, bool):
+ return int(value)
+ if isinstance(value, (int, float)):
+ v = int(value)
+ else:
+ # Strip thousand separators and surrounding whitespace.
+ v = int(float(str(value).replace(",", "").strip()))
+ if tg_type == "UINT" and v < 0:
+ return None
+ return v
+ if tg_type in ("DOUBLE", "FLOAT"):
+ if isinstance(value, bool):
+ return float(value)
+ if isinstance(value, (int, float)):
+ return float(value)
+ return float(str(value).replace(",", "").strip())
+ if tg_type == "BOOL":
+ if isinstance(value, bool):
+ return value
+ s = str(value).strip().casefold()
+ if s in ("true", "yes", "y", "1"):
+ return True
+ if s in ("false", "no", "n", "0"):
+ return False
+ return None
+ if tg_type == "DATETIME":
+ # TG accepts 'YYYY-MM-DD HH:MM:SS' (space-separated).
+ # Accept ISO-8601 with 'T' and normalize.
+ s = str(value).strip()
+ if not s:
+ return None
+ try:
+ from dateutil import parser as _dt_parser # type: ignore
+ dt = _dt_parser.parse(s)
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
+ except Exception:
+ # Fall back to a few common formats without dateutil.
+ from datetime import datetime as _dt
+ for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%d %H:%M:%S", "%Y/%m/%d"):
+ try:
+ return _dt.strptime(s, fmt).strftime("%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ continue
+ return None
+ # STRING (and anything we don't recognize): coerce to str.
+ s = str(value).strip()
+ return s or None
+ except (ValueError, TypeError):
+ return None
+
+
async def upsert_batch(conn: AsyncTigerGraphConnection, data: str):
async with tg_sem:
try:
diff --git a/ecc/app/graphrag/workers.py b/ecc/app/graphrag/workers.py
index 7d15244..0f692b8 100644
--- a/ecc/app/graphrag/workers.py
+++ b/ecc/app/graphrag/workers.py
@@ -288,14 +288,14 @@ async def extract(
strict_mode = bool(extractor.strict_mode)
# ``has_domain_types`` distinguishes the two meta-layer cases:
- # Case 1: no domain types on the graph or extracted — the
- # EntityType / RelationshipType layer becomes a free-text
- # catalog of whatever the LLM emitted (legacy behaviour).
+ # Case 1: no domain types on the graph — the EntityType /
+ # RelationshipType layer becomes a free-text catalog of
+ # whatever the LLM emitted.
# Case 2: at least one domain type exists — the meta-layer
- # is restricted to the declared / matched domain types
- # only. Non-matched extractions still write to the legacy
- # Entity / RELATIONSHIP layer but DO NOT pollute the meta
- # layer.
+ # is restricted to declared / matched domain types only.
+ # Non-matched extractions still write to the parallel
+ # Entity / RELATIONSHIP layer but do not pollute the
+ # meta layer.
has_domain_types = bool(domain_vt_canonical) or bool(domain_edge_canonical)
# upsert nodes and edges to the graph
@@ -320,9 +320,8 @@ async def extract(
domain_vt = domain_vt_canonical.get(node_type_lower)
# Strict mode: drop nodes whose type isn't in the
- # schema. The legacy raw-Entity fallback applies only
- # when strict_mode is off OR the node matches a domain
- # type.
+ # schema. When strict_mode is off, non-matched nodes
+ # fall through to the parallel Entity layer.
if strict_mode and domain_vt is None:
continue
@@ -421,9 +420,25 @@ async def extract(
logger.debug(
f"extract writes domain {domain_vt} vert + CONTAINS_ENTITY pair"
)
- # Domain VTs don't carry the ECC bookkeeping
- # ``epoch_added`` attribute — sending it makes TG
- # reject the whole batch.
+ # Coerce + filter LLM-emitted properties against
+ # the domain VT's attribute schema before upsert.
+ # The ``description`` key is for the Entity row and
+ # never belongs on the domain VT row, so strip it
+ # before coercion. Domain VTs don't carry the ECC
+ # bookkeeping ``epoch_added`` attribute either —
+ # sending it makes TG reject the whole batch.
+ raw_props = {
+ k: v for k, v in (node.properties or {}).items()
+ if k != "description"
+ }
+ attr_schema = (
+ extractor.entity_type_attributes.get(domain_vt)
+ if isinstance(extractor, LLMEntityRelationshipExtractor)
+ else {}
+ )
+ domain_attrs = util.coerce_attrs_for_schema(
+ raw_props, attr_schema or {}
+ )
await upsert_chan.put(
(
util.upsert_vertex,
@@ -431,7 +446,7 @@ async def extract(
conn,
domain_vt,
v_id,
- {},
+ domain_attrs,
),
)
)
@@ -564,7 +579,7 @@ async def extract(
# Case 2 (domain types exist) with valid_pair:
# same writes but using canonical (declared) names.
# Case 2 without valid_pair: skip the meta-layer
- # entirely. The legacy Entity / RELATIONSHIP write
+ # entirely. The Entity / RELATIONSHIP write
# above is the only persistence for unmatched
# extractions.
#
@@ -686,6 +701,22 @@ async def extract(
(conn, canonical_tgt_vt, tgt_id, {}),
)
)
+ # Coerce + filter LLM-emitted edge properties
+ # against the edge's attribute schema. ``description``
+ # is the Entity-side payload and never belongs on
+ # the domain edge row.
+ edge_raw_props = {
+ k: v for k, v in (edge.properties or {}).items()
+ if k != "description"
+ }
+ edge_attr_schema = (
+ extractor.relationship_type_attributes.get(canonical_rel)
+ if isinstance(extractor, LLMEntityRelationshipExtractor)
+ else {}
+ )
+ domain_edge_attrs = util.coerce_edge_attrs_for_schema(
+ edge_raw_props, edge_attr_schema or {}
+ )
await upsert_chan.put(
(
util.upsert_edge,
@@ -696,7 +727,7 @@ async def extract(
canonical_rel,
canonical_tgt_vt,
tgt_id,
- None,
+ domain_edge_attrs or None,
),
)
)
diff --git a/graphrag-ui/src/actions/ActionProvider.tsx b/graphrag-ui/src/actions/ActionProvider.tsx
index 4bda231..eb69c60 100644
--- a/graphrag-ui/src/actions/ActionProvider.tsx
+++ b/graphrag-ui/src/actions/ActionProvider.tsx
@@ -161,12 +161,17 @@ const ActionProvider: React.FC = ({
});
loadedMessages.push(userMessage);
} else if (msg.role === "system") {
- // Create bot message
+ // Carry message_id + feedback through so history bubbles can
+ // open the trace page and reflect the prior thumbs-up/down
+ // state after a reload.
const botMessage = createChatBotMessage({
content: msg.content || "",
response_type: "history",
query_sources: msg.query_sources,
answered_question: msg.answered_question,
+ message_id: msg.message_id,
+ messageId: msg.message_id,
+ feedback: msg.feedback,
});
loadedMessages.push(botMessage);
}
diff --git a/graphrag-ui/src/components/Bot.tsx b/graphrag-ui/src/components/Bot.tsx
index 283e2af..b951de5 100644
--- a/graphrag-ui/src/components/Bot.tsx
+++ b/graphrag-ui/src/components/Bot.tsx
@@ -150,14 +150,14 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo
-
+
Select a KnowledgeGraph
{store?.graphs?.length > 0 ? (
store.graphs.map((f, i) => (
handleSelect(f)}>
- {f}
+ {f}
))
) : (
diff --git a/graphrag-ui/src/components/ConfigScopeToggle.tsx b/graphrag-ui/src/components/ConfigScopeToggle.tsx
index b311631..4ef5c23 100644
--- a/graphrag-ui/src/components/ConfigScopeToggle.tsx
+++ b/graphrag-ui/src/components/ConfigScopeToggle.tsx
@@ -71,7 +71,7 @@ const ConfigScopeToggle: React.FC = ({
disabled={configScope !== "graph"}
onValueChange={(value) => onGraphChange(value)}
>
-
+
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 4330609..4110d3c 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -13,6 +13,7 @@ import { IoIosCloseCircleOutline } from "react-icons/io";
import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
+import { useAlert } from "@/hooks/useAlert";
interface IChatbotMessageProps {
message?: any;
withAvatar?: boolean;
@@ -172,6 +173,7 @@ export const CustomChatMessage: FC = ({
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
+ const [alert, alertDialog] = useAlert();
// Error handling functions
const handleShowExplain = () => {
@@ -191,7 +193,11 @@ export const CustomChatMessage: FC = ({
};
const handleShowTable = () => {
- if (message.response_type == 'history' || !message.query_sources?.result) {
+ // Allow opening the table view on history messages too — the
+ // chat-history backend preserves ``query_sources.result``, so
+ // there's no reason to deny it just because the message arrived
+ // from a reload rather than a fresh answer.
+ if (!message.query_sources?.result) {
return false;
}
setShowTableVis(prev => !prev);
@@ -213,6 +219,7 @@ export const CustomChatMessage: FC = ({
return (
<>
+ {alertDialog}
{typeof message === "string" ? (
{message}
@@ -233,10 +240,40 @@ export const CustomChatMessage: FC
= ({
showExplain={handleShowExplain}
showTable={handleShowTable}
showGraph={handleShowGraph}
- onViewTrace={() => {
+ onViewTrace={async () => {
const messageId = message.messageId || message.message_id || "";
- // Store message in sessionStorage so the new tab reads it directly
- // without needing an authenticated API fetch (which triggers browser auth dialog).
+ if (!messageId) {
+ await alert("Trace log unavailable: this message has no trace ID.");
+ return;
+ }
+ // Guard against a missing/invalid creds value. If we send
+ // ``Basic null`` (or other unparsable base64), FastAPI's
+ // HTTPBasic returns 401 + ``WWW-Authenticate: Basic`` and
+ // the browser pops up its native auth dialog. Better to
+ // tell the user to sign in again than to flash that popup.
+ const creds = sessionStorage.getItem("creds");
+ if (!creds) {
+ await alert("Your session has expired. Please log in again.");
+ return;
+ }
+ // Trace JSON lives under /code/trace_logs inside the
+ // graphrag container and is wiped on container recreate.
+ // Probe first so we never open a blank tab when the file is gone.
+ try {
+ const probe = await fetch(`/ui/trace/${messageId}`, {
+ method: "GET",
+ headers: { Authorization: `Basic ${creds}` },
+ });
+ if (!probe.ok) {
+ await alert("Trace log not found.");
+ return;
+ }
+ } catch (err) {
+ await alert("Failed to reach the trace log endpoint. Please try again.");
+ return;
+ }
+ // Pass the message via sessionStorage so the new tab can
+ // render without a second authenticated fetch.
sessionStorage.setItem(`trace_msg_${messageId}`, JSON.stringify(message));
window.open(`/trace/${messageId}`, "_blank");
}}
diff --git a/graphrag-ui/src/components/Interact.tsx b/graphrag-ui/src/components/Interact.tsx
index 9f259f4..ae93539 100644
--- a/graphrag-ui/src/components/Interact.tsx
+++ b/graphrag-ui/src/components/Interact.tsx
@@ -22,14 +22,19 @@ interface Interactions {
onViewTrace?: () => void;
}
-export const Interactions: FC = ({
+export const Interactions: FC = ({
message,
showExplain,
showTable,
showGraph,
onViewTrace,
}: Interactions) => {
- const [feedback, setFeedback] = useState(Feedback.NoFeedback);
+ // Seed from the persisted feedback when re-rendering a history
+ // message so the up/down state matches what the user already
+ // submitted before the page reloaded.
+ const [feedback, setFeedback] = useState(
+ (message?.feedback as Feedback) ?? Feedback.NoFeedback
+ );
const { isSuperuser, isGlobalDesigner, isGraphAdmin } = useRoles();
const canViewTrace = isSuperuser || isGlobalDesigner || isGraphAdmin;
@@ -47,9 +52,30 @@ export const Interactions: FC = ({
});
};
+ // Suppress the toolbar for non-answer message types where the
+ // buttons would be meaningless (progress chips, greeting cards,
+ // hard errors).
+ const responseType = message?.response_type;
+ if (responseType === "progress" || responseType === "greeting" || responseType === "error") {
+ return null;
+ }
+ // Hide the row entirely for the welcome / loading placeholder
+ // bubble that has neither a real answer nor an answered question.
+ if (!message?.content && !message?.answered_question) {
+ return null;
+ }
+
+ const hasGraphData = Boolean(message?.query_sources?.result?.edges);
+ const hasTableData = Boolean(message?.query_sources?.result);
+ // The trace page is keyed by message_id. Some history payloads pre-date
+ // the message_id capture, so suppress the button when we can't build a
+ // valid /trace/ URL — otherwise the click opens a blank tab.
+ const traceMessageId = message?.messageId || message?.message_id || "";
+ const hasTraceId = Boolean(traceMessageId);
+
return (
- {(message.query_sources?.result || message.query_sources?.cypher || message.query_sources?.answer) ? (
+ {true ? (
<>
*/}
- {canViewTrace ? (
+ {canViewTrace && hasTraceId ? (
onViewTrace?.()}
@@ -115,10 +141,11 @@ export const Interactions: FC
= ({
{
- if (message.query_sources?.result?.edges) {
+ if (hasGraphData) {
showGraph();
}
}}
@@ -128,10 +155,11 @@ export const Interactions: FC
= ({
{
- if (message.query_sources?.result) {
+ if (hasTableData) {
showTable();
}
}}
diff --git a/graphrag-ui/src/components/ModeToggle.tsx b/graphrag-ui/src/components/ModeToggle.tsx
index 053ac9f..e2b22b3 100644
--- a/graphrag-ui/src/components/ModeToggle.tsx
+++ b/graphrag-ui/src/components/ModeToggle.tsx
@@ -47,7 +47,7 @@ export function ModeToggle() {
};
return (
-
+
{!isLoginRoute && rolesLoaded && canAccessSetup && (
{
+const SideMenu = ({
+ height,
+ setGetConversationId,
+ width,
+}: {
+ height?: string;
+ setGetConversationId?: any;
+ width?: number;
+}) => {
const getTheme = useTheme().theme;
// const [conhistory, setConHistory] = useState([]);
const [conversationId, setConversationId] = useState([]);
@@ -409,7 +417,8 @@ const SideMenu = ({ height, setGetConversationId }: { height?: string, setGetCon
return (
diff --git a/graphrag-ui/src/hooks/useIdleTimeout.ts b/graphrag-ui/src/hooks/useIdleTimeout.ts
index 07f0486..0b54892 100644
--- a/graphrag-ui/src/hooks/useIdleTimeout.ts
+++ b/graphrag-ui/src/hooks/useIdleTimeout.ts
@@ -44,16 +44,19 @@ export function useIdleTimeout(timeoutMs: number = DEFAULT_TIMEOUT_MS) {
const onPause = () => pause();
const onResume = () => resetTimer();
+ const onPing = () => resetTimer();
events.forEach((event) => window.addEventListener(event, resetTimer));
window.addEventListener("idle-timer-pause", onPause);
window.addEventListener("idle-timer-resume", onResume);
+ window.addEventListener("idle-timer-ping", onPing);
resetTimer(); // Start the timer
return () => {
events.forEach((event) => window.removeEventListener(event, resetTimer));
window.removeEventListener("idle-timer-pause", onPause);
window.removeEventListener("idle-timer-resume", onResume);
+ window.removeEventListener("idle-timer-ping", onPing);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
@@ -70,3 +73,13 @@ export function pauseIdleTimer() {
export function resumeIdleTimer() {
window.dispatchEvent(new Event("idle-timer-resume"));
}
+
+/**
+ * Reset the idle timer without requiring a user UI event. Call this
+ * after a successful status poll for a long-running, user-initiated
+ * backend flow (init, ingest, rebuild) so the session stays alive
+ * while the user is watching progress.
+ */
+export function pingIdleTimer() {
+ window.dispatchEvent(new Event("idle-timer-ping"));
+}
diff --git a/graphrag-ui/src/index.css b/graphrag-ui/src/index.css
index d2dc878..6b5cc77 100755
--- a/graphrag-ui/src/index.css
+++ b/graphrag-ui/src/index.css
@@ -185,7 +185,11 @@
.fp .react-chatbot-kit-chat-message-container,
.open-dg .react-chatbot-kit-chat-message-container {
height: calc(100vh - 100px) !important;
- max-width: 960px;
+ /* Scale with the available width so wider screens use more
+ horizontal space, but cap reading-line at a comfortable
+ length. Switch from a hard 960px cap that left huge gutters
+ on 1440p / 4K monitors. */
+ max-width: min(1280px, 90%);
margin: 0 auto;
}
diff --git a/graphrag-ui/src/pages/Chat.tsx b/graphrag-ui/src/pages/Chat.tsx
index e2032eb..a34e237 100644
--- a/graphrag-ui/src/pages/Chat.tsx
+++ b/graphrag-ui/src/pages/Chat.tsx
@@ -1,16 +1,115 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import Bot from "@/components/Bot";
import SideMenu from "@/components/SideMenu";
-import { RxHamburgerMenu } from "react-icons/rx";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+
+// Sidebar width is user-resizable via the vertical separator. The
+// chosen width is persisted to localStorage so it survives reloads.
+const SIDEBAR_STORAGE_KEY = "graphrag:sidebarWidth";
+const DEFAULT_SIDEBAR_WIDTH = 320;
+const MIN_SIDEBAR_WIDTH = 220;
+const MAX_SIDEBAR_WIDTH = 600;
+
+const readStoredWidth = (): number => {
+ try {
+ const raw = parseInt(localStorage.getItem(SIDEBAR_STORAGE_KEY) || "");
+ if (!isNaN(raw) && raw >= MIN_SIDEBAR_WIDTH && raw <= MAX_SIDEBAR_WIDTH) {
+ return raw;
+ }
+ } catch {
+ // localStorage may be unavailable (private mode); fall through.
+ }
+ return DEFAULT_SIDEBAR_WIDTH;
+};
const Chat = () => {
const [showSidebar, setShowSidebar] = useState
(true);
+ const [sidebarWidth, setSidebarWidth] = useState(readStoredWidth);
+ const [isDragging, setIsDragging] = useState(false);
const [getConversationId, setGetConversationId] = useState(['lkjh']);
+
+ // Drag-to-resize. Track mouse globally while the user holds the
+ // separator so the resize keeps working even if the cursor strays
+ // outside the thin handle strip.
+ useEffect(() => {
+ if (!isDragging) return;
+ const onMouseMove = (e: MouseEvent) => {
+ const clamped = Math.max(
+ MIN_SIDEBAR_WIDTH,
+ Math.min(MAX_SIDEBAR_WIDTH, e.clientX)
+ );
+ setSidebarWidth(clamped);
+ };
+ const onMouseUp = () => {
+ setIsDragging(false);
+ try {
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth));
+ } catch {
+ // ignore — width simply won't persist
+ }
+ };
+ document.addEventListener("mousemove", onMouseMove);
+ document.addEventListener("mouseup", onMouseUp);
+ // Prevent text selection during drag.
+ document.body.style.userSelect = "none";
+ document.body.style.cursor = "col-resize";
+ return () => {
+ document.removeEventListener("mousemove", onMouseMove);
+ document.removeEventListener("mouseup", onMouseUp);
+ document.body.style.userSelect = "";
+ document.body.style.cursor = "";
+ };
+ }, [isDragging, sidebarWidth]);
+
return (
<>
+ {/* No `relative` on this container — adding one would create a
+ stacking context that hides the top-right ModeToggle (Setup /
+ Logout / theme) underneath the Bot header. The chevron below
+ positions itself against the viewport instead, which is fine
+ because this container starts flush at the viewport edge. */}
- {showSidebar ?
: null}
-
setShowSidebar(prev => !prev)}>
+ {showSidebar ? (
+
+ ) : null}
+ {/* Drag handle: thin vertical strip on the sidebar's right edge.
+ Hovering shows a subtle highlight; mousedown enters resize
+ mode. Sits behind the chevron so the chevron click still
+ registers as toggle. */}
+ {showSidebar && (
+
{
+ e.preventDefault();
+ setIsDragging(true);
+ }}
+ aria-label="Resize left menu"
+ role="separator"
+ className={
+ "hidden md:block fixed top-0 bottom-0 z-10 w-1.5 cursor-col-resize " +
+ "hover:bg-blue-500/30 dark:hover:bg-blue-400/30 transition-colors"
+ }
+ style={{ left: `${sidebarWidth - 3}px` }}
+ />
+ )}
+ setShowSidebar((prev) => !prev)}
+ className={
+ "hidden md:flex fixed top-1/2 -translate-y-1/2 z-20 " +
+ "w-5 h-14 items-center justify-center cursor-pointer " +
+ "bg-white dark:bg-shadeA border border-gray-300 dark:border-[#3D3D3D] " +
+ "rounded-r-md shadow-sm hover:bg-gray-100 dark:hover:bg-gray-800 " +
+ "text-gray-600 dark:text-gray-300 transition-colors"
+ }
+ style={{ left: showSidebar ? `${sidebarWidth - 1}px` : "0" }}
+ >
+ {showSidebar ? (
+
+ ) : (
+
+ )}
+
>
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 858fd57..26aee4d 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -722,8 +722,17 @@ const TraceLogs: FC = () => {
useEffect(() => {
if (resolvedMessage || !messageId) return;
- setLoading(true);
const creds = sessionStorage.getItem("creds");
+ // Skip the API call when there are no creds — sending ``Basic null``
+ // makes FastAPI's HTTPBasic challenge with ``WWW-Authenticate: Basic``
+ // which triggers the browser's native auth popup. Better to show
+ // "no data" and let the user log back in via the normal flow.
+ if (!creds) {
+ setLoading(false);
+ setApiData(null);
+ return;
+ }
+ setLoading(true);
fetch(`/ui/trace/${messageId}`, {
headers: { Authorization: `Basic ${creds}` },
})
diff --git a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
index d7af2b6..962f505 100644
--- a/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
+++ b/graphrag-ui/src/pages/setup/GraphRAGConfig.tsx
@@ -29,8 +29,13 @@ const GraphRAGConfig = () => {
const [docOnly, setDocOnly] = useState(false);
const [enableRouterFallback, setEnableRouterFallback] = useState(true);
- // Advanced ingestion settings
+ // Collapsible section toggles (Configuration Scope and General Settings
+ // are always shown). Advanced Ingestion stays collapsed by default —
+ // matches the prior behavior.
+ const [showChunker, setShowChunker] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);
+ const [showSchema, setShowSchema] = useState(false);
+ const [showEndpoints, setShowEndpoints] = useState(false);
const [loadBatchSize, setLoadBatchSize] = useState("500");
const [upsertDelay, setUpsertDelay] = useState("0");
const [maxConcurrency, setMaxConcurrency] = useState("10");
@@ -38,6 +43,11 @@ const GraphRAGConfig = () => {
// Schema-aware initialization (Phase 1 sample-doc path)
const [schemaMaxSampleFiles, setSchemaMaxSampleFiles] = useState("5");
const [schemaMaxTotalMb, setSchemaMaxTotalMb] = useState("50");
+ // Dynamic schema behavior at extraction and retrieval time
+ const [strictMode, setStrictMode] = useState(false);
+ // Tri-state: "auto" leaves the key unset (server picks based on whether
+ // a domain schema exists); "true"/"false" force the behavior.
+ const [retrievalIncludeEntity, setRetrievalIncludeEntity] = useState<"auto" | "true" | "false">("auto");
// Chunker-specific settings
const [chunkSize, setChunkSize] = useState("");
@@ -83,6 +93,9 @@ const GraphRAGConfig = () => {
setMaxConcurrency(String(graphragConfig.default_concurrency ?? 10));
setSchemaMaxSampleFiles(String(graphragConfig.schema_max_sample_files ?? 5));
setSchemaMaxTotalMb(String(graphragConfig.schema_max_total_mb ?? 50));
+ setStrictMode(graphragConfig.strict_mode ?? false);
+ const rie = graphragConfig.retrieval_include_entity;
+ setRetrievalIncludeEntity(rie === undefined || rie === null ? "auto" : rie ? "true" : "false");
const chunkerConfig = graphragConfig.chunker_config || {};
setChunkSize(String(chunkerConfig.chunk_size ?? ""));
@@ -96,43 +109,79 @@ const GraphRAGConfig = () => {
setIsLoading(true);
const effectiveScope = scope ?? configScope;
const effectiveGraph = graphname ?? selectedGraph;
- try {
- const creds = sessionStorage.getItem("creds");
- const params = new URLSearchParams();
- if (effectiveGraph) params.set("graphname", effectiveGraph);
- if (effectiveScope === "graph") params.set("scope", "graph");
- const queryString = params.toString() ? `?${params.toString()}` : "";
- const response = await fetch(`/ui/config${queryString}`, {
- headers: { Authorization: `Basic ${creds}` },
- });
-
- if (!response.ok) {
- throw new Error("Failed to fetch configuration");
- }
-
- const data = await response.json();
-
- const deepCopy = (obj: any) => JSON.parse(JSON.stringify(obj || {}));
- loadedGlobalConfig.current = deepCopy(data.graphrag_config);
+ const creds = sessionStorage.getItem("creds");
+ const params = new URLSearchParams();
+ if (effectiveGraph) params.set("graphname", effectiveGraph);
+ if (effectiveScope === "graph") params.set("scope", "graph");
+ const queryString = params.toString() ? `?${params.toString()}` : "";
+ const url = `/ui/config${queryString}`;
+
+ // Transient backend failures (cold start, brief upstream timeouts via
+ // nginx, momentary 502/503/504) are common right after a service
+ // restart and produced the intermittent "Failed to fetch configuration"
+ // on this page. Retry a few times with backoff before surfacing an
+ // error to the user. Auth failures (401/403) and 4xx are not retried.
+ const shouldRetry = (status: number | null, err: unknown) => {
+ if (err && status === null) return true; // network error
+ if (status !== null && status >= 500) return true;
+ return false;
+ };
+
+ const maxAttempts = 3;
+ let lastErr: any = null;
+ let lastStatus: number | null = null;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ const response = await fetch(url, {
+ headers: { Authorization: `Basic ${creds}` },
+ });
+ lastStatus = response.status;
+ if (!response.ok) {
+ if (attempt < maxAttempts && shouldRetry(response.status, null)) {
+ await new Promise((r) => setTimeout(r, 500 * attempt));
+ continue;
+ }
+ throw new Error(`HTTP ${response.status}`);
+ }
- if (effectiveScope === "graph" && data.graphrag_overrides) {
- loadedGraphOverrides.current = deepCopy(data.graphrag_overrides);
- setGraphOverrides(data.graphrag_overrides);
- // Show per-graph values: merge global + overrides for display
- const merged = { ...data.graphrag_config, ...data.graphrag_overrides };
- applyGraphragConfig(merged);
- } else {
- loadedGraphOverrides.current = {};
- setGraphOverrides({});
- applyGraphragConfig(data.graphrag_config);
+ const data = await response.json();
+
+ const deepCopy = (obj: any) => JSON.parse(JSON.stringify(obj || {}));
+ loadedGlobalConfig.current = deepCopy(data.graphrag_config);
+
+ if (effectiveScope === "graph" && data.graphrag_overrides) {
+ loadedGraphOverrides.current = deepCopy(data.graphrag_overrides);
+ setGraphOverrides(data.graphrag_overrides);
+ // Show per-graph values: merge global + overrides for display
+ const merged = { ...data.graphrag_config, ...data.graphrag_overrides };
+ applyGraphragConfig(merged);
+ } else {
+ loadedGraphOverrides.current = {};
+ setGraphOverrides({});
+ applyGraphragConfig(data.graphrag_config);
+ }
+ // Clear any prior transient error banner on success.
+ setMessage("");
+ setMessageType("");
+ setIsLoading(false);
+ return;
+ } catch (error: any) {
+ lastErr = error;
+ if (attempt < maxAttempts && shouldRetry(lastStatus, error)) {
+ await new Promise((r) => setTimeout(r, 500 * attempt));
+ continue;
+ }
+ break;
}
- } catch (error: any) {
- console.error("Error fetching config:", error);
- setMessage(`Failed to load configuration: ${error.message}`);
- setMessageType("error");
- } finally {
- setIsLoading(false);
}
+
+ console.error("Error fetching config:", lastErr, "status=", lastStatus);
+ setMessage(
+ `Failed to load configuration${lastStatus ? ` (HTTP ${lastStatus})` : ""}. Please retry.`
+ );
+ setMessageType("error");
+ setIsLoading(false);
};
const handleSave = async () => {
@@ -168,7 +217,15 @@ const GraphRAGConfig = () => {
default_concurrency: parseInt(maxConcurrency),
schema_max_sample_files: parseInt(schemaMaxSampleFiles),
schema_max_total_mb: parseInt(schemaMaxTotalMb),
+ strict_mode: strictMode,
};
+ // retrieval_include_entity: only include the key when the user has
+ // picked an explicit value. "auto" should leave it unset so the
+ // server-side fallback (False with domain schema, True otherwise)
+ // applies.
+ if (retrievalIncludeEntity !== "auto") {
+ currentConfig.retrieval_include_entity = retrievalIncludeEntity === "true";
+ }
// Display defaults — used to avoid saving values the user never changed
const displayDefaults: Record
= {
@@ -187,6 +244,7 @@ const GraphRAGConfig = () => {
default_concurrency: 10,
schema_max_sample_files: 5,
schema_max_total_mb: 50,
+ strict_mode: false,
};
// Determine which config to diff against based on scope
@@ -484,7 +542,7 @@ const GraphRAGConfig = () => {
- When function or cypher generation fails after 3 retries, automatically try vector search instead of giving up.
+ Fall back to vector search when structured-data retrieval fails.
@@ -492,14 +550,29 @@ const GraphRAGConfig = () => {
{/* Chunker Settings */}
-
- Chunker Settings
-
-
- Configure document chunking for ingestion
-
+
setShowChunker(!showChunker)}
+ className="w-full flex items-center justify-between"
+ >
+
+ Chunker Settings
+
+
+ {showChunker ? "▲ Collapse" : "▼ Expand"}
+
+
+ {!showChunker && (
+
+ Configure document chunking for ingestion.
+
+ )}
-
+ {showChunker && (
+
+
+ Configure document chunking for ingestion.
+
Default Chunker
@@ -555,7 +628,7 @@ const GraphRAGConfig = () => {
onChange={(e) => setOverlapSize(e.target.value)}
/>
- Overlap between consecutive chunks. Defaults to 1/8 of chunk size if empty.
+ Overlap between consecutive chunks.
@@ -627,6 +700,7 @@ const GraphRAGConfig = () => {
+ )}
{message && (
@@ -681,7 +755,7 @@ const GraphRAGConfig = () => {
onChange={(e) => setLoadBatchSize(e.target.value)}
/>
- Vertices per upsert batch
+ Number of vertices written per batch.
@@ -723,65 +797,167 @@ const GraphRAGConfig = () => {
)}
- {/* Schema-aware initialization (Phase 1) */}
+ {/* Schema extraction (sample-doc proposal path) + dynamic schema
+ behavior (how domain types are enforced and retrieved). */}
-
- Schema Initialization
-
+
setShowSchema(!showSchema)}
+ className="w-full flex items-center justify-between"
+ >
+
+ Schema Extraction
+
+
+ {showSchema ? "▲ Collapse" : "▼ Expand"}
+
+
+ {!showSchema && (
+
+ Sample-doc schema extraction limits and runtime behavior of
+ the domain schema.
+
+ )}
+
+ {showSchema && (
+
- Limits for the Generate from sample documents path on
- the Initialize Knowledge Graph dialog.
+ Controls for the schema-extraction sample-doc workflow and the
+ runtime behavior of the dynamic (domain) schema during entity
+ extraction and retrieval.
-
-
-
- Max Sample Files
-
-
setSchemaMaxSampleFiles(e.target.value)}
- />
-
- Maximum number of sample documents per schema-extraction run
-
+ {/* Schema extraction sub-section */}
+
+
+ Schema Extraction (Sample Documents)
+
+
+ Limits for the Generate from sample documents path on
+ the Initialize Knowledge Graph dialog.
+
+
+
+
+ Max Sample Files
+
+
setSchemaMaxSampleFiles(e.target.value)}
+ />
+
+ Maximum number of sample documents per schema-extraction run
+
+
+
+
+ Max Total Size (MB)
+
+
setSchemaMaxTotalMb(e.target.value)}
+ />
+
+ Combined upload cap across all sample files
+
+
+
-
-
- Max Total Size (MB)
-
-
setSchemaMaxTotalMb(e.target.value)}
- />
-
- Combined upload cap across all sample files (per-file cap is fixed at 10 MB)
-
+ {/* Dynamic schema runtime sub-section */}
+
+
+ Dynamic Schema Runtime
+
+
+ How the domain schema is enforced during entity extraction and
+ used by retrievers at query time.
+
+
+
+
+
+ setStrictMode(e.target.checked)}
+ />
+
+ Strict Mode
+
+
+
+ Drop extracted entities and relationships that don't match
+ the domain schema.
+
+
+
+
+
+ Include Generic Entity in Retrieval
+
+
+ setRetrievalIncludeEntity(v as "auto" | "true" | "false")
+ }
+ >
+
+
+
+
+ Auto (default)
+ Yes – always include Entity
+ No – domain types only
+
+
+
+ Include generic entities in retrieval alongside domain types.
+
+
+
+ )}
{/* Service Endpoints (global only) */}
{configScope !== "graph" && (
-
- Service Endpoints
-
-
- Configure internal service URLs. These are global settings and cannot be overridden per graph.
-
+
setShowEndpoints(!showEndpoints)}
+ className="w-full flex items-center justify-between"
+ >
+
+ Service Endpoints
+
+
+ {showEndpoints ? "▲ Collapse" : "▼ Expand"}
+
+
+ {!showEndpoints && (
+
+ Internal service URLs (global only).
+
+ )}
-
+ {showEndpoints && (
+
+
+ Configure internal service URLs. These are global settings and cannot be overridden per graph.
+
ECC Service URL
@@ -814,6 +990,7 @@ const GraphRAGConfig = () => {
+ )}
)}
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index 71efb12..157bf77 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -1070,10 +1070,7 @@ const IngestGraph: React.FC
= ({ isModal = false }) => {
- Maximum upload per request: {MAX_UPLOAD_SIZE_MB} MB.{" "}
- {ingestGraphName
- ? `Upload destination: uploads/${ingestGraphName}/`
- : ""}
+ Maximum upload per request: {MAX_UPLOAD_SIZE_MB} MB.
{selectedFiles && (() => {
const SUPPORTED_EXTENSIONS = new Set([".txt", ".md", ".pdf", ".docx", ".doc", ".html", ".htm", ".json", ".csv", ".xlsx", ".xls", ".xml", ".jpeg", ".jpg", ".png", ".gif", ".jsonl"]);
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index fb10ed5..3557ef6 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TagInput, TypeHint } from "@/components/ui/tag-input";
import { Database, Loader2, RefreshCw, Upload } from "lucide-react";
-import { pauseIdleTimer, resumeIdleTimer } from "@/hooks/useIdleTimeout";
+import { pauseIdleTimer, resumeIdleTimer, pingIdleTimer } from "@/hooks/useIdleTimeout";
import {
Dialog,
DialogContent,
@@ -20,11 +20,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useConfirm } from "@/hooks/useConfirm";
+import { useAlert } from "@/hooks/useAlert";
import { useNavigate } from "react-router-dom";
import IngestGraph from "./IngestGraph";
const KGAdmin = () => {
const [confirm, confirmDialog, isConfirmDialogOpen] = useConfirm();
+ const [showAlert, alertDialog] = useAlert();
const navigate = useNavigate();
const [availableGraphs, setAvailableGraphs] = useState
([]);
@@ -63,6 +65,10 @@ const KGAdmin = () => {
setEdgeHints([]);
setRenderedSchemaPrompt("");
setIsInitComplete(false);
+ setPrecheckPassed(false);
+ setPrecheckMessage("");
+ setCollectedVertexDescs({});
+ setCollectedEdgeDescs({});
};
const handleRefreshDialogChange = (open: boolean) => {
@@ -81,6 +87,44 @@ const KGAdmin = () => {
const [isInitializing, setIsInitializing] = useState(false);
const [statusMessage, setStatusMessage] = useState("");
const [statusType, setStatusType] = useState<"success" | "error" | "">("");
+
+ // Graph-name combobox: dropdown of existing graphs that the user can
+ // filter by typing. The input is the single source of truth; clicking
+ // a row replaces the typed text.
+ const [graphNameDropdownOpen, setGraphNameDropdownOpen] = useState(false);
+ const graphNameComboRef = useRef(null);
+ useEffect(() => {
+ if (!graphNameDropdownOpen) return;
+ const handleOutside = (e: MouseEvent) => {
+ if (
+ graphNameComboRef.current &&
+ !graphNameComboRef.current.contains(e.target as Node)
+ ) {
+ setGraphNameDropdownOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handleOutside);
+ return () => document.removeEventListener("mousedown", handleOutside);
+ }, [graphNameDropdownOpen]);
+
+ // Precheck state for the "none" schema-source path. The user clicks
+ // Precheck before Create & Init to either confirm the graph is new
+ // OR review/edit descriptions for pre-existing user-defined types.
+ const [precheckPassed, setPrecheckPassed] = useState(false);
+ const [precheckRunning, setPrecheckRunning] = useState(false);
+ const [precheckMessage, setPrecheckMessage] = useState("");
+ // Descriptions collected from the description-edit dialog. When non-empty,
+ // the Create & Init submission carries use_existing_schema=true and these
+ // maps so the backend stamps them onto EntityType / RelationshipType.
+ const [collectedVertexDescs, setCollectedVertexDescs] = useState>({});
+ const [collectedEdgeDescs, setCollectedEdgeDescs] = useState>({});
+ // Description-edit dialog state
+ const [descDialogOpen, setDescDialogOpen] = useState(false);
+ const [descDialogVertices, setDescDialogVertices] = useState([]);
+ const [descDialogEdges, setDescDialogEdges] = useState>([]);
+ const [descDialogVertexDescs, setDescDialogVertexDescs] = useState>({});
+ const [descDialogEdgeDescs, setDescDialogEdgeDescs] = useState>({});
+ const [descDialogLoading, setDescDialogLoading] = useState(false);
// True only after the full create-graph + initialize-graph round
// succeeds. The "Done" button gates on this — extraction success
// alone (statusType === "success" mid-flow) must NOT show Done,
@@ -374,6 +418,15 @@ const KGAdmin = () => {
});
}, [initializeDialogOpen]);
+ // Any change to the graph name or schema source invalidates a prior
+ // precheck — the next Create & Init must re-run the eligibility flow.
+ useEffect(() => {
+ setPrecheckPassed(false);
+ setPrecheckMessage("");
+ setCollectedVertexDescs({});
+ setCollectedEdgeDescs({});
+ }, [graphName, schemaSource]);
+
const handleSampleFileSelect = (e: React.ChangeEvent) => {
const list = Array.from(e.target.files || []);
if (list.length > maxSampleFiles) {
@@ -521,6 +574,104 @@ const KGAdmin = () => {
}
};
+ // Precheck for the "none" schema-source path.
+ // * Empty graph → precheckPassed = true, Create & Init becomes clickable.
+ // * Structural → alert "manual cleanup required", precheckPassed stays false.
+ // * User types → LLM seeds descriptions, description-edit dialog opens.
+ const handlePrecheck = async () => {
+ if (!graphName.trim()) {
+ setPrecheckMessage("Please enter a graph name first.");
+ return;
+ }
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(graphName)) {
+ setPrecheckMessage("Invalid graph name — must start with a letter or underscore.");
+ return;
+ }
+ setPrecheckRunning(true);
+ setPrecheckMessage("");
+ try {
+ const creds = sessionStorage.getItem("creds");
+ const eligResp = await fetch(`/ui/${graphName}/check_init_eligibility`, {
+ headers: { Authorization: `Basic ${creds}` },
+ });
+ const elig = await eligResp.json();
+ if (!eligResp.ok) {
+ setPrecheckMessage(`Precheck failed: ${elig?.detail || eligResp.statusText}`);
+ return;
+ }
+ if (elig.state === "structural_present") {
+ await showAlert(
+ "Existing GraphRAG schema detected, manual cleanup required."
+ );
+ setPrecheckPassed(false);
+ setPrecheckMessage("Existing GraphRAG schema present — cannot initialize.");
+ return;
+ }
+ if (elig.state === "empty") {
+ setPrecheckPassed(true);
+ setPrecheckMessage("Graph is empty or new — ready to initialize.");
+ return;
+ }
+ // state === "user_types_present" — seed descriptions and open the
+ // edit dialog. The Create & Init button stays disabled until the
+ // user accepts.
+ const vts: string[] = elig.user_vertex_types || [];
+ const ets: string[] = elig.user_edge_types || [];
+ const pairsMap: Record = elig.user_edge_pairs || {};
+ const edges = ets.map((name) => {
+ const pair = (pairsMap[name] || [])[0] || ["", ""];
+ return { name, from: pair[0] || "", to: pair[1] || "" };
+ });
+ setDescDialogVertices(vts);
+ setDescDialogEdges(edges);
+ setDescDialogVertexDescs(Object.fromEntries(vts.map((v) => [v, ""])));
+ setDescDialogEdgeDescs(Object.fromEntries(edges.map((e) => [e.name, ""])));
+ setDescDialogOpen(true);
+ setDescDialogLoading(true);
+ // LLM call to seed each description; best-effort. The dialog stays
+ // editable regardless.
+ try {
+ const sugResp = await fetch(
+ `/ui/${graphName}/suggest_type_descriptions`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Basic ${creds}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ vertex_types: vts,
+ edge_types: edges,
+ }),
+ }
+ );
+ if (sugResp.ok) {
+ const sug = await sugResp.json();
+ setDescDialogVertexDescs((prev) => ({ ...prev, ...(sug.vertex_descriptions || {}) }));
+ setDescDialogEdgeDescs((prev) => ({ ...prev, ...(sug.edge_descriptions || {}) }));
+ }
+ } catch {
+ // Silent — leave descriptions blank for the user to fill in.
+ } finally {
+ setDescDialogLoading(false);
+ }
+ } catch (err: any) {
+ setPrecheckMessage(`Precheck failed: ${err.message}`);
+ } finally {
+ setPrecheckRunning(false);
+ }
+ };
+
+ const handleAcceptDescriptions = () => {
+ setCollectedVertexDescs({ ...descDialogVertexDescs });
+ setCollectedEdgeDescs({ ...descDialogEdgeDescs });
+ setPrecheckPassed(true);
+ setPrecheckMessage(
+ `Adopting ${descDialogVertices.length} vertex and ${descDialogEdges.length} edge type${descDialogEdges.length === 1 ? "" : "s"} from existing schema.`
+ );
+ setDescDialogOpen(false);
+ };
+
// Initialize Graph
const handleInitializeGraph = async () => {
if (!graphName.trim()) {
@@ -585,17 +736,22 @@ const KGAdmin = () => {
}
setStatusMessage("Step 2/2: Submitting GraphRAG schema initialization...");
- const initBody: { schema_gsql?: string } = {};
+ const initBody: Record = {};
+ const adoptingExisting =
+ schemaSource === "none" &&
+ (Object.keys(collectedVertexDescs).length > 0 ||
+ Object.keys(collectedEdgeDescs).length > 0);
if (schemaSource === "gsql" && pasteGsql.trim()) {
initBody.schema_gsql = pasteGsql;
} else if (schemaSource === "samples" && draftProposal) {
const gsql = draftProposalToGsql(draftProposal).trim();
if (gsql) initBody.schema_gsql = gsql;
+ } else if (adoptingExisting) {
+ initBody.use_existing_schema = true;
+ initBody.vertex_descriptions = collectedVertexDescs;
+ initBody.edge_descriptions = collectedEdgeDescs;
}
- // Submit the init job. The backend kicks off a BackgroundTask
- // and returns 202 immediately so the browser doesn't drop the
- // request mid-flight on long inits (TG schema-change + retriever
- // installs can take 10+ minutes).
+
const initResponse = await fetch(`/ui/${graphName}/initialize_graph`, {
method: "POST",
headers: {
@@ -604,13 +760,28 @@ const KGAdmin = () => {
},
body: JSON.stringify(initBody),
});
-
const initData = await initResponse.json();
+ // Server-side preflight may still reject (e.g. graph state
+ // changed between Precheck and Create & Init, or the caller
+ // skipped Precheck on samples/gsql with pre-existing types).
+ if (initResponse.status === 409 && initData?.detail?.reason) {
+ const message: string = initData.detail.message;
+ await showAlert(message);
+ setStatusMessage(message);
+ setStatusType("error");
+ setIsInitializing(false);
+ return;
+ }
+
if (!initResponse.ok) {
- throw new Error(
- initData.detail || `Failed to submit init: ${initResponse.statusText}`
- );
+ const detail = initData?.detail;
+ const msg = typeof detail === "string"
+ ? detail
+ : detail?.message
+ ? detail.message
+ : `Failed to submit init: ${initResponse.statusText}`;
+ throw new Error(msg);
}
if (initData.status !== "submitted") {
@@ -646,6 +817,10 @@ const KGAdmin = () => {
continue;
}
if (!statusResp.ok) continue;
+ // Successful status poll on a user-initiated long flow — keep
+ // the UI idle timer alive so the user isn't logged out while
+ // watching the init progress.
+ pingIdleTimer();
const statusData = await statusResp.json();
if (statusData.message) {
setStatusMessage(`Step 2/2: ${statusData.message}`);
@@ -760,6 +935,9 @@ const KGAdmin = () => {
isRebuildRunningRef.current = isCurrentlyRunning;
if (isCurrentlyRunning) {
+ // Long-running flow with active user interest — keep the
+ // UI idle timer alive on each successful poll.
+ pingIdleTimer();
setPollingActive(true);
const startTime = statusData.started_at
? new Date(statusData.started_at * 1000).toLocaleString()
@@ -796,7 +974,7 @@ const KGAdmin = () => {
}
};
- // Refresh Graph
+ // Rebuild Graph
const handleRefreshGraph = async () => {
if (!refreshGraphName) {
setRefreshMessage("Please select a graph");
@@ -813,7 +991,7 @@ const KGAdmin = () => {
setIsRefreshing(true);
const shouldRefresh = await confirm(
- `Are you sure you want to refresh the knowledge graph "${refreshGraphName}"? This will rebuild the graph content.`
+ `Are you sure you want to rebuild the knowledge graph "${refreshGraphName}"? This will rerun entity extraction and community detection.`
);
if (!shouldRefresh) {
setRefreshMessage("Operation cancelled by user.");
@@ -858,7 +1036,7 @@ const KGAdmin = () => {
return;
}
throw new Error(
- errorData.detail || `Failed to refresh graph: ${response.statusText}`
+ errorData.detail || `Failed to rebuild graph: ${response.statusText}`
);
}
@@ -866,7 +1044,7 @@ const KGAdmin = () => {
console.log("Refresh response:", data);
setRefreshMessage(
- `✅ Refresh submitted successfully! The knowledge graph "${refreshGraphName}" is being rebuilt.`
+ `✅ Rebuild submitted successfully! The knowledge graph "${refreshGraphName}" is being rebuilt.`
);
setIsRebuildRunning(true);
isRebuildRunningRef.current = true;
@@ -963,17 +1141,17 @@ const KGAdmin = () => {
- {/* Refresh Card */}
+ {/* Rebuild Card */}
- Refresh Knowledge Graph
+ Rebuild Knowledge Graph
- Process new documents in your knowledge graph to refresh its content.
+ Process documents and rerun entity extraction + community detection.
@@ -982,7 +1160,7 @@ const KGAdmin = () => {
className="gradient w-full text-white"
>
- Refresh Graph
+ Rebuild Graph
@@ -1007,18 +1185,91 @@ const KGAdmin = () => {
Knowledge Graph Name
-
setGraphName(e.target.value)}
- disabled={isInitializing || isExtractingSchema}
- className="dark:border-[#3D3D3D] dark:bg-shadeA"
- onKeyDown={(e) => {
- if (e.key === "Enter" && !isInitializing && !isExtractingSchema) {
- handleInitializeGraph();
+
+ {/* Wrapper carries the visual styling (matching the
+ SelectTrigger used by other graph selectors); the
+ inner
is borderless/transparent so its
+ native text rendering can't clip the underscore
+ glyph against the bottom border. */}
+
+ >
+
{
+ setGraphName(e.target.value);
+ if (!graphNameDropdownOpen) setGraphNameDropdownOpen(true);
+ }}
+ onFocus={() => setGraphNameDropdownOpen(true)}
+ disabled={isInitializing || isExtractingSchema}
+ className="flex-1 bg-transparent outline-none border-0 p-0 text-sm text-black dark:text-white placeholder:text-muted-foreground disabled:opacity-50"
+ // appearance:none disables Chrome's native input
+ // rendering (which on macOS clips descenders like
+ // '_' even when the wrapper has plenty of room).
+ // lineHeight + a slightly taller wrapper finish
+ // the job of making the underscore glyph fully
+ // visible in long names.
+ style={{
+ WebkitAppearance: "none",
+ appearance: "none",
+ lineHeight: "1.5",
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !isInitializing && !isExtractingSchema) {
+ handleInitializeGraph();
+ } else if (e.key === "Escape") {
+ setGraphNameDropdownOpen(false);
+ }
+ }}
+ />
+
setGraphNameDropdownOpen((o) => !o)}
+ disabled={
+ isInitializing ||
+ isExtractingSchema ||
+ availableGraphs.length === 0
+ }
+ aria-label="Toggle existing graphs"
+ className="ml-2 p-0.5 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50"
+ >
+
+
+
+
+
+ {graphNameDropdownOpen && (() => {
+ const q = graphName.trim().toLowerCase();
+ const filtered = q
+ ? availableGraphs.filter((g) => g.toLowerCase().includes(q))
+ : availableGraphs;
+ if (filtered.length === 0) return null;
+ return (
+
+ {filtered.map((g) => (
+ {
+ setGraphName(g);
+ setGraphNameDropdownOpen(false);
+ }}
+ className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-800 text-black dark:text-white"
+ >
+ {g}
+
+ ))}
+
+ );
+ })()}
+
@@ -1072,6 +1323,45 @@ const KGAdmin = () => {
+ {schemaSource === "none" && (
+
+ {precheckMessage ? (
+
+ {precheckMessage}
+
+ ) : (
+
+ Click Check existing schema to verify the graph before initializing.
+
+ )}
+
+ {precheckRunning ? (
+ <>
+
+ Checking…
+ >
+ ) : (
+ <>Check existing schema>
+ )}
+
+
+ )}
+
{schemaSource === "samples" && (
{
isInitializing ||
isExtractingSchema ||
!graphName.trim() ||
+ // "None" requires a successful precheck before
+ // Create & Init becomes clickable. Precheck
+ // verifies the graph is new or, if it has
+ // existing types, collects descriptions for them.
+ (schemaSource === "none" && !precheckPassed) ||
// "Generate from sample documents" is only ready
// to submit once the LLM has returned a draft
// proposal with at least one vertex.
@@ -1942,6 +2237,118 @@ const KGAdmin = () => {
+ {/* Description-edit dialog for the adopt-existing path. Names
+ are read-only; the user reviews/edits LLM-seeded descriptions
+ and clicks Accept to unlock Create & Initialize. */}
+
{
+ if (!open) setDescDialogOpen(false);
+ }}
+ >
+ e.preventDefault()}
+ >
+
+
+ Use existing schema as domain types
+
+
+ Review and edit descriptions for each existing type. They will be saved with the graph and used by query and extraction tools.
+
+
+
+
+ {descDialogLoading && (
+
+
+ Generating description suggestions...
+
+ )}
+
+ {descDialogVertices.length > 0 && (
+
+
+ Vertex types
+
+
+ {descDialogVertices.map((v) => (
+
+
+ {v}
+
+
+ setDescDialogVertexDescs((prev) => ({
+ ...prev,
+ [v]: e.target.value,
+ }))
+ }
+ placeholder="One-sentence description"
+ className="dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ ))}
+
+
+ )}
+
+ {descDialogEdges.length > 0 && (
+
+
+ Edge types
+
+
+ {descDialogEdges.map((e) => (
+
+
+
+ {e.name}
+
+ {e.from && e.to && (
+
+ {e.from} → {e.to}
+
+ )}
+
+
+ setDescDialogEdgeDescs((prev) => ({
+ ...prev,
+ [e.name]: ev.target.value,
+ }))
+ }
+ placeholder="One-sentence description"
+ className="dark:border-[#3D3D3D] dark:bg-shadeA"
+ />
+
+ ))}
+
+
+ )}
+
+
+
+ setDescDialogOpen(false)}
+ className="dark:border-[#3D3D3D]"
+ >
+ Cancel
+
+
+ Accept
+
+
+
+
+
{/* Ingest Dialog */}
{
- {/* Refresh Dialog */}
+ {/* Rebuild Dialog */}
e.preventDefault()}
>
- Refresh Knowledge Graph
+ Rebuild Knowledge Graph
- Rebuild the graph content and rerun community detection for your knowledge graph
+ Process documents and rerun entity extraction + community detection for your knowledge graph.
- Select Graph to Refresh
+ Select Graph to Rebuild
{
) : (
<>
- Confirm & Refresh
+ Confirm & Rebuild
>
)}
@@ -2088,6 +2495,7 @@ const KGAdmin = () => {
{confirmDialog}
+ {alertDialog}
);
};
diff --git a/graphrag-ui/src/pages/setup/LLMConfig.tsx b/graphrag-ui/src/pages/setup/LLMConfig.tsx
index 836e17c..a33379c 100644
--- a/graphrag-ui/src/pages/setup/LLMConfig.tsx
+++ b/graphrag-ui/src/pages/setup/LLMConfig.tsx
@@ -1260,7 +1260,7 @@ const LLMConfig = () => {
Completion Service
- Configure the LLM provider used by the ECC service for document processing (entity extraction and community summarization)
+ LLM provider for entity extraction and community summarization during document ingestion.
@@ -1297,7 +1297,7 @@ const LLMConfig = () => {
}}
/>
- Used by ECC for entity extraction and community summarization during document ingestion
+ Model for entity extraction and community summarization.
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index 05990c1..edc2f40 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -147,7 +147,12 @@ def _save_trace_log(message_id: str, conversation_id: str, user_query: str, resp
# Key: (username, password_hash) -> (timestamp, (global_roles, graph_roles))
_role_cache: dict[tuple[str, str], tuple[float, tuple[list[str], dict[str, list[str]]]]] = {}
_role_cache_lock = threading.Lock()
-_ROLE_CACHE_TTL = 60 # seconds
+# Role changes (granting/revoking TG roles) are infrequent operator
+# actions, and the cache key already includes a password hash so credential
+# changes are picked up immediately. Match the UI idle timeout (1 hour) —
+# past that point the user gets logged out anyway and the next sign-in
+# refreshes roles. Increase if your operator workflows can wait longer.
+_ROLE_CACHE_TTL = 60 * 60 # seconds (1 hour)
def _normalize_roles(raw_roles: str) -> list[str]:
cleaned = re.sub(r"[\[\]]", "", raw_roles).strip()
@@ -220,13 +225,25 @@ def _get_user_role_details(username: str, password: str) -> tuple[list[str], dic
restppPort=db_config.get("restppPort"),
graphname="",
)
- user_info = conn.gsql("SHOW USER")
- result = _parse_user_roles_detail(user_info, username)
- with _role_cache_lock:
- _role_cache[cache_key] = (now, result)
-
- return result
+ # Transient GSQL hiccups when the role-cache TTL expires were
+ # surfacing as 403 "Unable to verify user roles" banners on the
+ # config pages. Retry once with a short backoff before giving up —
+ # the next attempt usually succeeds when the blip is over.
+ last_exc: Exception | None = None
+ for attempt in range(2):
+ try:
+ user_info = conn.gsql("SHOW USER")
+ result = _parse_user_roles_detail(user_info, username)
+ with _role_cache_lock:
+ _role_cache[cache_key] = (now, result)
+ return result
+ except Exception as exc:
+ last_exc = exc
+ if attempt == 0:
+ time.sleep(0.5)
+ assert last_exc is not None
+ raise last_exc
def _get_user_roles(username: str, password: str) -> list[str]:
@@ -593,13 +610,10 @@ def create_graph(
}
-# Per-graph init state store. Init runs as a BackgroundTask so the HTTP
-# request returns immediately; the UI polls /initialize_status for
-# progress/completion. The browser used to drop the request after ~5
-# minutes of silent response on long inits (TG schema-change + retriever
-# installs can run for 10+ minutes), even though the backend completed
-# successfully — see ``ERR_TIMED_OUT`` reports during v1.4.0 schema-aware
-# init.
+# Per-graph init state store. Init runs as a BackgroundTask so the
+# HTTP request returns immediately; clients poll /initialize_status
+# for progress / completion. Avoids browser timeouts on long inits
+# (TG schema-change + retriever installs can run for 10+ minutes).
_init_state: dict[str, dict] = {}
_init_state_lock = threading.Lock()
@@ -616,6 +630,270 @@ def _get_init_state(graphname: str) -> dict:
return dict(_init_state.get(graphname, {"state": "unknown"}))
+def _build_proposal_from_live_schema(
+ conn,
+ vertex_descriptions: dict | None = None,
+ edge_descriptions: dict | None = None,
+):
+ """Build a :class:`SchemaProposal` from the graph's current
+ user-defined vertex/edge types, suitable for feeding into
+ :func:`apply_proposal` on the ``use_existing_schema`` path.
+
+ The proposal carries names + edge pairs and optionally
+ user-supplied descriptions (collected by the Precheck dialog or
+ seeded by the suggest-description LLM call). Diff against the live
+ schema is a no-op; ``apply_proposal`` only installs retrievers and
+ writes type metadata.
+ """
+ from common.db.schema_utils import (
+ EdgeProposal,
+ GRAPHRAG_STRUCTURAL_EDGE_TYPES,
+ GRAPHRAG_STRUCTURAL_VERTEX_TYPES,
+ SchemaProposal,
+ VertexProposal,
+ read_existing_schema,
+ )
+ structural_v = {t.casefold() for t in GRAPHRAG_STRUCTURAL_VERTEX_TYPES}
+ structural_e = {t.casefold() for t in GRAPHRAG_STRUCTURAL_EDGE_TYPES}
+
+ vd = vertex_descriptions or {}
+ ed = edge_descriptions or {}
+
+ existing = read_existing_schema(conn)
+ vertices = [
+ VertexProposal(name=v, description=(vd.get(v) or "").strip())
+ for v in sorted(existing.vertex_types)
+ if v.casefold() not in structural_v
+ ]
+ edges: list[EdgeProposal] = []
+ for et, pairs in existing.edge_pairs.items():
+ folded = et.casefold()
+ if folded in structural_e or folded.startswith("reverse_"):
+ continue
+ edges.append(
+ EdgeProposal(
+ name=et,
+ pairs=list(pairs),
+ directed=et in existing.directed_edges,
+ description=(ed.get(et) or "").strip(),
+ )
+ )
+ return SchemaProposal(vertices=vertices, edges=edges)
+
+
+def _check_init_eligibility(auth_b64: str, graphname: str) -> dict:
+ """Introspect *graphname* and categorize its current schema state.
+
+ Returns a dict with key ``state`` set to one of:
+
+ * ``"empty"`` — graph has no schema, or none of its existing types
+ are GraphRAG structural or user-defined. Safe to initialize from
+ scratch.
+ * ``"structural_present"`` — graph already has one or more
+ GraphRAG structural vertex/edge types. Caller must reject.
+ ``structural_types`` lists the offending names.
+ * ``"user_types_present"`` — graph has user-defined vertex/edge
+ types (none structural). Lists in ``user_vertex_types`` and
+ ``user_edge_types``. Caller decides whether to reject or adopt.
+
+ Graphs that don't yet exist in TigerGraph behave like ``empty`` —
+ ``getVertexTypes`` raises or returns empty for missing graphs, which
+ we treat as "no schema yet".
+ """
+ from common.db.schema_utils import (
+ GRAPHRAG_STRUCTURAL_VERTEX_TYPES,
+ GRAPHRAG_STRUCTURAL_EDGE_TYPES,
+ )
+ structural_v = {t.casefold() for t in GRAPHRAG_STRUCTURAL_VERTEX_TYPES}
+ structural_e = {t.casefold() for t in GRAPHRAG_STRUCTURAL_EDGE_TYPES}
+
+ try:
+ _, conn = ws_basic_auth(auth_b64, graphname)
+ except Exception:
+ # Graph doesn't exist (or auth failed mid-flight); treat as empty
+ # so the create_graph + init path handles it.
+ return {"state": "empty"}
+
+ try:
+ vertex_types = list(conn.getVertexTypes() or [])
+ edge_types = list(conn.getEdgeTypes() or [])
+ except Exception:
+ return {"state": "empty"}
+
+ structural_hits: list[str] = []
+ user_vts: list[str] = []
+ for vt in vertex_types:
+ if vt.casefold() in structural_v:
+ structural_hits.append(vt)
+ else:
+ user_vts.append(vt)
+ user_edges: list[str] = []
+ for et in edge_types:
+ folded = et.casefold()
+ if folded in structural_e or folded.startswith("reverse_"):
+ structural_hits.append(et)
+ else:
+ user_edges.append(et)
+
+ if structural_hits:
+ return {
+ "state": "structural_present",
+ "structural_types": sorted(set(structural_hits)),
+ }
+ if user_vts or user_edges:
+ return {
+ "state": "user_types_present",
+ "user_vertex_types": sorted(user_vts),
+ "user_edge_types": sorted(user_edges),
+ }
+ return {"state": "empty"}
+
+
+@router.get(route_prefix + "/{graphname}/check_init_eligibility")
+def check_init_eligibility(
+ graphname: ValidGraphName,
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+):
+ """Introspect *graphname* and return an init-eligibility verdict.
+
+ Used by the Initialize Knowledge Graph dialog's *Precheck* button to
+ surface the same categorization that ``POST /initialize_graph`` runs
+ internally, without starting an init job.
+
+ Response::
+
+ {
+ "state": "empty" | "user_types_present" | "structural_present",
+ "structural_types": [...], # present when state=structural_present
+ "user_vertex_types": [...], # present when state=user_types_present
+ "user_edge_types": [...], # present when state=user_types_present
+ "user_edge_pairs": {edge: [[from, to], ...]} # for description hints
+ }
+ """
+ cred_obj = creds[1]
+ auth_b64 = base64.b64encode(
+ f"{cred_obj.username}:{cred_obj.password}".encode()
+ ).decode()
+ result = _check_init_eligibility(auth_b64, graphname)
+ # Include edge endpoint pairs so the UI can show "FILED_BY (Filing → Company)"
+ # alongside each edge name in the description-edit dialog.
+ if result.get("state") == "user_types_present" and result.get("user_edge_types"):
+ try:
+ _, conn = ws_basic_auth(auth_b64, graphname)
+ from common.db.schema_utils import read_existing_schema
+ existing = read_existing_schema(conn)
+ pairs_map: dict[str, list[list[str]]] = {}
+ for et in result["user_edge_types"]:
+ pairs = existing.edge_pairs.get(et, set())
+ pairs_map[et] = [[s, t] for s, t in sorted(pairs)]
+ result["user_edge_pairs"] = pairs_map
+ except Exception:
+ result["user_edge_pairs"] = {}
+ return result
+
+
+@router.post(route_prefix + "/{graphname}/suggest_type_descriptions")
+def suggest_type_descriptions(
+ graphname: ValidGraphName,
+ creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
+ payload: Annotated[dict, Body(...)],
+):
+ """Call the completion LLM to suggest one-sentence descriptions for a
+ set of vertex/edge type names.
+
+ Request body::
+
+ {
+ "vertex_types": ["Company", "Person", ...],
+ "edge_types": [{"name": "FILED_BY", "from": "Filing", "to": "Company"}, ...]
+ }
+
+ Response::
+
+ {
+ "vertex_descriptions": {"Company": "...", "Person": "...", ...},
+ "edge_descriptions": {"FILED_BY": "...", ...}
+ }
+
+ Best-effort: on any LLM failure, the corresponding keys are empty
+ strings so the dialog can still render an editable form.
+ """
+ from langchain_core.prompts import PromptTemplate
+ from langchain_core.output_parsers import JsonOutputParser
+
+ vertex_types = [
+ str(v) for v in (payload.get("vertex_types") or [])
+ if isinstance(v, str) and v
+ ]
+ edge_items = payload.get("edge_types") or []
+ edges_brief: list[str] = []
+ for e in edge_items:
+ if not isinstance(e, dict):
+ continue
+ name = e.get("name")
+ f = e.get("from") or ""
+ t = e.get("to") or ""
+ if not name:
+ continue
+ if f and t:
+ edges_brief.append(f"{name} (FROM {f}, TO {t})")
+ else:
+ edges_brief.append(str(name))
+
+ if not vertex_types and not edges_brief:
+ return {"vertex_descriptions": {}, "edge_descriptions": {}}
+
+ llm_service = get_llm_service(get_completion_config(graphname))
+ prompt = PromptTemplate.from_template(
+ "Given the following graph-schema type names from a domain knowledge "
+ "graph, write a concise one-sentence description for each. Use plain "
+ "English; describe what the type represents, not its attributes.\n\n"
+ "Vertex types: {vertex_types}\n"
+ "Edge types: {edge_types}\n\n"
+ "Return JSON with this exact shape:\n"
+ "{{\"vertex_descriptions\": {{\"\": \"\"}}, "
+ "\"edge_descriptions\": {{\"\": \"\"}}}}\n"
+ )
+ try:
+ parsed = llm_service.invoke_with_parser(
+ prompt,
+ JsonOutputParser(),
+ {
+ "vertex_types": ", ".join(vertex_types) or "(none)",
+ "edge_types": ", ".join(edges_brief) or "(none)",
+ },
+ caller_name="suggest_type_descriptions",
+ )
+ except Exception as exc:
+ LogWriter.warning(
+ f"suggest_type_descriptions LLM call failed for {graphname}: {exc}"
+ )
+ return {
+ "vertex_descriptions": {v: "" for v in vertex_types},
+ "edge_descriptions": {
+ (e.get("name") if isinstance(e, dict) else ""): ""
+ for e in edge_items
+ },
+ }
+
+ vds = parsed.get("vertex_descriptions") if isinstance(parsed, dict) else {}
+ eds = parsed.get("edge_descriptions") if isinstance(parsed, dict) else {}
+ return {
+ "vertex_descriptions": {
+ v: (vds.get(v) or "").strip() if isinstance(vds, dict) else ""
+ for v in vertex_types
+ },
+ "edge_descriptions": {
+ (e.get("name") if isinstance(e, dict) else ""): (
+ (eds.get(e.get("name")) or "").strip()
+ if isinstance(eds, dict) and isinstance(e, dict)
+ else ""
+ )
+ for e in edge_items
+ },
+ }
+
+
@router.post(route_prefix + "/{graphname}/initialize_graph")
def init_graph(
graphname: ValidGraphName,
@@ -635,14 +913,32 @@ def init_graph(
EntityType, RelationshipType, Content, Community, Image and their
structural edges) is always created if missing.
- Optionally accepts a JSON body with a domain-schema proposal:
+ Optionally accepts a JSON body:
- {"schema_gsql": "ADD VERTEX Company(...); ..."}
+ {"schema_gsql": "ADD VERTEX Company(...); ...",
+ "use_existing_schema": true}
When ``schema_gsql`` is provided, the pasted text is parsed
permissively, structural-type collisions and dangling pairs are
dropped, the diff against the current graph is computed, and the
additive delta is applied. Existing types are never dropped.
+
+ When ``use_existing_schema`` is true, the graph's current
+ user-defined vertex/edge types are adopted as the domain schema
+ (retrievers are installed against them). Mutually exclusive with
+ ``schema_gsql`` — sending both is a 400.
+
+ Pre-flight eligibility check rejects:
+ * ``structural_present`` — graph already has GraphRAG structural
+ types (Entity / Document / etc.). User must manually drop them
+ before re-initializing.
+ * ``user_types_present_strict`` — graph has user-defined types
+ AND the caller asked for a new schema (``schema_gsql``). Mixing
+ a fresh schema on top of pre-existing types risks corruption;
+ force a manual cleanup.
+ * ``user_types_present`` — graph has user-defined types and the
+ caller asked for ``none`` (no domain schema). The UI re-submits
+ with ``use_existing_schema=true`` if the user confirms.
"""
cur = _get_init_state(graphname)
if cur.get("state") in {"queued", "running"}:
@@ -654,11 +950,68 @@ def init_graph(
schema_gsql = (
(payload or {}).get("schema_gsql") if isinstance(payload, dict) else None
)
+ use_existing_schema = bool(
+ (payload or {}).get("use_existing_schema") if isinstance(payload, dict) else False
+ )
+ existing_vertex_descs = (payload or {}).get("vertex_descriptions") or {}
+ existing_edge_descs = (payload or {}).get("edge_descriptions") or {}
+ if not isinstance(existing_vertex_descs, dict):
+ existing_vertex_descs = {}
+ if not isinstance(existing_edge_descs, dict):
+ existing_edge_descs = {}
+ if schema_gsql and use_existing_schema:
+ raise HTTPException(
+ status_code=400,
+ detail="schema_gsql and use_existing_schema are mutually exclusive.",
+ )
cred_obj = creds[1]
auth_b64 = base64.b64encode(
f"{cred_obj.username}:{cred_obj.password}".encode()
).decode()
+ # Pre-flight eligibility check: introspect the live schema and
+ # decide whether to proceed, reject, or adopt existing types.
+ eligibility = _check_init_eligibility(auth_b64, graphname)
+ if eligibility["state"] == "structural_present":
+ raise HTTPException(
+ status_code=409,
+ detail={
+ "reason": "structural_present",
+ "message": "Existing GraphRAG schema detected, manual cleanup required.",
+ "structural_types": eligibility["structural_types"],
+ },
+ )
+ if eligibility["state"] == "user_types_present":
+ user_types = eligibility["user_vertex_types"] + eligibility["user_edge_types"]
+ if schema_gsql:
+ raise HTTPException(
+ status_code=409,
+ detail={
+ "reason": "user_types_present_strict",
+ "message": (
+ f"Graph already has types: {', '.join(user_types)}. "
+ "Manual cleanup required before extracting or applying a new schema."
+ ),
+ "user_vertex_types": eligibility["user_vertex_types"],
+ "user_edge_types": eligibility["user_edge_types"],
+ },
+ )
+ if not use_existing_schema:
+ raise HTTPException(
+ status_code=409,
+ detail={
+ "reason": "user_types_present",
+ "message": (
+ f"Graph '{graphname}' already has types: "
+ f"{', '.join(user_types)}. "
+ "Use them as the domain schema, or cancel and clean manually."
+ ),
+ "user_vertex_types": eligibility["user_vertex_types"],
+ "user_edge_types": eligibility["user_edge_types"],
+ },
+ )
+ # else: state == "empty" → proceed normally
+
_set_init_state(
graphname,
state="queued",
@@ -681,13 +1034,32 @@ def _run_init():
schema_res, index_res, query_res = resp[0], resp[1], resp[2]
domain_schema_status: dict | None = None
+ proposal = None
if isinstance(schema_gsql, str) and schema_gsql.strip():
+ proposal = schema_utils_mod.parse_gsql_schema(schema_gsql)
+ proposal.drop_dangling_pairs()
+ elif use_existing_schema:
+ # Build a proposal from the live user-defined types so
+ # apply_proposal registers them as the domain schema and
+ # installs retrievers. The diff is a no-op because the
+ # types already exist; this run only writes type
+ # metadata (descriptions) and re-creates retriever queries.
+ proposal = _build_proposal_from_live_schema(
+ conn,
+ vertex_descriptions=existing_vertex_descs,
+ edge_descriptions=existing_edge_descs,
+ )
+ LogWriter.info(
+ f"Adopting existing schema as domain for {graphname}: "
+ f"{len(proposal.vertices)} vertex types, "
+ f"{len(proposal.edges)} edge types"
+ )
+
+ if proposal is not None:
_set_init_state(graphname, message="Applying domain schema")
LogWriter.info(
f"Applying domain schema proposal for graph: {graphname}"
)
- proposal = schema_utils_mod.parse_gsql_schema(schema_gsql)
- proposal.drop_dangling_pairs()
# Surface apply_proposal's sub-phases (schema-change,
# metadata, retriever installs) in the init-dialog
# poll instead of a static "Applying domain schema".
@@ -792,13 +1164,15 @@ async def convert_sample_files(
to ``POST /ui//extract_schema_from_jsonl``.
No LLM call. Caps come from ``graphrag_config``:
- * ``schema_max_sample_files`` (default 5)
- * ``schema_max_total_mb`` (default 50)
+ * ``schema_max_sample_files`` (default 5) — file count
+ * ``schema_max_total_mb`` (default 50) — cumulative upload size
+
+ Per-file size is bounded only by the cumulative cap, so a single
+ file may use the full budget.
"""
max_files = int(graphrag_config.get("schema_max_sample_files", 5))
max_total_mb = int(graphrag_config.get("schema_max_total_mb", 50))
max_total_bytes = max_total_mb * 1024 * 1024
- per_file_max_bytes = 10 * 1024 * 1024 # 10 MB per file (Phase 1 cap)
if len(files) > max_files:
raise HTTPException(
@@ -817,13 +1191,6 @@ async def convert_sample_files(
total_bytes = 0
for f in files:
data = await f.read()
- if len(data) > per_file_max_bytes:
- raise HTTPException(
- status_code=400,
- detail=(
- f"File {f.filename} exceeds the 10 MB per-file cap."
- ),
- )
total_bytes += len(data)
if total_bytes > max_total_bytes:
raise HTTPException(
@@ -3099,8 +3466,8 @@ async def save_graphrag_config(
#: Per-prompt-type list of regex patterns that mark the start of the
#: placeholder-variables block. The first matching pattern wins.
#: Patterns are tried in order so the canonical Markdown headers
-#: (``## Inputs`` / ``## Data``) match first; legacy patterns are
-#: kept as fallbacks for any older saved files.
+#: (``## Inputs`` / ``## Data``) match first; additional patterns
+#: are kept as fallbacks for files saved under earlier formats.
_TEMPLATE_VAR_MARKERS = {
"chatbot_response": [
r'(?ms)^##\s*Inputs\b.*$',
@@ -3314,12 +3681,12 @@ async def save_prompts(
if (llm_cfg.get("prompt_path") or "").rstrip("/") != new_path.rstrip("/"):
llm_cfg["prompt_path"] = new_path
changed = True
- # Strip per-service copies — they're redundant once the
- # top-level field is set. Keeps the config clean and
- # avoids stale per-service entries shadowing future
- # global changes. ``embedding_service`` is included
- # only to scrub stray legacy entries; embedding models
- # never read prompt_path.
+ # Strip per-service copies — they're redundant once
+ # the top-level field is set. Keeps the config clean
+ # and avoids stale per-service entries shadowing the
+ # global value. ``embedding_service`` is included
+ # only to scrub stray entries; embedding models never
+ # read prompt_path.
for svc_key in (
"completion_service",
"chat_service",
diff --git a/graphrag/app/tools/generate_cypher.py b/graphrag/app/tools/generate_cypher.py
index c5d9294..c1a1afc 100644
--- a/graphrag/app/tools/generate_cypher.py
+++ b/graphrag/app/tools/generate_cypher.py
@@ -58,7 +58,8 @@ def _generate_schema_rep(self):
# Schema rendering is shared with generate_gsql + the
# question-mapping tools via ``schema_utils.render_schema_rep``;
# we only keep the per-instance cache here.
- text, schema_ver = render_schema_rep(self.conn)
+ snap = render_schema_rep(self.conn)
+ text, schema_ver = snap.schema_rep, snap.schema_version
if schema_ver is not None and self.schema_ver == schema_ver and self.schema_rep:
logger.info(f"Reusing existing schema rep for schema version {schema_ver}")
return self.schema_rep
diff --git a/graphrag/app/tools/generate_gsql.py b/graphrag/app/tools/generate_gsql.py
index 60c13f9..05a8017 100644
--- a/graphrag/app/tools/generate_gsql.py
+++ b/graphrag/app/tools/generate_gsql.py
@@ -58,7 +58,8 @@ def _generate_schema_rep(self):
# Schema rendering is shared with generate_cypher + the
# question-mapping tools via ``schema_utils.render_schema_rep``;
# we only keep the per-instance cache here.
- text, schema_ver = render_schema_rep(self.conn)
+ snap = render_schema_rep(self.conn)
+ text, schema_ver = snap.schema_rep, snap.schema_version
if self.schema_rep and self.schema_ver == schema_ver:
logger.info(f"Reusing existing schema rep for schema version {schema_ver}")
return self.schema_rep
From c8736b246e2c67f0d8670d3f8768d71f91c261ba Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 14 May 2026 13:27:20 -0700
Subject: [PATCH 64/70] Keep upload session alive across long file conversion
- Replace pause/resume guard with periodic ping while upload, file
conversion, or ingest is in flight, so the idle timer can't expire
mid-operation when the page is a nested modal
---
graphrag-ui/src/pages/setup/IngestGraph.tsx | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx
index 157bf77..69197be 100644
--- a/graphrag-ui/src/pages/setup/IngestGraph.tsx
+++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx
@@ -19,7 +19,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useConfirm } from "@/hooks/useConfirm";
-import { pauseIdleTimer, resumeIdleTimer } from "@/hooks/useIdleTimeout";
+import { pingIdleTimer } from "@/hooks/useIdleTimeout";
interface IngestGraphProps {
isModal?: boolean;
@@ -893,14 +893,16 @@ const IngestGraph: React.FC = ({ isModal = false }) => {
}
};
- // Pause idle timer while ingestion is running
+ // Keep the idle timer alive while any long-running upload / conversion
+ // / ingest is in flight. Ping every 60s — actively resets the idle
+ // countdown instead of relying on a pause/resume event sequence that
+ // can drift in nested-modal contexts.
useEffect(() => {
- if (isIngesting) {
- pauseIdleTimer();
- } else {
- resumeIdleTimer();
- }
- }, [isIngesting]);
+ if (!(isUploading || isProcessingFiles || isIngesting)) return;
+ pingIdleTimer();
+ const id = setInterval(() => pingIdleTimer(), 60_000);
+ return () => clearInterval(id);
+ }, [isUploading, isProcessingFiles, isIngesting]);
// Load available graphs. Seed from sessionStorage for instant render,
// then refresh from /ui/list_graphs so newly-initialized graphs show
From be2967363d278bb9130e4e6347d8fb6b78f3c41d Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 14 May 2026 16:09:56 -0700
Subject: [PATCH 65/70] Add alert dialog hook missed in prior commit
- Include useAlert hook and AlertDialog component referenced by KGAdmin and CustomChatMessage; their absence broke fresh-checkout builds
---
.../src/components/ui/alert-dialog.tsx | 48 +++++++++++++++++++
graphrag-ui/src/hooks/useAlert.tsx | 34 +++++++++++++
2 files changed, 82 insertions(+)
create mode 100644 graphrag-ui/src/components/ui/alert-dialog.tsx
create mode 100644 graphrag-ui/src/hooks/useAlert.tsx
diff --git a/graphrag-ui/src/components/ui/alert-dialog.tsx b/graphrag-ui/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..72bc0bc
--- /dev/null
+++ b/graphrag-ui/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,48 @@
+import { createPortal } from "react-dom";
+
+interface AlertDialogProps {
+ message: string;
+ onClose: () => void;
+}
+
+export function AlertDialog({ message, onClose }: AlertDialogProps) {
+ const handleClose = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ onClose();
+ };
+
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ return createPortal(
+
+
e.stopPropagation()}
+ style={{ pointerEvents: "auto" }}
+ >
+
+ {message}
+
+
+
+ OK
+
+
+
+
,
+ document.body
+ );
+}
diff --git a/graphrag-ui/src/hooks/useAlert.tsx b/graphrag-ui/src/hooks/useAlert.tsx
new file mode 100644
index 0000000..cfc8db4
--- /dev/null
+++ b/graphrag-ui/src/hooks/useAlert.tsx
@@ -0,0 +1,34 @@
+import { useState, ReactElement } from "react";
+import { AlertDialog } from "@/components/ui/alert-dialog";
+
+interface AlertOptions {
+ message: string;
+ onClose: () => void;
+}
+
+export function useAlert(): [
+ (message: string) => Promise,
+ ReactElement | null,
+ boolean
+] {
+ const [options, setOptions] = useState(null);
+
+ const alert = (message: string): Promise =>
+ new Promise((resolve) => {
+ setOptions({
+ message,
+ onClose: () => {
+ resolve();
+ setOptions(null);
+ },
+ });
+ });
+
+ const alertDialog: ReactElement | null = options ? (
+
+ ) : null;
+
+ const isOpen = options !== null;
+
+ return [alert, alertDialog, isOpen];
+}
From a78a7103787aade9cac52c37e34d8113c53910d5 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 14 May 2026 16:28:20 -0700
Subject: [PATCH 66/70] Always fetch trace data on tab open
- Drop the cache that copied a chat message into sessionStorage so the new trace tab could skip a fetch; large traces overflowed the 5MB quota and silently aborted the popup
---
graphrag-ui/src/components/CustomChatMessage.tsx | 3 ---
graphrag-ui/src/pages/TraceLogs.tsx | 9 ++-------
2 files changed, 2 insertions(+), 10 deletions(-)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index 4110d3c..c7ad93b 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -272,9 +272,6 @@ export const CustomChatMessage: FC = ({
await alert("Failed to reach the trace log endpoint. Please try again.");
return;
}
- // Pass the message via sessionStorage so the new tab can
- // render without a second authenticated fetch.
- sessionStorage.setItem(`trace_msg_${messageId}`, JSON.stringify(message));
window.open(`/trace/${messageId}`, "_blank");
}}
/>
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 26aee4d..22ff52e 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -710,12 +710,7 @@ const TraceLogs: FC = () => {
const stateMessage = location.state?.message;
const stateUserQuery = location.state?.userQuery;
- // Check sessionStorage for message stored by the opener tab before API fetch.
- const sessionKey = messageId ? `trace_msg_${messageId}` : null;
- const sessionRaw = sessionKey ? sessionStorage.getItem(sessionKey) : null;
- const sessionMessage = sessionRaw ? JSON.parse(sessionRaw) : null;
-
- const resolvedMessage = stateMessage || sessionMessage;
+ const resolvedMessage = stateMessage;
const [apiData, setApiData] = useState(null);
const [loading, setLoading] = useState(!resolvedMessage);
@@ -751,7 +746,7 @@ const TraceLogs: FC = () => {
response_type: apiData.response_type,
query_sources: apiData.query_sources,
} : null);
- const userQuery = stateUserQuery || sessionMessage?.userQuery || apiData?.user_query;
+ const userQuery = stateUserQuery || apiData?.user_query;
const trace = useMemo(
() => (message ? buildTraceFromMessage(message, userQuery) : null),
From 882293630fa6a1c6f915bf3e1340c769be5f577f Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Thu, 14 May 2026 23:27:00 -0700
Subject: [PATCH 67/70] Inline trace dialog and lock chat UI during streaming
- Show trace log in an inline dialog sized to its content instead of a new tab
- Always fetch trace data on open so messages from history render fully
- Disable Setup, side menu, and chat input while an answer is streaming
- Warn before logout while an answer streams; fix the misleading history claim
- Rename Cancel to Close on the graph initialize dialog footer
---
graphrag-ui/src/actions/ActionProvider.tsx | 16 +-
.../src/components/CustomChatMessage.tsx | 12 +-
graphrag-ui/src/components/ModeToggle.tsx | 39 ++++-
graphrag-ui/src/components/SideMenu.tsx | 22 ++-
graphrag-ui/src/index.css | 8 +
graphrag-ui/src/pages/TraceLogs.tsx | 162 ++++++++++++------
graphrag-ui/src/pages/setup/KGAdmin.tsx | 2 +-
7 files changed, 198 insertions(+), 63 deletions(-)
diff --git a/graphrag-ui/src/actions/ActionProvider.tsx b/graphrag-ui/src/actions/ActionProvider.tsx
index eb69c60..c73c182 100644
--- a/graphrag-ui/src/actions/ActionProvider.tsx
+++ b/graphrag-ui/src/actions/ActionProvider.tsx
@@ -231,6 +231,13 @@ const ActionProvider: React.FC = ({
messages: [...prev.messages, loading],
}));
+ // Signal that the chat is now waiting on an answer. Layout chrome
+ // (Setup / Logout / conversation list / new-chat button) listens for
+ // this and disables itself so the user can't unmount the in-flight
+ // streaming connection by navigating away.
+ document.body.classList.add("chat-streaming");
+ window.dispatchEvent(new Event("chat:streaming-start"));
+
// Dispatch event to refresh conversation list when user sends a question
// This ensures the side menu updates when a new message is sent
window.dispatchEvent(new CustomEvent('conversationUpdated'));
@@ -284,8 +291,15 @@ const ActionProvider: React.FC = ({
const botMessage = createChatBotMessage(messageData);
setState((prev) => {
const newPrevMsg = prev.messages.slice(0, -1);
- return {...prev, messages: [...newPrevMsg, botMessage]};
+ return {...prev, messages: [...newPrevMsg, botMessage]};
});
+
+ // Final (non-progress) message ends the streaming gate; layout
+ // chrome re-enables. Progress messages keep the gate held.
+ if (messageData.response_type !== "progress") {
+ document.body.classList.remove("chat-streaming");
+ window.dispatchEvent(new Event("chat:streaming-end"));
+ }
} catch (error) {
console.error("Error parsing WebSocket message:", error);
// Handle string messages (progress updates)
diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx
index c7ad93b..43937ef 100755
--- a/graphrag-ui/src/components/CustomChatMessage.tsx
+++ b/graphrag-ui/src/components/CustomChatMessage.tsx
@@ -14,6 +14,7 @@ import { Interactions } from "./Interact";
import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro";
import { KnowledgeTablPro } from "./tables/KnowledgeTablePro";
import { useAlert } from "@/hooks/useAlert";
+import TraceLogs from "@/pages/TraceLogs";
interface IChatbotMessageProps {
message?: any;
withAvatar?: boolean;
@@ -173,6 +174,7 @@ export const CustomChatMessage: FC = ({
const [showResult, setShowResult] = useState(false);
const [showGraphVis, setShowGraphVis] = useState(false);
const [showTableVis, setShowTableVis] = useState(false);
+ const [traceMessageId, setTraceMessageId] = useState(null);
const [alert, alertDialog] = useAlert();
// Error handling functions
@@ -220,6 +222,12 @@ export const CustomChatMessage: FC = ({
return (
<>
{alertDialog}
+ {traceMessageId && (
+ setTraceMessageId(null)}
+ />
+ )}
{typeof message === "string" ? (
{message}
@@ -258,7 +266,7 @@ export const CustomChatMessage: FC = ({
}
// Trace JSON lives under /code/trace_logs inside the
// graphrag container and is wiped on container recreate.
- // Probe first so we never open a blank tab when the file is gone.
+ // Probe first so we never open an empty dialog when the file is gone.
try {
const probe = await fetch(`/ui/trace/${messageId}`, {
method: "GET",
@@ -272,7 +280,7 @@ export const CustomChatMessage: FC = ({
await alert("Failed to reach the trace log endpoint. Please try again.");
return;
}
- window.open(`/trace/${messageId}`, "_blank");
+ setTraceMessageId(messageId);
}}
/>
diff --git a/graphrag-ui/src/components/ModeToggle.tsx b/graphrag-ui/src/components/ModeToggle.tsx
index e2b22b3..73f072b 100644
--- a/graphrag-ui/src/components/ModeToggle.tsx
+++ b/graphrag-ui/src/components/ModeToggle.tsx
@@ -1,4 +1,5 @@
import { Moon, Sun, LogOut, Settings } from "lucide-react";
+import { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
@@ -19,10 +20,31 @@ export function ModeToggle() {
const isLoginRoute = location.pathname === "/";
const [confirm, confirmDialog] = useConfirm();
const { rolesLoaded, canAccessSetup } = useRoles(location.pathname);
+ // Disable Settings / Logout while a chat answer is streaming so the
+ // user can't accidentally unmount Chat and lose the in-flight reply.
+ const [chatStreaming, setChatStreaming] = useState(false);
+ useEffect(() => {
+ const onStart = () => setChatStreaming(true);
+ const onEnd = () => setChatStreaming(false);
+ window.addEventListener("chat:streaming-start", onStart);
+ window.addEventListener("chat:streaming-end", onEnd);
+ return () => {
+ window.removeEventListener("chat:streaming-start", onStart);
+ window.removeEventListener("chat:streaming-end", onEnd);
+ };
+ }, []);
+ const streamingTitle = chatStreaming
+ ? "Disabled while the chat is generating an answer"
+ : undefined;
const handleLogout = async () => {
- // Show confirmation dialog
- const shouldLogout = await confirm("Are you sure you want to logout? This will clear all your chat history.");
+ // Show confirmation dialog; flag pending-chat loss when applicable
+ // so the user isn't surprised by a generic error-answer afterwards.
+ // Chat history itself is server-side and survives logout.
+ const message = chatStreaming
+ ? "An answer is still being generated. Logging out will drop the connection, and the in-flight answer will be lost (saved as an error in your history). Continue?"
+ : "Log out of GraphRAG? You'll need to sign in again to continue.";
+ const shouldLogout = await confirm(message);
if (!shouldLogout) {
return;
}
@@ -49,19 +71,20 @@ export function ModeToggle() {
return (
{!isLoginRoute && rolesLoaded && canAccessSetup && (
-
)}
-
+
{!isLoginRoute && (
-
([]);
const [expandedConversations, setExpandedConversations] = useState>(new Set());
const [activeConversationId, setActiveConversationId] = useState(null);
+ // Fade + disable the side menu (conversation list + New Chat) while
+ // the chat is streaming an answer, so the user can't unmount Chat by
+ // switching conversations mid-response.
+ const [chatStreaming, setChatStreaming] = useState(false);
+ useEffect(() => {
+ const onStart = () => setChatStreaming(true);
+ const onEnd = () => setChatStreaming(false);
+ window.addEventListener("chat:streaming-start", onStart);
+ window.addEventListener("chat:streaming-end", onEnd);
+ return () => {
+ window.removeEventListener("chat:streaming-start", onStart);
+ window.removeEventListener("chat:streaming-end", onEnd);
+ };
+ }, []);
const navigate = useNavigate();
@@ -417,8 +431,10 @@ const SideMenu = ({
return (
diff --git a/graphrag-ui/src/index.css b/graphrag-ui/src/index.css
index 6b5cc77..1be79de 100755
--- a/graphrag-ui/src/index.css
+++ b/graphrag-ui/src/index.css
@@ -88,6 +88,14 @@
.react-chatbot-kit-chat-input-container {
@apply !bg-background !border-[#3D3D3D];
}
+ /* Block submitting another question while the previous answer is
+ still streaming. ActionProvider toggles ``chat-streaming`` on
+ ``document.body`` at stream start / end. */
+ body.chat-streaming .react-chatbot-kit-chat-input-container,
+ body.chat-streaming .react-chatbot-kit-chat-input-form {
+ pointer-events: none;
+ opacity: 0.5;
+ }
.open-dg {
@apply bg-background;
}
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 22ff52e..54be602 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -1,6 +1,8 @@
import { FC, useState, useMemo, useEffect } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
import {
LuArrowLeft,
LuChevronDown,
@@ -702,21 +704,40 @@ const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => {
// ─── Main Page ────────────────────────────────────────────────────────────────
-const TraceLogs: FC = () => {
+interface TraceLogsProps {
+ // When provided, the component renders inside a Dialog and uses these
+ // props instead of route params / location state. Closing the dialog
+ // calls onClose. When omitted, the component renders as a full page
+ // route (the original ``/trace/:messageId`` behaviour, kept for
+ // direct-link backward compat).
+ messageIdProp?: string;
+ onClose?: () => void;
+}
+
+const TraceLogs: FC
= ({ messageIdProp, onClose }) => {
const location = useLocation();
const navigate = useNavigate();
- const { messageId } = useParams<{ messageId: string }>();
-
- const stateMessage = location.state?.message;
- const stateUserQuery = location.state?.userQuery;
+ const params = useParams<{ messageId: string }>();
+ const messageId = messageIdProp || params.messageId;
+ const isDialog = !!onClose;
- const resolvedMessage = stateMessage;
+ const stateMessage = isDialog ? null : location.state?.message;
+ const stateUserQuery = isDialog ? null : location.state?.userQuery;
const [apiData, setApiData] = useState(null);
- const [loading, setLoading] = useState(!resolvedMessage);
-
+ const [loading, setLoading] = useState(true);
+
+ // Always fetch the trace JSON — the backend writes it on every response,
+ // so the API is the canonical source. Falls back to the navigation-state
+ // message only if the API call fails (e.g. trace file got wiped by
+ // container recreation). Page-mode messages restored from chat history
+ // don't carry the full ``query_sources.agent_steps`` payload that the
+ // trace view needs, so we can't trust ``stateMessage`` alone.
useEffect(() => {
- if (resolvedMessage || !messageId) return;
+ if (!messageId) {
+ setLoading(false);
+ return;
+ }
const creds = sessionStorage.getItem("creds");
// Skip the API call when there are no creds — sending ``Basic null``
// makes FastAPI's HTTPBasic challenge with ``WWW-Authenticate: Basic``
@@ -738,15 +759,15 @@ const TraceLogs: FC = () => {
.then((data) => setApiData(data))
.catch(() => setApiData(null))
.finally(() => setLoading(false));
- }, [messageId, resolvedMessage]);
+ }, [messageId]);
- const message = resolvedMessage || (apiData ? {
+ const message = apiData ? {
content: apiData.natural_language_response,
response_time: apiData.response_time,
response_type: apiData.response_type,
query_sources: apiData.query_sources,
- } : null);
- const userQuery = stateUserQuery || apiData?.user_query;
+ } : stateMessage;
+ const userQuery = apiData?.user_query || stateUserQuery;
const trace = useMemo(
() => (message ? buildTraceFromMessage(message, userQuery) : null),
@@ -754,12 +775,14 @@ const TraceLogs: FC = () => {
);
const handleBack = () => {
- // Trace opens in a new tab — closing it returns the user to the chat tab.
- // If the tab cannot be closed (e.g. opened via direct link), fall back to navigate.
- if (window.opener || window.history.length <= 1) {
- window.close();
- } else {
+ if (onClose) {
+ onClose();
+ return;
+ }
+ if (window.history.length > 1) {
navigate(-1);
+ } else {
+ navigate("/chat");
}
};
@@ -776,50 +799,74 @@ const TraceLogs: FC = () => {
URL.revokeObjectURL(url);
};
+ const wrap = (inner: JSX.Element) => {
+ if (isDialog) {
+ return (
+ !o && onClose && onClose()}>
+ e.preventDefault()}
+ onOpenAutoFocus={(e) => e.preventDefault()}
+ >
+ {inner}
+
+
+ );
+ }
+ return {inner}
;
+ };
+
if (loading) {
- return (
-
+ return wrap(
+
);
}
if (!trace) {
- return (
-
+ return wrap(
+
);
}
- return (
-
- {/* Header */}
-
-
-
-
-
- Close & Back to Chat
-
-
Trace Logs
-
-
-
-
- Download
-
+ return wrap(
+ <>
+ {/* Header — only the title in dialog mode (Download moves to the
+ footer alongside Close), so nothing constrains dialog width.
+ Page mode keeps the original sticky flex bar with Back +
+ Download. */}
+ {isDialog ? (
+
Trace Logs
+ ) : (
+
+
+
+
+
+ Close & Back to Chat
+
+
Trace Logs
+
+
+
+
+ Download
+
+
-
+ )}
-
+
{/* Original Query */}
Original Query
@@ -952,8 +999,27 @@ const TraceLogs: FC = () => {
)}
+
+ {/* Footer — Download (primary action style) + Close (outline,
+ matches other dialogs). Replaces the top-right Download in
+ dialog mode so the dialog can size to content. */}
+
+ {isDialog && (
+
+
+ Download
+
+ )}
+
+ Close
+
+
-
+ >
);
};
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index 3557ef6..bd0987a 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -2193,7 +2193,7 @@ const KGAdmin = () => {
disabled={isInitializing}
className="dark:border-[#3D3D3D]"
>
- Cancel
+ Close
Date: Fri, 15 May 2026 00:05:07 -0700
Subject: [PATCH 68/70] Drop final response from trace dialog and add
accessibility labels
- Remove the Final Response section from the trace dialog so heavy markdown with many image references no longer blocks the dialog render
- Add accessible title and description to the trace dialog to satisfy screen-reader requirements
---
graphrag-ui/src/pages/TraceLogs.tsx | 26 +++++++++-----------------
1 file changed, 9 insertions(+), 17 deletions(-)
diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx
index 54be602..d5f0a0e 100644
--- a/graphrag-ui/src/pages/TraceLogs.tsx
+++ b/graphrag-ui/src/pages/TraceLogs.tsx
@@ -1,7 +1,7 @@
import { FC, useState, useMemo, useEffect } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
-import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
LuArrowLeft,
@@ -15,8 +15,6 @@ import {
LuCoins,
LuInfo,
} from "react-icons/lu";
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -837,9 +835,15 @@ const TraceLogs: FC = ({ messageIdProp, onClose }) => {
{/* Header — only the title in dialog mode (Download moves to the
footer alongside Close), so nothing constrains dialog width.
Page mode keeps the original sticky flex bar with Back +
- Download. */}
+ Download. DialogTitle / DialogDescription wire up the
+ accessibility labels Radix expects on a Dialog. */}
{isDialog ? (
- Trace Logs
+ <>
+ Trace Logs
+
+ Execution trace, citations, and token usage for the selected chat response.
+
+ >
) : (
@@ -988,18 +992,6 @@ const TraceLogs: FC
= ({ messageIdProp, onClose }) => {
- {/* Final Response */}
- {trace.finalResponse && (
-
-
Final Response
-
-
- {trace.finalResponse}
-
-
-
- )}
-
{/* Footer — Download (primary action style) + Close (outline,
matches other dialogs). Replaces the top-right Download in
dialog mode so the dialog can size to content. */}
From 5f7987ca83fb4634ab8f01cdeddb91cbe440f723 Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Fri, 15 May 2026 22:56:00 -0700
Subject: [PATCH 69/70] Address v1.4.0 code-review feedback
- Atomically reserve graph init state under one lock so concurrent initialize_graph requests can't enqueue duplicate background jobs
- Isolate each sample-doc upload into its own subdirectory so a new schema-extraction request can't re-process stale files from prior sessions
- Carry source / target node types through relationship extraction so endpoint-pair validation accepts valid domain edges
- Validate remote /version payloads before forwarding them so a malformed response can't change the /ui/version response shape
- Remove a stale unreachable block left behind below the build_allowed_schema alias
---
common/db/schema_utils.py | 15 ---
.../LLMEntityRelationshipExtractor.py | 32 ++++-
graphrag-ui/src/pages/setup/KGAdmin.tsx | 1 +
graphrag/app/routers/ui.py | 124 ++++++++++++------
.../tests/test_e2e_schema_aware_ingest.py | 6 +-
5 files changed, 118 insertions(+), 60 deletions(-)
diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py
index cdb3360..dc9c5af 100644
--- a/common/db/schema_utils.py
+++ b/common/db/schema_utils.py
@@ -1369,21 +1369,6 @@ def build_allowed_schema(conn) -> AllowedSchema:
return render_schema_rep(conn, exclude_structural=True)
- domain_entity_defs = {v: entity_descs[v] for v in domain_verts if entity_descs.get(v)}
- domain_rel_defs = {e: rel_defs[e] for e in domain_edge_types if rel_defs.get(e)}
-
- return AllowedSchema(
- schema_rep=text,
- vertex_types=domain_verts,
- edge_types=domain_edge_types,
- vertex_attributes=vertex_attributes,
- edge_attributes=edge_attributes,
- vertex_definitions=domain_entity_defs,
- edge_definitions=domain_rel_defs,
- edge_endpoints=edge_endpoints,
- )
-
-
async def render_schema_rep_async(
conn, exclude_structural: bool = False,
) -> AllowedSchema:
diff --git a/common/extractors/LLMEntityRelationshipExtractor.py b/common/extractors/LLMEntityRelationshipExtractor.py
index 3b77410..43fdb67 100644
--- a/common/extractors/LLMEntityRelationshipExtractor.py
+++ b/common/extractors/LLMEntityRelationshipExtractor.py
@@ -304,19 +304,28 @@ def _extract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]:
def _resolve_id_and_props(value):
"""Source / target in the LLM's ``rels`` list may come as a
bare id string or as a dict with ``id`` + optional
- ``properties``. Return ``(id_str, props_dict)``.
+ ``node_type`` + optional ``properties``. Return
+ ``(id_str, node_type_str, props_dict)``. ``node_type`` is the
+ empty string when the LLM didn't carry one on this endpoint;
+ callers should fall back to the entity's own node entry to
+ recover the type in that case.
"""
if isinstance(value, dict):
props = value.get("properties") or value.get("attributes") or {}
- return str(value.get("id", "")), props if isinstance(props, dict) else {}
- return str(value), {}
+ node_type = value.get("node_type") or value.get("type") or ""
+ return (
+ str(value.get("id", "")),
+ str(node_type),
+ props if isinstance(props, dict) else {},
+ )
+ return str(value), "", {}
def _format_rels(self, rels_in: list) -> list:
formatted = []
for rels in rels_in or []:
try:
- src_id, src_props = self._resolve_id_and_props(rels["source"])
- tgt_id, tgt_props = self._resolve_id_and_props(rels["target"])
+ src_id, src_type, src_props = self._resolve_id_and_props(rels["source"])
+ tgt_id, tgt_type, tgt_props = self._resolve_id_and_props(rels["target"])
if not (src_id and tgt_id):
continue
# Edge-level properties (typed attrs the LLM extracted
@@ -328,6 +337,8 @@ def _format_rels(self, rels_in: list) -> list:
formatted.append({
"source": src_id,
"target": tgt_id,
+ "source_type": src_type.replace(" ", "_").capitalize() if src_type else "",
+ "target_type": tgt_type.replace(" ", "_").capitalize() if tgt_type else "",
"source_props": src_props,
"target_props": tgt_props,
"type": rels["relation_type"].replace(" ", "_").upper(),
@@ -379,10 +390,17 @@ def _build_rels(self, formatted_rels: list) -> list:
"description": rel["definition"]}
edge_props = {**(rel.get("properties") or {}),
"description": rel["definition"]}
+ # Use the canonical entity types when the LLM provided them
+ # on the relationship endpoints; fall back to the id so the
+ # field is never empty. Downstream endpoint-pair validation
+ # in the worker relies on these values matching the live
+ # schema's declared edge endpoints.
+ src_type = rel.get("source_type") or rel["source"]
+ tgt_type = rel.get("target_type") or rel["target"]
relationships.append(Relationship(
- source=Node(id=rel["source"], type=rel["source"],
+ source=Node(id=rel["source"], type=src_type,
properties=src_props),
- target=Node(id=rel["target"], type=rel["target"],
+ target=Node(id=rel["target"], type=tgt_type,
properties=tgt_props),
type=rel["type"],
properties=edge_props,
diff --git a/graphrag-ui/src/pages/setup/KGAdmin.tsx b/graphrag-ui/src/pages/setup/KGAdmin.tsx
index bd0987a..19b8ae8 100644
--- a/graphrag-ui/src/pages/setup/KGAdmin.tsx
+++ b/graphrag-ui/src/pages/setup/KGAdmin.tsx
@@ -508,6 +508,7 @@ const KGAdmin = () => {
"Content-Type": "application/json",
},
body: JSON.stringify({
+ request_id: convertData.request_id || "",
filenames: convertData.saved_files || [],
vertex_hints: vertexHints,
edge_hints: edgeHints,
diff --git a/graphrag/app/routers/ui.py b/graphrag/app/routers/ui.py
index edc2f40..ff27e57 100644
--- a/graphrag/app/routers/ui.py
+++ b/graphrag/app/routers/ui.py
@@ -417,6 +417,23 @@ def _unknown_version(component: str) -> dict:
return {"component": component, "version": "unknown", "build_date": "unknown"}
+def _coerce_version_payload(payload, component: str) -> dict:
+ """Return a payload shaped like ``_unknown_version`` regardless of
+ what a remote ``/version`` endpoint actually sent. A malformed or
+ compromised response (non-dict, missing keys, non-string values)
+ falls back to the unknown shape so clients always see the same
+ schema.
+ """
+ if not isinstance(payload, dict):
+ return _unknown_version(component)
+ result = _unknown_version(component)
+ for key in ("component", "version", "build_date"):
+ value = payload.get(key)
+ if isinstance(value, str) and value:
+ result[key] = value
+ return result
+
+
@router.get(f"{route_prefix}/version")
def get_version():
"""Return image-build version info for all running components.
@@ -432,10 +449,10 @@ def get_version():
ecc_base = graphrag_config.get("ecc", "http://graphrag-ecc:8001")
try:
ecc_resp = httpx.get(f"{ecc_base}/version", timeout=5.0)
- ecc_version = (
- ecc_resp.json() if ecc_resp.status_code == 200
- else _unknown_version("graphrag-ecc")
- )
+ if ecc_resp.status_code == 200:
+ ecc_version = _coerce_version_payload(ecc_resp.json(), "graphrag-ecc")
+ else:
+ ecc_version = _unknown_version("graphrag-ecc")
except Exception:
ecc_version = _unknown_version("graphrag-ecc")
@@ -446,7 +463,7 @@ def get_version():
# (e.g. running graphrag in isolation).
ui_resp = httpx.get("http://graphrag-ui:3000/version.json", timeout=5.0)
if ui_resp.status_code == 200:
- ui_version = ui_resp.json()
+ ui_version = _coerce_version_payload(ui_resp.json(), "graphrag-ui")
except Exception:
pass
@@ -630,6 +647,29 @@ def _get_init_state(graphname: str) -> dict:
return dict(_init_state.get(graphname, {"state": "unknown"}))
+def _try_reserve_init(graphname: str) -> str | None:
+ """Atomically transition the graph into the ``queued`` state. Returns
+ ``None`` on success; returns the existing state string when another
+ request has already reserved or is running the init. Combines the
+ in-progress check and the queued-state set under the same lock so
+ concurrent ``POST /initialize_graph`` requests for the same graph
+ can't both pass the gate and enqueue duplicate background jobs.
+ """
+ with _init_state_lock:
+ cur = _init_state.get(graphname, {"state": "unknown"})
+ if cur.get("state") in {"queued", "running"}:
+ return cur.get("state")
+ _init_state[graphname] = {
+ "state": "queued",
+ "message": "Initialization queued",
+ "started_at": time.time(),
+ "completed_at": None,
+ "result": None,
+ "error": None,
+ }
+ return None
+
+
def _build_proposal_from_live_schema(
conn,
vertex_descriptions: dict | None = None,
@@ -940,13 +980,6 @@ def init_graph(
caller asked for ``none`` (no domain schema). The UI re-submits
with ``use_existing_schema=true`` if the user confirms.
"""
- cur = _get_init_state(graphname)
- if cur.get("state") in {"queued", "running"}:
- raise HTTPException(
- status_code=409,
- detail=f"Initialization already in progress for graph '{graphname}'",
- )
-
schema_gsql = (
(payload or {}).get("schema_gsql") if isinstance(payload, dict) else None
)
@@ -1012,15 +1045,14 @@ def init_graph(
)
# else: state == "empty" → proceed normally
- _set_init_state(
- graphname,
- state="queued",
- message="Initialization queued",
- started_at=time.time(),
- completed_at=None,
- result=None,
- error=None,
- )
+ # Atomically check-and-reserve so two concurrent /initialize_graph
+ # requests for the same graph can't both enqueue background jobs.
+ reserved_collision = _try_reserve_init(graphname)
+ if reserved_collision is not None:
+ raise HTTPException(
+ status_code=409,
+ detail=f"Initialization already in progress for graph '{graphname}'",
+ )
def _run_init():
try:
@@ -1155,13 +1187,15 @@ async def convert_sample_files(
"""
Step 1/2 of the sample-doc schema extraction flow:
- Save uploaded sample files to ``uploads//`` and convert
- each to JSONL under ``uploads/ingestion_temp//``. Files
- are persisted so the Ingest Document dialog can reuse them, and
- the JSONL cache means a subsequent Ingest run won't re-convert.
+ Save uploaded sample files into a fresh per-request subdirectory
+ under ``uploads//_schema_/`` and convert
+ each to JSONL under
+ ``uploads/ingestion_temp//_schema_/``.
+ Returns the list of saved filenames and the ``request_id`` so the
+ caller can pass both to ``POST /ui//extract_schema_from_jsonl``.
- Returns the list of saved filenames so the caller can pass them
- to ``POST /ui//extract_schema_from_jsonl``.
+ Each sample-upload request is isolated so stale files from prior
+ sessions can't be re-converted or pollute the resulting schema.
No LLM call. Caps come from ``graphrag_config``:
* ``schema_max_sample_files`` (default 5) — file count
@@ -1182,9 +1216,11 @@ async def convert_sample_files(
if not files:
raise HTTPException(status_code=400, detail="No files supplied.")
- upload_dir = os.path.join("uploads", graphname)
+ request_id = uuid.uuid4().hex[:12]
+ request_subdir = f"_schema_{request_id}"
+ upload_dir = os.path.join("uploads", graphname, request_subdir)
os.makedirs(upload_dir, exist_ok=True)
- temp_folder = os.path.join("uploads", "ingestion_temp", graphname)
+ temp_folder = os.path.join("uploads", "ingestion_temp", graphname, request_subdir)
os.makedirs(temp_folder, exist_ok=True)
saved_basenames: list[str] = []
@@ -1225,12 +1261,13 @@ async def convert_sample_files(
)
LogWriter.info(
- f"Converted sample files for {graphname}: {len(files)} uploaded, "
- f"{result.get('num_documents', 0)} docs in JSONL"
+ f"Converted sample files for {graphname} (request {request_id}): "
+ f"{len(files)} uploaded, {result.get('num_documents', 0)} docs in JSONL"
)
return {
"status": "success",
"graphname": graphname,
+ "request_id": request_id,
"saved_files": list(saved_basenames),
"num_documents": result.get("num_documents", 0),
}
@@ -1251,12 +1288,25 @@ def extract_schema_from_jsonl(
form-mode editor.
Body:
- ``{"filenames": ["report1.pdf", "report2.docx"]}``
- The endpoint reads ``uploads/ingestion_temp//.jsonl``
- for each name. If ``filenames`` is absent or empty, every JSONL in
- the temp folder is consumed.
- """
- temp_folder = os.path.join("uploads", "ingestion_temp", graphname)
+ ``{"request_id": "", "filenames": ["report1.pdf", "report2.docx"]}``
+ ``request_id`` (returned by ``convert_sample_files``) selects the
+ per-request subdirectory under
+ ``uploads/ingestion_temp//_schema_/`` so
+ only the JSONLs belonging to this sample-upload session feed the
+ LLM. If ``request_id`` is absent, the endpoint falls back to the
+ legacy per-graph temp folder for backward compatibility.
+ """
+ request_id = ""
+ if isinstance(payload, dict):
+ request_id = str(payload.get("request_id") or "")
+ if request_id and not re.fullmatch(r"[A-Za-z0-9_-]+", request_id):
+ raise HTTPException(status_code=400, detail="Invalid request_id")
+ if request_id:
+ temp_folder = os.path.join(
+ "uploads", "ingestion_temp", graphname, f"_schema_{request_id}"
+ )
+ else:
+ temp_folder = os.path.join("uploads", "ingestion_temp", graphname)
if not os.path.isdir(temp_folder):
raise HTTPException(
status_code=400,
diff --git a/graphrag/tests/test_e2e_schema_aware_ingest.py b/graphrag/tests/test_e2e_schema_aware_ingest.py
index 7d99a9a..e3688bd 100644
--- a/graphrag/tests/test_e2e_schema_aware_ingest.py
+++ b/graphrag/tests/test_e2e_schema_aware_ingest.py
@@ -185,6 +185,7 @@ def test_02_convert_sample_files():
print(f"Saved files: {saved}")
print(f"Total documents: {body.get('num_documents')}")
_state["saved_files"] = saved
+ _state["request_id"] = body.get("request_id") or ""
@skip_unless_graphrag
@@ -194,7 +195,10 @@ def test_03_extract_schema_from_jsonl():
print(f"\n--- Stage 3: Running schema extraction ---")
resp = requests.post(
f"{GRAPHRAG_URL}/ui/{GRAPH_NAME}/extract_schema_from_jsonl",
- json={"filenames": _state["saved_files"]},
+ json={
+ "filenames": _state["saved_files"],
+ "request_id": _state.get("request_id", ""),
+ },
auth=AUTH,
timeout=SCHEMA_EXTRACT_TIMEOUT,
)
From fd844f282990304bd886f6f108507c341bbedd6c Mon Sep 17 00:00:00 2001
From: Chengbiao Jin
Date: Sat, 16 May 2026 08:46:52 -0700
Subject: [PATCH 70/70] Mark v1.4.0 released in README and drop stale roadmap
note
- Set the v1.4.0 release date on the Releases list
- Replace the obsolete "schema-aware init is on the roadmap" line with a pointer to the shipped Initialize Knowledge Graph flow
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 662708d..ce4f10d 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@
---
## Releases
-* **GraphRAG v1.4.0** (in progress): Schema-aware initialization. The *Initialize Knowledge Graph* dialog accepts an optional pasted GSQL schema; the backend parses, diffs, and applies domain types as a single atomic schema-change job. Type definitions captured at init time flow through to the entity-extraction prompt and the query-routing schema rep. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.4.0) when published.
+* **5/16/2026**: GraphRAG v1.4.0 released. Added schema-aware knowledge graphs, auto retrieval method selection, and a Trace Logs UI, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.4.0) for details.
* **4/10/2026**: GraphRAG v1.3.0 released. Added an admin configuration UI with role-based access and per-graph chatbot LLM override, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.3.0) for details.
* **2/28/2026**: GraphRAG v1.2.0 released. Added Admin UI for graph initialization, document ingestion, and knowledge graph rebuild, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.2.0) for details.
* **9/22/2025**: GraphRAG is available now officially v1.1 (v1.1.0). AWS Bedrock support is completed with BDA integration for multimodal document ingestion. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.1.0) for details.
@@ -932,7 +932,7 @@ Today's primary lever is the **entity-extraction prompt**:
- **Add 1–2 short domain examples** in the prompt. Even one well-chosen exemplar (an extracted entity with type and definition) dramatically improves consistency across chunks.
- **List the canonical edge verbs you want.** Encourage `PUBLISHES`, `OWNS`, `ISSUES`, `MANAGES`, `REPORTS_ON` in the relationship-extraction prompt rather than letting the LLM emit ad-hoc nominal phrases.
-If extraction quality is still poor after iterating on the prompt, the next-best option today is to clear the graph's domain types and re-ingest with the improved prompt — schema growth is currently driven entirely by what extraction produces. (A schema-aware initialization flow that lets you supply a curated schema up front is on the roadmap.)
+If extraction quality is still poor after iterating on the prompt, declare a domain schema up front via the *Initialize Knowledge Graph* dialog (paste GSQL, or generate a draft from sample documents) so extraction populates the types you actually want instead of growing them organically from what the LLM happens to emit. See the Configuration table above for `strict_mode` and `retrieval_include_entity` for the schema-aware behavior knobs.
**Note on LLM faithfulness.** Entity, relationship, and attribute extraction is best-effort and may include occasional errors, especially for well-known entities. For high-stakes applications, validate critical extracted values against your source documents before relying on them.