From f9be937adb87587d735e5e51d476dc0c54004591 Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:23:41 -0400 Subject: [PATCH 01/25] Add files via upload --- src/main.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main.py diff --git a/src/main.py b/src/main.py new file mode 100644 index 000000000..e69de29bb From 03c4218491213932d2e68f97784378cf4b3c2e39 Mon Sep 17 00:00:00 2001 From: Ayaan Agarwal Date: Fri, 25 Jul 2025 12:43:09 -0600 Subject: [PATCH 02/25] feat: Add Next.js frontend application --- frontend/.gitignore | 41 + frontend/README.md | 36 + frontend/eslint.config.mjs | 16 + frontend/next.config.ts | 7 + frontend/package-lock.json | 6149 ++++++++++++++++++++++++++++++++++ frontend/package.json | 27 + frontend/postcss.config.mjs | 5 + frontend/public/file.svg | 1 + frontend/public/globe.svg | 1 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/public/window.svg | 1 + frontend/src/app/favicon.ico | Bin 0 -> 25931 bytes frontend/src/app/globals.css | 26 + frontend/src/app/layout.tsx | 34 + frontend/src/app/page.tsx | 103 + frontend/tsconfig.json | 27 + 17 files changed, 6476 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/file.svg create mode 100644 frontend/public/globe.svg create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/public/window.svg create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/tsconfig.json diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..e215bc4cc --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 000000000..c85fb67c4 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 000000000..e9ffa3083 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..0d2dcf1e9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6149 @@ +{ + "name": "frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.1.0", + "dependencies": { + "next": "15.4.4", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.4.4", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "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/eslint-utils/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-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.4.tgz", + "integrity": "sha512-SJKOOkULKENyHSYXE5+KiFU6itcIb6wSBjgM92meK0HVKpo94dNOLZVdLLuS7/BxImROkGoPsjR4EnuDucqiiA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.4.tgz", + "integrity": "sha512-1FDsyN//ai3Jd97SEd7scw5h1yLdzDACGOPRofr2GD3sEFsBylEEoL0MHSerd4n2dq9Zm/mFMqi4+NRMOreOKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.4.tgz", + "integrity": "sha512-eVG55dnGwfUuG+TtnUCt+mEJ+8TGgul6nHEvdb8HEH7dmJIFYOCApAaFrIrxwtEq2Cdf+0m5sG1Np8cNpw9EAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.4.tgz", + "integrity": "sha512-zqG+/8apsu49CltEj4NAmCGZvHcZbOOOsNoTVeIXphYWIbE4l6A/vuQHyqll0flU2o3dmYCXsBW5FmbrGDgljQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.4.tgz", + "integrity": "sha512-LRD4l2lq4R+2QCHBQVC0wjxxkLlALGJCwigaJ5FSRSqnje+MRKHljQNZgDCaKUZQzO/TXxlmUdkZP/X3KNGZaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.4.tgz", + "integrity": "sha512-LsGUCTvuZ0690fFWerA4lnQvjkYg9gHo12A3wiPUR4kCxbx/d+SlwmonuTH2SWZI+RVGA9VL3N0S03WTYv6bYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.4.tgz", + "integrity": "sha512-aOy5yNRpLL3wNiJVkFYl6w22hdREERNjvegE6vvtix8LHRdsTHhWTpgvcYdCK7AIDCQW5ATmzr9XkPHvSoAnvg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.4.tgz", + "integrity": "sha512-FL7OAn4UkR8hKQRGBmlHiHinzOb07tsfARdGh7v0Z0jEJ3sz8/7L5bR23ble9E6DZMabSStqlATHlSxv1fuzAg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.4.tgz", + "integrity": "sha512-eEdNW/TXwjYhOulQh0pffTMMItWVwKCQpbziSBmgBNFZIIRn2GTXrhrewevs8wP8KXWYMx8Z+mNU0X+AfvtrRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.4.tgz", + "integrity": "sha512-SE5pYNbn/xZKMy1RE3pAs+4xD32OI4rY6mzJa4XUkp/ItZY+OMjIgilskmErt8ls/fVJ+Ihopi2QIeW6O3TrMw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, + "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/@typescript-eslint/typescript-estree/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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-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/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-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "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/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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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==", + "devOptional": 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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.4.4.tgz", + "integrity": "sha512-sK/lWLUVF5om18O5w76Jt3F8uzu/LP5mVa6TprCMWkjWHUmByq80iHGHcdH7k1dLiJlj+DRIWf98d5piwRsSuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.4.4", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/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/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/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/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/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/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.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "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.4" + }, + "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==", + "dev": true, + "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.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.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==", + "dev": true, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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.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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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/napi-postinstall": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.4.4.tgz", + "integrity": "sha512-kNcubvJjOL9yUOfwtZF3HfDhuhp+kVD+FM2A6Tyua1eI/xfmY4r/8ZS913MMz+oWKDlbps/dQOWdDricuIkXLw==", + "license": "MIT", + "dependencies": { + "@next/env": "15.4.4", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.4.4", + "@next/swc-darwin-x64": "15.4.4", + "@next/swc-linux-arm64-gnu": "15.4.4", + "@next/swc-linux-arm64-musl": "15.4.4", + "@next/swc-linux-x64-gnu": "15.4.4", + "@next/swc-linux-x64-musl": "15.4.4", + "@next/swc-win32-arm64-msvc": "15.4.4", + "@next/swc-win32-x64-msvc": "15.4.4", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "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.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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==", + "dev": true, + "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": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "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/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.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/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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..616401496 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + "next": "15.4.4" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@tailwindcss/postcss": "^4", + "tailwindcss": "^4", + "eslint": "^9", + "eslint-config-next": "15.4.4", + "@eslint/eslintrc": "^3" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 000000000..c7bcb4b1e --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 000000000..a2dc41ece --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 000000000..f7fa87eb8 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 000000000..a93289420 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,103 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + src/app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ + +
+ +
+ ); +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 000000000..c1334095f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 8a50b91c93a7e97cf9e89abf205b15cc38a8d5e2 Mon Sep 17 00:00:00 2001 From: shivajipanam Date: Fri, 25 Jul 2025 13:48:33 -0500 Subject: [PATCH 03/25] First draft of architecture updated --- ARCHITECTURE.md | 322 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 223b4737d..928a55dc4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,6 +5,100 @@ Below, sketch (ASCII, hand-drawn JPEG/PNG pasted in, or ASCII art) the high-level components of your agent. +life-witness-agent/ +├── frontend/ +│ ├── src/ +│ │ ├── components/ +│ │ │ ├── VoiceInterface.jsx # Voice input/output UI component +│ │ │ ├── PhotoUpload.jsx # Drag & drop photo upload interface +│ │ │ ├── MemoryTimeline.jsx # Timeline view of life events +│ │ │ ├── MemoryCard.jsx # Individual memory display card +│ │ │ └── AgentStatus.jsx # Real-time agent activity indicator +│ │ ├── services/ +│ │ │ ├── apiClient.js # HTTP client for backend API calls +│ │ │ ├── voiceService.js # Web Speech API wrapper +│ │ │ ├── uploadService.js # File upload handling +│ │ │ └── websocketClient.js # Real-time agent communication +│ │ ├── utils/ +│ │ │ ├── audioUtils.js # Audio processing utilities +│ │ │ ├── imageUtils.js # Image compression/validation +│ │ │ └── formatters.js # Data formatting helpers (optional) +│ │ ├── App.jsx # Main React application component +│ │ └── main.jsx # React app entry point +│ ├── public/ +│ │ ├── index.html # HTML template +│ │ └── manifest.json # PWA manifest +│ ├── package.json # Frontend dependencies +│ └── vite.config.js # Vite build configuration (Or similar config file) +├── backend/ +│ ├── agents/ +│ │ ├── base_agent.py # Abstract base class for all agents (Should be implemented first) +│ │ ├── planner_agent.py # Intent parsing and task planning +│ │ ├── voice_agent.py # Speech processing and TTS +│ │ ├── vision_agent.py # Image/video analysis via Gemini +│ │ ├── context_agent.py # Gmail/Calendar data integration +│ │ ├── memory_agent.py # Memory storage and retrieval +│ │ ├── insight_agent.py # Pattern recognition and analysis +│ │ └── response_agent.py # Conversational response generation +│ ├── orchestration/ +│ │ ├── orchestrator.py # Main agent coordination engine (using Langgraph or the execution engine) +│ │ ├── event_bus.py # Event-driven communication system (optional) +│ │ ├── task_queue.py # Async task management +│ │ └── agent_pool.py # Agent instance management +│ ├── services/ +│ │ ├── gemini_service.py # Gemini API client and utilities +│ │ ├── gmail_service.py # Gmail API integration +│ │ ├── calendar_service.py # Google Calendar API integration +│ │ └── storage_service.py # File system and JSON persistence (Need other storage services like vertex or bigquery?) +│ ├── utils/ +│ │ ├── logging_config.py # Structured logging setup +│ │ ├── tracing.py # Distributed tracing utilities (Optional) +│ │ ├── metrics.py # Performance monitoring (Optional) +│ │ ├── error_handling.py # Exception handling patterns (Optional) +│ │ └── config.py # Environment configuration +│ ├── api/ +│ │ ├── routes/ +│ │ │ ├── memory_routes.py # Memory CRUD API endpoints +│ │ │ ├── voice_routes.py # Voice processing endpoints +│ │ │ ├── upload_routes.py # File upload endpoints +│ │ │ └── query_routes.py # Memory query endpoints +│ │ └── middleware.py # Authentication and logging middleware (Skip for now) +│ ├── main.py # FastAPI application entry point +│ └── requirements.txt # Python dependencies +├── data/ +│ ├── memories/ +│ │ ├── events.json # Episodic memory storage (Can be temporal storage) +│ │ ├── relationships.json # Person/place relationship graph (Not required for now) +│ │ └── embeddings.json # Vector embeddings for search +│ ├── cache/ +│ │ └── api_cache.json # Cached API responses +│ └── demo/ +│ ├── sample_memories.json # Pre-loaded demo data +│ └── demo_photos/ # Sample images for demo +├── tests/ +│ ├── unit/ +│ │ ├── test_agents.py # Unit tests for agent classes +│ │ ├── test_orchestrator.py # Orchestration logic tests +│ │ └── test_services.py # Service layer tests +│ ├── integration/ +│ │ ├── test_api_endpoints.py # API integration tests +│ │ └── test_agent_coordination.py # Multi-agent workflow tests +│ └── fixtures/ +│ ├── sample_data.py # Test data fixtures +│ └── mock_responses.py # Mock API responses +├── scripts/ # Optional +│ ├── setup_dev.sh # Development environment setup +│ ├── run_demo.sh # Demo mode launcher +│ └── deploy.sh # Production deployment script +├── .env.example # Environment variables template +├── .gitignore # Git ignore patterns +├── docker-compose.yml # Local development containers +├── README.md # Project overview and setup instructions, dependencies and how to run the agent +├── ARCHITECTURE.md # System architecture overview (This file) +├── EXPLANATION.md # Natural Language Explanation of agent's reasoning, memory, planning, tools and limitations +├── LICENSE # License and policy +└── DEMO.md # Demo instructions and scenarios + ## Components 1. **User Interface** @@ -22,3 +116,231 @@ Below, sketch (ASCII, hand-drawn JPEG/PNG pasted in, or ASCII art) the high-leve - Logging of each reasoning step - Error handling / retries +## Architecture Overview +------------------------- +1. Planner Agent (Brain) + + Intent Analysis: Parses voice input to understand user goals + Task Decomposition: Breaks complex requests into agent-specific tasks + Agent Selection: Dynamically chooses which agents to activate + Execution Planning: Determines parallel vs sequential execution patterns + +2. Executor Engine (Coordinator) + + Task Queue: Manages parallel/sequential agent execution + Agent Pool: Maintains agent instances and load balancing + Result Aggregation: Combines outputs from multiple agents + Error Handling: Graceful degradation when agents fail + +3. Memory Structure (Knowledge Base) + + Event Storage: Structured life event data with rich metadata + Relationship Graph: Connections between people, places, activities + Semantic Index: Vector embeddings for intelligent search + -- Tiered Storage: Hot/Warm/Cold storage for performance optimization + +4. Specialized Agents (Workers) + + Voice Agent: Speech-to-text, emotion detection, text-to-speech + Vision Agent: Photo analysis, object/person recognition + Context Agent: Gmail/Calendar integration, event correlation + -- Insight Agent: Pattern detection, relationship analysis + Response Agent: Conversational AI, personality, context-aware replies + +## Tool Integrations +Gemini API + + Multimodal Live API: Real-time voice and image processing + Function Calling: Direct integration with agent methods + Long Context: Maintains conversation and memory context + Vision Processing: Advanced photo and document analysis + +Google APIs + + Gmail API: Event extraction from emails + Calendar API: Scheduled event correlation + +## Storage & Performance + + JSON Files: Simple, easy to implement persistence + Vector Embeddings: Semantic search capabilities + Caching Layer: Performance optimization + Logging layer: In-memory logs for fault tolerance + +Metrics Collection + + Agent response times and success rates + API usage and rate limiting + Memory query performance + -- User interaction patterns + +Logging Architecture + + Structured JSON logs with trace IDs + Request/response payload logging (sanitized) + -- Error tracking with full context + +## Use cases: +1. Conference memory +Feeding to memory: +🎤 YOU: "Hey Life Witness, I just met an amazing person at the conference" + +🤖 AGENT: "I'm listening! Tell me about this person while it's fresh in your memory." + +🎤 YOU: [Uploads photo of you with a woman at a conference booth] + "This is Jennifer Chen, she's a VP of Engineering at Stripe. We talked about + their new payment APIs and she mentioned they're hiring senior engineers. + She gave me her card and said to reach out about the role." + +🤖 AGENT: "I can see you're both at the Stripe booth - that's perfect context! + Let me capture this. I notice this is day 2 of the TechCrunch conference + based on your calendar. Should I also note that she specifically + mentioned hiring for senior engineers?" + +🎤 YOU: "Yes, and she said their team is working on crypto payments. Also, + she went to Stanford like me - we bonded over that." + +🤖 AGENT: "Perfect! I've saved this memory with rich context: + • Person: Jennifer Chen, VP Engineering at Stripe + • Connection: Stanford alumni, both interested in crypto + • Opportunity: Senior engineering role on crypto payments team + • Action item: Follow up about the position + • Location: TechCrunch Conference, Stripe booth + • Relationship strength: Strong initial connection + + Should I remind you to follow up with her next week?" + +🎤 YOU: "Yes, remind me Tuesday morning." + +🤖 AGENT: "Calendar reminder set for Tuesday 9 AM. I'll include her contact details + and our conversation context when I remind you." + +Query on memory: +🎤 YOU: "Who was that person I met at TechCrunch who worked on crypto payments?" + +🤖 AGENT: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her + at their booth on day 2 of TechCrunch conference. She mentioned they were + hiring senior engineers for their crypto payments team." + +🎤 YOU: "Right! What else did we talk about?" + +🤖 AGENT: "You both bonded over being Stanford alumni, and she specifically + mentioned their new payment APIs. You took a photo together at their booth, + and she gave you her business card with instructions to reach out about + the senior engineering role." + +🎤 YOU: "Did I ever follow up with her?" + +🤖 AGENT: "Yes, I reminded you on Tuesday morning as requested, and you sent her + a LinkedIn message that same day. She responded within 2 hours suggesting + a coffee meeting, which you scheduled for the next Friday. You met at + Blue Bottle Coffee downtown and had what you described as a 'great conversation + about team culture and technical challenges.'" + +🎤 YOU: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention + my connection to Stripe's crypto work. Can you give me some talking points?" + +🤖 AGENT: "Based on your conversation with Jennifer, here are some relevant points: + • Stripe is actively investing in crypto infrastructure with dedicated teams + • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) + • Their approach prioritizes regulatory compliance and security + • Jennifer mentioned they're seeing huge demand from fintech companies + + This shows you're plugged into industry trends and have insights from + a senior leader in the space. Would you like me to find other crypto-related + conversations from your memory?" + + +2. Birthday event memory +Feeding in +🎤 YOU: "Life Witness, I want to remember this moment" + +🤖 AGENT: "I'm here! What's happening right now?" + +🎤 YOU: [Uploads video of nephew blowing out candles] + "It's my nephew Jake's 8th birthday party. He just blew out the candles + and made a wish. My sister Sarah organized this whole Minecraft-themed party + in their backyard. Jake's been obsessed with Minecraft all year." + +🤖 AGENT: "I can see the Minecraft decorations and Jake's huge smile! This looks + like such a special moment. I notice your sister really went all out + with the theme - there are Minecraft blocks everywhere!" + +🎤 YOU: "Yeah, she's amazing at this stuff. Jake was so excited he couldn't + even speak when he saw the cake - it was shaped like a Creeper. + And all his school friends are here running around in Minecraft costumes." + +🤖 AGENT: "That's so sweet! I can hear the joy in your voice. Should I also note + that this is at your sister's house in Berkeley? I see from your calendar + this party was planned for 2 PM today." + +🎤 YOU: "Yes, and I brought him that Lego Minecraft set he's been wanting. + His reaction was priceless - he literally jumped up and down. + These are the moments I never want to forget." + +🤖 AGENT: "I've captured this beautiful memory + • Event: Jake's 8th birthday party, Minecraft theme + • Location: Sister Sarah's backyard, Berkeley + • Key moments: Candle blowing, Creeper cake, costume parade + • Your gift: Lego Minecraft set - huge success! + • Emotional context: Pure joy, family connection, pride in sister's effort + • Attendees: School friends, family + + This feels like one of those core family memories. I'll make sure it's + easy to find when you want to remember these precious moments with Jake." + +Query on: +🎤 YOU: "What did Jake love most about his last birthday party?" + +🤖 AGENT: "At Jake's 8th birthday last March, he absolutely loved the Minecraft theme! + Your sister Sarah went all out with decorations, and the Creeper-shaped cake + was a huge hit. But what really made him light up was the Lego Minecraft set + you brought - he literally jumped up and down with excitement." + +🎤 YOU: "What else was he into around that time?" + +🤖 AGENT: "Based on your memories from that period, Jake was completely obsessed with + Minecraft - you mentioned he'd been into it 'all year.' But I also have + a memory from two weeks after his party where you took him to the science + museum, and he was fascinated by the robotics exhibit. He kept asking + how robots are programmed." + +🎤 YOU: "Oh right! Has he mentioned robots since then?" + +🤖 AGENT: "Yes! In July, you attended his 'invention presentation' at summer camp + where he built a simple robot car. You were so proud - you said he + explained the programming logic like a little engineer. And just last month + at Thanksgiving, he showed everyone a robot he coded to draw patterns." + +🎤 YOU: "Wow, I didn't realize he'd gotten so into robotics. What should I get + him for his 9th birthday?" + +🤖 AGENT: "Based on his evolution from Minecraft to robotics, I'd suggest something + that bridges both interests: + + • A programmable Minecraft robot kit (combines both passions) + • Lego Mindstorms (he loved the Lego Minecraft set, now with programming) + • A beginner Arduino kit with Minecraft-themed projects + + Also, remember how much he loved explaining his robot car? He seems to + enjoy teaching others, so maybe something he can demonstrate to friends + would be perfect. Want me to check what specific robotics projects he's + mentioned recently?" + +🎤 YOU: "That's brilliant! Can you remind me what Sarah did for decorations last + year? I want to help her plan something even better." + +🤖 AGENT: "Sarah created an amazing Minecraft wonderland in her Berkeley backyard: + • Cardboard Minecraft blocks scattered everywhere + • Creeper-shaped birthday cake (huge hit) + • All the kids wore Minecraft costumes + • Green and brown color scheme + + For a robotics theme this year, you could suggest: + • Silver and blue decorations (tech colors) + • Robot-shaped cake or cupcakes that look like circuit boards + • A 'robot building station' where kids can assemble simple bots + • Maybe coordinate with that science museum he loved? + + Sarah clearly has amazing party planning skills - she'll probably love + collaborating on this evolution from Minecraft to robotics!" \ No newline at end of file From a3c936b18854b7c01919bade1526e03ef731e2ea Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:02:00 -0400 Subject: [PATCH 04/25] Add files via upload Initialize backend folder structure with core agents, services, and orchestration modules --- backend/agents/base_agent.py | 0 backend/agents/context_agent.py | 0 backend/agents/insight_agent.py | 0 backend/agents/memory_agent.py | 0 backend/agents/planner_agent.py | 0 backend/agents/response_agent.py | 0 backend/agents/vision_agent.py | 0 backend/agents/voice_agent.py | 0 backend/api/middleware.py | 0 backend/api/routes/memory_routes.py | 0 backend/api/routes/query_routes.py | 0 backend/api/routes/upload_routes.py | 0 backend/api/routes/voice_routes.py | 0 backend/main.py | 0 backend/orchestration/agent_pool.py | 0 backend/orchestration/event_bus.py | 0 backend/orchestration/orchestrator.py | 0 backend/orchestration/task_queue.py | 0 backend/requirements.txt | 0 backend/services/calendar_service.py | 0 backend/services/gemini_service.py | 0 backend/services/gmail_service.py | 0 backend/services/storage_service.py | 0 backend/utils/config.py | 0 backend/utils/error_handling.py | 0 backend/utils/logging_config.py | 0 backend/utils/metrics.py | 0 backend/utils/tracing.py | 0 28 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/agents/base_agent.py create mode 100644 backend/agents/context_agent.py create mode 100644 backend/agents/insight_agent.py create mode 100644 backend/agents/memory_agent.py create mode 100644 backend/agents/planner_agent.py create mode 100644 backend/agents/response_agent.py create mode 100644 backend/agents/vision_agent.py create mode 100644 backend/agents/voice_agent.py create mode 100644 backend/api/middleware.py create mode 100644 backend/api/routes/memory_routes.py create mode 100644 backend/api/routes/query_routes.py create mode 100644 backend/api/routes/upload_routes.py create mode 100644 backend/api/routes/voice_routes.py create mode 100644 backend/main.py create mode 100644 backend/orchestration/agent_pool.py create mode 100644 backend/orchestration/event_bus.py create mode 100644 backend/orchestration/orchestrator.py create mode 100644 backend/orchestration/task_queue.py create mode 100644 backend/requirements.txt create mode 100644 backend/services/calendar_service.py create mode 100644 backend/services/gemini_service.py create mode 100644 backend/services/gmail_service.py create mode 100644 backend/services/storage_service.py create mode 100644 backend/utils/config.py create mode 100644 backend/utils/error_handling.py create mode 100644 backend/utils/logging_config.py create mode 100644 backend/utils/metrics.py create mode 100644 backend/utils/tracing.py diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/context_agent.py b/backend/agents/context_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/insight_agent.py b/backend/agents/insight_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/memory_agent.py b/backend/agents/memory_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/planner_agent.py b/backend/agents/planner_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/response_agent.py b/backend/agents/response_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/vision_agent.py b/backend/agents/vision_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/voice_agent.py b/backend/agents/voice_agent.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/api/middleware.py b/backend/api/middleware.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/api/routes/memory_routes.py b/backend/api/routes/memory_routes.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/api/routes/query_routes.py b/backend/api/routes/query_routes.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/api/routes/upload_routes.py b/backend/api/routes/upload_routes.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/api/routes/voice_routes.py b/backend/api/routes/voice_routes.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/orchestration/agent_pool.py b/backend/orchestration/agent_pool.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/orchestration/event_bus.py b/backend/orchestration/event_bus.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/orchestration/orchestrator.py b/backend/orchestration/orchestrator.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/orchestration/task_queue.py b/backend/orchestration/task_queue.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/backend/services/calendar_service.py b/backend/services/calendar_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/services/gemini_service.py b/backend/services/gemini_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/services/gmail_service.py b/backend/services/gmail_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/services/storage_service.py b/backend/services/storage_service.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/utils/config.py b/backend/utils/config.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/utils/error_handling.py b/backend/utils/error_handling.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/utils/logging_config.py b/backend/utils/logging_config.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/utils/metrics.py b/backend/utils/metrics.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/utils/tracing.py b/backend/utils/tracing.py new file mode 100644 index 000000000..e69de29bb From d11739fd07e914ffb7c2ca49434fc396cf53802b Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:02:38 -0400 Subject: [PATCH 05/25] Delete src directory --- src/main.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main.py diff --git a/src/main.py b/src/main.py deleted file mode 100644 index e69de29bb..000000000 From e0bef7a5ee3bc3d58f0551247ce8077f8cfd40fd Mon Sep 17 00:00:00 2001 From: Munish Patel <53735021+munishpatel@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:04:13 -0500 Subject: [PATCH 06/25] folder structure - frontend --- frontend/src/components/agentStatus.tsx | 0 frontend/src/components/memoryCard.tsx | 0 frontend/src/components/memoryTimeline.tsx | 0 frontend/src/components/photoUpload.tsx | 0 frontend/src/components/voiceInterface.tsx | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/src/components/agentStatus.tsx create mode 100644 frontend/src/components/memoryCard.tsx create mode 100644 frontend/src/components/memoryTimeline.tsx create mode 100644 frontend/src/components/photoUpload.tsx create mode 100644 frontend/src/components/voiceInterface.tsx diff --git a/frontend/src/components/agentStatus.tsx b/frontend/src/components/agentStatus.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/memoryCard.tsx b/frontend/src/components/memoryCard.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/memoryTimeline.tsx b/frontend/src/components/memoryTimeline.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/photoUpload.tsx b/frontend/src/components/photoUpload.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/voiceInterface.tsx b/frontend/src/components/voiceInterface.tsx new file mode 100644 index 000000000..e69de29bb From 1fcdf33c1686776c05e9478aa826abf4e5df2792 Mon Sep 17 00:00:00 2001 From: Munish Patel <53735021+munishpatel@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:09:01 -0500 Subject: [PATCH 07/25] folder structure - frontend --- frontend/src/services/apiClient.ts | 0 frontend/src/services/uploadService.ts | 0 frontend/src/services/voiceService.ts | 0 frontend/src/services/websocketClient.ts | 0 frontend/src/utils/audioUtils.ts | 0 frontend/src/utils/formatters.ts | 0 frontend/src/utils/imageUtils.ts | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 frontend/src/services/apiClient.ts create mode 100644 frontend/src/services/uploadService.ts create mode 100644 frontend/src/services/voiceService.ts create mode 100644 frontend/src/services/websocketClient.ts create mode 100644 frontend/src/utils/audioUtils.ts create mode 100644 frontend/src/utils/formatters.ts create mode 100644 frontend/src/utils/imageUtils.ts diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/services/uploadService.ts b/frontend/src/services/uploadService.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/services/voiceService.ts b/frontend/src/services/voiceService.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/services/websocketClient.ts b/frontend/src/services/websocketClient.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/utils/audioUtils.ts b/frontend/src/utils/audioUtils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/utils/imageUtils.ts b/frontend/src/utils/imageUtils.ts new file mode 100644 index 000000000..e69de29bb From 1487cdbaa8d1e8a0f5250507d0738bf248cfebfd Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:09:35 -0400 Subject: [PATCH 08/25] Add files via upload Add data, tests, and scripts directories with placeholders for memory, testing, and deployment --- data/cache/api_cache.json | 0 data/demo/sample_memories.json | 0 data/memories/embeddings.json | 0 data/memories/events.json | 0 data/memories/relationships.json | 0 scripts/deploy.sh | 0 scripts/run_demo.sh | 0 scripts/setup_dev.sh | 0 tests/fixtures/mock_responses.py | 0 tests/fixtures/sample_data.py | 0 tests/integration/test_agent_coordination.py | 0 tests/integration/test_api_endpoints.py | 0 tests/unit/test_agents.py | 0 tests/unit/test_orchestrator.py | 0 tests/unit/test_services.py | 0 15 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/cache/api_cache.json create mode 100644 data/demo/sample_memories.json create mode 100644 data/memories/embeddings.json create mode 100644 data/memories/events.json create mode 100644 data/memories/relationships.json create mode 100644 scripts/deploy.sh create mode 100644 scripts/run_demo.sh create mode 100644 scripts/setup_dev.sh create mode 100644 tests/fixtures/mock_responses.py create mode 100644 tests/fixtures/sample_data.py create mode 100644 tests/integration/test_agent_coordination.py create mode 100644 tests/integration/test_api_endpoints.py create mode 100644 tests/unit/test_agents.py create mode 100644 tests/unit/test_orchestrator.py create mode 100644 tests/unit/test_services.py diff --git a/data/cache/api_cache.json b/data/cache/api_cache.json new file mode 100644 index 000000000..e69de29bb diff --git a/data/demo/sample_memories.json b/data/demo/sample_memories.json new file mode 100644 index 000000000..e69de29bb diff --git a/data/memories/embeddings.json b/data/memories/embeddings.json new file mode 100644 index 000000000..e69de29bb diff --git a/data/memories/events.json b/data/memories/events.json new file mode 100644 index 000000000..e69de29bb diff --git a/data/memories/relationships.json b/data/memories/relationships.json new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/run_demo.sh b/scripts/run_demo.sh new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/setup_dev.sh b/scripts/setup_dev.sh new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/mock_responses.py b/tests/fixtures/mock_responses.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/sample_data.py b/tests/fixtures/sample_data.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/test_agent_coordination.py b/tests/integration/test_agent_coordination.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/test_api_endpoints.py b/tests/integration/test_api_endpoints.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/test_agents.py b/tests/unit/test_agents.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py new file mode 100644 index 000000000..e69de29bb From 769695b4fa2b8652fc2e6dcc4fc041fa8193a5e0 Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:14:43 -0400 Subject: [PATCH 09/25] base-agent --- backend/agents/base_agent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py index e69de29bb..8068a9c56 100644 --- a/backend/agents/base_agent.py +++ b/backend/agents/base_agent.py @@ -0,0 +1,6 @@ +from abc import ABC, abstractmethod + +class BaseAgent(ABC): + @abstractmethod + def process(self, input_data: str) -> str: + pass From 41e11f4600ad3417ba4e1f0d9bab0de1a684b827 Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:18:11 -0400 Subject: [PATCH 10/25] baseagent --- backend/agents/base_agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py index 8068a9c56..a0c8f81f1 100644 --- a/backend/agents/base_agent.py +++ b/backend/agents/base_agent.py @@ -2,5 +2,6 @@ class BaseAgent(ABC): @abstractmethod - def process(self, input_data: str) -> str: + def run(self, input_data: str) -> str: + """Runs the agent logic and returns a string output.""" pass From df9049c15f98e961aae881f3a85bd57a955ac231 Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:33:45 -0400 Subject: [PATCH 11/25] base_agent --- backend/agents/base_agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py index a0c8f81f1..466da54ca 100644 --- a/backend/agents/base_agent.py +++ b/backend/agents/base_agent.py @@ -1,7 +1,7 @@ -from abc import ABC, abstractmethod +from google.adk.agents import BaseAgent +from abc import abstractmethod -class BaseAgent(ABC): +class MyBaseAgent(BaseAgent): @abstractmethod - def run(self, input_data: str) -> str: - """Runs the agent logic and returns a string output.""" + def run(self, input: str) -> str: pass From 05ba6009722a4ab12762569a0b79df8281e5fcd3 Mon Sep 17 00:00:00 2001 From: Rakesh C <160077191+rakeshcr92@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:33:52 -0400 Subject: [PATCH 12/25] base_agent --- backend/agents/base_agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py index 466da54ca..84e87e6f3 100644 --- a/backend/agents/base_agent.py +++ b/backend/agents/base_agent.py @@ -1,7 +1,7 @@ -from google.adk.agents import BaseAgent -from abc import abstractmethod +from abc import ABC, abstractmethod -class MyBaseAgent(BaseAgent): +class BaseAgent(ABC): @abstractmethod - def run(self, input: str) -> str: + def run(self, input_text: str) -> str: + """Run the agent on the input text and return a response.""" pass From ec2ca55a595da99b2d3001265a02a1f4fa5935f4 Mon Sep 17 00:00:00 2001 From: Ayaan Agarwal Date: Fri, 25 Jul 2025 15:44:44 -0600 Subject: [PATCH 13/25] Official 'mock' draft of UI --- frontend/package-lock.json | 22 ++- frontend/package.json | 11 +- frontend/public/placeholder.png | Bin 0 -> 21641 bytes frontend/src/app/page.tsx | 234 +++++++++++++++++++------------- 4 files changed, 165 insertions(+), 102 deletions(-) create mode 100644 frontend/public/placeholder.png diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d2dcf1e9..32d863e69 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "next": "15.4.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "swiper": "^11.2.10" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -5706,6 +5707,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swiper": { + "version": "11.2.10", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.10.tgz", + "integrity": "sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 616401496..a2f153248 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,19 +9,20 @@ "lint": "next lint" }, "dependencies": { + "next": "15.4.4", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.4.4" + "swiper": "^11.2.10" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.4.4", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/frontend/public/placeholder.png b/frontend/public/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..df97f5f317b91e813f4c78b17559f1b47c1a9b5c GIT binary patch literal 21641 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 -

- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ // Here, I'm setting up the state for my component. This is the information + // that will change as the user interacts with the page. + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [currentUserQuery, setCurrentUserQuery] = useState(''); + const [currentAgentReply, setCurrentAgentReply] = useState(''); + const [highlightedMemoryId, setHighlightedMemoryId] = useState(null); + + // This ref gives me direct control over the carousel component. + const carouselRef = useRef(null); + + // This is the hard-coded conversation I'm using for the demo. + const demoConversation = [ + { + query: "Who was that person I met at TechCrunch who worked on crypto payments?", + response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team." + }, + { + query: "Right! What else did we talk about?", + response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role." + }, + { + query: "Did I ever follow up with her?", + response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'" + }, + { + query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", + response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?" + } + ]; + + // This is the list of photos for the carousel. + const memoryPhotos = [ + { id: 1, name: 'Jennifer Chen', imageUrl: '/placeholder.png' }, + { id: 2, name: 'Jake\'s Bday', imageUrl: '/placeholder.png' }, + { id: 3, name: 'Coffee Meetup', imageUrl: '/placeholder.png' }, + { id: 4, name: 'Team Lunch', imageUrl: '/placeholder.png' }, + { id: 5, name: 'Project Demo', imageUrl: '/placeholder.png' }, + ]; + + // This function runs when the microphone button is clicked. + const runNextStepInDemo = () => { + const jenniferId = 1; + const jenniferIndex = memoryPhotos.findIndex(m => m.id === jenniferId); + + // On the first click of the demo, I'll stop the autoplay and slide to Jennifer's photo. + if (currentStepIndex === 0) { + setHighlightedMemoryId(jenniferId); + if (carouselRef.current && jenniferIndex !== -1) { + carouselRef.current.autoplay.stop(); + carouselRef.current.slideToLoop(jenniferIndex); + } + } -
- - Vercel logomark - Deploy now - - - Read our docs - + // I'll display the current step of the conversation... + const currentStep = demoConversation[currentStepIndex]; + setCurrentUserQuery(currentStep.query); + setCurrentAgentReply(currentStep.response); + + // ...and then get ready for the next click by advancing the step index. + setCurrentStepIndex((prevIndex) => (prevIndex + 1) % demoConversation.length); + }; + + return ( +
+ +
+
Live Witness
+ +
+ +
+ + { carouselRef.current = swiper; }} + effect={'coverflow'} + grabCursor={true} + centeredSlides={true} + slidesPerView={3} + loop={true} + autoplay={{ delay: 3000, disableOnInteraction: false }} + coverflowEffect={{ rotate: 0, stretch: 0, depth: 150, modifier: 1, slideShadows: false }} + pagination={false} + modules={[EffectCoverflow, Pagination, Autoplay]} + className="w-full max-w-5xl" + > + {memoryPhotos.map(photo => ( + +
+ {photo.name + {photo.name} +
+
+ ))} +
+ +
+
+ +

"{currentUserQuery || 'Click the mic to start the conversation...'}"

+
+
+ + {currentAgentReply && ( +
+ AI Response: +

{currentAgentReply}

+
+ )}
-
); -} +} \ No newline at end of file From 29953242c15d5a69e4edf7340a9ba20f1b460f04 Mon Sep 17 00:00:00 2001 From: shivajipanam Date: Fri, 25 Jul 2025 17:03:43 -0500 Subject: [PATCH 14/25] Orchestrator skeleton code --- backend/orchestration/agent_pool.py | 453 +++++++++++++++++++++++ backend/orchestration/event_bus.py | 373 +++++++++++++++++++ backend/orchestration/orchestrator.py | 365 +++++++++++++++++++ backend/orchestration/task_queue.py | 503 ++++++++++++++++++++++++++ 4 files changed, 1694 insertions(+) diff --git a/backend/orchestration/agent_pool.py b/backend/orchestration/agent_pool.py index e69de29bb..6a5294afe 100644 --- a/backend/orchestration/agent_pool.py +++ b/backend/orchestration/agent_pool.py @@ -0,0 +1,453 @@ +""" +Agent pool management system for maintaining and distributing agent instances. +Handles agent lifecycle, load balancing, and resource management. +""" + +import asyncio +import logging +from typing import Dict, List, Optional, Any, Type +from datetime import datetime, timedelta +from dataclasses import dataclass, field +from enum import Enum +import weakref +from collections import defaultdict +import importlib + +from ..agents.base_agent import BaseAgent +from ..agents.planner_agent import PlannerAgent +from ..agents.voice_agent import VoiceAgent +from ..agents.vision_agent import VisionAgent +from ..agents.context_agent import ContextAgent +from ..agents.memory_agent import MemoryAgent +from ..agents.insight_agent import InsightAgent +from ..agents.response_agent import ResponseAgent + + +class AgentType(Enum): + PLANNER = "planner" + VOICE = "voice" + VISION = "vision" + CONTEXT = "context" + MEMORY = "memory" + INSIGHT = "insight" + RESPONSE = "response" + + +@dataclass +class AgentInstance: + """Represents an agent instance in the pool.""" + id: str + agent_type: AgentType + agent: BaseAgent + created_at: datetime = field(default_factory=datetime.now) + last_used: datetime = field(default_factory=datetime.now) + usage_count: int = 0 + is_busy: bool = False + current_task_id: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class PoolStats: + """Statistics for the agent pool.""" + total_agents: int + active_agents: int + idle_agents: int + agent_counts_by_type: Dict[str, int] + average_usage: float + total_requests: int + pool_efficiency: float + + +class AgentPool: + """ + Manages a pool of agent instances with load balancing and lifecycle management. + """ + + def __init__( + self, + gemini_api_key: str, + config: Dict[str, Any], + min_instances_per_type: int = 1, + max_instances_per_type: int = 5, + idle_timeout: int = 300, # 5 minutes + cleanup_interval: int = 60 # 1 minute + ): + self.gemini_api_key = gemini_api_key + self.config = config + self.min_instances_per_type = min_instances_per_type + self.max_instances_per_type = max_instances_per_type + self.idle_timeout = idle_timeout + self.cleanup_interval = cleanup_interval + + self.logger = logging.getLogger(__name__) + + # Agent registry and pools + self._agents: Dict[str, AgentInstance] = {} + self._agent_pools: Dict[AgentType, List[str]] = defaultdict(list) + self._agent_classes: Dict[AgentType, Type[BaseAgent]] = { + AgentType.PLANNER: PlannerAgent, + AgentType.VOICE: VoiceAgent, + AgentType.VISION: VisionAgent, + AgentType.CONTEXT: ContextAgent, + AgentType.MEMORY: MemoryAgent, + AgentType.INSIGHT: InsightAgent, + AgentType.RESPONSE: ResponseAgent, + } + + # Load balancing and monitoring + self._round_robin_counters: Dict[AgentType, int] = defaultdict(int) + self._request_queue: Dict[AgentType, asyncio.Queue] = { + agent_type: asyncio.Queue() for agent_type in AgentType + } + + # Statistics + self._stats = { + "total_requests": 0, + "successful_requests": 0, + "failed_requests": 0, + "agents_created": 0, + "agents_destroyed": 0, + } + + # Background tasks + self._cleanup_task: Optional[asyncio.Task] = None + self._monitoring_task: Optional[asyncio.Task] = None + self._shutdown_event = asyncio.Event() + + # Weak references to avoid memory leaks + self._weak_agents = weakref.WeakSet() + + # Initialize the pool + asyncio.create_task(self._initialize_pool()) + + async def _initialize_pool(self): + """Initialize the agent pool with minimum instances.""" + self.logger.info("Initializing agent pool...") + + # Create minimum instances for each agent type + for agent_type in AgentType: + for _ in range(self.min_instances_per_type): + await self._create_agent_instance(agent_type) + + # Start background tasks + self._cleanup_task = asyncio.create_task(self._cleanup_loop()) + self._monitoring_task = asyncio.create_task(self._monitoring_loop()) + + self.logger.info(f"Agent pool initialized with {len(self._agents)} agents") + + async def _create_agent_instance(self, agent_type: AgentType) -> str: + """Create a new agent instance of the specified type.""" + try: + agent_class = self._agent_classes[agent_type] + + # Create agent with configuration + agent_config = self.config.get(agent_type.value, {}) + agent_config["gemini_api_key"] = self.gemini_api_key + + agent = agent_class(**agent_config) + await agent.initialize() + + # Create instance wrapper + instance_id = f"{agent_type.value}_{len(self._agents)}" + instance = AgentInstance( + id=instance_id, + agent_type=agent_type, + agent=agent, + metadata={"config": agent_config} + ) + + # Register instance + self._agents[instance_id] = instance + self._agent_pools[agent_type].append(instance_id) + self._weak_agents.add(instance) + + self._stats["agents_created"] += 1 + + self.logger.debug(f"Created {agent_type.value} agent: {instance_id}") + return instance_id + + except Exception as e: + self.logger.error(f"Failed to create {agent_type.value} agent: {e}") + raise + + async def _destroy_agent_instance(self, instance_id: str): + """Destroy an agent instance and cleanup resources.""" + if instance_id not in self._agents: + return + + instance = self._agents[instance_id] + + try: + # Cleanup agent resources + await instance.agent.cleanup() + + # Remove from pools + self._agent_pools[instance.agent_type].remove(instance_id) + del self._agents[instance_id] + + self._stats["agents_destroyed"] += 1 + + self.logger.debug(f"Destroyed agent instance: {instance_id}") + + except Exception as e: + self.logger.error(f"Error destroying agent {instance_id}: {e}") + + async def get_agent(self, agent_type: AgentType) -> BaseAgent: + """ + Get an available agent instance of the specified type. + + Args: + agent_type: Type of agent to retrieve + + Returns: + BaseAgent: An available agent instance + + Raises: + RuntimeError: If no agents are available and pool is at capacity + """ + self._stats["total_requests"] += 1 + + try: + # Get an available instance + instance = await self._get_available_instance(agent_type) + + if instance: + # Mark as busy and update usage + instance.is_busy = True + instance.last_used = datetime.now() + instance.usage_count += 1 + + self._stats["successful_requests"] += 1 + return instance.agent + else: + # Try to create a new instance if under capacity + if len(self._agent_pools[agent_type]) < self.max_instances_per_type: + instance_id = await self._create_agent_instance(agent_type) + instance = self._agents[instance_id] + instance.is_busy = True + instance.last_used = datetime.now() + instance.usage_count += 1 + + self._stats["successful_requests"] += 1 + return instance.agent + else: + # Wait for an agent to become available + return await self._wait_for_available_agent(agent_type) + + except Exception as e: + self._stats["failed_requests"] += 1 + self.logger.error(f"Failed to get {agent_type.value} agent: {e}") + raise + + async def _get_available_instance(self, agent_type: AgentType) -> Optional[AgentInstance]: + """Get an available agent instance using round-robin load balancing.""" + pool = self._agent_pools[agent_type] + + if not pool: + return None + + # Round-robin selection starting from last counter position + start_idx = self._round_robin_counters[agent_type] % len(pool) + + for i in range(len(pool)): + idx = (start_idx + i) % len(pool) + instance_id = pool[idx] + instance = self._agents[instance_id] + + if not instance.is_busy: + self._round_robin_counters[agent_type] = idx + 1 + return instance + + return None # All instances are busy + + async def _wait_for_available_agent(self, agent_type: AgentType, timeout: float = 30.0) -> BaseAgent: + """Wait for an agent to become available.""" + start_time = datetime.now() + + while (datetime.now() - start_time).total_seconds() < timeout: + instance = await self._get_available_instance(agent_type) + if instance: + instance.is_busy = True + instance.last_used = datetime.now() + instance.usage_count += 1 + return instance.agent + + await asyncio.sleep(0.1) # Small delay before retrying + + raise TimeoutError(f"No {agent_type.value} agents available within timeout") + + async def release_agent(self, agent: BaseAgent): + """ + Release an agent back to the pool. + + Args: + agent: The agent instance to release + """ + # Find the instance wrapper + for instance in self._agents.values(): + if instance.agent is agent: + instance.is_busy = False + instance.current_task_id = None + self.logger.debug(f"Released agent: {instance.id}") + break + + async def _cleanup_loop(self): + """Background task to cleanup idle agents.""" + while not self._shutdown_event.is_set(): + try: + await self._cleanup_idle_agents() + await asyncio.sleep(self.cleanup_interval) + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"Error in cleanup loop: {e}") + + async def _cleanup_idle_agents(self): + """Remove agents that have been idle for too long.""" + current_time = datetime.now() + agents_to_remove = [] + + for agent_type, pool in self._agent_pools.items(): + # Keep minimum instances + if len(pool) <= self.min_instances_per_type: + continue + + # Find idle agents + for instance_id in pool: + instance = self._agents[instance_id] + + if (not instance.is_busy and + (current_time - instance.last_used).total_seconds() > self.idle_timeout): + agents_to_remove.append(instance_id) + + # Remove idle agents + for instance_id in agents_to_remove: + await self._destroy_agent_instance(instance_id) + + async def _monitoring_loop(self): + """Background task for monitoring pool health and performance.""" + while not self._shutdown_event.is_set(): + try: + await self._update_pool_metrics() + await asyncio.sleep(30) # Update metrics every 30 seconds + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"Error in monitoring loop: {e}") + + async def _update_pool_metrics(self): + """Update pool performance metrics.""" + total_agents = len(self._agents) + active_agents = sum(1 for instance in self._agents.values() if instance.is_busy) + + if total_agents > 0: + pool_efficiency = active_agents / total_agents + else: + pool_efficiency = 0.0 + + # Log metrics if efficiency is low + if pool_efficiency < 0.3 and total_agents > self.min_instances_per_type: + self.logger.warning(f"Low pool efficiency: {pool_efficiency:.2%}") + + def get_pool_stats(self) -> PoolStats: + """Get current pool statistics.""" + total_agents = len(self._agents) + active_agents = sum(1 for instance in self._agents.values() if instance.is_busy) + idle_agents = total_agents - active_agents + + agent_counts = defaultdict(int) + total_usage = 0 + + for instance in self._agents.values(): + agent_counts[instance.agent_type.value] += 1 + total_usage += instance.usage_count + + average_usage = total_usage / total_agents if total_agents > 0 else 0 + pool_efficiency = active_agents / total_agents if total_agents > 0 else 0 + + return PoolStats( + total_agents=total_agents, + active_agents=active_agents, + idle_agents=idle_agents, + agent_counts_by_type=dict(agent_counts), + average_usage=average_usage, + total_requests=self._stats["total_requests"], + pool_efficiency=pool_efficiency + ) + + def get_agent_details(self, agent_type: Optional[AgentType] = None) -> List[Dict[str, Any]]: + """Get detailed information about agents in the pool.""" + agents = [] + + for instance in self._agents.values(): + if agent_type is None or instance.agent_type == agent_type: + agents.append({ + "id": instance.id, + "type": instance.agent_type.value, + "created_at": instance.created_at.isoformat(), + "last_used": instance.last_used.isoformat(), + "usage_count": instance.usage_count, + "is_busy": instance.is_busy, + "current_task_id": instance.current_task_id, + "metadata": instance.metadata + }) + + return agents + + async def scale_pool(self, agent_type: AgentType, target_count: int): + """ + Scale the pool for a specific agent type. + + Args: + agent_type: Type of agent to scale + target_count: Target number of instances + """ + current_count = len(self._agent_pools[agent_type]) + + if target_count > current_count: + # Scale up + for _ in range(target_count - current_count): + if len(self._agent_pools[agent_type]) < self.max_instances_per_type: + await self._create_agent_instance(agent_type) + else: + break + elif target_count < current_count: + # Scale down (but respect minimum) + target_count = max(target_count, self.min_instances_per_type) + instances_to_remove = current_count - target_count + + # Remove idle instances first + pool = self._agent_pools[agent_type] + for i in range(instances_to_remove): + for instance_id in pool[:]: # Copy to avoid modification during iteration + instance = self._agents[instance_id] + if not instance.is_busy: + await self._destroy_agent_instance(instance_id) + break + + async def health_check(self) -> Dict[str, Any]: + """Perform a health check on all agents in the pool.""" + results = { + "healthy_agents": 0, + "unhealthy_agents": 0, + "agent_health": {} + } + + for instance in self._agents.values(): + try: + # Perform health check on agent + health = await instance.agent.health_check() + results["agent_health"][instance.id] = health + + if health.get("status") == "healthy": + results["healthy_agents"] += 1 + else: + results["unhealthy_agents"] += 1 + + except Exception as e: + results["unhealthy_agents"] += 1 + results["agent_health"][instance.id] = { + "status": "error", + "error": str(e) + } \ No newline at end of file diff --git a/backend/orchestration/event_bus.py b/backend/orchestration/event_bus.py index e69de29bb..9bad2195d 100644 --- a/backend/orchestration/event_bus.py +++ b/backend/orchestration/event_bus.py @@ -0,0 +1,373 @@ +""" +Event-driven communication system for coordinating between agents and components. +Provides pub/sub messaging for loose coupling between system components. +""" + +import asyncio +import logging +from typing import Dict, List, Callable, Any, Optional +from datetime import datetime +from dataclasses import dataclass, field +from enum import Enum +import json +import weakref + + +class EventPriority(Enum): + LOW = 1 + NORMAL = 2 + HIGH = 3 + CRITICAL = 4 + + +@dataclass +class Event: + """Represents an event in the system.""" + event_type: str + data: Dict[str, Any] + timestamp: datetime = field(default_factory=datetime.now) + priority: EventPriority = EventPriority.NORMAL + source: Optional[str] = None + correlation_id: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class EventSubscription: + """Represents a subscription to an event type.""" + callback: Callable + event_type: str + filter_func: Optional[Callable] = None + once: bool = False + created_at: datetime = field(default_factory=datetime.now) + + +class EventBus: + """ + Central event bus for managing pub/sub communication between agents. + Supports async event handling, filtering, and priority-based processing. + """ + + def __init__(self, max_queue_size: int = 1000): + self.max_queue_size = max_queue_size + self.logger = logging.getLogger(__name__) + + # Event subscriptions organized by event type + self._subscriptions: Dict[str, List[EventSubscription]] = {} + + # Event queues organized by priority + self._event_queues: Dict[EventPriority, asyncio.Queue] = { + priority: asyncio.Queue(maxsize=max_queue_size) + for priority in EventPriority + } + + # Event processing tasks + self._processing_tasks: List[asyncio.Task] = [] + + # Event history for debugging and monitoring + self._event_history: List[Event] = [] + self._max_history_size = 1000 + + # Statistics + self._stats = { + "events_published": 0, + "events_processed": 0, + "subscribers_count": 0, + "processing_errors": 0 + } + + # Weak references to avoid memory leaks + self._weak_subscriptions = weakref.WeakSet() + + # Start event processing + self._start_event_processing() + + def _start_event_processing(self): + """Start background tasks for processing events by priority.""" + for priority in EventPriority: + task = asyncio.create_task(self._process_events(priority)) + self._processing_tasks.append(task) + + async def _process_events(self, priority: EventPriority): + """Process events from a specific priority queue.""" + queue = self._event_queues[priority] + + while True: + try: + event = await queue.get() + if event is None: # Shutdown signal + break + + await self._handle_event(event) + self._stats["events_processed"] += 1 + + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"Error processing {priority.name} priority event: {e}") + self._stats["processing_errors"] += 1 + + async def _handle_event(self, event: Event): + """Handle a single event by notifying all relevant subscribers.""" + subscriptions = self._subscriptions.get(event.event_type, []) + + # Also check for wildcard subscriptions + wildcard_subscriptions = self._subscriptions.get("*", []) + all_subscriptions = subscriptions + wildcard_subscriptions + + # Process subscriptions concurrently + tasks = [] + subscriptions_to_remove = [] + + for subscription in all_subscriptions: + try: + # Apply filter if present + if subscription.filter_func: + if not await self._apply_filter(subscription.filter_func, event): + continue + + # Create task for async callback + task = asyncio.create_task( + self._invoke_callback(subscription.callback, event) + ) + tasks.append(task) + + # Mark one-time subscriptions for removal + if subscription.once: + subscriptions_to_remove.append(subscription) + + except Exception as e: + self.logger.error(f"Error preparing callback for {event.event_type}: {e}") + + # Wait for all callbacks to complete + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + # Remove one-time subscriptions + for subscription in subscriptions_to_remove: + self.unsubscribe(subscription.event_type, subscription.callback) + + # Add to history + self._add_to_history(event) + + async def _apply_filter(self, filter_func: Callable, event: Event) -> bool: + """Apply filter function to determine if event should be processed.""" + try: + if asyncio.iscoroutinefunction(filter_func): + return await filter_func(event) + else: + return filter_func(event) + except Exception as e: + self.logger.error(f"Error applying event filter: {e}") + return False + + async def _invoke_callback(self, callback: Callable, event: Event): + """Invoke a callback function with error handling.""" + try: + if asyncio.iscoroutinefunction(callback): + await callback(event) + else: + callback(event) + except Exception as e: + self.logger.error(f"Error in event callback for {event.event_type}: {e}") + + def _add_to_history(self, event: Event): + """Add event to history with size management.""" + self._event_history.append(event) + + # Trim history if it exceeds max size + if len(self._event_history) > self._max_history_size: + self._event_history = self._event_history[-self._max_history_size:] + + async def emit( + self, + event_type: str, + data: Dict[str, Any], + priority: EventPriority = EventPriority.NORMAL, + source: Optional[str] = None, + correlation_id: Optional[str] = None, + metadata: Dict[str, Any] = None + ) -> bool: + """ + Emit an event to the bus. + + Args: + event_type: Type identifier for the event + data: Event payload data + priority: Event processing priority + source: Source identifier (optional) + correlation_id: Correlation ID for request tracking (optional) + metadata: Additional metadata (optional) + + Returns: + bool: True if event was queued successfully + """ + try: + event = Event( + event_type=event_type, + data=data, + priority=priority, + source=source, + correlation_id=correlation_id, + metadata=metadata or {} + ) + + queue = self._event_queues[priority] + + # Try to put event in queue (non-blocking) + try: + queue.put_nowait(event) + self._stats["events_published"] += 1 + return True + except asyncio.QueueFull: + self.logger.warning(f"Event queue full for priority {priority.name}") + return False + + except Exception as e: + self.logger.error(f"Error emitting event {event_type}: {e}") + return False + + def subscribe( + self, + event_type: str, + callback: Callable, + filter_func: Optional[Callable] = None, + once: bool = False + ) -> EventSubscription: + """ + Subscribe to events of a specific type. + + Args: + event_type: Type of events to subscribe to (use "*" for all events) + callback: Function to call when event occurs + filter_func: Optional filter function to apply before calling callback + once: If True, subscription is automatically removed after first event + + Returns: + EventSubscription: The created subscription object + """ + subscription = EventSubscription( + callback=callback, + event_type=event_type, + filter_func=filter_func, + once=once + ) + + if event_type not in self._subscriptions: + self._subscriptions[event_type] = [] + + self._subscriptions[event_type].append(subscription) + self._weak_subscriptions.add(subscription) + self._stats["subscribers_count"] += 1 + + self.logger.debug(f"New subscription for {event_type}") + return subscription + + def unsubscribe(self, event_type: str, callback: Callable): + """ + Unsubscribe from events. + + Args: + event_type: Type of events to unsubscribe from + callback: The callback function to remove + """ + if event_type in self._subscriptions: + self._subscriptions[event_type] = [ + sub for sub in self._subscriptions[event_type] + if sub.callback != callback + ] + + # Clean up empty subscription lists + if not self._subscriptions[event_type]: + del self._subscriptions[event_type] + + self._stats["subscribers_count"] -= 1 + self.logger.debug(f"Unsubscribed from {event_type}") + + def once( + self, + event_type: str, + callback: Callable, + filter_func: Optional[Callable] = None + ) -> EventSubscription: + """ + Subscribe to an event type but only receive it once. + + Args: + event_type: Type of events to subscribe to + callback: Function to call when event occurs + filter_func: Optional filter function + + Returns: + EventSubscription: The created subscription object + """ + return self.subscribe(event_type, callback, filter_func, once=True) + + async def wait_for_event( + self, + event_type: str, + timeout: Optional[float] = None, + filter_func: Optional[Callable] = None + ) -> Optional[Event]: + """ + Wait for a specific event to occur. + + Args: + event_type: Type of event to wait for + timeout: Maximum time to wait in seconds + filter_func: Optional filter function + + Returns: + Event or None if timeout occurred + """ + future = asyncio.Future() + + def callback(event: Event): + if not future.done(): + future.set_result(event) + + subscription = self.subscribe(event_type, callback, filter_func, once=True) + + try: + return await asyncio.wait_for(future, timeout=timeout) + except asyncio.TimeoutError: + self.unsubscribe(event_type, callback) + return None + + def get_stats(self) -> Dict[str, Any]: + """Get event bus statistics.""" + return { + **self._stats, + "queue_sizes": { + priority.name: queue.qsize() + for priority, queue in self._event_queues.items() + }, + "subscription_types": list(self._subscriptions.keys()), + "active_subscriptions": sum(len(subs) for subs in self._subscriptions.values()) + } + + def get_recent_events(self, limit: int = 10) -> List[Event]: + """Get recent events from history.""" + return self._event_history[-limit:] + + def clear_history(self): + """Clear event history.""" + self._event_history.clear() + + async def shutdown(self): + """Shutdown the event bus and cleanup resources.""" + self.logger.info("Shutting down event bus...") + + # Signal all processing tasks to stop + for priority, queue in self._event_queues.items(): + await queue.put(None) + + # Wait for processing tasks to complete + if self._processing_tasks: + await asyncio.gather(*self._processing_tasks, return_exceptions=True) + + # Clear all subscriptions + self._subscriptions.clear() + self._weak_subscriptions.clear() + + self.logger.info("Event bus shutdown complete") \ No newline at end of file diff --git a/backend/orchestration/orchestrator.py b/backend/orchestration/orchestrator.py index e69de29bb..096cc022c 100644 --- a/backend/orchestration/orchestrator.py +++ b/backend/orchestration/orchestrator.py @@ -0,0 +1,365 @@ +""" +Main orchestrator for coordinating multiple agents using LangGraph and LangChain. +Manages the flow of tasks between different specialized agents. +""" + +from typing import Dict, Any, List, Optional, TypedDict +from langchain_google_genai import ChatGoogleGenerativeAI +from langgraph.graph import StateGraph, END +from langgraph.prebuilt import ToolExecutor +from langchain.schema import BaseMessage +import logging +from enum import Enum + +from .event_bus import EventBus +from .task_queue import TaskQueue +from .agent_pool import AgentPool +from ..agents.base_agent import BaseAgent +from ..models.agent_models import AgentState, TaskRequest, TaskResponse + + +class AgentType(Enum): + PLANNER = "planner" + VOICE = "voice" + VISION = "vision" + CONTEXT = "context" + MEMORY = "memory" + INSIGHT = "insight" + RESPONSE = "response" + + +class OrchestratorState(TypedDict): + """State shared across all agents in the orchestration graph.""" + user_input: str + intent: Optional[str] + extracted_entities: Dict[str, Any] + context_data: Dict[str, Any] + memories: List[Dict[str, Any]] + insights: List[Dict[str, Any]] + response: Optional[str] + current_agent: Optional[str] + task_history: List[Dict[str, Any]] + error: Optional[str] + metadata: Dict[str, Any] + + +class LifeWitnessOrchestrator: + """ + Main orchestrator that coordinates multiple specialized agents using LangGraph. + """ + + def __init__(self, gemini_api_key: str, config: Dict[str, Any]): + self.gemini_api_key = gemini_api_key + self.config = config + self.logger = logging.getLogger(__name__) + + # Initialize core components + self.event_bus = EventBus() + self.task_queue = TaskQueue() + self.agent_pool = AgentPool(gemini_api_key, config) + + # Initialize LangChain Gemini model + self.llm = ChatGoogleGenerativeAI( + model="gemini-pro", + google_api_key=gemini_api_key, + temperature=0.3, + convert_system_message_to_human=True + ) + + # Build the orchestration graph + self.graph = self._build_orchestration_graph() + + def _build_orchestration_graph(self) -> StateGraph: + """Build the LangGraph state graph for agent orchestration.""" + workflow = StateGraph(OrchestratorState) + + # Add nodes for each agent type + workflow.add_node("planner", self._run_planner_agent) + workflow.add_node("voice_processor", self._run_voice_agent) + workflow.add_node("vision_processor", self._run_vision_agent) + workflow.add_node("context_gatherer", self._run_context_agent) + workflow.add_node("memory_manager", self._run_memory_agent) + workflow.add_node("insight_analyzer", self._run_insight_agent) + workflow.add_node("response_generator", self._run_response_agent) + workflow.add_node("error_handler", self._handle_error) + + # Define the workflow edges + workflow.set_entry_point("planner") + + # Conditional routing based on planner output + workflow.add_conditional_edges( + "planner", + self._route_after_planning, + { + "voice": "voice_processor", + "vision": "vision_processor", + "context": "context_gatherer", + "memory": "memory_manager", + "error": "error_handler" + } + ) + + # Sequential processing for multi-modal inputs + workflow.add_edge("voice_processor", "context_gatherer") + workflow.add_edge("vision_processor", "context_gatherer") + workflow.add_edge("context_gatherer", "memory_manager") + workflow.add_edge("memory_manager", "insight_analyzer") + workflow.add_edge("insight_analyzer", "response_generator") + workflow.add_edge("response_generator", END) + workflow.add_edge("error_handler", END) + + return workflow.compile() + + def _route_after_planning(self, state: OrchestratorState) -> str: + """Route to appropriate agent based on planner output.""" + try: + intent = state.get("intent", "") + + if state.get("error"): + return "error" + elif "voice" in intent.lower(): + return "voice" + elif "image" in intent.lower() or "photo" in intent.lower(): + return "vision" + elif "search" in intent.lower() or "remember" in intent.lower(): + return "memory" + else: + return "context" + + except Exception as e: + self.logger.error(f"Routing error: {e}") + return "error" + + async def _run_planner_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute the planner agent to analyze intent and plan tasks.""" + try: + agent = await self.agent_pool.get_agent(AgentType.PLANNER) + + task_request = TaskRequest( + task_type="plan", + input_data={"user_input": state["user_input"]}, + context=state.get("metadata", {}) + ) + + response = await agent.process_task(task_request) + + state["intent"] = response.result.get("intent") + state["extracted_entities"] = response.result.get("entities", {}) + state["current_agent"] = "planner" + + # Emit planning event + await self.event_bus.emit("agent.planning.completed", { + "intent": state["intent"], + "entities": state["extracted_entities"] + }) + + return state + + except Exception as e: + self.logger.error(f"Planner agent error: {e}") + state["error"] = str(e) + return state + + async def _run_voice_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute voice processing agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.VOICE) + + task_request = TaskRequest( + task_type="voice_process", + input_data={"audio_data": state.get("user_input")}, + context=state.get("context_data", {}) + ) + + response = await agent.process_task(task_request) + state["context_data"]["voice_analysis"] = response.result + state["current_agent"] = "voice" + + return state + + except Exception as e: + self.logger.error(f"Voice agent error: {e}") + state["error"] = str(e) + return state + + async def _run_vision_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute vision processing agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.VISION) + + task_request = TaskRequest( + task_type="vision_analyze", + input_data={"image_data": state.get("user_input")}, + context=state.get("context_data", {}) + ) + + response = await agent.process_task(task_request) + state["context_data"]["vision_analysis"] = response.result + state["current_agent"] = "vision" + + return state + + except Exception as e: + self.logger.error(f"Vision agent error: {e}") + state["error"] = str(e) + return state + + async def _run_context_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute context gathering agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.CONTEXT) + + task_request = TaskRequest( + task_type="gather_context", + input_data={ + "entities": state["extracted_entities"], + "intent": state["intent"] + }, + context=state.get("context_data", {}) + ) + + response = await agent.process_task(task_request) + state["context_data"].update(response.result) + state["current_agent"] = "context" + + return state + + except Exception as e: + self.logger.error(f"Context agent error: {e}") + state["error"] = str(e) + return state + + async def _run_memory_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute memory management agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.MEMORY) + + task_request = TaskRequest( + task_type="memory_operation", + input_data={ + "query": state["user_input"], + "entities": state["extracted_entities"], + "context": state["context_data"] + } + ) + + response = await agent.process_task(task_request) + state["memories"] = response.result.get("memories", []) + state["current_agent"] = "memory" + + return state + + except Exception as e: + self.logger.error(f"Memory agent error: {e}") + state["error"] = str(e) + return state + + async def _run_insight_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute insight analysis agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.INSIGHT) + + task_request = TaskRequest( + task_type="analyze_patterns", + input_data={ + "memories": state["memories"], + "context": state["context_data"] + } + ) + + response = await agent.process_task(task_request) + state["insights"] = response.result.get("insights", []) + state["current_agent"] = "insight" + + return state + + except Exception as e: + self.logger.error(f"Insight agent error: {e}") + state["error"] = str(e) + return state + + async def _run_response_agent(self, state: OrchestratorState) -> OrchestratorState: + """Execute response generation agent.""" + try: + agent = await self.agent_pool.get_agent(AgentType.RESPONSE) + + task_request = TaskRequest( + task_type="generate_response", + input_data={ + "user_input": state["user_input"], + "memories": state["memories"], + "insights": state["insights"], + "context": state["context_data"] + } + ) + + response = await agent.process_task(task_request) + state["response"] = response.result.get("response") + state["current_agent"] = "response" + + return state + + except Exception as e: + self.logger.error(f"Response agent error: {e}") + state["error"] = str(e) + return state + + async def _handle_error(self, state: OrchestratorState) -> OrchestratorState: + """Handle errors in the orchestration flow.""" + error_msg = state.get("error", "Unknown error occurred") + self.logger.error(f"Orchestration error: {error_msg}") + + state["response"] = f"I apologize, but I encountered an error: {error_msg}" + return state + + async def process_user_request(self, user_input: str, metadata: Dict[str, Any] = None) -> Dict[str, Any]: + """ + Main entry point for processing user requests through the orchestration graph. + """ + initial_state = OrchestratorState( + user_input=user_input, + intent=None, + extracted_entities={}, + context_data={}, + memories=[], + insights=[], + response=None, + current_agent=None, + task_history=[], + error=None, + metadata=metadata or {} + ) + + try: + # Execute the orchestration graph + final_state = await self.graph.ainvoke(initial_state) + + # Emit completion event + await self.event_bus.emit("orchestration.completed", { + "response": final_state.get("response"), + "agents_used": final_state.get("task_history", []) + }) + + return { + "response": final_state.get("response"), + "memories": final_state.get("memories", []), + "insights": final_state.get("insights", []), + "context": final_state.get("context_data", {}), + "success": final_state.get("error") is None + } + + except Exception as e: + self.logger.error(f"Orchestration failed: {e}") + return { + "response": f"I'm sorry, but I encountered an error processing your request: {str(e)}", + "memories": [], + "insights": [], + "context": {}, + "success": False + } + + async def shutdown(self): + """Cleanup resources.""" + await self.agent_pool.shutdown() + await self.task_queue.shutdown() + await self.event_bus.shutdown() \ No newline at end of file diff --git a/backend/orchestration/task_queue.py b/backend/orchestration/task_queue.py index e69de29bb..b3cef1a7e 100644 --- a/backend/orchestration/task_queue.py +++ b/backend/orchestration/task_queue.py @@ -0,0 +1,503 @@ +""" +Async task management system with priority queues, retry logic, and monitoring. +Handles distributed task execution across multiple agents. +""" + +import asyncio +import logging +from typing import Dict, List, Optional, Any, Callable, Union +from datetime import datetime, timedelta +from dataclasses import dataclass, field +from enum import Enum +import uuid +import json +from collections import defaultdict +import time + + +class TaskStatus(Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + RETRYING = "retrying" + + +class TaskPriority(Enum): + LOW = 1 + NORMAL = 2 + HIGH = 3 + URGENT = 4 + + +@dataclass +class Task: + """Represents a task in the queue.""" + id: str + task_type: str + data: Dict[str, Any] + callback: Optional[Callable] = None + priority: TaskPriority = TaskPriority.NORMAL + max_retries: int = 3 + retry_delay: float = 1.0 + timeout: Optional[float] = None + created_at: datetime = field(default_factory=datetime.now) + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + status: TaskStatus = TaskStatus.PENDING + retries: int = 0 + error: Optional[str] = None + result: Any = None + metadata: Dict[str, Any] = field(default_factory=dict) + dependencies: List[str] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + + +@dataclass +class TaskResult: + """Result of task execution.""" + task_id: str + success: bool + result: Any = None + error: Optional[str] = None + execution_time: float = 0.0 + metadata: Dict[str, Any] = field(default_factory=dict) + + +class TaskQueue: + """ + Async task queue with priority handling, retry logic, and dependency management. + """ + + def __init__( + self, + max_workers: int = 10, + max_queue_size: int = 1000, + default_timeout: float = 300.0 + ): + self.max_workers = max_workers + self.max_queue_size = max_queue_size + self.default_timeout = default_timeout + self.logger = logging.getLogger(__name__) + + # Priority queues for different task types + self._queues: Dict[TaskPriority, asyncio.Queue] = { + priority: asyncio.Queue(maxsize=max_queue_size) + for priority in TaskPriority + } + + # Task registry for tracking all tasks + self._tasks: Dict[str, Task] = {} + self._task_results: Dict[str, TaskResult] = {} + + # Worker management + self._workers: List[asyncio.Task] = [] + self._worker_semaphore = asyncio.Semaphore(max_workers) + self._shutdown_event = asyncio.Event() + + # Dependency tracking + self._task_dependencies: Dict[str, List[str]] = defaultdict(list) + self._dependent_tasks: Dict[str, List[str]] = defaultdict(list) + + # Statistics and monitoring + self._stats = { + "tasks_submitted": 0, + "tasks_completed": 0, + "tasks_failed": 0, + "tasks_cancelled": 0, + "total_execution_time": 0.0, + "average_execution_time": 0.0 + } + + # Performance metrics + self._performance_history: List[Dict[str, Any]] = [] + self._max_history_size = 1000 + + # Start worker pool + self._start_workers() + + def _start_workers(self): + """Start the worker pool for processing tasks.""" + for i in range(self.max_workers): + worker = asyncio.create_task(self._worker(f"worker-{i}")) + self._workers.append(worker) + + async def _worker(self, worker_id: str): + """Worker coroutine that processes tasks from priority queues.""" + self.logger.debug(f"Started worker {worker_id}") + + while not self._shutdown_event.is_set(): + try: + # Get next task from highest priority queue + task = await self._get_next_task() + if task is None: + continue + + # Acquire worker semaphore + async with self._worker_semaphore: + await self._execute_task(task, worker_id) + + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"Worker {worker_id} error: {e}") + await asyncio.sleep(1) # Brief pause before retrying + + self.logger.debug(f"Worker {worker_id} stopped") + + async def _get_next_task(self) -> Optional[Task]: + """Get the next task to execute, checking dependencies.""" + # Check queues in priority order (highest first) + for priority in sorted(TaskPriority, key=lambda x: x.value, reverse=True): + queue = self._queues[priority] + + if not queue.empty(): + try: + task = await asyncio.wait_for(queue.get(), timeout=1.0) + + # Check if dependencies are satisfied + if self._are_dependencies_satisfied(task): + return task + else: + # Re-queue if dependencies not met + await queue.put(task) + + except asyncio.TimeoutError: + continue + + return None + + def _are_dependencies_satisfied(self, task: Task) -> bool: + """Check if all task dependencies have completed successfully.""" + if not task.dependencies: + return True + + for dep_id in task.dependencies: + if dep_id not in self._task_results: + return False + + result = self._task_results[dep_id] + if not result.success: + # Dependency failed, mark this task as failed too + task.status = TaskStatus.FAILED + task.error = f"Dependency {dep_id} failed" + return False + + return True + + async def _execute_task(self, task: Task, worker_id: str): + """Execute a single task with error handling and retry logic.""" + task.status = TaskStatus.RUNNING + task.started_at = datetime.now() + + start_time = time.time() + + try: + # Apply timeout if specified + timeout = task.timeout or self.default_timeout + + # Execute the task callback + if task.callback: + if asyncio.iscoroutinefunction(task.callback): + result = await asyncio.wait_for( + task.callback(task.data), + timeout=timeout + ) + else: + result = await asyncio.wait_for( + asyncio.get_event_loop().run_in_executor( + None, task.callback, task.data + ), + timeout=timeout + ) + else: + result = task.data # No callback, just return data + + # Task completed successfully + execution_time = time.time() - start_time + task.status = TaskStatus.COMPLETED + task.completed_at = datetime.now() + task.result = result + + # Store result + task_result = TaskResult( + task_id=task.id, + success=True, + result=result, + execution_time=execution_time, + metadata={"worker_id": worker_id} + ) + self._task_results[task.id] = task_result + + # Update statistics + self._stats["tasks_completed"] += 1 + self._stats["total_execution_time"] += execution_time + self._stats["average_execution_time"] = ( + self._stats["total_execution_time"] / self._stats["tasks_completed"] + ) + + # Process dependent tasks + await self._process_dependent_tasks(task.id) + + self.logger.debug(f"Task {task.id} completed by {worker_id} in {execution_time:.2f}s") + + except asyncio.TimeoutError: + await self._handle_task_failure(task, "Task timed out", worker_id) + except asyncio.CancelledError: + task.status = TaskStatus.CANCELLED + self._stats["tasks_cancelled"] += 1 + raise + except Exception as e: + await self._handle_task_failure(task, str(e), worker_id) + + async def _handle_task_failure(self, task: Task, error: str, worker_id: str): + """Handle task failure with retry logic.""" + task.error = error + task.retries += 1 + + if task.retries <= task.max_retries: + # Retry the task + task.status = TaskStatus.RETRYING + + # Apply exponential backoff + delay = task.retry_delay * (2 ** (task.retries - 1)) + await asyncio.sleep(delay) + + # Re-queue the task + await self._queues[task.priority].put(task) + + self.logger.warning( + f"Task {task.id} failed (attempt {task.retries}/{task.max_retries}), " + f"retrying in {delay}s: {error}" + ) + else: + # Task failed permanently + task.status = TaskStatus.FAILED + task.completed_at = datetime.now() + + execution_time = time.time() - task.started_at.timestamp() + task_result = TaskResult( + task_id=task.id, + success=False, + error=error, + execution_time=execution_time, + metadata={"worker_id": worker_id, "retries": task.retries} + ) + self._task_results[task.id] = task_result + + self._stats["tasks_failed"] += 1 + + self.logger.error(f"Task {task.id} failed permanently: {error}") + + async def _process_dependent_tasks(self, completed_task_id: str): + """Process tasks that were waiting for the completed task.""" + dependent_tasks = self._dependent_tasks.get(completed_task_id, []) + + for dep_task_id in dependent_tasks: + if dep_task_id in self._tasks: + dep_task = self._tasks[dep_task_id] + if dep_task.status == TaskStatus.PENDING and self._are_dependencies_satisfied(dep_task): + # Dependencies satisfied, queue the task for execution + await self._queues[dep_task.priority].put(dep_task) + + async def submit_task( + self, + task_type: str, + data: Dict[str, Any], + callback: Optional[Callable] = None, + priority: TaskPriority = TaskPriority.NORMAL, + max_retries: int = 3, + retry_delay: float = 1.0, + timeout: Optional[float] = None, + dependencies: List[str] = None, + tags: List[str] = None, + metadata: Dict[str, Any] = None + ) -> str: + """ + Submit a task to the queue. + + Args: + task_type: Type identifier for the task + data: Task input data + callback: Function to execute for the task + priority: Task priority level + max_retries: Maximum number of retry attempts + retry_delay: Base delay between retries (with exponential backoff) + timeout: Maximum execution time in seconds + dependencies: List of task IDs this task depends on + tags: Tags for task categorization + metadata: Additional metadata + + Returns: + str: Unique task ID + """ + task_id = str(uuid.uuid4()) + + task = Task( + id=task_id, + task_type=task_type, + data=data, + callback=callback, + priority=priority, + max_retries=max_retries, + retry_delay=retry_delay, + timeout=timeout, + dependencies=dependencies or [], + tags=tags or [], + metadata=metadata or {} + ) + + # Register task + self._tasks[task_id] = task + + # Set up dependency tracking + if task.dependencies: + for dep_id in task.dependencies: + self._dependent_tasks[dep_id].append(task_id) + self._task_dependencies[task_id].append(dep_id) + + # Queue task if dependencies are satisfied, otherwise it will be queued later + if self._are_dependencies_satisfied(task): + await self._queues[priority].put(task) + + self._stats["tasks_submitted"] += 1 + + self.logger.debug(f"Submitted task {task_id} with priority {priority.name}") + return task_id + + async def get_task_result(self, task_id: str, timeout: Optional[float] = None) -> TaskResult: + """ + Wait for a task to complete and return its result. + + Args: + task_id: ID of the task to wait for + timeout: Maximum time to wait in seconds + + Returns: + TaskResult: The task execution result + + Raises: + asyncio.TimeoutError: If timeout is exceeded + KeyError: If task ID is not found + """ + if task_id not in self._tasks: + raise KeyError(f"Task {task_id} not found") + + start_time = time.time() + + while task_id not in self._task_results: + if timeout and (time.time() - start_time) > timeout: + raise asyncio.TimeoutError(f"Timeout waiting for task {task_id}") + + await asyncio.sleep(0.1) # Small delay to avoid busy waiting + + return self._task_results[task_id] + + def get_task_status(self, task_id: str) -> Optional[TaskStatus]: + """Get the current status of a task.""" + if task_id in self._tasks: + return self._tasks[task_id].status + return None + + def get_task(self, task_id: str) -> Optional[Task]: + """Get task details by ID.""" + return self._tasks.get(task_id) + + async def cancel_task(self, task_id: str) -> bool: + """ + Cancel a task if it hasn't started executing yet. + + Args: + task_id: ID of the task to cancel + + Returns: + bool: True if task was cancelled, False otherwise + """ + if task_id not in self._tasks: + return False + + task = self._tasks[task_id] + + if task.status in [TaskStatus.PENDING, TaskStatus.RETRYING]: + task.status = TaskStatus.CANCELLED + self._stats["tasks_cancelled"] += 1 + + # Create cancelled result + self._task_results[task_id] = TaskResult( + task_id=task_id, + success=False, + error="Task was cancelled", + metadata={"cancelled_at": datetime.now().isoformat()} + ) + + return True + + return False + + def get_queue_stats(self) -> Dict[str, Any]: + """Get current queue statistics.""" + return { + "queue_sizes": { + priority.name: queue.qsize() + for priority, queue in self._queues.items() + }, + "total_tasks": len(self._tasks), + "active_workers": len([w for w in self._workers if not w.done()]), + "statistics": self._stats.copy() + } + + def get_tasks_by_status(self, status: TaskStatus) -> List[Task]: + """Get all tasks with a specific status.""" + return [task for task in self._tasks.values() if task.status == status] + + def get_tasks_by_tag(self, tag: str) -> List[Task]: + """Get all tasks with a specific tag.""" + return [task for task in self._tasks.values() if tag in task.tags] + + async def wait_for_completion(self, timeout: Optional[float] = None): + """Wait for all queued tasks to complete.""" + start_time = time.time() + + while True: + # Check if all queues are empty and no tasks are running + all_empty = all(queue.empty() for queue in self._queues.values()) + no_running = not any( + task.status == TaskStatus.RUNNING + for task in self._tasks.values() + ) + + if all_empty and no_running: + break + + if timeout and (time.time() - start_time) > timeout: + raise asyncio.TimeoutError("Timeout waiting for task completion") + + await asyncio.sleep(0.5) + + async def shutdown(self, timeout: float = 30.0): + """Shutdown the task queue and cleanup resources.""" + self.logger.info("Shutting down task queue...") + + # Signal shutdown + self._shutdown_event.set() + + # Cancel all pending tasks + for task in self._tasks.values(): + if task.status in [TaskStatus.PENDING, TaskStatus.RETRYING]: + await self.cancel_task(task.id) + + # Cancel all workers + for worker in self._workers: + worker.cancel() + + # Wait for workers to finish + try: + await asyncio.wait_for( + asyncio.gather(*self._workers, return_exceptions=True), + timeout=timeout + ) + except asyncio.TimeoutError: + self.logger.warning("Some workers did not shutdown gracefully") + + self.logger.info("Task queue shutdown complete") \ No newline at end of file From 0b85a3e132f55e8861317e9335e4c9e4e000a796 Mon Sep 17 00:00:00 2001 From: Likitha Date: Fri, 25 Jul 2025 19:28:25 -0400 Subject: [PATCH 15/25] orchestration working code --- .../__pycache__/context_agent.cpython-313.pyc | Bin 0 -> 188 bytes .../__pycache__/agent_pool.cpython-313.pyc | Bin 0 -> 24497 bytes .../__pycache__/event_bus.cpython-313.pyc | Bin 0 -> 20737 bytes .../__pycache__/orchestrator.cpython-313.pyc | Bin 0 -> 19442 bytes .../__pycache__/task_queue.cpython-313.pyc | Bin 0 -> 20115 bytes backend/orchestration/agent_pool.py | 805 +++++++++--------- backend/orchestration/event_bus.py | 610 +++++++------ backend/orchestration/orchestrator.py | 659 ++++++++------ backend/orchestration/task_queue.py | 746 +++++++--------- backend/orchestration/test_orchestrator.py | 273 ++++++ backend/requirements.txt | 6 + .../gemini_service.cpython-313.pyc | Bin 0 -> 191 bytes 12 files changed, 1653 insertions(+), 1446 deletions(-) create mode 100644 backend/agents/__pycache__/context_agent.cpython-313.pyc create mode 100644 backend/orchestration/__pycache__/agent_pool.cpython-313.pyc create mode 100644 backend/orchestration/__pycache__/event_bus.cpython-313.pyc create mode 100644 backend/orchestration/__pycache__/orchestrator.cpython-313.pyc create mode 100644 backend/orchestration/__pycache__/task_queue.cpython-313.pyc create mode 100644 backend/orchestration/test_orchestrator.py create mode 100644 backend/services/__pycache__/gemini_service.cpython-313.pyc diff --git a/backend/agents/__pycache__/context_agent.cpython-313.pyc b/backend/agents/__pycache__/context_agent.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f8e3c38adb4026f5b1595e718d4e1d3d4b4249f GIT binary patch literal 188 zcmey&%ge<81X0hLGeGoX5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa|L!erR!OQL%nb zW_D&tMq+kpc4=N>QDQ-gjEsy$%s>_Z DxiK@- literal 0 HcmV?d00001 diff --git a/backend/orchestration/__pycache__/agent_pool.cpython-313.pyc b/backend/orchestration/__pycache__/agent_pool.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c6f2e6bff93c5c331384855778174a19936198f GIT binary patch literal 24497 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 Np$6ac5uA(x|9?-#E@l7# literal 0 HcmV?d00001 diff --git a/backend/orchestration/__pycache__/event_bus.cpython-313.pyc b/backend/orchestration/__pycache__/event_bus.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f6d09b8d76c03815d737cfdc77574995d566cd4 GIT binary patch literal 20737 zcmdUXdvF`)o!Bn0i{}y`2)u4xMxj6tn=Ip^Zeod4(?B`Vc9 zm+AEP`xc9rAlqp&o%SQ~+i$Ivf zOx05YCD2DH5wDt~^sq+M3~NR0Fe5UipF7+Xd?H_QS-1_ zG?O@clpD?y^GKXNY7s4@U8`uFu#EER!&BE<3esy+_i9Yg?WK9lbE+w+E7Y;~K(8@~ zg%ie7s+JP;<&I@@R2nFSoSUF)A?7b%`%3BKMsL|fU!_zL%U&XjkFKSaQUrlbM47Usz_21*EX79SEJ54MW~w&^sCQ%ZB6U!hyMvHz;cbf7qpw zjZ@yR*BA6klFUvA{J|*~EgK-s9}dj=A#R@XPkZNs;VEbmZrPy!DSyzH)+dTd+-zTi z$ji`kkrpUXBhaE2c7sM_CRmtPEzAkCmlkys`U!);k~z?cMslaeJ1jL}B4x}I9Hi%U zYsicl1Vf^*LT(*nrfdDeW(5K1TSudb}~56 zuPU zkayPa@yI5RXLfFCK8SJ7<9Tx48%*R_J)Y@+D20Q8kUumB*?Ny>YR>2Jh)AczN(6QQ zG8FWNLUUmdXuUD00L^IYhCrdR0&b|7O<>L3Bb_Fn_YL`y_Up-9jQjKEH?-Hf0^ zGIR!zQ_us7u@eRb^#nu4ol!7#>jWTpK(dSiqG+ll<}m^gKFBi(mIZpss?)u+?Q_a@WfSwP~^ke7GglfY?j$8n@KT)XDsy znnRuBgGnr(p~M2nxT+D`;X;}L9A+eca5|MDVM9F6ll_9*JP-A$4=nD6WM)3SbUK8$PVOR@_n z@dsNvLF1-7=-+$kYH}?du#>cfvnmTWbo`-*j~#-g69-`&;ywT)Vjn=4R#_5~jJz@v zDY)cBL$h+#{nr7qo3J*DNI6%bcB7!;%A*lm{im&iH~`gU)-Q^{5YuPg2Tn|l<;HFg+A!~uFn-zWsG^)tC7B~>8O*Sj5aA>$9B?Ou>O#^qWJ^hiX(k}jvagG9ujAUM3umaBQVZ%EJ-I^ zB#$gfCe=b*2?l2qG%F{rB(srN-5*zAJw7k47(gaIHy4up|DT~tRrYdZlC3;kj1
    hzkrKyU!>qS3MyaqEjPU~8?m)~8d8W4VefVV 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 ip%`vyA*LPJs`=l3agIj- literal 0 HcmV?d00001 diff --git a/backend/orchestration/__pycache__/orchestrator.cpython-313.pyc b/backend/orchestration/__pycache__/orchestrator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4146cba7dfd3a7d2b268fbe80db3dc464516239 GIT binary patch literal 19442 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 literal 0 HcmV?d00001 diff --git a/backend/orchestration/__pycache__/task_queue.cpython-313.pyc b/backend/orchestration/__pycache__/task_queue.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc66815c2ff1c86ae1234d4fb4b70d130f539fd1 GIT binary patch literal 20115 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 literal 0 HcmV?d00001 diff --git a/backend/orchestration/agent_pool.py b/backend/orchestration/agent_pool.py index 6a5294afe..95973c2e1 100644 --- a/backend/orchestration/agent_pool.py +++ b/backend/orchestration/agent_pool.py @@ -1,453 +1,444 @@ -""" -Agent pool management system for maintaining and distributing agent instances. -Handles agent lifecycle, load balancing, and resource management. -""" - import asyncio -import logging -from typing import Dict, List, Optional, Any, Type -from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Type from dataclasses import dataclass, field -from enum import Enum -import weakref +from datetime import datetime from collections import defaultdict -import importlib - -from ..agents.base_agent import BaseAgent -from ..agents.planner_agent import PlannerAgent -from ..agents.voice_agent import VoiceAgent -from ..agents.vision_agent import VisionAgent -from ..agents.context_agent import ContextAgent -from ..agents.memory_agent import MemoryAgent -from ..agents.insight_agent import InsightAgent -from ..agents.response_agent import ResponseAgent - +import logging +from abc import ABC, abstractmethod +import uuid -class AgentType(Enum): - PLANNER = "planner" - VOICE = "voice" - VISION = "vision" - CONTEXT = "context" - MEMORY = "memory" - INSIGHT = "insight" - RESPONSE = "response" +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) @dataclass class AgentInstance: - """Represents an agent instance in the pool.""" - id: str - agent_type: AgentType - agent: BaseAgent + """Represents an instance of an agent in the pool""" + id: str = field(default_factory=lambda: str(uuid.uuid4())) + agent_type: str = "" + agent: Optional[Any] = None # The actual agent instance + status: str = "available" # available, busy, error created_at: datetime = field(default_factory=datetime.now) - last_used: datetime = field(default_factory=datetime.now) + last_used_at: Optional[datetime] = None usage_count: int = 0 - is_busy: bool = False current_task_id: Optional[str] = None - metadata: Dict[str, Any] = field(default_factory=dict) + memory: Optional[Dict[str, Any]] = None -@dataclass -class PoolStats: - """Statistics for the agent pool.""" - total_agents: int - active_agents: int - idle_agents: int - agent_counts_by_type: Dict[str, int] - average_usage: float - total_requests: int - pool_efficiency: float +class BasePooledAgent(ABC): + """Base class for agents that can be pooled""" + + def __init__(self, agent_id: str): + self.agent_id = agent_id + self.created_at = datetime.now() + self.usage_count = 0 + + @abstractmethod + async def initialize(self) -> None: + """Initialize the agent (load models, etc.)""" + pass + + @abstractmethod + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Process a request""" + pass + + @abstractmethod + async def cleanup(self) -> None: + """Cleanup resources""" + pass + + async def reset(self) -> None: + """Reset the agent state between uses""" + pass -class AgentPool: - """ - Manages a pool of agent instances with load balancing and lifecycle management. - """ +class MockVoiceAgent(BasePooledAgent): + """Mock voice agent for testing""" - def __init__( - self, - gemini_api_key: str, - config: Dict[str, Any], - min_instances_per_type: int = 1, - max_instances_per_type: int = 5, - idle_timeout: int = 300, # 5 minutes - cleanup_interval: int = 60 # 1 minute - ): - self.gemini_api_key = gemini_api_key - self.config = config - self.min_instances_per_type = min_instances_per_type - self.max_instances_per_type = max_instances_per_type - self.idle_timeout = idle_timeout - self.cleanup_interval = cleanup_interval - - self.logger = logging.getLogger(__name__) - - # Agent registry and pools - self._agents: Dict[str, AgentInstance] = {} - self._agent_pools: Dict[AgentType, List[str]] = defaultdict(list) - self._agent_classes: Dict[AgentType, Type[BaseAgent]] = { - AgentType.PLANNER: PlannerAgent, - AgentType.VOICE: VoiceAgent, - AgentType.VISION: VisionAgent, - AgentType.CONTEXT: ContextAgent, - AgentType.MEMORY: MemoryAgent, - AgentType.INSIGHT: InsightAgent, - AgentType.RESPONSE: ResponseAgent, - } - - # Load balancing and monitoring - self._round_robin_counters: Dict[AgentType, int] = defaultdict(int) - self._request_queue: Dict[AgentType, asyncio.Queue] = { - agent_type: asyncio.Queue() for agent_type in AgentType + async def initialize(self) -> None: + logger.info(f"Initializing MockVoiceAgent {self.agent_id}") + await asyncio.sleep(0.1) # Simulate initialization + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + audio = data.get("audio_data", "") + # Simulate processing + await asyncio.sleep(0.5) + return { + "transcription": f"Transcribed audio (agent {self.agent_id})", + "confidence": 0.95 } - # Statistics - self._stats = { - "total_requests": 0, - "successful_requests": 0, - "failed_requests": 0, - "agents_created": 0, - "agents_destroyed": 0, + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockVoiceAgent {self.agent_id}") + + +class MockPlannerAgent(BasePooledAgent): + """Mock planner agent for testing""" + + async def initialize(self) -> None: + logger.info(f"Initializing MockPlannerAgent {self.agent_id}") + self.memory = {} # Simple dict instead of ConversationBufferMemory + await asyncio.sleep(0.1) + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + transcription = data.get("transcription", "") + # Simulate planning + await asyncio.sleep(0.3) + return { + "plan": { + "steps": ["analyze", "search", "synthesize", "respond"], + "intent": "information_request", + "complexity": "medium" + } } - # Background tasks - self._cleanup_task: Optional[asyncio.Task] = None - self._monitoring_task: Optional[asyncio.Task] = None - self._shutdown_event = asyncio.Event() + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockPlannerAgent {self.agent_id}") - # Weak references to avoid memory leaks - self._weak_agents = weakref.WeakSet() - - # Initialize the pool - asyncio.create_task(self._initialize_pool()) + async def reset(self) -> None: + """Reset memory between uses""" + if self.memory: + self.memory.clear() + + +class MockInsightAgent(BasePooledAgent): + """Mock insight agent for testing""" - async def _initialize_pool(self): - """Initialize the agent pool with minimum instances.""" - self.logger.info("Initializing agent pool...") - - # Create minimum instances for each agent type - for agent_type in AgentType: - for _ in range(self.min_instances_per_type): - await self._create_agent_instance(agent_type) - - # Start background tasks - self._cleanup_task = asyncio.create_task(self._cleanup_loop()) - self._monitoring_task = asyncio.create_task(self._monitoring_loop()) + async def initialize(self) -> None: + logger.info(f"Initializing MockInsightAgent {self.agent_id}") + await asyncio.sleep(0.1) + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + context = data.get("context", {}) + # Simulate insight generation + await asyncio.sleep(0.4) + return { + "insights": [ + {"type": "pattern", "description": "User prefers concise answers"}, + {"type": "recommendation", "description": "Use bullet points"} + ] + } - self.logger.info(f"Agent pool initialized with {len(self._agents)} agents") - - async def _create_agent_instance(self, agent_type: AgentType) -> str: - """Create a new agent instance of the specified type.""" - try: - agent_class = self._agent_classes[agent_type] - - # Create agent with configuration - agent_config = self.config.get(agent_type.value, {}) - agent_config["gemini_api_key"] = self.gemini_api_key - - agent = agent_class(**agent_config) - await agent.initialize() - - # Create instance wrapper - instance_id = f"{agent_type.value}_{len(self._agents)}" - instance = AgentInstance( - id=instance_id, - agent_type=agent_type, - agent=agent, - metadata={"config": agent_config} - ) - - # Register instance - self._agents[instance_id] = instance - self._agent_pools[agent_type].append(instance_id) - self._weak_agents.add(instance) - - self._stats["agents_created"] += 1 - - self.logger.debug(f"Created {agent_type.value} agent: {instance_id}") - return instance_id - - except Exception as e: - self.logger.error(f"Failed to create {agent_type.value} agent: {e}") - raise + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockInsightAgent {self.agent_id}") + + +class AgentPool: + """Pool manager for LangChain agents""" - async def _destroy_agent_instance(self, instance_id: str): - """Destroy an agent instance and cleanup resources.""" - if instance_id not in self._agents: - return + def __init__(self): + self.pools: Dict[str, List[AgentInstance]] = defaultdict(list) + self.agent_configs: Dict[str, Dict[str, Any]] = {} + self.agent_classes: Dict[str, Type[BasePooledAgent]] = {} + self.min_pool_size: Dict[str, int] = {} + self.max_pool_size: Dict[str, int] = {} + self._lock = asyncio.Lock() + self._condition = asyncio.Condition() + self.statistics: Dict[str, Dict[str, Any]] = defaultdict(lambda: { + "total_created": 0, + "total_acquired": 0, + "total_released": 0, + "current_busy": 0, + "current_available": 0 + }) + + # Register default agent types + self._register_default_agents() + + def _register_default_agents(self): + """Register default agent types""" + self.register_agent_type( + "voice_agent", + MockVoiceAgent, + min_size=1, + max_size=3 + ) + self.register_agent_type( + "planner_agent", + MockPlannerAgent, + min_size=2, + max_size=5 + ) + self.register_agent_type( + "insight_agent", + MockInsightAgent, + min_size=1, + max_size=3 + ) - instance = self._agents[instance_id] + def register_agent_type( + self, + agent_type: str, + agent_class: Type[BasePooledAgent], + min_size: int = 1, + max_size: int = 5, + config: Optional[Dict[str, Any]] = None + ): + """Register a new agent type""" + self.agent_classes[agent_type] = agent_class + self.min_pool_size[agent_type] = min_size + self.max_pool_size[agent_type] = max_size + self.agent_configs[agent_type] = config or {} + logger.info(f"Registered agent type: {agent_type}") + + async def initialize(self): + """Initialize all agent pools""" + logger.info("Initializing agent pools...") + + # Create minimum number of agents for each type + for agent_type in self.agent_classes: + min_size = self.min_pool_size[agent_type] + for _ in range(min_size): + await self._create_agent(agent_type) + + logger.info("Agent pools initialized") - try: - # Cleanup agent resources - await instance.agent.cleanup() - - # Remove from pools - self._agent_pools[instance.agent_type].remove(instance_id) - del self._agents[instance_id] - - self._stats["agents_destroyed"] += 1 + async def _create_agent(self, agent_type: str) -> AgentInstance: + """Create a new agent instance""" + if agent_type not in self.agent_classes: + raise ValueError(f"Unknown agent type: {agent_type}") - self.logger.debug(f"Destroyed agent instance: {instance_id}") - - except Exception as e: - self.logger.error(f"Error destroying agent {instance_id}: {e}") - - async def get_agent(self, agent_type: AgentType) -> BaseAgent: - """ - Get an available agent instance of the specified type. + agent_class = self.agent_classes[agent_type] + agent_id = str(uuid.uuid4()) + + # Create the agent + agent = agent_class(agent_id) + await agent.initialize() + + # Create instance wrapper + instance = AgentInstance( + id=agent_id, + agent_type=agent_type, + agent=agent, + status="available" + ) - Args: - agent_type: Type of agent to retrieve + # Add to pool + async with self._lock: + self.pools[agent_type].append(instance) + self.statistics[agent_type]["total_created"] += 1 + self.statistics[agent_type]["current_available"] += 1 - Returns: - BaseAgent: An available agent instance - - Raises: - RuntimeError: If no agents are available and pool is at capacity - """ - self._stats["total_requests"] += 1 + logger.info(f"Created new {agent_type} agent: {agent_id}") + return instance - try: - # Get an available instance - instance = await self._get_available_instance(agent_type) + async def acquire(self, agent_type: str, timeout: float = 30.0) -> Any: + """Acquire an agent from the pool""" + if agent_type not in self.agent_classes: + raise ValueError(f"Unknown agent type: {agent_type}") - if instance: - # Mark as busy and update usage - instance.is_busy = True - instance.last_used = datetime.now() - instance.usage_count += 1 - - self._stats["successful_requests"] += 1 - return instance.agent - else: - # Try to create a new instance if under capacity - if len(self._agent_pools[agent_type]) < self.max_instances_per_type: - instance_id = await self._create_agent_instance(agent_type) - instance = self._agents[instance_id] - instance.is_busy = True - instance.last_used = datetime.now() - instance.usage_count += 1 + start_time = asyncio.get_event_loop().time() + + async with self._condition: + while True: + # Check for available agents + available_agent = None + async with self._lock: + pool = self.pools[agent_type] + for instance in pool: + if instance.status == "available": + instance.status = "busy" + instance.last_used_at = datetime.now() + instance.usage_count += 1 + available_agent = instance + self.statistics[agent_type]["total_acquired"] += 1 + self.statistics[agent_type]["current_busy"] += 1 + self.statistics[agent_type]["current_available"] -= 1 + break + + if available_agent: + logger.info(f"Acquired {agent_type} agent: {available_agent.id}") + return available_agent.agent + + # Check if we can create a new agent + async with self._lock: + current_size = len(self.pools[agent_type]) + max_size = self.max_pool_size[agent_type] - self._stats["successful_requests"] += 1 + if current_size < max_size: + # Create a new agent + instance = await self._create_agent(agent_type) + async with self._lock: + instance.status = "busy" + instance.last_used_at = datetime.now() + instance.usage_count += 1 + self.statistics[agent_type]["total_acquired"] += 1 + self.statistics[agent_type]["current_busy"] += 1 + self.statistics[agent_type]["current_available"] -= 1 + logger.info(f"Created and acquired new {agent_type} agent: {instance.id}") return instance.agent + + # Wait for an agent to become available + elapsed = asyncio.get_event_loop().time() - start_time + if elapsed >= timeout: + raise TimeoutError(f"Timeout waiting for {agent_type} agent") + + try: + await asyncio.wait_for( + self._condition.wait(), + timeout=timeout - elapsed + ) + except asyncio.TimeoutError: + raise TimeoutError(f"Timeout waiting for {agent_type} agent") + + async def release(self, agent_type: str, agent: Any) -> None: + """Release an agent back to the pool""" + if not isinstance(agent, BasePooledAgent): + logger.warning(f"Attempted to release non-pooled agent: {agent}") + return + + async with self._lock: + pool = self.pools[agent_type] + for instance in pool: + if instance.agent == agent: + # Reset the agent + await agent.reset() + + # Mark as available + instance.status = "available" + instance.current_task_id = None + self.statistics[agent_type]["total_released"] += 1 + self.statistics[agent_type]["current_busy"] -= 1 + self.statistics[agent_type]["current_available"] += 1 + + logger.info(f"Released {agent_type} agent: {instance.id}") + break + + # Notify waiting tasks + async with self._condition: + self._condition.notify() + + async def get_pool_stats(self) -> Dict[str, Any]: + """Get statistics for all pools""" + stats = {} + async with self._lock: + for agent_type, pool in self.pools.items(): + type_stats = self.statistics[agent_type].copy() + type_stats["pool_size"] = len(pool) + type_stats["min_size"] = self.min_pool_size[agent_type] + type_stats["max_size"] = self.max_pool_size[agent_type] + + # Calculate average usage + if pool: + total_usage = sum(instance.usage_count for instance in pool) + type_stats["average_usage"] = total_usage / len(pool) else: - # Wait for an agent to become available - return await self._wait_for_available_agent(agent_type) + type_stats["average_usage"] = 0 - except Exception as e: - self._stats["failed_requests"] += 1 - self.logger.error(f"Failed to get {agent_type.value} agent: {e}") - raise - - async def _get_available_instance(self, agent_type: AgentType) -> Optional[AgentInstance]: - """Get an available agent instance using round-robin load balancing.""" - pool = self._agent_pools[agent_type] - - if not pool: - return None - - # Round-robin selection starting from last counter position - start_idx = self._round_robin_counters[agent_type] % len(pool) - - for i in range(len(pool)): - idx = (start_idx + i) % len(pool) - instance_id = pool[idx] - instance = self._agents[instance_id] + stats[agent_type] = type_stats + + return stats + + async def cleanup(self): + """Cleanup all agent pools""" + logger.info("Cleaning up agent pools...") + + async with self._lock: + for agent_type, pool in self.pools.items(): + for instance in pool: + if instance.agent: + await instance.agent.cleanup() + + self.pools.clear() - if not instance.is_busy: - self._round_robin_counters[agent_type] = idx + 1 - return instance + logger.info("Agent pools cleaned up") - return None # All instances are busy - - async def _wait_for_available_agent(self, agent_type: AgentType, timeout: float = 30.0) -> BaseAgent: - """Wait for an agent to become available.""" - start_time = datetime.now() + async def health_check(self) -> Dict[str, Any]: + """Perform health check on all pools""" + health = { + "status": "healthy", + "pools": {}, + "issues": [] + } - while (datetime.now() - start_time).total_seconds() < timeout: - instance = await self._get_available_instance(agent_type) - if instance: - instance.is_busy = True - instance.last_used = datetime.now() - instance.usage_count += 1 - return instance.agent + async with self._lock: + for agent_type, pool in self.pools.items(): + pool_health = { + "size": len(pool), + "available": sum(1 for i in pool if i.status == "available"), + "busy": sum(1 for i in pool if i.status == "busy"), + "error": sum(1 for i in pool if i.status == "error") + } + + # Check for issues + if pool_health["error"] > 0: + health["issues"].append(f"{agent_type} has {pool_health['error']} agents in error state") + + if pool_health["size"] < self.min_pool_size[agent_type]: + health["issues"].append(f"{agent_type} pool size below minimum") + + health["pools"][agent_type] = pool_health + + if health["issues"]: + health["status"] = "degraded" - await asyncio.sleep(0.1) # Small delay before retrying - - raise TimeoutError(f"No {agent_type.value} agents available within timeout") - - async def release_agent(self, agent: BaseAgent): - """ - Release an agent back to the pool. - - Args: - agent: The agent instance to release - """ - # Find the instance wrapper - for instance in self._agents.values(): - if instance.agent is agent: - instance.is_busy = False - instance.current_task_id = None - self.logger.debug(f"Released agent: {instance.id}") - break - - async def _cleanup_loop(self): - """Background task to cleanup idle agents.""" - while not self._shutdown_event.is_set(): - try: - await self._cleanup_idle_agents() - await asyncio.sleep(self.cleanup_interval) - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in cleanup loop: {e}") + return health + + +# Test function +async def test_agent_pool(): + """Test the agent pool independently""" + pool = AgentPool() - async def _cleanup_idle_agents(self): - """Remove agents that have been idle for too long.""" - current_time = datetime.now() - agents_to_remove = [] - - for agent_type, pool in self._agent_pools.items(): - # Keep minimum instances - if len(pool) <= self.min_instances_per_type: - continue - - # Find idle agents - for instance_id in pool: - instance = self._agents[instance_id] - - if (not instance.is_busy and - (current_time - instance.last_used).total_seconds() > self.idle_timeout): - agents_to_remove.append(instance_id) - - # Remove idle agents - for instance_id in agents_to_remove: - await self._destroy_agent_instance(instance_id) + # Initialize pools + await pool.initialize() - async def _monitoring_loop(self): - """Background task for monitoring pool health and performance.""" - while not self._shutdown_event.is_set(): - try: - await self._update_pool_metrics() - await asyncio.sleep(30) # Update metrics every 30 seconds - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in monitoring loop: {e}") + # Get initial stats + stats = await pool.get_pool_stats() + print(f"Initial pool stats: {stats}") - async def _update_pool_metrics(self): - """Update pool performance metrics.""" - total_agents = len(self._agents) - active_agents = sum(1 for instance in self._agents.values() if instance.is_busy) - - if total_agents > 0: - pool_efficiency = active_agents / total_agents - else: - pool_efficiency = 0.0 - - # Log metrics if efficiency is low - if pool_efficiency < 0.3 and total_agents > self.min_instances_per_type: - self.logger.warning(f"Low pool efficiency: {pool_efficiency:.2%}") + # Test acquiring and releasing agents + agents = [] + agent_types = ["voice_agent", "planner_agent", "insight_agent"] - def get_pool_stats(self) -> PoolStats: - """Get current pool statistics.""" - total_agents = len(self._agents) - active_agents = sum(1 for instance in self._agents.values() if instance.is_busy) - idle_agents = total_agents - active_agents - - agent_counts = defaultdict(int) - total_usage = 0 - - for instance in self._agents.values(): - agent_counts[instance.agent_type.value] += 1 - total_usage += instance.usage_count - - average_usage = total_usage / total_agents if total_agents > 0 else 0 - pool_efficiency = active_agents / total_agents if total_agents > 0 else 0 - - return PoolStats( - total_agents=total_agents, - active_agents=active_agents, - idle_agents=idle_agents, - agent_counts_by_type=dict(agent_counts), - average_usage=average_usage, - total_requests=self._stats["total_requests"], - pool_efficiency=pool_efficiency - ) + # Acquire multiple agents + for agent_type in agent_types: + agent = await pool.acquire(agent_type) + agents.append((agent_type, agent)) + print(f"Acquired {agent_type}") + + # Use the agents + for agent_type, agent in agents: + result = await agent.process({"test": "data"}) + print(f"{agent_type} result: {result}") + + # Release agents + for agent_type, agent in agents: + await pool.release(agent_type, agent) + print(f"Released {agent_type}") + + # Test concurrent acquisition + async def acquire_and_use(pool, agent_type, duration): + agent = await pool.acquire(agent_type) + await asyncio.sleep(duration) + await pool.release(agent_type, agent) + + # Create concurrent tasks + tasks = [] + for i in range(10): + agent_type = agent_types[i % len(agent_types)] + task = asyncio.create_task(acquire_and_use(pool, agent_type, 0.5)) + tasks.append(task) + + # Wait for all tasks + await asyncio.gather(*tasks) - def get_agent_details(self, agent_type: Optional[AgentType] = None) -> List[Dict[str, Any]]: - """Get detailed information about agents in the pool.""" - agents = [] - - for instance in self._agents.values(): - if agent_type is None or instance.agent_type == agent_type: - agents.append({ - "id": instance.id, - "type": instance.agent_type.value, - "created_at": instance.created_at.isoformat(), - "last_used": instance.last_used.isoformat(), - "usage_count": instance.usage_count, - "is_busy": instance.is_busy, - "current_task_id": instance.current_task_id, - "metadata": instance.metadata - }) - - return agents + # Final stats + final_stats = await pool.get_pool_stats() + print(f"Final pool stats: {final_stats}") - async def scale_pool(self, agent_type: AgentType, target_count: int): - """ - Scale the pool for a specific agent type. - - Args: - agent_type: Type of agent to scale - target_count: Target number of instances - """ - current_count = len(self._agent_pools[agent_type]) - - if target_count > current_count: - # Scale up - for _ in range(target_count - current_count): - if len(self._agent_pools[agent_type]) < self.max_instances_per_type: - await self._create_agent_instance(agent_type) - else: - break - elif target_count < current_count: - # Scale down (but respect minimum) - target_count = max(target_count, self.min_instances_per_type) - instances_to_remove = current_count - target_count - - # Remove idle instances first - pool = self._agent_pools[agent_type] - for i in range(instances_to_remove): - for instance_id in pool[:]: # Copy to avoid modification during iteration - instance = self._agents[instance_id] - if not instance.is_busy: - await self._destroy_agent_instance(instance_id) - break + # Health check + health = await pool.health_check() + print(f"Health check: {health}") - async def health_check(self) -> Dict[str, Any]: - """Perform a health check on all agents in the pool.""" - results = { - "healthy_agents": 0, - "unhealthy_agents": 0, - "agent_health": {} - } - - for instance in self._agents.values(): - try: - # Perform health check on agent - health = await instance.agent.health_check() - results["agent_health"][instance.id] = health - - if health.get("status") == "healthy": - results["healthy_agents"] += 1 - else: - results["unhealthy_agents"] += 1 - - except Exception as e: - results["unhealthy_agents"] += 1 - results["agent_health"][instance.id] = { - "status": "error", - "error": str(e) - } \ No newline at end of file + # Cleanup + await pool.cleanup() + + +if __name__ == "__main__": + asyncio.run(test_agent_pool()) \ No newline at end of file diff --git a/backend/orchestration/event_bus.py b/backend/orchestration/event_bus.py index 9bad2195d..81727489f 100644 --- a/backend/orchestration/event_bus.py +++ b/backend/orchestration/event_bus.py @@ -1,373 +1,343 @@ -""" -Event-driven communication system for coordinating between agents and components. -Provides pub/sub messaging for loose coupling between system components. -""" - import asyncio -import logging -from typing import Dict, List, Callable, Any, Optional -from datetime import datetime +from typing import Dict, List, Any, Callable, Optional, Set from dataclasses import dataclass, field -from enum import Enum -import json -import weakref - +from datetime import datetime +from collections import defaultdict +import logging +import uuid -class EventPriority(Enum): - LOW = 1 - NORMAL = 2 - HIGH = 3 - CRITICAL = 4 +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) @dataclass class Event: - """Represents an event in the system.""" - event_type: str - data: Dict[str, Any] + """Represents an event in the system""" + id: str = field(default_factory=lambda: str(uuid.uuid4())) + type: str = "" + data: Dict[str, Any] = field(default_factory=dict) + source: str = "" timestamp: datetime = field(default_factory=datetime.now) - priority: EventPriority = EventPriority.NORMAL - source: Optional[str] = None - correlation_id: Optional[str] = None metadata: Dict[str, Any] = field(default_factory=dict) -@dataclass +class EventBusCallback: + """Callback handler that publishes events to the event bus""" + + def __init__(self, event_bus: 'EventBus', source: str = "langchain"): + self.event_bus = event_bus + self.source = source + + async def on_chain_start(self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs) -> None: + """Called when a chain starts running.""" + await self.event_bus.publish(Event( + type="CHAIN_STARTED", + data={"serialized": serialized, "inputs": inputs}, + source=self.source + )) + + async def on_chain_end(self, outputs: Dict[str, Any], **kwargs) -> None: + """Called when a chain ends running.""" + await self.event_bus.publish(Event( + type="CHAIN_COMPLETED", + data={"outputs": outputs}, + source=self.source + )) + + async def on_chain_error(self, error: Exception, **kwargs) -> None: + """Called when a chain errors.""" + await self.event_bus.publish(Event( + type="CHAIN_ERROR", + data={"error": str(error)}, + source=self.source + )) + + async def on_llm_start(self, serialized: Dict[str, Any], prompts: List[str], **kwargs) -> None: + """Called when LLM starts running.""" + await self.event_bus.publish(Event( + type="LLM_STARTED", + data={"prompts": prompts}, + source=self.source + )) + + async def on_llm_end(self, response: Any, **kwargs) -> None: + """Called when LLM ends running.""" + await self.event_bus.publish(Event( + type="LLM_COMPLETED", + data={"response": str(response)}, + source=self.source + )) + + class EventSubscription: - """Represents a subscription to an event type.""" - callback: Callable - event_type: str - filter_func: Optional[Callable] = None - once: bool = False - created_at: datetime = field(default_factory=datetime.now) + """Represents a subscription to events""" + + def __init__(self, event_type: str, handler: Callable, filter_fn: Optional[Callable] = None): + self.id = str(uuid.uuid4()) + self.event_type = event_type + self.handler = handler + self.filter_fn = filter_fn + self.created_at = datetime.now() + self.call_count = 0 class EventBus: - """ - Central event bus for managing pub/sub communication between agents. - Supports async event handling, filtering, and priority-based processing. - """ + """Event bus implementation for LangChain agents""" - def __init__(self, max_queue_size: int = 1000): - self.max_queue_size = max_queue_size - self.logger = logging.getLogger(__name__) + def __init__(self, max_event_history: int = 1000): + self.subscriptions: Dict[str, List[EventSubscription]] = defaultdict(list) + self.event_history: List[Event] = [] + self.max_event_history = max_event_history + self.active_handlers: Set[str] = set() + self._lock = asyncio.Lock() - # Event subscriptions organized by event type - self._subscriptions: Dict[str, List[EventSubscription]] = {} + def create_callback_handler(self, source: str = "langchain") -> EventBusCallback: + """Create a LangChain callback handler that publishes to this event bus""" + return EventBusCallback(self, source) - # Event queues organized by priority - self._event_queues: Dict[EventPriority, asyncio.Queue] = { - priority: asyncio.Queue(maxsize=max_queue_size) - for priority in EventPriority - } + async def subscribe(self, event_type: str, handler: Callable, filter_fn: Optional[Callable] = None) -> str: + """Subscribe to an event type""" + if not asyncio.iscoroutinefunction(handler): + raise ValueError("Handler must be an async function") + + subscription = EventSubscription(event_type, handler, filter_fn) - # Event processing tasks - self._processing_tasks: List[asyncio.Task] = [] + async with self._lock: + self.subscriptions[event_type].append(subscription) + + logger.info(f"Subscribed to {event_type} with handler {handler.__name__} (ID: {subscription.id})") + return subscription.id - # Event history for debugging and monitoring - self._event_history: List[Event] = [] - self._max_history_size = 1000 + async def unsubscribe(self, subscription_id: str) -> bool: + """Unsubscribe from events""" + async with self._lock: + for event_type, subs in self.subscriptions.items(): + for sub in subs: + if sub.id == subscription_id: + subs.remove(sub) + logger.info(f"Unsubscribed {subscription_id} from {event_type}") + return True + return False - # Statistics - self._stats = { - "events_published": 0, - "events_processed": 0, - "subscribers_count": 0, - "processing_errors": 0 - } - - # Weak references to avoid memory leaks - self._weak_subscriptions = weakref.WeakSet() + async def publish(self, event: Event) -> None: + """Publish an event to all subscribers""" + if isinstance(event, dict): + # Convert dict to Event object for backwards compatibility + event = Event( + type=event.get("type", "UNKNOWN"), + data=event.get("data", {}), + source=event.get("source", "unknown") + ) + + # Add to history + async with self._lock: + self.event_history.append(event) + # Trim history if needed + if len(self.event_history) > self.max_event_history: + self.event_history = self.event_history[-self.max_event_history:] + + logger.info(f"Publishing event: {event.type} from {event.source}") - # Start event processing - self._start_event_processing() - - def _start_event_processing(self): - """Start background tasks for processing events by priority.""" - for priority in EventPriority: - task = asyncio.create_task(self._process_events(priority)) - self._processing_tasks.append(task) - - async def _process_events(self, priority: EventPriority): - """Process events from a specific priority queue.""" - queue = self._event_queues[priority] + # Get subscribers for this event type + subscribers = self.subscriptions.get(event.type, []) - while True: - try: - event = await queue.get() - if event is None: # Shutdown signal - break - - await self._handle_event(event) - self._stats["events_processed"] += 1 - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error processing {priority.name} priority event: {e}") - self._stats["processing_errors"] += 1 - - async def _handle_event(self, event: Event): - """Handle a single event by notifying all relevant subscribers.""" - subscriptions = self._subscriptions.get(event.event_type, []) + # Also get wildcard subscribers + wildcard_subscribers = self.subscriptions.get("*", []) - # Also check for wildcard subscriptions - wildcard_subscriptions = self._subscriptions.get("*", []) - all_subscriptions = subscriptions + wildcard_subscriptions + all_subscribers = subscribers + wildcard_subscribers - # Process subscriptions concurrently + # Call all handlers concurrently tasks = [] - subscriptions_to_remove = [] - - for subscription in all_subscriptions: - try: - # Apply filter if present - if subscription.filter_func: - if not await self._apply_filter(subscription.filter_func, event): - continue - - # Create task for async callback - task = asyncio.create_task( - self._invoke_callback(subscription.callback, event) - ) - tasks.append(task) + for subscription in all_subscribers: + # Apply filter if present + if subscription.filter_fn and not subscription.filter_fn(event): + continue - # Mark one-time subscriptions for removal - if subscription.once: - subscriptions_to_remove.append(subscription) - - except Exception as e: - self.logger.error(f"Error preparing callback for {event.event_type}: {e}") - - # Wait for all callbacks to complete + # Create handler task + task = asyncio.create_task(self._call_handler(subscription, event)) + tasks.append(task) + + # Wait for all handlers to complete if tasks: - await asyncio.gather(*tasks, return_exceptions=True) + results = await asyncio.gather(*tasks, return_exceptions=True) + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.error(f"Handler error: {result}") + + async def _call_handler(self, subscription: EventSubscription, event: Event) -> None: + """Call a single event handler""" + handler_id = f"{subscription.id}-{event.id}" - # Remove one-time subscriptions - for subscription in subscriptions_to_remove: - self.unsubscribe(subscription.event_type, subscription.callback) + # Prevent duplicate handling + if handler_id in self.active_handlers: + return + + self.active_handlers.add(handler_id) - # Add to history - self._add_to_history(event) - - async def _apply_filter(self, filter_func: Callable, event: Event) -> bool: - """Apply filter function to determine if event should be processed.""" try: - if asyncio.iscoroutinefunction(filter_func): - return await filter_func(event) - else: - return filter_func(event) + logger.debug(f"Calling handler {subscription.handler.__name__} for event {event.type}") + subscription.call_count += 1 + await subscription.handler(event) + except Exception as e: - self.logger.error(f"Error applying event filter: {e}") - return False - - async def _invoke_callback(self, callback: Callable, event: Event): - """Invoke a callback function with error handling.""" - try: - if asyncio.iscoroutinefunction(callback): - await callback(event) + logger.error(f"Error in handler {subscription.handler.__name__}: {str(e)}") + # Publish error event + error_event = Event( + type="HANDLER_ERROR", + data={ + "original_event": event.type, + "handler": subscription.handler.__name__, + "error": str(e) + }, + source="event_bus" + ) + # Don't await to avoid recursion + asyncio.create_task(self.publish(error_event)) + + finally: + self.active_handlers.discard(handler_id) + + async def publish_batch(self, events: List[Event]) -> None: + """Publish multiple events""" + tasks = [self.publish(event) for event in events] + await asyncio.gather(*tasks) + + async def get_event_history(self, event_type: Optional[str] = None, limit: int = 100) -> List[Event]: + """Get event history, optionally filtered by type""" + async with self._lock: + if event_type: + filtered = [e for e in self.event_history if e.type == event_type] + return filtered[-limit:] else: - callback(event) - except Exception as e: - self.logger.error(f"Error in event callback for {event.event_type}: {e}") - - def _add_to_history(self, event: Event): - """Add event to history with size management.""" - self._event_history.append(event) + return self.event_history[-limit:] + + async def get_subscription_stats(self) -> Dict[str, Any]: + """Get statistics about subscriptions""" + stats = { + "total_subscriptions": sum(len(subs) for subs in self.subscriptions.values()), + "event_types": list(self.subscriptions.keys()), + "subscriptions_by_type": {} + } - # Trim history if it exceeds max size - if len(self._event_history) > self._max_history_size: - self._event_history = self._event_history[-self._max_history_size:] - - async def emit( - self, - event_type: str, - data: Dict[str, Any], - priority: EventPriority = EventPriority.NORMAL, - source: Optional[str] = None, - correlation_id: Optional[str] = None, - metadata: Dict[str, Any] = None - ) -> bool: - """ - Emit an event to the bus. + for event_type, subs in self.subscriptions.items(): + stats["subscriptions_by_type"][event_type] = { + "count": len(subs), + "handlers": [sub.handler.__name__ for sub in subs], + "total_calls": sum(sub.call_count for sub in subs) + } + + return stats - Args: - event_type: Type identifier for the event - data: Event payload data - priority: Event processing priority - source: Source identifier (optional) - correlation_id: Correlation ID for request tracking (optional) - metadata: Additional metadata (optional) + async def clear_event_history(self) -> None: + """Clear the event history""" + async with self._lock: + self.event_history.clear() - Returns: - bool: True if event was queued successfully - """ - try: + def create_typed_publisher(self, event_type: str, source: str) -> Callable: + """Create a publisher function for a specific event type""" + async def publisher(data: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None): event = Event( - event_type=event_type, + type=event_type, data=data, - priority=priority, source=source, - correlation_id=correlation_id, metadata=metadata or {} ) - - queue = self._event_queues[priority] - - # Try to put event in queue (non-blocking) - try: - queue.put_nowait(event) - self._stats["events_published"] += 1 - return True - except asyncio.QueueFull: - self.logger.warning(f"Event queue full for priority {priority.name}") - return False - - except Exception as e: - self.logger.error(f"Error emitting event {event_type}: {e}") - return False + await self.publish(event) + return publisher + + +# Global event bus instance for convenience +_global_event_bus: Optional[EventBus] = None + + +def get_event_bus() -> EventBus: + """Get the global event bus instance""" + global _global_event_bus + if _global_event_bus is None: + _global_event_bus = EventBus() + return _global_event_bus + + +# Test function +async def test_event_bus(): + """Test the event bus independently""" + event_bus = EventBus() - def subscribe( - self, - event_type: str, - callback: Callable, - filter_func: Optional[Callable] = None, - once: bool = False - ) -> EventSubscription: - """ - Subscribe to events of a specific type. - - Args: - event_type: Type of events to subscribe to (use "*" for all events) - callback: Function to call when event occurs - filter_func: Optional filter function to apply before calling callback - once: If True, subscription is automatically removed after first event - - Returns: - EventSubscription: The created subscription object - """ - subscription = EventSubscription( - callback=callback, - event_type=event_type, - filter_func=filter_func, - once=once - ) + # Test data collection + received_events = [] + + # Define test handlers + async def handler1(event: Event): + logger.info(f"Handler1 received: {event.type}") + received_events.append(("handler1", event)) - if event_type not in self._subscriptions: - self._subscriptions[event_type] = [] + async def handler2(event: Event): + logger.info(f"Handler2 received: {event.type}") + received_events.append(("handler2", event)) + await asyncio.sleep(0.1) # Simulate some work - self._subscriptions[event_type].append(subscription) - self._weak_subscriptions.add(subscription) - self._stats["subscribers_count"] += 1 + async def wildcard_handler(event: Event): + logger.info(f"Wildcard handler received: {event.type}") + received_events.append(("wildcard", event)) - self.logger.debug(f"New subscription for {event_type}") - return subscription + # Subscribe to events + sub1 = await event_bus.subscribe("TEST_EVENT", handler1) + sub2 = await event_bus.subscribe("TEST_EVENT", handler2) + sub3 = await event_bus.subscribe("ANOTHER_EVENT", handler1) + sub_wildcard = await event_bus.subscribe("*", wildcard_handler) - def unsubscribe(self, event_type: str, callback: Callable): - """ - Unsubscribe from events. - - Args: - event_type: Type of events to unsubscribe from - callback: The callback function to remove - """ - if event_type in self._subscriptions: - self._subscriptions[event_type] = [ - sub for sub in self._subscriptions[event_type] - if sub.callback != callback - ] - - # Clean up empty subscription lists - if not self._subscriptions[event_type]: - del self._subscriptions[event_type] - - self._stats["subscribers_count"] -= 1 - self.logger.debug(f"Unsubscribed from {event_type}") + # Publish events + await event_bus.publish(Event( + type="TEST_EVENT", + data={"message": "Hello from test 1"}, + source="test" + )) - def once( - self, - event_type: str, - callback: Callable, - filter_func: Optional[Callable] = None - ) -> EventSubscription: - """ - Subscribe to an event type but only receive it once. - - Args: - event_type: Type of events to subscribe to - callback: Function to call when event occurs - filter_func: Optional filter function - - Returns: - EventSubscription: The created subscription object - """ - return self.subscribe(event_type, callback, filter_func, once=True) + await event_bus.publish(Event( + type="ANOTHER_EVENT", + data={"message": "Another event"}, + source="test" + )) - async def wait_for_event( - self, - event_type: str, - timeout: Optional[float] = None, - filter_func: Optional[Callable] = None - ) -> Optional[Event]: - """ - Wait for a specific event to occur. - - Args: - event_type: Type of event to wait for - timeout: Maximum time to wait in seconds - filter_func: Optional filter function - - Returns: - Event or None if timeout occurred - """ - future = asyncio.Future() - - def callback(event: Event): - if not future.done(): - future.set_result(event) - - subscription = self.subscribe(event_type, callback, filter_func, once=True) - - try: - return await asyncio.wait_for(future, timeout=timeout) - except asyncio.TimeoutError: - self.unsubscribe(event_type, callback) - return None + # Test batch publish + batch_events = [ + Event(type="TEST_EVENT", data={"batch": i}, source="batch_test") + for i in range(3) + ] + await event_bus.publish_batch(batch_events) - def get_stats(self) -> Dict[str, Any]: - """Get event bus statistics.""" - return { - **self._stats, - "queue_sizes": { - priority.name: queue.qsize() - for priority, queue in self._event_queues.items() - }, - "subscription_types": list(self._subscriptions.keys()), - "active_subscriptions": sum(len(subs) for subs in self._subscriptions.values()) - } + # Wait for handlers to complete + await asyncio.sleep(0.5) - def get_recent_events(self, limit: int = 10) -> List[Event]: - """Get recent events from history.""" - return self._event_history[-limit:] + # Get statistics + stats = await event_bus.get_subscription_stats() + print(f"Subscription stats: {stats}") - def clear_history(self): - """Clear event history.""" - self._event_history.clear() + # Get event history + history = await event_bus.get_event_history() + print(f"Event history count: {len(history)}") - async def shutdown(self): - """Shutdown the event bus and cleanup resources.""" - self.logger.info("Shutting down event bus...") - - # Signal all processing tasks to stop - for priority, queue in self._event_queues.items(): - await queue.put(None) - - # Wait for processing tasks to complete - if self._processing_tasks: - await asyncio.gather(*self._processing_tasks, return_exceptions=True) - - # Clear all subscriptions - self._subscriptions.clear() - self._weak_subscriptions.clear() + # Test typed publisher + test_publisher = event_bus.create_typed_publisher("TYPED_EVENT", "typed_test") + await test_publisher({"value": 42}) + + # Test LangChain callback + callback = event_bus.create_callback_handler("langchain_test") + await callback.on_chain_start({}, {"input": "test"}) + await callback.on_chain_end({"output": "result"}) + + # Final statistics + print(f"Total events received: {len(received_events)}") + for handler_name, event in received_events: + print(f" {handler_name}: {event.type}") - self.logger.info("Event bus shutdown complete") \ No newline at end of file + # Unsubscribe + await event_bus.unsubscribe(sub1) + + # Clear history + await event_bus.clear_event_history() + + +if __name__ == "__main__": + asyncio.run(test_event_bus()) \ No newline at end of file diff --git a/backend/orchestration/orchestrator.py b/backend/orchestration/orchestrator.py index 096cc022c..991049328 100644 --- a/backend/orchestration/orchestrator.py +++ b/backend/orchestration/orchestrator.py @@ -1,365 +1,476 @@ -""" -Main orchestrator for coordinating multiple agents using LangGraph and LangChain. -Manages the flow of tasks between different specialized agents. -""" - -from typing import Dict, Any, List, Optional, TypedDict -from langchain_google_genai import ChatGoogleGenerativeAI +from typing import Dict, List, Any, Optional, TypedDict, Annotated, Sequence from langgraph.graph import StateGraph, END -from langgraph.prebuilt import ToolExecutor -from langchain.schema import BaseMessage -import logging +from langchain.schema import BaseMessage, HumanMessage, AIMessage +from dataclasses import dataclass from enum import Enum +import logging +import json +import uuid +from datetime import datetime -from .event_bus import EventBus -from .task_queue import TaskQueue -from .agent_pool import AgentPool -from ..agents.base_agent import BaseAgent -from ..models.agent_models import AgentState, TaskRequest, TaskResponse - - -class AgentType(Enum): - PLANNER = "planner" - VOICE = "voice" - VISION = "vision" - CONTEXT = "context" - MEMORY = "memory" - INSIGHT = "insight" - RESPONSE = "response" +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -class OrchestratorState(TypedDict): - """State shared across all agents in the orchestration graph.""" - user_input: str - intent: Optional[str] - extracted_entities: Dict[str, Any] - context_data: Dict[str, Any] +class AgentState(TypedDict): + """State passed between nodes in the graph""" + messages: Sequence[BaseMessage] + current_agent: str + task_data: Dict[str, Any] + workflow_id: str + user_id: str + context: Dict[str, Any] + plan: Dict[str, Any] memories: List[Dict[str, Any]] insights: List[Dict[str, Any]] - response: Optional[str] - current_agent: Optional[str] - task_history: List[Dict[str, Any]] + response: str error: Optional[str] - metadata: Dict[str, Any] + retry_count: int -class LifeWitnessOrchestrator: - """ - Main orchestrator that coordinates multiple specialized agents using LangGraph. - """ +class EventType(Enum): + """Enumeration of all possible event types in the system""" + TRANSCRIPTION_COMPLETE = "transcription_complete" + PLAN_CREATED = "plan_created" + CONTEXT_ANALYZED = "context_analyzed" + MEMORY_STORED = "memory_stored" + INSIGHT_GENERATED = "insight_generated" + RESPONSE_READY = "response_ready" + VISION_ANALYZED = "vision_analyzed" + + +class Orchestrator: + """Main orchestrator that coordinates all agents using LangGraph""" - def __init__(self, gemini_api_key: str, config: Dict[str, Any]): - self.gemini_api_key = gemini_api_key - self.config = config - self.logger = logging.getLogger(__name__) + def __init__(self, agent_pool=None, event_bus=None, task_queue=None): + self.agent_pool = agent_pool + self.event_bus = event_bus + self.task_queue = task_queue + self.graph = self._build_graph() + self.compiled_graph = self.graph.compile() - # Initialize core components - self.event_bus = EventBus() - self.task_queue = TaskQueue() - self.agent_pool = AgentPool(gemini_api_key, config) - - # Initialize LangChain Gemini model - self.llm = ChatGoogleGenerativeAI( - model="gemini-pro", - google_api_key=gemini_api_key, - temperature=0.3, - convert_system_message_to_human=True - ) + def _build_graph(self) -> StateGraph: + """Build the LangGraph workflow""" + # Define the graph + workflow = StateGraph(AgentState) - # Build the orchestration graph - self.graph = self._build_orchestration_graph() + # Add nodes for each agent + workflow.add_node("voice_agent", self._voice_agent_node) + workflow.add_node("planner_agent", self._planner_agent_node) + workflow.add_node("context_agent", self._context_agent_node) + workflow.add_node("memory_agent", self._memory_agent_node) + workflow.add_node("insight_agent", self._insight_agent_node) + workflow.add_node("response_agent", self._response_agent_node) + workflow.add_node("error_handler", self._error_handler_node) - def _build_orchestration_graph(self) -> StateGraph: - """Build the LangGraph state graph for agent orchestration.""" - workflow = StateGraph(OrchestratorState) + # Set the entry point + workflow.set_entry_point("voice_agent") - # Add nodes for each agent type - workflow.add_node("planner", self._run_planner_agent) - workflow.add_node("voice_processor", self._run_voice_agent) - workflow.add_node("vision_processor", self._run_vision_agent) - workflow.add_node("context_gatherer", self._run_context_agent) - workflow.add_node("memory_manager", self._run_memory_agent) - workflow.add_node("insight_analyzer", self._run_insight_agent) - workflow.add_node("response_generator", self._run_response_agent) - workflow.add_node("error_handler", self._handle_error) + # Add edges + workflow.add_edge("voice_agent", "planner_agent") + workflow.add_edge("planner_agent", "context_agent") + workflow.add_edge("context_agent", "memory_agent") + workflow.add_edge("memory_agent", "insight_agent") + workflow.add_edge("insight_agent", "response_agent") + workflow.add_edge("response_agent", END) - # Define the workflow edges - workflow.set_entry_point("planner") - - # Conditional routing based on planner output + # Add conditional edges for error handling workflow.add_conditional_edges( - "planner", - self._route_after_planning, + "voice_agent", + self._check_error, { - "voice": "voice_processor", - "vision": "vision_processor", - "context": "context_gatherer", - "memory": "memory_manager", - "error": "error_handler" + "error": "error_handler", + "continue": "planner_agent" } ) - # Sequential processing for multi-modal inputs - workflow.add_edge("voice_processor", "context_gatherer") - workflow.add_edge("vision_processor", "context_gatherer") - workflow.add_edge("context_gatherer", "memory_manager") - workflow.add_edge("memory_manager", "insight_analyzer") - workflow.add_edge("insight_analyzer", "response_generator") - workflow.add_edge("response_generator", END) - workflow.add_edge("error_handler", END) + workflow.add_conditional_edges( + "planner_agent", + self._check_error, + { + "error": "error_handler", + "continue": "context_agent" + } + ) - return workflow.compile() + return workflow - def _route_after_planning(self, state: OrchestratorState) -> str: - """Route to appropriate agent based on planner output.""" - try: - intent = state.get("intent", "") - - if state.get("error"): - return "error" - elif "voice" in intent.lower(): - return "voice" - elif "image" in intent.lower() or "photo" in intent.lower(): - return "vision" - elif "search" in intent.lower() or "remember" in intent.lower(): - return "memory" - else: - return "context" - - except Exception as e: - self.logger.error(f"Routing error: {e}") + def _check_error(self, state: AgentState) -> str: + """Check if there's an error in the state""" + if state.get("error"): return "error" + return "continue" - async def _run_planner_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute the planner agent to analyze intent and plan tasks.""" + async def _voice_agent_node(self, state: AgentState) -> AgentState: + """Process voice input""" try: - agent = await self.agent_pool.get_agent(AgentType.PLANNER) + logger.info("Processing voice input...") + + # Get agent from pool if available + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("voice_agent") - task_request = TaskRequest( - task_type="plan", - input_data={"user_input": state["user_input"]}, - context=state.get("metadata", {}) - ) + # Process the voice data + audio_data = state["task_data"].get("audio_data", "") - response = await agent.process_task(task_request) + # Simulate processing (in real implementation, use actual voice agent) + transcription = f"Transcribed: {state['task_data'].get('text', 'Hello, this is a test')}" - state["intent"] = response.result.get("intent") - state["extracted_entities"] = response.result.get("entities", {}) - state["current_agent"] = "planner" + # Update state + state["messages"].append(HumanMessage(content=transcription)) + state["task_data"]["transcription"] = transcription + state["current_agent"] = "voice_agent" - # Emit planning event - await self.event_bus.emit("agent.planning.completed", { - "intent": state["intent"], - "entities": state["extracted_entities"] - }) + # Publish event if event bus is available + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.TRANSCRIPTION_COMPLETE, + "data": { + "transcription": transcription, + "user_id": state["user_id"] + } + }) - return state + # Release agent back to pool + if self.agent_pool and agent: + await self.agent_pool.release("voice_agent", agent) + + logger.info(f"Voice transcription complete: {transcription[:50]}...") except Exception as e: - self.logger.error(f"Planner agent error: {e}") + logger.error(f"Error in voice agent: {str(e)}") state["error"] = str(e) - return state + state["retry_count"] = state.get("retry_count", 0) + 1 + + return state - async def _run_voice_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute voice processing agent.""" + async def _planner_agent_node(self, state: AgentState) -> AgentState: + """Create a plan based on the transcription""" try: - agent = await self.agent_pool.get_agent(AgentType.VOICE) + logger.info("Creating plan...") + + # Get agent from pool + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("planner_agent") + + transcription = state["task_data"].get("transcription", "") + + # Simulate planning (in real implementation, use actual planner agent) + plan = { + "steps": [ + "Analyze user intent", + "Gather relevant context", + "Generate insights", + "Formulate response" + ], + "intent": "user_query", + "priority": "high" + } - task_request = TaskRequest( - task_type="voice_process", - input_data={"audio_data": state.get("user_input")}, - context=state.get("context_data", {}) - ) + # Update state + state["plan"] = plan + state["current_agent"] = "planner_agent" + state["messages"].append(AIMessage(content=f"Plan created: {json.dumps(plan)}")) - response = await agent.process_task(task_request) - state["context_data"]["voice_analysis"] = response.result - state["current_agent"] = "voice" + # Publish event + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.PLAN_CREATED, + "data": { + "plan": plan, + "user_id": state["user_id"] + } + }) - return state + # Release agent + if self.agent_pool and agent: + await self.agent_pool.release("planner_agent", agent) + + logger.info("Plan created successfully") except Exception as e: - self.logger.error(f"Voice agent error: {e}") + logger.error(f"Error in planner agent: {str(e)}") state["error"] = str(e) - return state + + return state - async def _run_vision_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute vision processing agent.""" + async def _context_agent_node(self, state: AgentState) -> AgentState: + """Analyze context based on the plan""" try: - agent = await self.agent_pool.get_agent(AgentType.VISION) + logger.info("Analyzing context...") + + # Get agent from pool + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("context_agent") - task_request = TaskRequest( - task_type="vision_analyze", - input_data={"image_data": state.get("user_input")}, - context=state.get("context_data", {}) - ) + plan = state.get("plan", {}) - response = await agent.process_task(task_request) - state["context_data"]["vision_analysis"] = response.result - state["current_agent"] = "vision" + # Simulate context analysis + context = { + "user_profile": { + "preferences": ["technical", "detailed"], + "history": ["previous_queries"] + }, + "environment": { + "time": "afternoon", + "location": "office" + }, + "relevant_data": ["data1", "data2"] + } + + # Update state + state["context"] = context + state["current_agent"] = "context_agent" + state["messages"].append(AIMessage(content=f"Context analyzed: {json.dumps(context)}")) - return state + # Publish event + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.CONTEXT_ANALYZED, + "data": { + "context": context, + "user_id": state["user_id"] + } + }) + + # Release agent + if self.agent_pool and agent: + await self.agent_pool.release("context_agent", agent) + + logger.info("Context analysis complete") except Exception as e: - self.logger.error(f"Vision agent error: {e}") + logger.error(f"Error in context agent: {str(e)}") state["error"] = str(e) - return state + + return state - async def _run_context_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute context gathering agent.""" + async def _memory_agent_node(self, state: AgentState) -> AgentState: + """Store relevant information in memory""" try: - agent = await self.agent_pool.get_agent(AgentType.CONTEXT) + logger.info("Storing memory...") - task_request = TaskRequest( - task_type="gather_context", - input_data={ - "entities": state["extracted_entities"], - "intent": state["intent"] - }, - context=state.get("context_data", {}) - ) + # Get agent from pool + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("memory_agent") + + context = state.get("context", {}) + + # Simulate memory storage + memory_entry = { + "timestamp": "2024-01-01T00:00:00", + "interaction": state["task_data"].get("transcription", ""), + "context": context, + "tags": ["voice", "query"] + } + + # Update state + state["memories"] = [memory_entry] + state["current_agent"] = "memory_agent" + state["messages"].append(AIMessage(content="Memory stored successfully")) - response = await agent.process_task(task_request) - state["context_data"].update(response.result) - state["current_agent"] = "context" + # Publish event + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.MEMORY_STORED, + "data": { + "memory": memory_entry, + "user_id": state["user_id"] + } + }) - return state + # Release agent + if self.agent_pool and agent: + await self.agent_pool.release("memory_agent", agent) + + logger.info("Memory stored successfully") except Exception as e: - self.logger.error(f"Context agent error: {e}") + logger.error(f"Error in memory agent: {str(e)}") state["error"] = str(e) - return state + + return state - async def _run_memory_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute memory management agent.""" + async def _insight_agent_node(self, state: AgentState) -> AgentState: + """Generate insights based on context and memories""" try: - agent = await self.agent_pool.get_agent(AgentType.MEMORY) - - task_request = TaskRequest( - task_type="memory_operation", - input_data={ - "query": state["user_input"], - "entities": state["extracted_entities"], - "context": state["context_data"] + logger.info("Generating insights...") + + # Get agent from pool + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("insight_agent") + + context = state.get("context", {}) + memories = state.get("memories", []) + + # Simulate insight generation + insights = [ + { + "type": "pattern", + "description": "User frequently asks technical questions", + "confidence": 0.85 + }, + { + "type": "recommendation", + "description": "Provide detailed technical explanations", + "confidence": 0.90 } - ) + ] - response = await agent.process_task(task_request) - state["memories"] = response.result.get("memories", []) - state["current_agent"] = "memory" + # Update state + state["insights"] = insights + state["current_agent"] = "insight_agent" + state["messages"].append(AIMessage(content=f"Insights generated: {len(insights)} insights")) - return state + # Publish event + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.INSIGHT_GENERATED, + "data": { + "insights": insights, + "user_id": state["user_id"] + } + }) + + # Release agent + if self.agent_pool and agent: + await self.agent_pool.release("insight_agent", agent) + + logger.info(f"Generated {len(insights)} insights") except Exception as e: - self.logger.error(f"Memory agent error: {e}") + logger.error(f"Error in insight agent: {str(e)}") state["error"] = str(e) - return state + + return state - async def _run_insight_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute insight analysis agent.""" + async def _response_agent_node(self, state: AgentState) -> AgentState: + """Generate final response based on all previous processing""" try: - agent = await self.agent_pool.get_agent(AgentType.INSIGHT) + logger.info("Generating response...") - task_request = TaskRequest( - task_type="analyze_patterns", - input_data={ - "memories": state["memories"], - "context": state["context_data"] - } - ) + # Get agent from pool + agent = None + if self.agent_pool: + agent = await self.agent_pool.acquire("response_agent") - response = await agent.process_task(task_request) - state["insights"] = response.result.get("insights", []) - state["current_agent"] = "insight" + plan = state.get("plan", {}) + context = state.get("context", {}) + insights = state.get("insights", []) - return state + # Simulate response generation + response = f"""Based on your query: {state['task_data'].get('transcription', '')} - except Exception as e: - self.logger.error(f"Insight agent error: {e}") - state["error"] = str(e) - return state - - async def _run_response_agent(self, state: OrchestratorState) -> OrchestratorState: - """Execute response generation agent.""" - try: - agent = await self.agent_pool.get_agent(AgentType.RESPONSE) - - task_request = TaskRequest( - task_type="generate_response", - input_data={ - "user_input": state["user_input"], - "memories": state["memories"], - "insights": state["insights"], - "context": state["context_data"] - } - ) +I've analyzed the context and generated the following response: +- Intent: {plan.get('intent', 'unknown')} +- Relevant insights: {len(insights)} insights found +- Personalized based on your preferences + +This is a sample response that would be generated by the actual response agent.""" + + # Update state + state["response"] = response + state["current_agent"] = "response_agent" + state["messages"].append(AIMessage(content=response)) - response = await agent.process_task(task_request) - state["response"] = response.result.get("response") - state["current_agent"] = "response" + # Publish event + if self.event_bus: + await self.event_bus.publish({ + "type": EventType.RESPONSE_READY, + "data": { + "response": response, + "user_id": state["user_id"] + } + }) - return state + # Release agent + if self.agent_pool and agent: + await self.agent_pool.release("response_agent", agent) + + logger.info("Response generated successfully") except Exception as e: - self.logger.error(f"Response agent error: {e}") + logger.error(f"Error in response agent: {str(e)}") state["error"] = str(e) - return state + + return state - async def _handle_error(self, state: OrchestratorState) -> OrchestratorState: - """Handle errors in the orchestration flow.""" - error_msg = state.get("error", "Unknown error occurred") - self.logger.error(f"Orchestration error: {error_msg}") + async def _error_handler_node(self, state: AgentState) -> AgentState: + """Handle errors in the workflow""" + error = state.get("error", "Unknown error") + retry_count = state.get("retry_count", 0) + + logger.error(f"Error in workflow: {error}, retry count: {retry_count}") - state["response"] = f"I apologize, but I encountered an error: {error_msg}" + if retry_count < 3: + # Clear error and retry + state["error"] = None + logger.info(f"Retrying workflow, attempt {retry_count + 1}") + else: + # Max retries reached, set final error response + state["response"] = f"I apologize, but I encountered an error processing your request: {error}" + return state - async def process_user_request(self, user_input: str, metadata: Dict[str, Any] = None) -> Dict[str, Any]: - """ - Main entry point for processing user requests through the orchestration graph. - """ - initial_state = OrchestratorState( - user_input=user_input, - intent=None, - extracted_entities={}, - context_data={}, - memories=[], - insights=[], - response=None, - current_agent=None, - task_history=[], - error=None, - metadata=metadata or {} - ) + async def process_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]: + """Process a request through the orchestration workflow""" + initial_state = { + "messages": [], + "current_agent": "orchestrator", + "task_data": request_data, + "workflow_id": request_data.get("workflow_id", "default"), + "user_id": request_data.get("user_id", "default_user"), + "context": {}, + "plan": {}, + "memories": [], + "insights": [], + "response": "", + "error": None, + "retry_count": 0 + } try: - # Execute the orchestration graph - final_state = await self.graph.ainvoke(initial_state) - - # Emit completion event - await self.event_bus.emit("orchestration.completed", { - "response": final_state.get("response"), - "agents_used": final_state.get("task_history", []) - }) + # Run the graph + final_state = await self.compiled_graph.ainvoke(initial_state) return { - "response": final_state.get("response"), - "memories": final_state.get("memories", []), - "insights": final_state.get("insights", []), - "context": final_state.get("context_data", {}), - "success": final_state.get("error") is None + "success": True, + "response": final_state.get("response", ""), + "workflow_id": final_state.get("workflow_id"), + "insights": final_state.get("insights", []) } except Exception as e: - self.logger.error(f"Orchestration failed: {e}") + logger.error(f"Error in orchestration: {str(e)}") return { - "response": f"I'm sorry, but I encountered an error processing your request: {str(e)}", - "memories": [], - "insights": [], - "context": {}, - "success": False + "success": False, + "error": str(e), + "workflow_id": initial_state.get("workflow_id") } + + +# Test function +async def test_orchestrator(): + """Test the orchestrator independently""" + orchestrator = Orchestrator() + + test_request = { + "audio_data": "test_audio_base64", + "text": "What's the weather like today?", + "user_id": "test_user", + "workflow_id": "test_workflow" + } - async def shutdown(self): - """Cleanup resources.""" - await self.agent_pool.shutdown() - await self.task_queue.shutdown() - await self.event_bus.shutdown() \ No newline at end of file + result = await orchestrator.process_request(test_request) + print(f"Test result: {json.dumps(result, indent=2)}") + + +if __name__ == "__main__": + import asyncio + asyncio.run(test_orchestrator()) \ No newline at end of file diff --git a/backend/orchestration/task_queue.py b/backend/orchestration/task_queue.py index b3cef1a7e..fca6cfe68 100644 --- a/backend/orchestration/task_queue.py +++ b/backend/orchestration/task_queue.py @@ -1,18 +1,15 @@ -""" -Async task management system with priority queues, retry logic, and monitoring. -Handles distributed task execution across multiple agents. -""" - import asyncio -import logging -from typing import Dict, List, Optional, Any, Callable, Union -from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Callable from dataclasses import dataclass, field +from datetime import datetime +from collections import deque from enum import Enum +import logging import uuid -import json -from collections import defaultdict -import time + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) class TaskStatus(Enum): @@ -20,484 +17,343 @@ class TaskStatus(Enum): RUNNING = "running" COMPLETED = "completed" FAILED = "failed" - CANCELLED = "cancelled" RETRYING = "retrying" -class TaskPriority(Enum): - LOW = 1 - NORMAL = 2 - HIGH = 3 - URGENT = 4 - - @dataclass class Task: - """Represents a task in the queue.""" - id: str - task_type: str - data: Dict[str, Any] - callback: Optional[Callable] = None - priority: TaskPriority = TaskPriority.NORMAL - max_retries: int = 3 - retry_delay: float = 1.0 - timeout: Optional[float] = None + """Represents a task in the queue""" + id: str = field(default_factory=lambda: str(uuid.uuid4())) + type: str = "" + data: Dict[str, Any] = field(default_factory=dict) + priority: int = 1 created_at: datetime = field(default_factory=datetime.now) started_at: Optional[datetime] = None completed_at: Optional[datetime] = None status: TaskStatus = TaskStatus.PENDING retries: int = 0 + max_retries: int = 3 + result: Optional[Any] = None error: Optional[str] = None - result: Any = None - metadata: Dict[str, Any] = field(default_factory=dict) - dependencies: List[str] = field(default_factory=list) - tags: List[str] = field(default_factory=list) + callback_handler: Optional['TaskQueueCallback'] = None -@dataclass -class TaskResult: - """Result of task execution.""" - task_id: str - success: bool - result: Any = None - error: Optional[str] = None - execution_time: float = 0.0 - metadata: Dict[str, Any] = field(default_factory=dict) +class TaskQueueCallback: + """Callback handler for task execution tracking""" + + def __init__(self, task_queue: 'TaskQueue', task: Task): + self.task_queue = task_queue + self.task = task + + async def on_chain_start(self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs) -> None: + """Called when a chain starts running.""" + logger.info(f"Task {self.task.id} chain started") + self.task.status = TaskStatus.RUNNING + self.task.started_at = datetime.now() + + async def on_chain_end(self, outputs: Dict[str, Any], **kwargs) -> None: + """Called when a chain ends running.""" + logger.info(f"Task {self.task.id} chain completed") + self.task.status = TaskStatus.COMPLETED + self.task.completed_at = datetime.now() + self.task.result = outputs + + async def on_chain_error(self, error: Exception, **kwargs) -> None: + """Called when a chain errors.""" + logger.error(f"Task {self.task.id} chain error: {str(error)}") + self.task.status = TaskStatus.FAILED + self.task.error = str(error) + + # Handle retry logic + if self.task.retries < self.task.max_retries: + self.task.retries += 1 + self.task.status = TaskStatus.RETRYING + await self.task_queue.enqueue(self.task) + + +class PriorityQueue: + """Simple priority queue implementation""" + + def __init__(self): + self.queues = {1: deque(), 2: deque(), 3: deque()} # 3 priority levels + self.lock = asyncio.Lock() + + async def put(self, task: Task): + """Add a task to the queue""" + async with self.lock: + priority = min(max(task.priority, 1), 3) # Clamp between 1-3 + self.queues[priority].append(task) + + async def get(self) -> Optional[Task]: + """Get the highest priority task""" + async with self.lock: + # Check from highest priority (3) to lowest (1) + for priority in [3, 2, 1]: + if self.queues[priority]: + return self.queues[priority].popleft() + return None + + async def size(self) -> int: + """Get total queue size""" + async with self.lock: + return sum(len(q) for q in self.queues.values()) + + def empty(self) -> bool: + """Check if queue is empty""" + return all(len(q) == 0 for q in self.queues.values()) class TaskQueue: - """ - Async task queue with priority handling, retry logic, and dependency management. - """ + """Task queue implementation for LangChain agents""" - def __init__( - self, - max_workers: int = 10, - max_queue_size: int = 1000, - default_timeout: float = 300.0 - ): - self.max_workers = max_workers - self.max_queue_size = max_queue_size - self.default_timeout = default_timeout - self.logger = logging.getLogger(__name__) - - # Priority queues for different task types - self._queues: Dict[TaskPriority, asyncio.Queue] = { - priority: asyncio.Queue(maxsize=max_queue_size) - for priority in TaskPriority - } - - # Task registry for tracking all tasks - self._tasks: Dict[str, Task] = {} - self._task_results: Dict[str, TaskResult] = {} - - # Worker management - self._workers: List[asyncio.Task] = [] - self._worker_semaphore = asyncio.Semaphore(max_workers) - self._shutdown_event = asyncio.Event() - - # Dependency tracking - self._task_dependencies: Dict[str, List[str]] = defaultdict(list) - self._dependent_tasks: Dict[str, List[str]] = defaultdict(list) + def __init__(self, max_concurrent_tasks: int = 5): + self.queue = PriorityQueue() + self.max_concurrent_tasks = max_concurrent_tasks + self.running_tasks: Dict[str, Task] = {} + self.completed_tasks: Dict[str, Task] = {} + self.workers: List[asyncio.Task] = [] + self.is_running = False + self.event_bus = None # Will be injected + self.agent_pool = None # Will be injected + + async def start(self, num_workers: int = 3): + """Start the task queue workers""" + self.is_running = True + logger.info(f"Starting task queue with {num_workers} workers") + + # Create worker tasks + for i in range(num_workers): + worker = asyncio.create_task(self._worker(f"worker-{i}")) + self.workers.append(worker) + + async def stop(self): + """Stop the task queue""" + logger.info("Stopping task queue...") + self.is_running = False - # Statistics and monitoring - self._stats = { - "tasks_submitted": 0, - "tasks_completed": 0, - "tasks_failed": 0, - "tasks_cancelled": 0, - "total_execution_time": 0.0, - "average_execution_time": 0.0 - } + # Cancel all workers + for worker in self.workers: + worker.cancel() + + # Wait for workers to finish + await asyncio.gather(*self.workers, return_exceptions=True) + self.workers.clear() - # Performance metrics - self._performance_history: List[Dict[str, Any]] = [] - self._max_history_size = 1000 + async def enqueue(self, task: Task) -> str: + """Add a task to the queue""" + if not isinstance(task, Task): + raise ValueError("task must be an instance of Task") + + # Set callback handler + task.callback_handler = TaskQueueCallback(self, task) + + await self.queue.put(task) + logger.info(f"Enqueued task {task.id} of type {task.type} with priority {task.priority}") + + # Emit event if event bus is available + if self.event_bus: + await self.event_bus.publish({ + "type": "TASK_CREATED", + "data": { + "task_id": task.id, + "task_type": task.type, + "priority": task.priority + } + }) + + return task.id + + async def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]: + """Get the status of a task""" + # Check running tasks + if task_id in self.running_tasks: + task = self.running_tasks[task_id] + return { + "id": task.id, + "status": task.status.value, + "type": task.type, + "started_at": task.started_at.isoformat() if task.started_at else None + } + + # Check completed tasks + if task_id in self.completed_tasks: + task = self.completed_tasks[task_id] + return { + "id": task.id, + "status": task.status.value, + "type": task.type, + "result": task.result, + "completed_at": task.completed_at.isoformat() if task.completed_at else None + } + + return None - # Start worker pool - self._start_workers() - - def _start_workers(self): - """Start the worker pool for processing tasks.""" - for i in range(self.max_workers): - worker = asyncio.create_task(self._worker(f"worker-{i}")) - self._workers.append(worker) - async def _worker(self, worker_id: str): - """Worker coroutine that processes tasks from priority queues.""" - self.logger.debug(f"Started worker {worker_id}") + """Worker coroutine that processes tasks from the queue""" + logger.info(f"Worker {worker_id} started") - while not self._shutdown_event.is_set(): + while self.is_running: try: - # Get next task from highest priority queue - task = await self._get_next_task() - if task is None: + # Check if we can run more tasks + if len(self.running_tasks) >= self.max_concurrent_tasks: + await asyncio.sleep(0.1) continue - - # Acquire worker semaphore - async with self._worker_semaphore: - await self._execute_task(task, worker_id) - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Worker {worker_id} error: {e}") - await asyncio.sleep(1) # Brief pause before retrying - - self.logger.debug(f"Worker {worker_id} stopped") - - async def _get_next_task(self) -> Optional[Task]: - """Get the next task to execute, checking dependencies.""" - # Check queues in priority order (highest first) - for priority in sorted(TaskPriority, key=lambda x: x.value, reverse=True): - queue = self._queues[priority] - - if not queue.empty(): + # Get a task from the queue + task = await self.queue.get() + if not task: + await asyncio.sleep(0.1) + continue + + # Process the task + logger.info(f"Worker {worker_id} processing task {task.id}") + self.running_tasks[task.id] = task + try: - task = await asyncio.wait_for(queue.get(), timeout=1.0) + # Execute the task based on its type + result = await self._execute_task(task) + task.result = result + task.status = TaskStatus.COMPLETED + task.completed_at = datetime.now() + + # Move to completed + self.completed_tasks[task.id] = task + del self.running_tasks[task.id] - # Check if dependencies are satisfied - if self._are_dependencies_satisfied(task): - return task + # Emit completion event + if self.event_bus: + await self.event_bus.publish({ + "type": "TASK_COMPLETED", + "data": { + "task_id": task.id, + "task_type": task.type, + "result": result + } + }) + + except Exception as e: + logger.error(f"Task {task.id} failed: {str(e)}") + task.error = str(e) + task.status = TaskStatus.FAILED + + # Handle retry + if task.retries < task.max_retries: + task.retries += 1 + task.status = TaskStatus.RETRYING + await self.enqueue(task) + logger.info(f"Retrying task {task.id} (attempt {task.retries})") else: - # Re-queue if dependencies not met - await queue.put(task) + # Move to completed with failed status + self.completed_tasks[task.id] = task - except asyncio.TimeoutError: - continue - - return None - - def _are_dependencies_satisfied(self, task: Task) -> bool: - """Check if all task dependencies have completed successfully.""" - if not task.dependencies: - return True - - for dep_id in task.dependencies: - if dep_id not in self._task_results: - return False - - result = self._task_results[dep_id] - if not result.success: - # Dependency failed, mark this task as failed too - task.status = TaskStatus.FAILED - task.error = f"Dependency {dep_id} failed" - return False + del self.running_tasks[task.id] + + # Emit failure event + if self.event_bus: + await self.event_bus.publish({ + "type": "TASK_FAILED", + "data": { + "task_id": task.id, + "task_type": task.type, + "error": str(e), + "retries": task.retries + } + }) + + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Worker {worker_id} error: {str(e)}") + await asyncio.sleep(1) + + logger.info(f"Worker {worker_id} stopped") - return True - - async def _execute_task(self, task: Task, worker_id: str): - """Execute a single task with error handling and retry logic.""" - task.status = TaskStatus.RUNNING - task.started_at = datetime.now() + async def _execute_task(self, task: Task) -> Any: + """Execute a task based on its type""" + logger.info(f"Executing task {task.id} of type {task.type}") - start_time = time.time() + # Simulate task execution based on type + # In real implementation, this would call the appropriate agent - try: - # Apply timeout if specified - timeout = task.timeout or self.default_timeout + if task.type == "voice_agent": + # Simulate voice processing + await asyncio.sleep(1) + return {"transcription": "Simulated transcription"} - # Execute the task callback - if task.callback: - if asyncio.iscoroutinefunction(task.callback): - result = await asyncio.wait_for( - task.callback(task.data), - timeout=timeout - ) - else: - result = await asyncio.wait_for( - asyncio.get_event_loop().run_in_executor( - None, task.callback, task.data - ), - timeout=timeout - ) - else: - result = task.data # No callback, just return data - - # Task completed successfully - execution_time = time.time() - start_time - task.status = TaskStatus.COMPLETED - task.completed_at = datetime.now() - task.result = result - - # Store result - task_result = TaskResult( - task_id=task.id, - success=True, - result=result, - execution_time=execution_time, - metadata={"worker_id": worker_id} - ) - self._task_results[task.id] = task_result - - # Update statistics - self._stats["tasks_completed"] += 1 - self._stats["total_execution_time"] += execution_time - self._stats["average_execution_time"] = ( - self._stats["total_execution_time"] / self._stats["tasks_completed"] - ) - - # Process dependent tasks - await self._process_dependent_tasks(task.id) + elif task.type == "planner_agent": + # Simulate planning + await asyncio.sleep(0.5) + return {"plan": {"steps": ["step1", "step2"]}} - self.logger.debug(f"Task {task.id} completed by {worker_id} in {execution_time:.2f}s") + elif task.type == "context_agent": + # Simulate context analysis + await asyncio.sleep(0.3) + return {"context": {"user_intent": "query"}} - except asyncio.TimeoutError: - await self._handle_task_failure(task, "Task timed out", worker_id) - except asyncio.CancelledError: - task.status = TaskStatus.CANCELLED - self._stats["tasks_cancelled"] += 1 - raise - except Exception as e: - await self._handle_task_failure(task, str(e), worker_id) - - async def _handle_task_failure(self, task: Task, error: str, worker_id: str): - """Handle task failure with retry logic.""" - task.error = error - task.retries += 1 - - if task.retries <= task.max_retries: - # Retry the task - task.status = TaskStatus.RETRYING + elif task.type == "memory_agent": + # Simulate memory storage + await asyncio.sleep(0.2) + return {"memory_id": str(uuid.uuid4())} - # Apply exponential backoff - delay = task.retry_delay * (2 ** (task.retries - 1)) - await asyncio.sleep(delay) + elif task.type == "insight_agent": + # Simulate insight generation + await asyncio.sleep(0.8) + return {"insights": ["insight1", "insight2"]} - # Re-queue the task - await self._queues[task.priority].put(task) + elif task.type == "response_agent": + # Simulate response generation + await asyncio.sleep(0.5) + return {"response": "This is a simulated response"} - self.logger.warning( - f"Task {task.id} failed (attempt {task.retries}/{task.max_retries}), " - f"retrying in {delay}s: {error}" - ) else: - # Task failed permanently - task.status = TaskStatus.FAILED - task.completed_at = datetime.now() + raise ValueError(f"Unknown task type: {task.type}") - execution_time = time.time() - task.started_at.timestamp() - task_result = TaskResult( - task_id=task.id, - success=False, - error=error, - execution_time=execution_time, - metadata={"worker_id": worker_id, "retries": task.retries} - ) - self._task_results[task.id] = task_result - - self._stats["tasks_failed"] += 1 - - self.logger.error(f"Task {task.id} failed permanently: {error}") + async def get_queue_stats(self) -> Dict[str, Any]: + """Get queue statistics""" + return { + "queue_size": await self.queue.size(), + "running_tasks": len(self.running_tasks), + "completed_tasks": len(self.completed_tasks), + "max_concurrent_tasks": self.max_concurrent_tasks, + "is_running": self.is_running, + "num_workers": len(self.workers) + } + + +# Test function +async def test_task_queue(): + """Test the task queue independently""" + queue = TaskQueue(max_concurrent_tasks=3) - async def _process_dependent_tasks(self, completed_task_id: str): - """Process tasks that were waiting for the completed task.""" - dependent_tasks = self._dependent_tasks.get(completed_task_id, []) - - for dep_task_id in dependent_tasks: - if dep_task_id in self._tasks: - dep_task = self._tasks[dep_task_id] - if dep_task.status == TaskStatus.PENDING and self._are_dependencies_satisfied(dep_task): - # Dependencies satisfied, queue the task for execution - await self._queues[dep_task.priority].put(dep_task) + # Start the queue + await queue.start(num_workers=2) - async def submit_task( - self, - task_type: str, - data: Dict[str, Any], - callback: Optional[Callable] = None, - priority: TaskPriority = TaskPriority.NORMAL, - max_retries: int = 3, - retry_delay: float = 1.0, - timeout: Optional[float] = None, - dependencies: List[str] = None, - tags: List[str] = None, - metadata: Dict[str, Any] = None - ) -> str: - """ - Submit a task to the queue. - - Args: - task_type: Type identifier for the task - data: Task input data - callback: Function to execute for the task - priority: Task priority level - max_retries: Maximum number of retry attempts - retry_delay: Base delay between retries (with exponential backoff) - timeout: Maximum execution time in seconds - dependencies: List of task IDs this task depends on - tags: Tags for task categorization - metadata: Additional metadata - - Returns: - str: Unique task ID - """ - task_id = str(uuid.uuid4()) - + # Create test tasks + tasks = [] + for i in range(5): task = Task( - id=task_id, - task_type=task_type, - data=data, - callback=callback, - priority=priority, - max_retries=max_retries, - retry_delay=retry_delay, - timeout=timeout, - dependencies=dependencies or [], - tags=tags or [], - metadata=metadata or {} + type=["voice_agent", "planner_agent", "insight_agent"][i % 3], + data={"test_data": f"test_{i}"}, + priority=(i % 3) + 1 ) + task_id = await queue.enqueue(task) + tasks.append(task_id) - # Register task - self._tasks[task_id] = task - - # Set up dependency tracking - if task.dependencies: - for dep_id in task.dependencies: - self._dependent_tasks[dep_id].append(task_id) - self._task_dependencies[task_id].append(dep_id) - - # Queue task if dependencies are satisfied, otherwise it will be queued later - if self._are_dependencies_satisfied(task): - await self._queues[priority].put(task) - - self._stats["tasks_submitted"] += 1 - - self.logger.debug(f"Submitted task {task_id} with priority {priority.name}") - return task_id - - async def get_task_result(self, task_id: str, timeout: Optional[float] = None) -> TaskResult: - """ - Wait for a task to complete and return its result. - - Args: - task_id: ID of the task to wait for - timeout: Maximum time to wait in seconds - - Returns: - TaskResult: The task execution result - - Raises: - asyncio.TimeoutError: If timeout is exceeded - KeyError: If task ID is not found - """ - if task_id not in self._tasks: - raise KeyError(f"Task {task_id} not found") - - start_time = time.time() - - while task_id not in self._task_results: - if timeout and (time.time() - start_time) > timeout: - raise asyncio.TimeoutError(f"Timeout waiting for task {task_id}") - - await asyncio.sleep(0.1) # Small delay to avoid busy waiting - - return self._task_results[task_id] - - def get_task_status(self, task_id: str) -> Optional[TaskStatus]: - """Get the current status of a task.""" - if task_id in self._tasks: - return self._tasks[task_id].status - return None - - def get_task(self, task_id: str) -> Optional[Task]: - """Get task details by ID.""" - return self._tasks.get(task_id) - - async def cancel_task(self, task_id: str) -> bool: - """ - Cancel a task if it hasn't started executing yet. - - Args: - task_id: ID of the task to cancel - - Returns: - bool: True if task was cancelled, False otherwise - """ - if task_id not in self._tasks: - return False - - task = self._tasks[task_id] - - if task.status in [TaskStatus.PENDING, TaskStatus.RETRYING]: - task.status = TaskStatus.CANCELLED - self._stats["tasks_cancelled"] += 1 - - # Create cancelled result - self._task_results[task_id] = TaskResult( - task_id=task_id, - success=False, - error="Task was cancelled", - metadata={"cancelled_at": datetime.now().isoformat()} - ) - - return True - - return False - - def get_queue_stats(self) -> Dict[str, Any]: - """Get current queue statistics.""" - return { - "queue_sizes": { - priority.name: queue.qsize() - for priority, queue in self._queues.items() - }, - "total_tasks": len(self._tasks), - "active_workers": len([w for w in self._workers if not w.done()]), - "statistics": self._stats.copy() - } - - def get_tasks_by_status(self, status: TaskStatus) -> List[Task]: - """Get all tasks with a specific status.""" - return [task for task in self._tasks.values() if task.status == status] + # Wait a bit for processing + await asyncio.sleep(3) - def get_tasks_by_tag(self, tag: str) -> List[Task]: - """Get all tasks with a specific tag.""" - return [task for task in self._tasks.values() if tag in task.tags] + # Check task statuses + for task_id in tasks: + status = await queue.get_task_status(task_id) + print(f"Task {task_id}: {status}") + + # Get queue stats + stats = await queue.get_queue_stats() + print(f"Queue stats: {stats}") - async def wait_for_completion(self, timeout: Optional[float] = None): - """Wait for all queued tasks to complete.""" - start_time = time.time() - - while True: - # Check if all queues are empty and no tasks are running - all_empty = all(queue.empty() for queue in self._queues.values()) - no_running = not any( - task.status == TaskStatus.RUNNING - for task in self._tasks.values() - ) - - if all_empty and no_running: - break - - if timeout and (time.time() - start_time) > timeout: - raise asyncio.TimeoutError("Timeout waiting for task completion") - - await asyncio.sleep(0.5) - - async def shutdown(self, timeout: float = 30.0): - """Shutdown the task queue and cleanup resources.""" - self.logger.info("Shutting down task queue...") - - # Signal shutdown - self._shutdown_event.set() - - # Cancel all pending tasks - for task in self._tasks.values(): - if task.status in [TaskStatus.PENDING, TaskStatus.RETRYING]: - await self.cancel_task(task.id) - - # Cancel all workers - for worker in self._workers: - worker.cancel() - - # Wait for workers to finish - try: - await asyncio.wait_for( - asyncio.gather(*self._workers, return_exceptions=True), - timeout=timeout - ) - except asyncio.TimeoutError: - self.logger.warning("Some workers did not shutdown gracefully") - - self.logger.info("Task queue shutdown complete") \ No newline at end of file + # Stop the queue + await queue.stop() + + +if __name__ == "__main__": + asyncio.run(test_task_queue()) \ No newline at end of file diff --git a/backend/orchestration/test_orchestrator.py b/backend/orchestration/test_orchestrator.py new file mode 100644 index 000000000..f77ccde64 --- /dev/null +++ b/backend/orchestration/test_orchestrator.py @@ -0,0 +1,273 @@ +from typing import Dict, List, Any, Optional, TypedDict, Sequence +from langgraph.graph import StateGraph, END +from dataclasses import dataclass +from enum import Enum +import logging +import json +import uuid +from datetime import datetime +import asyncio + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class AgentState(TypedDict): + """State passed between nodes in the graph""" + messages: List[str] + current_agent: str + task_data: Dict[str, Any] + workflow_id: str + user_id: str + context: Dict[str, Any] + plan: Dict[str, Any] + memories: List[Dict[str, Any]] + insights: List[Dict[str, Any]] + response: str + error: Optional[str] + retry_count: int + + +class SimpleOrchestrator: + """Simplified orchestrator using only basic LangGraph""" + + def __init__(self): + self.graph = self._build_graph() + self.compiled_graph = self.graph.compile() + + def _build_graph(self) -> StateGraph: + """Build the LangGraph workflow""" + # Define the graph + workflow = StateGraph(AgentState) + + # Add nodes for each agent + workflow.add_node("voice_agent", self._voice_agent_node) + workflow.add_node("planner_agent", self._planner_agent_node) + workflow.add_node("context_agent", self._context_agent_node) + workflow.add_node("memory_agent", self._memory_agent_node) + workflow.add_node("insight_agent", self._insight_agent_node) + workflow.add_node("response_agent", self._response_agent_node) + + # Set the entry point + workflow.set_entry_point("voice_agent") + + # Add edges + workflow.add_edge("voice_agent", "planner_agent") + workflow.add_edge("planner_agent", "context_agent") + workflow.add_edge("context_agent", "memory_agent") + workflow.add_edge("memory_agent", "insight_agent") + workflow.add_edge("insight_agent", "response_agent") + workflow.add_edge("response_agent", END) + + return workflow + + def _voice_agent_node(self, state: AgentState) -> AgentState: + """Process voice input""" + logger.info("Processing voice input...") + + # Process the voice data + audio_data = state["task_data"].get("audio_data", "") + + # Simulate processing + transcription = f"Transcribed: {state['task_data'].get('text', 'Hello, this is a test')}" + + # Update state + state["messages"].append(transcription) + state["task_data"]["transcription"] = transcription + state["current_agent"] = "voice_agent" + + logger.info(f"Voice transcription complete: {transcription[:50]}...") + + return state + + def _planner_agent_node(self, state: AgentState) -> AgentState: + """Create a plan based on the transcription""" + logger.info("Creating plan...") + + transcription = state["task_data"].get("transcription", "") + + # Simulate planning + plan = { + "steps": [ + "Analyze user intent", + "Gather relevant context", + "Generate insights", + "Formulate response" + ], + "intent": "user_query", + "priority": "high" + } + + # Update state + state["plan"] = plan + state["current_agent"] = "planner_agent" + state["messages"].append(f"Plan created: {json.dumps(plan)}") + + logger.info("Plan created successfully") + + return state + + def _context_agent_node(self, state: AgentState) -> AgentState: + """Analyze context based on the plan""" + logger.info("Analyzing context...") + + plan = state.get("plan", {}) + + # Simulate context analysis + context = { + "user_profile": { + "preferences": ["technical", "detailed"], + "history": ["previous_queries"] + }, + "environment": { + "time": "afternoon", + "location": "office" + }, + "relevant_data": ["data1", "data2"] + } + + # Update state + state["context"] = context + state["current_agent"] = "context_agent" + state["messages"].append(f"Context analyzed: {json.dumps(context)}") + + logger.info("Context analysis complete") + + return state + + def _memory_agent_node(self, state: AgentState) -> AgentState: + """Store relevant information in memory""" + logger.info("Storing memory...") + + context = state.get("context", {}) + + # Simulate memory storage + memory_entry = { + "timestamp": datetime.now().isoformat(), + "interaction": state["task_data"].get("transcription", ""), + "context": context, + "tags": ["voice", "query"] + } + + # Update state + state["memories"] = [memory_entry] + state["current_agent"] = "memory_agent" + state["messages"].append("Memory stored successfully") + + logger.info("Memory stored successfully") + + return state + + def _insight_agent_node(self, state: AgentState) -> AgentState: + """Generate insights based on context and memories""" + logger.info("Generating insights...") + + context = state.get("context", {}) + memories = state.get("memories", []) + + # Simulate insight generation + insights = [ + { + "type": "pattern", + "description": "User frequently asks technical questions", + "confidence": 0.85 + }, + { + "type": "recommendation", + "description": "Provide detailed technical explanations", + "confidence": 0.90 + } + ] + + # Update state + state["insights"] = insights + state["current_agent"] = "insight_agent" + state["messages"].append(f"Insights generated: {len(insights)} insights") + + logger.info(f"Generated {len(insights)} insights") + + return state + + def _response_agent_node(self, state: AgentState) -> AgentState: + """Generate final response based on all previous processing""" + logger.info("Generating response...") + + plan = state.get("plan", {}) + context = state.get("context", {}) + insights = state.get("insights", []) + + # Simulate response generation + response = f"""Based on your query: {state['task_data'].get('transcription', '')} + +I've analyzed the context and generated the following response: +- Intent: {plan.get('intent', 'unknown')} +- Relevant insights: {len(insights)} insights found +- Personalized based on your preferences + +This is a sample response that would be generated by the actual response agent.""" + + # Update state + state["response"] = response + state["current_agent"] = "response_agent" + state["messages"].append(response) + + logger.info("Response generated successfully") + + return state + + def process_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]: + """Process a request through the orchestration workflow""" + initial_state = { + "messages": [], + "current_agent": "orchestrator", + "task_data": request_data, + "workflow_id": request_data.get("workflow_id", "default"), + "user_id": request_data.get("user_id", "default_user"), + "context": {}, + "plan": {}, + "memories": [], + "insights": [], + "response": "", + "error": None, + "retry_count": 0 + } + + try: + # Run the graph + final_state = self.compiled_graph.invoke(initial_state) + + return { + "success": True, + "response": final_state.get("response", ""), + "workflow_id": final_state.get("workflow_id"), + "insights": final_state.get("insights", []) + } + + except Exception as e: + logger.error(f"Error in orchestration: {str(e)}") + return { + "success": False, + "error": str(e), + "workflow_id": initial_state.get("workflow_id") + } + + +# Test function +def test_simple_orchestrator(): + """Test the simple orchestrator""" + orchestrator = SimpleOrchestrator() + + test_request = { + "audio_data": "test_audio_base64", + "text": "What's the weather like today?", + "user_id": "test_user", + "workflow_id": "test_workflow" + } + + result = orchestrator.process_request(test_request) + print(f"Test result: {json.dumps(result, indent=2)}") + + +if __name__ == "__main__": + test_simple_orchestrator() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index e69de29bb..65f5b1e77 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +langchain +langchain-google-genai +langgraph +python-dotenv +dataclasses-json +# Standard library modules (no need to install): asyncio, logging, typing, datetime, enum, uuid, json, time, weakref, collections, importlib \ No newline at end of file diff --git a/backend/services/__pycache__/gemini_service.cpython-313.pyc b/backend/services/__pycache__/gemini_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8485fed120d084ff6f2302b719928db2f533b9ff GIT binary patch literal 191 zcmey&%ge<81X0hLGeGoX5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa|LcerR!OQL%nb zW_D&tMq+kpc4=N>QDQ-&y(1fzg Date: Fri, 25 Jul 2025 16:56:48 -0700 Subject: [PATCH 16/25] added the photoUpload component without api integration --- frontend/src/app/page.tsx | 231 +++++++++++++-------- frontend/src/components/photoUpload.tsx | 255 ++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 88 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8ed3d331f..51306b5ec 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,58 +1,70 @@ -'use client'; +"use client"; // Importing the tools I need from React and the Swiper carousel library. -import React, { useState, useRef } from 'react'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { EffectCoverflow, Pagination, Autoplay } from 'swiper/modules'; -import type { Swiper as SwiperCore } from 'swiper'; -import 'swiper/css'; -import 'swiper/css/effect-coverflow'; -import 'swiper/css/pagination'; +import React, { useState, useRef } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { EffectCoverflow, Pagination, Autoplay } from "swiper/modules"; +import type { Swiper as SwiperCore } from "swiper"; +import "swiper/css"; +import "swiper/css/effect-coverflow"; +import "swiper/css/pagination"; +import PhotoUploadModal from "../components/photoUpload"; export default function Home() { // Here, I'm setting up the state for my component. This is the information // that will change as the user interacts with the page. const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [currentUserQuery, setCurrentUserQuery] = useState(''); - const [currentAgentReply, setCurrentAgentReply] = useState(''); - const [highlightedMemoryId, setHighlightedMemoryId] = useState(null); - + const [currentUserQuery, setCurrentUserQuery] = useState(""); + const [currentAgentReply, setCurrentAgentReply] = useState(""); + const [highlightedMemoryId, setHighlightedMemoryId] = useState( + null + ); + + // photoUpload component + const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false); + // This ref gives me direct control over the carousel component. const carouselRef = useRef(null); // This is the hard-coded conversation I'm using for the demo. const demoConversation = [ { - query: "Who was that person I met at TechCrunch who worked on crypto payments?", - response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team." + query: + "Who was that person I met at TechCrunch who worked on crypto payments?", + response: + "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team.", }, { query: "Right! What else did we talk about?", - response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role." + response: + "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role.", }, { query: "Did I ever follow up with her?", - response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'" + response: + "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'", }, { - query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", - response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?" - } + query: + "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", + response: + "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?", + }, ]; // This is the list of photos for the carousel. const memoryPhotos = [ - { id: 1, name: 'Jennifer Chen', imageUrl: '/placeholder.png' }, - { id: 2, name: 'Jake\'s Bday', imageUrl: '/placeholder.png' }, - { id: 3, name: 'Coffee Meetup', imageUrl: '/placeholder.png' }, - { id: 4, name: 'Team Lunch', imageUrl: '/placeholder.png' }, - { id: 5, name: 'Project Demo', imageUrl: '/placeholder.png' }, + { id: 1, name: "Jennifer Chen", imageUrl: "/placeholder.png" }, + { id: 2, name: "Jake's Bday", imageUrl: "/placeholder.png" }, + { id: 3, name: "Coffee Meetup", imageUrl: "/placeholder.png" }, + { id: 4, name: "Team Lunch", imageUrl: "/placeholder.png" }, + { id: 5, name: "Project Demo", imageUrl: "/placeholder.png" }, ]; // This function runs when the microphone button is clicked. const runNextStepInDemo = () => { const jenniferId = 1; - const jenniferIndex = memoryPhotos.findIndex(m => m.id === jenniferId); + const jenniferIndex = memoryPhotos.findIndex((m) => m.id === jenniferId); // On the first click of the demo, I'll stop the autoplay and slide to Jennifer's photo. if (currentStepIndex === 0) { @@ -67,79 +79,122 @@ export default function Home() { const currentStep = demoConversation[currentStepIndex]; setCurrentUserQuery(currentStep.query); setCurrentAgentReply(currentStep.response); - + // ...and then get ready for the next click by advancing the step index. - setCurrentStepIndex((prevIndex) => (prevIndex + 1) % demoConversation.length); + setCurrentStepIndex( + (prevIndex) => (prevIndex + 1) % demoConversation.length + ); }; return (
    - -
    -
    Live Witness
    - -
    - -
    - - { carouselRef.current = swiper; }} - effect={'coverflow'} - grabCursor={true} - centeredSlides={true} - slidesPerView={3} - loop={true} - autoplay={{ delay: 3000, disableOnInteraction: false }} - coverflowEffect={{ rotate: 0, stretch: 0, depth: 150, modifier: 1, slideShadows: false }} - pagination={false} - modules={[EffectCoverflow, Pagination, Autoplay]} - className="w-full max-w-5xl" - > - {memoryPhotos.map(photo => ( - -
    - {photo.name - {photo.name} -
    -
    - ))} -
    - -
    -
    - +

    + " + {currentUserQuery || + "Click the mic to start the conversation..."} + " +

    +
    + -

    "{currentUserQuery || 'Click the mic to start the conversation...'}"

    - -
    - - {currentAgentReply && ( + + {currentAgentReply && (
    - AI Response: -

    {currentAgentReply}

    + AI Response: +

    {currentAgentReply}

    - )} -
+ )} + + + setIsPhotoModalOpen(false)} + onPhotoUpload={(photo) => { + // Add the new photo to your memoryPhotos array + // api call for storing the image can be added here + console.log("New photo uploaded:", photo); + }} + /> ); -} \ No newline at end of file +} diff --git a/frontend/src/components/photoUpload.tsx b/frontend/src/components/photoUpload.tsx index e69de29bb..f5102d092 100644 --- a/frontend/src/components/photoUpload.tsx +++ b/frontend/src/components/photoUpload.tsx @@ -0,0 +1,255 @@ +import React, { useState, useRef } from "react"; + +// Define the types for the component +interface PhotoData { + id: number; + name: string; + imageUrl: string; + file: File; +} + +interface UploadedPhoto { + file: File; + preview: string; + name: string; +} + +interface PhotoUploadModalProps { + isOpen: boolean; + onClose: () => void; + onPhotoUpload: (photo: PhotoData) => void; +} + +const PhotoUploadModal: React.FC = ({ + isOpen, + onClose, + onPhotoUpload, +}) => { + const [dragActive, setDragActive] = useState(false); + const [uploadedPhoto, setUploadedPhoto] = useState( + null + ); + const [photoName, setPhotoName] = useState(""); + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + + // Handle drag events + const handleDrag = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + // Handle dropped files + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + handleFile(e.dataTransfer.files[0]); + } + }; + + // Handle file selection + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + handleFile(e.target.files[0]); + } + }; + + // Process the selected file + const handleFile = (file: File) => { + if (file.type.startsWith("image/")) { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + if (e.target && e.target.result) { + setUploadedPhoto({ + file: file, + preview: e.target.result as string, + name: file.name, + }); + setPhotoName(file.name.split(".")[0]); // Remove extension + } + }; + reader.readAsDataURL(file); + } else { + alert("Please select an image file."); + } + }; + + // Handle form submission + const handleSubmit = async () => { + if (!uploadedPhoto || !photoName.trim()) return; + + setIsUploading(true); + + // Simulate upload delay + setTimeout(() => { + // Call the parent component's upload handler + onPhotoUpload({ + id: Date.now(), // Generate a simple ID + name: photoName.trim(), + imageUrl: uploadedPhoto.preview, // In real app, this would be the uploaded URL + file: uploadedPhoto.file, + }); + + // Reset form + setUploadedPhoto(null); + setPhotoName(""); + setIsUploading(false); + onClose(); + }, 1500); + }; + + // Reset form when modal closes + const handleClose = () => { + setUploadedPhoto(null); + setPhotoName(""); + setDragActive(false); + setIsUploading(false); + onClose(); + }; + + // Handle backdrop click + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + handleClose(); + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + className="bg-gray-900 border border-gray-700 rounded-xl mx-auto w-[30%]" + > + {/* Header */} +
+

Upload Photo

+ +
+ + {/* Content */} +
+ {/* Drag and Drop Area */} +
+ {uploadedPhoto ? ( +
+ Preview +

+ {uploadedPhoto.file.name} +

+ +
+ ) : ( +
+
📸
+
+

+ Drag and drop your photo here +

+

or

+ +
+
+ )} + + +
+ + {/* Photo Name Input */} + {uploadedPhoto && ( +
+ + setPhotoName(e.target.value)} + placeholder="Enter a name for this photo..." + className="w-full bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-gray-100 placeholder-gray-500 focus:border-blue-400 focus:outline-none" + required + /> +
+ )} + + {/* Action Buttons */} +
+ + +
+
+
+
+ ); +}; + +export default PhotoUploadModal; From 738dc91b34a091568e1587bcc9653910ea1285d6 Mon Sep 17 00:00:00 2001 From: Likitha Date: Fri, 25 Jul 2025 20:00:11 -0400 Subject: [PATCH 17/25] orchestration update 2 --- .../__pycache__/agent_pool.cpython-313.pyc | Bin 24497 -> 29778 bytes .../__pycache__/event_bus.cpython-313.pyc | Bin 20737 -> 20875 bytes .../__pycache__/orchestrator.cpython-313.pyc | Bin 19442 -> 19837 bytes .../__pycache__/task_queue.cpython-313.pyc | Bin 20115 -> 20241 bytes backend/orchestration/agent_pool.py | 106 +++- backend/orchestration/event_bus.py | 6 +- backend/orchestration/orchestrator.py | 15 +- backend/orchestration/task_queue.py | 6 +- backend/orchestration/test_orchestrator.py | 585 +++++++++++------- 9 files changed, 480 insertions(+), 238 deletions(-) diff --git a/backend/orchestration/__pycache__/agent_pool.cpython-313.pyc b/backend/orchestration/__pycache__/agent_pool.cpython-313.pyc index 2c6f2e6bff93c5c331384855778174a19936198f..4781d7921ec91d4b97f54dddf905c364e84b86bf 100644 GIT binary patch delta 7529 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 Wj@xe5^b-44efLfF;$lR?LjDgjfBO&s delta 3377 zcmai0drVu`8NbKRV88?nwlNegE|8e=3IUSv2ziX8gw%whtWoF182io;jA7^6>6#Kj zr>bf zord7&?|iTGo$qD7{v3PsyUhDhdAUcSpA+Se&0gw0?+vo$^V`ynnD>5FsE4aTgvGO^ zz7p3SrYdx5274vu>tire6kZ)G2`XV_${kXsw)EL7ZlCp`@ER#AA(^Kj<9^92C0^ME zFCcm4#Pe?Of>Z2^wJ}#L=p=8k&{V}X`kdMt3-&hJrYb|qDF?5cstPH*KIWV94V+Mv zM35Af`v(wXeXKsL6yhjEt14-hW|3;~YnBrC+5XPFMz`J1LY3rMuOL`eUO4z8$7Rn; z9;57P`!ElPX~!Yv7tcG69H}Qc_Ys&C@pvMeUCbqNbBkF$9_J0E|K+YF$(daT#d4rgxZNF0 z6@Bi3_A278+jtkuRDy`zbm&^<3!5ArQK+iQgW_rT_5i}y%LrDL*J{J(ys!D|pDd*Z zn(mJd_7j`$B3QS}w^*-Gi4k3QNROs_DL<2$(`T(fA&NNoPWs@MvDjKt!-Ar1Nx-4bMTgU$m6IvG4GP>AKT;&@ebMN2tK9 z7h4Xc@*NKEE$e07#@Vti)*apOWra~3wUkLy2We5RkgV6syV-6E<}T6ZZKZ%Fz3;V6 zZ8|wZWZ^B6eaF>WMLXLk&X(OJE>*Ox+y}!Rf^}w=WD?nQayBuSja2fxA!r7)09pZP zaM@wJ7u04Kjo>&+V3v%2DXA@?@Ax>Fh?`FWJ`1=9P}EsD>xan)N@)I+ft9zNNo0#t&7bj~zW>w+=e-}%O zC;UBuov=*93fm7Y1V*p8;`g%?;%)zMsT^h%+})xpu-brvL~K^~f?aHA2E+$}fOseLoatNn>l>TebcAWA2;u;dJrUbM5NTpz?v9t z8DY!fY)hw0mTFl1u%(7QV*I|P-Nu%Uo7-NsugJ{8%342IV>Gm~;}(cE?TJ)MUT->6 z+Dtxeb)?KTUTQV%#?{tP%k`Co^gIA7&2*>c^kjmksLy2|Ekm-Vh(qmdVHw;7X!+%o z9LVdPaXDnOiEJvP@r%ZV_9tvzayLr9f|Luh_kGI~x%(zjMYAXF?P_B$8+w<=*0h;f zM15P2T!Q^Y@yw1O^~mKNJ?u}$dpqjvjdI`^e+v3G)7II|AA|ow?~CTX1I!fZzRR7r zG%>%_?LUUUuN13-aT)Lvf;Ys`{yO$kG27of{R=urO3+RVvYe*tbRv`2bUsG{(=o3- ztn)9!5J7Ba{#N*=Q>Slc-I@@@X5Cucm;dY^Vt+9ryIx_98yPU2$&8lZ5he?vpG70F3vcRYYRnt<2a1C%0AST z4yUvE;@$rie;B^mAXhGQ%*uE=vpAE;#5H`=#Aot4ZDDh7zVm9qP4O2dEXrx;6lwpN zc%LZZ?1;(&#xo<|aI!VgG5WK>#yYf?4_4E6{!JV1E&M>MMna(&!@2n8^mOB{ukwRs9$ zSqxd|5gbPWatLIFQ7>ALUEp2=TnD^GAVT9!)lU;)mdzw`$yqC}I{&Sh81Hk*(Rf{a zZ9Ejhdrfan(~DD&+PaNjj{n1MZ=iMl{6vCPi;-jD2j!xb>6FbS+c1VZWpBvJZpRU| z1kyK$`F;4oRCpUaT<^<{IZEF>#)g`)nZD zSm=hjP5rQgMAa{?x_zLsRH12_+Ny>0(?Ccodr~L*1!>xs0Mn*P>$-F9vy%`p zjXlZdo_o%@_nzN9_ug~-_~+#7Uy{lv4u?&E?{whG?8&27D+A>G)$MZwLq6&g1xb)B z!+z@LZ56HJZ8fci);b&*3ebQkO!~*#ae}A6Aj&)0x7^;9@@~Sd_C0NZJ+P|pBD$P5 z1fVb3Y6ZzYY3ec*rHx!#0aC}LZ(Z8NrIjFcc9j(^Mafkw(B{c{$sIi<2+4YZ`+s0K zNQ0BXu>sv|Glk`wgF;tPRB}WVW-K$7u!mhEm+NYeBr}<_$<*9SNhLWW(}65KGMh{* zV|k_EE!#_oyKvOdLr4p|Y|PmFFcn4OW`8&S%)Fa-*=wc*X=Z;leb~?ff|0O;wxS}0 zWE+z0NOrK_n^zj!kVP}f2SU-GDMO4nnNmbU$k6ld3 z3%PVwp?un%pbeWhFlio|{wMYZ>~lQTY_Y*ttzf?hjq8x7i|nn2?iF+-?55{YasdgC>_y}dAzeaJ2}CoV zQ?rU@n##{FsCwv^QOD0p6uIvq`96|oqNA5k#kZC_@dN0p?sa%Ybf=lz7$VmSuQkq_ z%wssBf&IJXohaO`5+>l5JxAa?$rp1XT$0SvvOE

1LSRya-ck{XbyRS8<^qBjLw; z17i=$o(m&~$35T}0b->d!Xm~A_I4;lJ}=w~Et<>{PFv3o?W(bFj$?9H+XjJ3ito^W!)MvJq52utokVl)0mo($1ZOeG zMQ~@>KRZJ3UN&|5%IxJ}{Ui82i5d&N`zlBzUbX|e_$A@S#mmO*dT%q08{)>eDNZB{ z`@mDJ_nSJH)`|VOiNvT*y4@i#EF!!cmm4Z|mH=w=SDY;7a zAazeT*jGMZQ1WzG6w9ea$&ML9GQcEWGLpdE8Ex7X`%m%NNyBtLtv_JY#_7FB2= zRY%IC0Q-YC#eNq&vA<>`$HhsjU=?CyF;NEKUYv3FQYGs22>=v$K@l_}PKE6%TT(_~sWWM~qm$$rB$3D>g+-Q#m{ zFr^zelSgjTg#&HaxK+;|D-l%7{*kOmWpyE|sImx;vj~~f?XzxZ z`PLKs^ge9Y%z!PQ$?1;q^Na^pzp6m>Ec;!i9^B?SOW@fyL-zfPa9LP0?q`wCo$TJh zp81DP--D*s>zCfVbiZD_-?Z(%xZ{3k=X*VOL;Jr70qwatx;l0*bmCFDDdKq|m_+YC z1XHc|b@N(#|#l_2FYU=%=f6Bd3py{aFc#W2J6U`esjzxd~72z zoAG0N|8D4fa)87f#!tErcSGm1b`oxFOn?w9SZ1&L;vlBbgx zNPHEwiefL3S7Gf8h(;)uP2mbwesuo|#VN=2nB#Q_8#PNYljuU6l0R|Ffn!)liD^a5 zt`}KQ#ze(|uz`MvX54ce5Vq)ehJc4^DXpB(&dGXNumKS(pYE_p$M^wSI?_rymriC9 zY7TCeW}i+&V%Ix*^1#Q1A4sS%pB!x`HwxR14HAi$E4Y?^83vIX01`HgjEsbF-K!OX?-y(TdsoakWByG$e`-Qj1JqU-(xnN?g3Wo5yiHch$0iOzT8R`T-{UwqAy$9Ihva+1&7nifJ>cd?X-! NJkD+pZ|7zFe*gd~1VaD- delta 3144 zcma)8Z%kX)6~C8fzvmkK53uI!8Z%hvQCWM1x*;lI6&XBWbDjuVQ8u5Iq zU4{u?V-UiIkW_P_G6I%~%HT1hSLvTUew?8H^2ls^4VCW-;ssEwrH{O)v5&fa{>mC) z)H7gKvYe(-->fJD*-T&e`Fp{6LsAm!`7}{U65d2*ZB5mdhy%1BE{3$0znV$wDKnTS z>EC^|gEB}%3_{}G1`Vr{RJ9y=9)z%#b2VEoI04@(AU-Jfg{eo+k=xu)xzgD>GU$u%~ zg$WJxNb8+}LqP61RC$I~ka{gAE1Hto-1>r3T)pU&CD{L>%%*>-Q()c+a0}o&00&$g z0Nzu%$i3q`D7olFJbT4revbYo7{nhGc7yBI_8Bk>%3D=E-95dX-4XVi?d~4nzsC~$ z-HxyiG!08mN$R8#W||?PzwMCg_a)U3SJl-l+2lE9d6mMAW(Ztgl2pfd#q~ZpHsD{2_eDS{?)YFqJgYZ}*Jib$Y*NvD^=gqIXA+ z1#9+sN4Tt*-IZhQrfcq}aLo`hs&+83r~a3J(%xrj)~>lpQ0F_~zDWqVirFouH%?o6 zgY0NL+gn{`w=wOP*#EOP=%cQW>9Z%g#h5gQV>aqOS?75sW-r|cP82Jj6X?SuUQb2L zv5)IqD5qaP=^b$$lQeN&iMiXDxtPFgcz|MKrIn2{aM&Dp7F;F9oEWR3N4ht#Nb`Yp z>WN&W$6Kl!`)p|XIrf*E(E`MiKN3;~5Yi<2cBIKsyUtF{> zC|0IPBh0JTBDIW{(U8!DmhlN;8HeiWn**~|vrLujeHsu!Iz3q9+F#i{wUKfP=RE8y|{6TS^r2418? zqRkR7ozj=-wV}gqg>?-2T?T(cH_@)8lfE;gHt&k4A+qD$x&A0}=8utyN0EsSTn{fT z{4NrI*nN2y30LUN(_w3E48Jl-e|-8aTMVPRN&HC{O%0FJn{8rZ!hv5N5ywSz&yL52 zE%(aLG_aTV+VFUp<-H?=jK1H3$1Rrot#v>Tm9Yu;hcTlclyS6h6zvms;eq?;gjIMj zEHa(}29B2>xeBlIw0$zrDF6uzlb-@SsZWc70HA|@bMgmO5vzB{ zzKhuKv#o!?u)9n{XOElxTeP5_9l&nhWn^F;|CZ|LyXS(J!31B$u&$9bJH>AS1Fpo7 zKz7(*RODC}wn)_VJR1#>s5(oS4qIel;SHPN;VRjE4IBA2lYdCNrf#rq_3f!tPx+FP zQxX|P*HxXsNo816Ex$_KKuVcxGMUzrd~w5)&!?A&ho;U4F0?SQdDQcEm$x5L;DLRX zN_|sHq_c)V^4ft;*07yg%`W9L>S^+j=|U^7|B(Lad;|UG`RR(dh@GDxz)z!e<|}O$ H=z#wVwIF__ diff --git a/backend/orchestration/__pycache__/task_queue.cpython-313.pyc b/backend/orchestration/__pycache__/task_queue.cpython-313.pyc index dc66815c2ff1c86ae1234d4fb4b70d130f539fd1..8e69731d96b15d53625e27053a3bd4a97b374708 100644 GIT binary patch delta 3724 zcmaJ^du&@*8NbJ`#BuD@iSuxhCT>zPZknd3ZD^A|)+}w>wN3hPDU@0>xAu*57spP| zz3wA`TvmoO!Me6*OcPp12M9LNZor*@iHC&Hg!pF^giyI-unmM1Kw6~^_y-}*@4Io_ zghHhL{GIQd?|jemeYfHoyYf5>-wlWB6#2Iy_Sxz8u7u<4@|6c>JCjPX=0J3RBX3M7 zlCC|_v_HmU2_@BVv|oHC7E4HC0AkDklEGm{hX!$|oN60l?g1WeQ+kwSUAvMDr6NP# zMQ(elmiYCOA0G0!+#&Zz;T^OWk$VlP#wD3f;;xe1XhnjrCSIfDH7)VFlFhMpg?Fdg zlFcI@QxvUDQBum$U9hY()!n01^i*t2C^?p6YK!;^(|Tf~=J`TqOv~p_X_=XQS|OX) zxyqd2!GjY=b+eSWu6jPsqT-~-W^JOu`)FNMn#}=GanZXr&`V4Ergw~WrMmobm8wm) zJRpCQt-L!|ueLe9W12a$5El`@2ub41o$AjtkTTkK>+pKnDQVI5p{XwwOt$c@V4*0s6sjNT;u%ty{L;^2IJs-|F7<%a+Z*@ojTs)F*~(Uubrs2;h;D zUeYUxT39FEseODvm6xa;P_X!$#h9VM*R07JN4VQBZySAm~GY!xEgp%vp`+Ow%V9QWuno z&=A{b|2*_ED<>s)epMnftr>+xrZ_v7*DXCesfPG5-0%^=3f0}tBzon`m@0!QjFar{yp=irlcyQ+*Dm(?-Ft(&P?a=*Q&c^}K*Hoi@Q8sLvY(mQJuBxvX8)$ts70c~zhrmYe;3Jc!k@)UdLN|qB{ zu2`~?;!evocErBaYO$@*#HRpIEH8vkS4quWQP$v;C0`B|GAV$cRJkMZ4tBym9v|^A zRb1)lU~9$Ajv=OtKXwd^x`vyj*+p~DX!(+E@);?7Ph{jHH0E`uak+)yow5&imRRdN zZ21&~Dr&(fi{GtY#|%;1wW$Ts;Ew_By;~gV>Sj4{x=Ssihe`*Qyqf%fl|w~C<%4v) zZR`xFF~CI$j_*{lnCDMm-2+(Bt}@^Os0el{BHlgfrZ*!#(fvl?VY#)C7n>7*3*(gI zr~2oY#QHTyH&B!>N%k^#seBrjTH8|T6vfMH##yiZ_cbPKeOgLY`-J1?S|O+NX;JQN z-z%_pT0*5GJu7L48BXAAkT4ziDaQavG`{iE-Y3yZ5hb_c2^Xy@*ueZYPk@ z3K>1mr)iPXtZAJ){!Cuic%^opms?9-iH5#7yC~GY#{*QkN9jU~~WplG-R6H}Uz*R$x};%i$rma8YJ^I|&B*CrM_yrfL4DEmxSen(Uz zN@@Qj?j%Q0u5RM}W?t9lIAwtz4SaOMY5km@DOq~jlEc|`hQ_9xUFG`^FY+3h;@<*L zST#^&WIw0`p!zXE6NQo%8KY8^PEvKKemBM6$9bwXMAKoqiGffrLxy{+8PwMS)LJi4RZNJG|L=GsP$wDCojJw4^Fvml82`uSS=Gc==kS2KA*`uL`AKk zV{uXH|9E%uMUE8jlELqUUg2%DN9e3X0M-4k`nnutTHc<)y-k z`RCMbpDkufd3`5;PactlteCHfHz#@nZz;oXDShJK6T|WCca#X*e%GsbLU%lpdcUWO S4c%%RcwbCRKIH3_GXDWeWj470 delta 3514 zcmaJ^Uu;`f8NbJW<0Q75#kKRV&b6DhyC!goA6U~h)vCBnGU$+k%&mQsxN&Un z-0P;TR5_J4_CU9_9GIq#0uzX-Fvi54Kxh+a@W2D2>JWl*AJ7m_Q41RC0}s4#e&3Dl zCMs0w?|$by=X~e;|2a3l&BQGhei#n7D)L`%8{uwt;l}nQ-)wX`Ew1i~ffx`| zL!F`EC{q==+6H!QHa5a=OHugdE+wtZwDc%5q1niYca0mLZ6SWEVZgV#;GO_JBq;PuQr(bK8$-r3kp=hzF1qIW6EtTKHNdiBlrrj`0D zbq!Tzp3O5YCO%@iIOaLaqT;5YjTV@j5 z<#(owC+FL>ZpZh$ZZ57S#H3%e#c`bOq{V%IUr&!*n~=7mygUAZ@8_*QZMEC~ICzAo zL@Mw`tpiu5sz%iq*Neq-dT!C#lv|^+3wkMEG&qM$t)C|#TAHTtZU7`Z!E82P&SkSa z1lDfA4)CMdtX?XWE!`@VOQzi41FkP$$XR?ZP9ZAXUjy#LmbXx{IKu9DOXX$$G&bNi z9sukIkYjp59RM5zi~$Y-4g-z?RKO#Cz;d7tFv9#;!Yj((d^^ND!FLn;$L{)K*B9>g z4}TKh_b1<&>%IYTzUA#8KO&E=YC;XYJP4m_9)3c8_({-S$7dSFxhBViRjR7;wD=^X zB?jfnY?rX2-0K;*7QGkidNssTc1P<4b_x<)hml=+D6zOyQTd-wm2oZmp7@o1-(#GbLFK<(XfnsMr-5KkT3(-@bnh& zHatxZhhH_X`~F!RdS1dKHl`wb12||-iEw+Lup-CVVfz=6oTo-wMAo#B8C2$DDj$|d zGg{bHgu>NSWCz5PBa)yk|# zmhcgAtMhB@IibaV+Xhdpq66_!@t@evU5K!W?j?f>E>v;iXLzB^3)Uqn&)wN6h7z~fY5VsHiyg#EJP*LbQXt-R9|=~>l&5?c#24FH4IDy(26q(tU$lUB%7n(we##^kUU8c|l&Tdq%w2w}pMhe!Z{C5*P92 zWo*{PgKj2!-^uJE!U7uw>>mTXohGJ zr?p@fN>T~=2oRHuaL)~ zLqj*}(6cz!9Gbh=+R+fF|<;i-{mYGEXw+PL5C^-V4ft^HD**2-U(pXR>f zr{s4H#bqZ{Fw1jgzNB0HI*vB)u65_z;F7kofdw!YO3s8}CbyNroU#-H7|m zqG41xxu2>9o^iri#hw@cT-dDy%u`bXpJSN8tV4}+cW;85Fzav^7A z^?5`7SQ)l`&m3m@<1z~A&OZc@Y8t4WfFA>XBB7C0V0$4saiX!!O6lm<-s}EM6#}oi_Ii}agM*j@e`LUgGaO= z1x`LLi@@Y>;R>ZARUrBV^;^p{gOaASq^e#ywb}ov`h{x{zjXEIyGrOasj f&}T}79eU_hJZ+zOB=zrfl8yXJ{4R6Mhv57dm0aw7 diff --git a/backend/orchestration/agent_pool.py b/backend/orchestration/agent_pool.py index 95973c2e1..136197c02 100644 --- a/backend/orchestration/agent_pool.py +++ b/backend/orchestration/agent_pool.py @@ -6,6 +6,9 @@ import logging from abc import ABC, abstractmethod import uuid +from langchain.agents import AgentExecutor +from langchain.memory import ConversationBufferMemory +from langchain.schema import BaseMessage # Setup logging logging.basicConfig(level=logging.INFO) @@ -23,7 +26,7 @@ class AgentInstance: last_used_at: Optional[datetime] = None usage_count: int = 0 current_task_id: Optional[str] = None - memory: Optional[Dict[str, Any]] = None + memory: Optional[ConversationBufferMemory] = None class BasePooledAgent(ABC): @@ -80,7 +83,7 @@ class MockPlannerAgent(BasePooledAgent): async def initialize(self) -> None: logger.info(f"Initializing MockPlannerAgent {self.agent_id}") - self.memory = {} # Simple dict instead of ConversationBufferMemory + self.memory = ConversationBufferMemory() await asyncio.sleep(0.1) async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: @@ -128,6 +131,87 @@ async def cleanup(self) -> None: logger.info(f"Cleaning up MockInsightAgent {self.agent_id}") +class MockContextAgent(BasePooledAgent): + """Mock context agent for testing""" + + async def initialize(self) -> None: + logger.info(f"Initializing MockContextAgent {self.agent_id}") + await asyncio.sleep(0.1) + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + plan = data.get("plan", {}) + # Simulate context analysis + await asyncio.sleep(0.2) + return { + "context": { + "user_profile": { + "preferences": ["technical", "detailed"], + "history": ["previous_queries"] + }, + "environment": { + "time": "afternoon", + "location": "office" + } + } + } + + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockContextAgent {self.agent_id}") + + +class MockMemoryAgent(BasePooledAgent): + """Mock memory agent for testing""" + + async def initialize(self) -> None: + logger.info(f"Initializing MockMemoryAgent {self.agent_id}") + self.storage = [] # Simple list for memory storage + await asyncio.sleep(0.1) + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + context = data.get("context", {}) + # Simulate memory storage + await asyncio.sleep(0.1) + memory_entry = { + "timestamp": datetime.now().isoformat(), + "interaction": data.get("interaction", ""), + "context": context + } + self.storage.append(memory_entry) + return { + "memory_id": str(uuid.uuid4()), + "stored": True + } + + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockMemoryAgent {self.agent_id}") + + +class MockResponseAgent(BasePooledAgent): + """Mock response agent for testing""" + + async def initialize(self) -> None: + logger.info(f"Initializing MockResponseAgent {self.agent_id}") + await asyncio.sleep(0.1) + + async def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + self.usage_count += 1 + insights = data.get("insights", []) + plan = data.get("plan", {}) + # Simulate response generation + await asyncio.sleep(0.3) + return { + "response": f"Based on your query, I found {len(insights)} insights. " + f"The intent was {plan.get('intent', 'unknown')}. " + f"This is a test response from agent {self.agent_id}.", + "confidence": 0.92 + } + + async def cleanup(self) -> None: + logger.info(f"Cleaning up MockResponseAgent {self.agent_id}") + + class AgentPool: """Pool manager for LangChain agents""" @@ -164,12 +248,30 @@ def _register_default_agents(self): min_size=2, max_size=5 ) + self.register_agent_type( + "context_agent", + MockContextAgent, + min_size=1, + max_size=3 + ) + self.register_agent_type( + "memory_agent", + MockMemoryAgent, + min_size=1, + max_size=3 + ) self.register_agent_type( "insight_agent", MockInsightAgent, min_size=1, max_size=3 ) + self.register_agent_type( + "response_agent", + MockResponseAgent, + min_size=1, + max_size=3 + ) def register_agent_type( self, diff --git a/backend/orchestration/event_bus.py b/backend/orchestration/event_bus.py index 81727489f..b9c403d6b 100644 --- a/backend/orchestration/event_bus.py +++ b/backend/orchestration/event_bus.py @@ -5,6 +5,8 @@ from collections import defaultdict import logging import uuid +from langchain.callbacks.base import AsyncCallbackHandler +from langchain.schema import BaseMessage # Setup logging logging.basicConfig(level=logging.INFO) @@ -22,8 +24,8 @@ class Event: metadata: Dict[str, Any] = field(default_factory=dict) -class EventBusCallback: - """Callback handler that publishes events to the event bus""" +class EventBusCallback(AsyncCallbackHandler): + """LangChain callback handler that publishes events to the event bus""" def __init__(self, event_bus: 'EventBus', source: str = "langchain"): self.event_bus = event_bus diff --git a/backend/orchestration/orchestrator.py b/backend/orchestration/orchestrator.py index 991049328..4437dfedb 100644 --- a/backend/orchestration/orchestrator.py +++ b/backend/orchestration/orchestrator.py @@ -1,6 +1,7 @@ from typing import Dict, List, Any, Optional, TypedDict, Annotated, Sequence from langgraph.graph import StateGraph, END from langchain.schema import BaseMessage, HumanMessage, AIMessage +from langchain.callbacks.manager import CallbackManagerForChainRun from dataclasses import dataclass from enum import Enum import logging @@ -364,8 +365,18 @@ async def _response_agent_node(self, state: AgentState) -> AgentState: context = state.get("context", {}) insights = state.get("insights", []) - # Simulate response generation - response = f"""Based on your query: {state['task_data'].get('transcription', '')} + if agent: + # Use the actual agent + result = await agent.process({ + "plan": plan, + "context": context, + "insights": insights, + "transcription": state['task_data'].get('transcription', '') + }) + response = result.get("response", "No response generated") + else: + # Fallback response + response = f"""Based on your query: {state['task_data'].get('transcription', '')} I've analyzed the context and generated the following response: - Intent: {plan.get('intent', 'unknown')} diff --git a/backend/orchestration/task_queue.py b/backend/orchestration/task_queue.py index fca6cfe68..4beb029c2 100644 --- a/backend/orchestration/task_queue.py +++ b/backend/orchestration/task_queue.py @@ -6,6 +6,8 @@ from enum import Enum import logging import uuid +from langchain.callbacks.base import AsyncCallbackHandler +from langchain.schema import LLMResult # Setup logging logging.basicConfig(level=logging.INFO) @@ -35,10 +37,10 @@ class Task: max_retries: int = 3 result: Optional[Any] = None error: Optional[str] = None - callback_handler: Optional['TaskQueueCallback'] = None + callback_handler: Optional[AsyncCallbackHandler] = None -class TaskQueueCallback: +class TaskQueueCallback(AsyncCallbackHandler): """Callback handler for task execution tracking""" def __init__(self, task_queue: 'TaskQueue', task: Task): diff --git a/backend/orchestration/test_orchestrator.py b/backend/orchestration/test_orchestrator.py index f77ccde64..8447541af 100644 --- a/backend/orchestration/test_orchestrator.py +++ b/backend/orchestration/test_orchestrator.py @@ -1,273 +1,398 @@ -from typing import Dict, List, Any, Optional, TypedDict, Sequence -from langgraph.graph import StateGraph, END -from dataclasses import dataclass -from enum import Enum +import asyncio import logging +from typing import Dict, Any import json -import uuid from datetime import datetime -import asyncio -# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +# Import our orchestration components +# Note: In real usage, these would be imported from the actual files +# from orchestrator import Orchestrator +# from event_bus import EventBus +# from agent_pool import AgentPool +# from task_queue import TaskQueue +# For testing purposes, we'll create a simplified integration test -class AgentState(TypedDict): - """State passed between nodes in the graph""" - messages: List[str] - current_agent: str - task_data: Dict[str, Any] - workflow_id: str - user_id: str - context: Dict[str, Any] - plan: Dict[str, Any] - memories: List[Dict[str, Any]] - insights: List[Dict[str, Any]] - response: str - error: Optional[str] - retry_count: int +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -class SimpleOrchestrator: - """Simplified orchestrator using only basic LangGraph""" +class IntegrationTest: + """Integration test for the orchestration system""" def __init__(self): - self.graph = self._build_graph() - self.compiled_graph = self.graph.compile() - - def _build_graph(self) -> StateGraph: - """Build the LangGraph workflow""" - # Define the graph - workflow = StateGraph(AgentState) - - # Add nodes for each agent - workflow.add_node("voice_agent", self._voice_agent_node) - workflow.add_node("planner_agent", self._planner_agent_node) - workflow.add_node("context_agent", self._context_agent_node) - workflow.add_node("memory_agent", self._memory_agent_node) - workflow.add_node("insight_agent", self._insight_agent_node) - workflow.add_node("response_agent", self._response_agent_node) - - # Set the entry point - workflow.set_entry_point("voice_agent") - - # Add edges - workflow.add_edge("voice_agent", "planner_agent") - workflow.add_edge("planner_agent", "context_agent") - workflow.add_edge("context_agent", "memory_agent") - workflow.add_edge("memory_agent", "insight_agent") - workflow.add_edge("insight_agent", "response_agent") - workflow.add_edge("response_agent", END) - - return workflow - - def _voice_agent_node(self, state: AgentState) -> AgentState: - """Process voice input""" - logger.info("Processing voice input...") + self.event_bus = None + self.agent_pool = None + self.task_queue = None + self.orchestrator = None + self.test_results = { + "events_published": 0, + "tasks_completed": 0, + "agents_used": set(), + "errors": [] + } + # Import these once at class level + self.Event = None + self.Task = None + self.EventType = None - # Process the voice data - audio_data = state["task_data"].get("audio_data", "") + async def setup(self): + """Setup all components""" + logger.info("Setting up orchestration components...") - # Simulate processing - transcription = f"Transcribed: {state['task_data'].get('text', 'Hello, this is a test')}" + # Import components (adjust the import based on your file structure) + try: + from event_bus import EventBus, Event + from agent_pool import AgentPool + from task_queue import TaskQueue, Task + from orchestrator import Orchestrator, EventType + except ImportError: + # If running from the orchestration directory + import sys + import os + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + from event_bus import EventBus, Event + from agent_pool import AgentPool + from task_queue import TaskQueue, Task + from orchestrator import Orchestrator, EventType + + # Store these as class attributes for use in test methods + self.Event = Event + self.Task = Task + self.EventType = EventType + + # Create components + self.event_bus = EventBus() + self.agent_pool = AgentPool() + self.task_queue = TaskQueue(max_concurrent_tasks=5) + + # Initialize agent pool + await self.agent_pool.initialize() + + # Start task queue + await self.task_queue.start(num_workers=3) + + # Create orchestrator and inject dependencies + self.orchestrator = Orchestrator( + agent_pool=self.agent_pool, + event_bus=self.event_bus, + task_queue=self.task_queue + ) + + # Setup event tracking + await self._setup_event_tracking() + + logger.info("Setup complete") + + async def _setup_event_tracking(self): + """Setup event tracking for test metrics""" + # Track all events + async def track_event(event): + self.test_results["events_published"] += 1 + logger.info(f"Event tracked: {event.type}") + + await self.event_bus.subscribe("*", track_event) - # Update state - state["messages"].append(transcription) - state["task_data"]["transcription"] = transcription - state["current_agent"] = "voice_agent" + # Track task completions + async def track_task_complete(event): + self.test_results["tasks_completed"] += 1 + task_type = event.data.get("task_type", "unknown") + self.test_results["agents_used"].add(task_type) + + await self.event_bus.subscribe("TASK_COMPLETED", track_task_complete) - logger.info(f"Voice transcription complete: {transcription[:50]}...") + async def test_simple_workflow(self): + """Test a simple voice processing workflow""" + logger.info("\n=== Testing Simple Workflow ===") - return state - - def _planner_agent_node(self, state: AgentState) -> AgentState: - """Create a plan based on the transcription""" - logger.info("Creating plan...") - - transcription = state["task_data"].get("transcription", "") - - # Simulate planning - plan = { - "steps": [ - "Analyze user intent", - "Gather relevant context", - "Generate insights", - "Formulate response" - ], - "intent": "user_query", - "priority": "high" - } + # For testing, let's directly test the graph execution + # since the full orchestrator might need actual agent implementations + try: + # First, let's test if we can create a simple request + request = { + "audio_data": "base64_encoded_audio_data", + "text": "What is the weather forecast for tomorrow?", + "user_id": "test_user_001", + "workflow_id": "test_workflow_001" + } + + logger.info(f"Testing with request: {request}") + + # Since we're using mock agents, let's test the components separately first + # Test task queue directly + test_task = self.Task( + type="voice_agent", + data=request, + priority=3 + ) + + task_id = await self.task_queue.enqueue(test_task) + logger.info(f"Enqueued test task: {task_id}") + + # Give it time to process + await asyncio.sleep(2) + + # Check task status + status = await self.task_queue.get_task_status(task_id) + logger.info(f"Task status: {status}") + + # Now test the orchestrator + result = await self.orchestrator.process_request(request) + + logger.info(f"Workflow result: {json.dumps(result, indent=2)}") + + # More flexible assertions + assert "success" in result, "Result should have success field" + assert "workflow_id" in result, "Result should have workflow_id" + + # Log what we got + if result.get("success"): + logger.info("Workflow completed successfully") + else: + logger.warning(f"Workflow failed: {result.get('error')}") + + return result + + except Exception as e: + logger.error(f"Error in simple workflow test: {str(e)}") + # Don't fail the entire test suite + return {"success": False, "error": str(e)} - # Update state - state["plan"] = plan - state["current_agent"] = "planner_agent" - state["messages"].append(f"Plan created: {json.dumps(plan)}") + async def test_concurrent_workflows(self): + """Test multiple concurrent workflows""" + logger.info("\n=== Testing Concurrent Workflows ===") - logger.info("Plan created successfully") + try: + # Create multiple test requests + requests = [] + for i in range(5): + request = { + "audio_data": f"audio_data_{i}", + "text": f"Test query {i}", + "user_id": f"test_user_{i:03d}", + "workflow_id": f"test_workflow_{i:03d}" + } + requests.append(request) + + # Process all requests concurrently + tasks = [self.orchestrator.process_request(req) for req in requests] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Check results + successful = 0 + failed = 0 + + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.error(f"Workflow {i} failed with exception: {result}") + failed += 1 + elif isinstance(result, dict) and result.get("success"): + logger.info(f"Workflow {i} completed successfully") + successful += 1 + else: + logger.warning(f"Workflow {i} returned: {result}") + failed += 1 + + logger.info(f"Concurrent test results: {successful} successful, {failed} failed") + + return results + + except Exception as e: + logger.error(f"Error in concurrent workflow test: {str(e)}") + return [] + + async def test_task_queue_operations(self): + """Test task queue operations directly""" + logger.info("\n=== Testing Task Queue Operations ===") + + # Import Task from the module we already imported + from task_queue import Task + + # Create test tasks + tasks = [] + for agent_type in ["voice_agent", "planner_agent", "insight_agent"]: + task = Task( + type=agent_type, + data={"test": f"data_for_{agent_type}"}, + priority=2 + ) + task_id = await self.task_queue.enqueue(task) + tasks.append(task_id) + logger.info(f"Enqueued task {task_id} for {agent_type}") + + # Wait for processing + await asyncio.sleep(2) - return state - - def _context_agent_node(self, state: AgentState) -> AgentState: - """Analyze context based on the plan""" - logger.info("Analyzing context...") - - plan = state.get("plan", {}) - - # Simulate context analysis - context = { - "user_profile": { - "preferences": ["technical", "detailed"], - "history": ["previous_queries"] - }, - "environment": { - "time": "afternoon", - "location": "office" - }, - "relevant_data": ["data1", "data2"] - } + # Check task statuses + for task_id in tasks: + status = await self.task_queue.get_task_status(task_id) + logger.info(f"Task {task_id} status: {status}") + + # Get queue stats + stats = await self.task_queue.get_queue_stats() + logger.info(f"Queue stats: {json.dumps(stats, indent=2)}") - # Update state - state["context"] = context - state["current_agent"] = "context_agent" - state["messages"].append(f"Context analyzed: {json.dumps(context)}") + return stats - logger.info("Context analysis complete") + async def test_event_bus_operations(self): + """Test event bus operations directly""" + logger.info("\n=== Testing Event Bus Operations ===") - return state - - def _memory_agent_node(self, state: AgentState) -> AgentState: - """Store relevant information in memory""" - logger.info("Storing memory...") - - context = state.get("context", {}) - - # Simulate memory storage - memory_entry = { - "timestamp": datetime.now().isoformat(), - "interaction": state["task_data"].get("transcription", ""), - "context": context, - "tags": ["voice", "query"] - } + # Test custom event handler + custom_events = [] - # Update state - state["memories"] = [memory_entry] - state["current_agent"] = "memory_agent" - state["messages"].append("Memory stored successfully") + async def custom_handler(event): + custom_events.append(event) + logger.info(f"Custom handler received: {event.type}") + + # Subscribe to custom events + await self.event_bus.subscribe("CUSTOM_EVENT", custom_handler) + + # Publish custom events + for i in range(3): + event = self.Event( + type="CUSTOM_EVENT", + data={"index": i, "message": f"Custom event {i}"}, + source="test" + ) + await self.event_bus.publish(event) + + # Wait for handlers + await asyncio.sleep(0.5) - logger.info("Memory stored successfully") + # Verify events received + assert len(custom_events) == 3, "Should receive all custom events" - return state - - def _insight_agent_node(self, state: AgentState) -> AgentState: - """Generate insights based on context and memories""" - logger.info("Generating insights...") - - context = state.get("context", {}) - memories = state.get("memories", []) - - # Simulate insight generation - insights = [ - { - "type": "pattern", - "description": "User frequently asks technical questions", - "confidence": 0.85 - }, - { - "type": "recommendation", - "description": "Provide detailed technical explanations", - "confidence": 0.90 - } - ] + # Get event history + history = await self.event_bus.get_event_history("CUSTOM_EVENT") + logger.info(f"Custom event history: {len(history)} events") - # Update state - state["insights"] = insights - state["current_agent"] = "insight_agent" - state["messages"].append(f"Insights generated: {len(insights)} insights") + # Get subscription stats + stats = await self.event_bus.get_subscription_stats() + logger.info(f"Event bus stats: {json.dumps(stats, indent=2)}") - logger.info(f"Generated {len(insights)} insights") + return stats - return state - - def _response_agent_node(self, state: AgentState) -> AgentState: - """Generate final response based on all previous processing""" - logger.info("Generating response...") + async def test_agent_pool_operations(self): + """Test agent pool operations directly""" + logger.info("\n=== Testing Agent Pool Operations ===") - plan = state.get("plan", {}) - context = state.get("context", {}) - insights = state.get("insights", []) + # Test acquiring and releasing agents + acquired_agents = [] - # Simulate response generation - response = f"""Based on your query: {state['task_data'].get('transcription', '')} + for agent_type in ["voice_agent", "planner_agent"]: + agent = await self.agent_pool.acquire(agent_type) + acquired_agents.append((agent_type, agent)) + logger.info(f"Acquired {agent_type}") -I've analyzed the context and generated the following response: -- Intent: {plan.get('intent', 'unknown')} -- Relevant insights: {len(insights)} insights found -- Personalized based on your preferences - -This is a sample response that would be generated by the actual response agent.""" + # Use the agent + result = await agent.process({"test": "data"}) + logger.info(f"{agent_type} processed: {result}") + + # Release agents + for agent_type, agent in acquired_agents: + await self.agent_pool.release(agent_type, agent) + logger.info(f"Released {agent_type}") + + # Get pool stats + stats = await self.agent_pool.get_pool_stats() + logger.info(f"Pool stats: {json.dumps(stats, indent=2)}") - # Update state - state["response"] = response - state["current_agent"] = "response_agent" - state["messages"].append(response) + # Health check + health = await self.agent_pool.health_check() + logger.info(f"Pool health: {json.dumps(health, indent=2)}") - logger.info("Response generated successfully") + return stats - return state - - def process_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]: - """Process a request through the orchestration workflow""" - initial_state = { - "messages": [], - "current_agent": "orchestrator", - "task_data": request_data, - "workflow_id": request_data.get("workflow_id", "default"), - "user_id": request_data.get("user_id", "default_user"), - "context": {}, - "plan": {}, - "memories": [], - "insights": [], - "response": "", - "error": None, - "retry_count": 0 + async def test_error_handling(self): + """Test error handling in the system""" + logger.info("\n=== Testing Error Handling ===") + + # Test with invalid request + invalid_request = { + "invalid_field": "test", + "user_id": "error_test_user" } + result = await self.orchestrator.process_request(invalid_request) + logger.info(f"Error handling result: {json.dumps(result, indent=2)}") + + # Test task retry + error_task = self.Task( + type="error_agent", # Non-existent agent type + data={"test": "error"}, + max_retries=2 + ) + + task_id = await self.task_queue.enqueue(error_task) + await asyncio.sleep(2) + + status = await self.task_queue.get_task_status(task_id) + logger.info(f"Error task status: {status}") + + return result + + async def cleanup(self): + """Cleanup all components""" + logger.info("\n=== Cleaning up ===") + + # Stop task queue + if self.task_queue: + await self.task_queue.stop() + + # Cleanup agent pool + if self.agent_pool: + await self.agent_pool.cleanup() + + # Clear event history + if self.event_bus: + await self.event_bus.clear_event_history() + + logger.info("Cleanup complete") + + async def run_all_tests(self): + """Run all integration tests""" + logger.info("Starting Orchestration Integration Tests") + logger.info("=" * 50) + try: - # Run the graph - final_state = self.compiled_graph.invoke(initial_state) - - return { - "success": True, - "response": final_state.get("response", ""), - "workflow_id": final_state.get("workflow_id"), - "insights": final_state.get("insights", []) - } + # Setup + await self.setup() + + # Run tests + await self.test_simple_workflow() + await self.test_concurrent_workflows() + await self.test_task_queue_operations() + await self.test_event_bus_operations() + await self.test_agent_pool_operations() + await self.test_error_handling() + + # Print summary + logger.info("\n" + "=" * 50) + logger.info("Test Summary:") + logger.info(f"Events published: {self.test_results['events_published']}") + logger.info(f"Tasks completed: {self.test_results['tasks_completed']}") + logger.info(f"Agents used: {self.test_results['agents_used']}") + logger.info(f"Errors encountered: {len(self.test_results['errors'])}") + + # Cleanup + await self.cleanup() + + logger.info("\nAll tests completed successfully! ✅") except Exception as e: - logger.error(f"Error in orchestration: {str(e)}") - return { - "success": False, - "error": str(e), - "workflow_id": initial_state.get("workflow_id") - } + logger.error(f"Test failed: {str(e)}") + self.test_results["errors"].append(str(e)) + await self.cleanup() + raise -# Test function -def test_simple_orchestrator(): - """Test the simple orchestrator""" - orchestrator = SimpleOrchestrator() - - test_request = { - "audio_data": "test_audio_base64", - "text": "What's the weather like today?", - "user_id": "test_user", - "workflow_id": "test_workflow" - } - - result = orchestrator.process_request(test_request) - print(f"Test result: {json.dumps(result, indent=2)}") +async def main(): + """Main test entry point""" + test = IntegrationTest() + await test.run_all_tests() if __name__ == "__main__": - test_simple_orchestrator() \ No newline at end of file + # Run the integration test + asyncio.run(main()) \ No newline at end of file From a3aa71a1abc3bf3a9eb8e208f7f1f7aa8b9027a1 Mon Sep 17 00:00:00 2001 From: Rakesh Cavala Date: Fri, 25 Jul 2025 20:48:32 -0400 Subject: [PATCH 18/25] created base_agent, voice_agent, response_agent, requirements and Dockerfile --- backend/agents/Docker | 0 backend/agents/response_agent.py | 86 +++++++++++++++++++++ backend/agents/voice_agent.py | 127 +++++++++++++++++++++++++++++++ backend/requirements.txt | 18 +++++ 4 files changed, 231 insertions(+) create mode 100644 backend/agents/Docker diff --git a/backend/agents/Docker b/backend/agents/Docker new file mode 100644 index 000000000..e69de29bb diff --git a/backend/agents/response_agent.py b/backend/agents/response_agent.py index e69de29bb..8144761be 100644 --- a/backend/agents/response_agent.py +++ b/backend/agents/response_agent.py @@ -0,0 +1,86 @@ +from langchain.llms.base import LLM +from langchain.chains import LLMChain +from langchain.prompts import PromptTemplate +from base_agent import BaseAgent # your custom base class +from langchain_google_vertexai import ChatVertexAI + +class ResponseAgent(BaseAgent): + def __init__(self): + # Initialize the Google Vertex AI chat model (Gemini) + self.llm = ChatVertexAI( + model_name="gemini-2.5-pro", # update model if needed + project="your-project-id", # replace with your GCP project ID + location="us-central1" # update region if needed + ) + + # Prompt template designed for Life Witness app with memory context + prompt_template = PromptTemplate( + input_variables=["input", "context"], + template=""" +You are Life Witness, a personal AI memory assistant that listens to life stories, captures moments, and helps recall memories with rich context. + +You have access to the user's life memories, including events, photos, calendar entries, and relationships. + +Respond in a warm, empathetic, and conversational tone — like a trusted friend who remembers important details. + +Use the provided context to ground your replies, including dates, locations, people, and emotional cues. + +If the user asks about past events, recall details clearly and helpfully. + +If the user wants to set reminders or follow up, help create clear action items. + +Do NOT say you are an AI or mention technical details. + +--- + +Context: +{context} + +User: +{input} + +Life Witness: +""" + ) + + self.chain = LLMChain(llm=self.llm, prompt=prompt_template) + + def run(self, input_text: str, context: str = "") -> str: + """ + Generate a conversational response using the input text and optional memory context. + + Args: + input_text (str): The user query or statement. + context (str, optional): A string summarizing relevant memories or context. Defaults to "". + + Returns: + str: The agent's generated response. + """ + return self.chain.run(input=input_text, context=context) + +#Example Usage: + +""" +response_agent = ResponseAgent() +response = response_agent.run(user_input, memory_context) +""" + + +""" +if __name__ == "__main__": + # Make sure you have authenticated your GCP credentials via: + # gcloud auth application-default login + + print("Initializing ResponseAgent...") + agent = ResponseAgent() + print("Agent initialized.") + + prompt = "Hello, how can you assist me today?" + print(f"Sending prompt: \"{prompt}\"") + try: + reply = agent.run(prompt) + print(f"Agent reply: {reply}") + except Exception as e: + print(f"An error occurred: {e}") + print("Check your Google Cloud authentication and project/location settings.") +""" diff --git a/backend/agents/voice_agent.py b/backend/agents/voice_agent.py index e69de29bb..c8b7d0175 100644 --- a/backend/agents/voice_agent.py +++ b/backend/agents/voice_agent.py @@ -0,0 +1,127 @@ +from base_agent import BaseAgent +from google.cloud import speech, texttospeech +import io +import os +import tempfile +from pydub import AudioSegment + + +class VoiceAgent(BaseAgent): + def __init__(self): + # Initialize Google Cloud clients + self.stt_client = speech.SpeechClient() + self.tts_client = texttospeech.TextToSpeechClient() + + + def speech_to_text(self, audio_file_path): + # Preprocess: convert to mono and 16kHz + print("[INFO] Preprocessing audio...") + sound = AudioSegment.from_file(audio_file_path) + sound = sound.set_channels(1).set_frame_rate(16000) + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file: + temp_path = tmp_file.name + sound.export(temp_path, format="wav") + + print(f"[INFO] Processed audio saved to: {temp_path}") + + # Load processed audio for Google STT + client = speech.SpeechClient() + with open(temp_path, "rb") as f: + audio_content = f.read() + + audio = speech.RecognitionAudio(content=audio_content) + config = speech.RecognitionConfig( + encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16, + sample_rate_hertz=16000, + language_code="en-US", + ) + + response = client.recognize(config=config, audio=audio) + + # Clean up temp file + os.remove(temp_path) + + if response.results: + transcript = " ".join([result.alternatives[0].transcript for result in response.results]) + return transcript + else: + return "No transcription available." + + + def text_to_speech(self, text: str, output_file_path: str = "output.mp3") -> str: + """ + Converts text to speech audio using Google Cloud Text-to-Speech. + Saves audio to output_file_path and returns the path. + """ + synthesis_input = texttospeech.SynthesisInput(text=text) + + voice = texttospeech.VoiceSelectionParams( + language_code="en-US", + ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL, + ) + + audio_config = texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding.MP3, + ) + + response = self.tts_client.synthesize_speech( + input=synthesis_input, + voice=voice, + audio_config=audio_config, + ) + + with open(output_file_path, "wb") as out: + out.write(response.audio_content) + + return output_file_path + + def run(self, input_data: str, mode: str = "text-to-speech") -> str: + """ + Main run method to either convert text to speech or speech to text. + mode: 'text-to-speech' or 'speech-to-text' + + If mode is 'speech-to-text', input_data should be path to audio file. + If mode is 'text-to-speech', input_data should be text to convert. + + Returns transcript text (for speech-to-text) or output audio path (for text-to-speech). + """ + if mode == "speech-to-text": + return self.speech_to_text(input_data) + elif mode == "text-to-speech": + return self.text_to_speech(input_data) + else: + raise ValueError("Invalid mode. Use 'text-to-speech' or 'speech-to-text'.") + + + + + + +""" + +if __name__ == "__main__": + agent = VoiceAgent() + + # Example 1: Convert speech audio file to text + original_audio = "/Users/rakeshcavala/agents-assemble/backend/agents/sample_audio.wav" # Input audio file + + try: + # Preprocess audio for Google STT + + # Run speech-to-text + transcript = agent.run(original_audio, mode="speech-to-text") + print(f"Transcript from '{original_audio}':\n{transcript}") + except Exception as e: + print(f"Error in speech-to-text: {e}") + + # Example 2: Convert text to speech audio file + text_input = "Hello, this is a test of the Google Cloud Text-to-Speech API." + try: + output_audio = agent.run(text_input, mode="text-to-speech") + print(f"Audio content saved to: {output_audio}") + except Exception as e: + print(f"Error in text-to-speech: {e}") + + """ + diff --git a/backend/requirements.txt b/backend/requirements.txt index e69de29bb..3df54dcd4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -0,0 +1,18 @@ +# Core dependencies for LangChain and Google Vertex AI LLM integration +langchain==0.0.202 +langchain-google-vertexai==0.0.5 + +# Google Cloud AI SDKs +google-cloud-aiplatform==1.26.2 +google-cloud-speech==2.15.1 +google-cloud-texttospeech==2.15.1 + +# Audio processing +pydub==0.25.1 + +# Optional but recommended for environment management +python-dotenv==1.0.0 + +# For testing if needed +pytest==7.4.0 +pytest-asyncio==0.21.0 From 5e87bd704db068364f7e5fa3189e6caa4b143a6c Mon Sep 17 00:00:00 2001 From: Ayaan Agarwal Date: Fri, 25 Jul 2025 19:25:51 -0600 Subject: [PATCH 19/25] Finalize UI redesign and updated overlay --- frontend/package-lock.json | 43 ++++++ frontend/package.json | 1 + frontend/src/app/page.tsx | 291 ++++++++++++++++++++++--------------- 3 files changed, 217 insertions(+), 118 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 32d863e69..985c09fc9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "framer-motion": "^12.23.9", "next": "15.4.4", "react": "19.1.0", "react-dom": "19.1.0", @@ -3327,6 +3328,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.23.9", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.9.tgz", + "integrity": "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.9", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4599,6 +4627,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.23.9", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.9.tgz", + "integrity": "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index a2f153248..d3ef3449b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "framer-motion": "^12.23.9", "next": "15.4.4", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8ed3d331f..0b4401ebc 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,145 +1,200 @@ 'use client'; -// Importing the tools I need from React and the Swiper carousel library. -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Swiper, SwiperSlide } from 'swiper/react'; import { EffectCoverflow, Pagination, Autoplay } from 'swiper/modules'; import type { Swiper as SwiperCore } from 'swiper'; +import { motion, AnimatePresence } from 'framer-motion'; import 'swiper/css'; import 'swiper/css/effect-coverflow'; import 'swiper/css/pagination'; export default function Home() { - // Here, I'm setting up the state for my component. This is the information - // that will change as the user interacts with the page. + // ---------------------------------------------------------------- + // --- 1. STATE MANAGEMENT --- + // All the variables that store information for the page. + // ---------------------------------------------------------------- + const [view, setView] = useState<'home' | 'thinking' | 'chat'>('home'); + const [conversation, setConversation] = useState<{ sender: 'user' | 'agent'; query: string; response?: string }[]>([]); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [currentUserQuery, setCurrentUserQuery] = useState(''); - const [currentAgentReply, setCurrentAgentReply] = useState(''); - const [highlightedMemoryId, setHighlightedMemoryId] = useState(null); - - // This ref gives me direct control over the carousel component. + const [isInitiated, setIsInitiated] = useState(false); + const [activeIndex, setActiveIndex] = useState(0); const carouselRef = useRef(null); + const chatContainerRef = useRef(null); - // This is the hard-coded conversation I'm using for the demo. - const demoConversation = [ - { - query: "Who was that person I met at TechCrunch who worked on crypto payments?", - response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team." - }, - { - query: "Right! What else did we talk about?", - response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role." - }, - { - query: "Did I ever follow up with her?", - response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'" - }, - { - query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", - response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?" - } - ]; - - // This is the list of photos for the carousel. - const memoryPhotos = [ - { id: 1, name: 'Jennifer Chen', imageUrl: '/placeholder.png' }, - { id: 2, name: 'Jake\'s Bday', imageUrl: '/placeholder.png' }, - { id: 3, name: 'Coffee Meetup', imageUrl: '/placeholder.png' }, - { id: 4, name: 'Team Lunch', imageUrl: '/placeholder.png' }, - { id: 5, name: 'Project Demo', imageUrl: '/placeholder.png' }, - ]; + // 2. DEMO DATA + // All the hard-coded content for the prototype. + const demoConversation = [ { query: "Who was that person I met at TechCrunch who worked on crypto payments?", response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team." }, { query: "Right! What else did we talk about?", response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role." }, { query: "Did I ever follow up with her?", response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'" }, { query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?" } ]; + const memoryPhotos = [ { id: 1, name: 'Jennifer Chen', imageUrl: '/placeholder.png' }, { id: 2, name: 'Jake\'s Bday', imageUrl: '/placeholder.png' }, { id: 3, name: 'Coffee Meetup', imageUrl: '/placeholder.png' }, { id: 4, name: 'Team Lunch', imageUrl: '/placeholder.png' }, { id: 5, name: 'Project Demo', imageUrl: '/placeholder.png' } ]; - // This function runs when the microphone button is clicked. - const runNextStepInDemo = () => { - const jenniferId = 1; - const jenniferIndex = memoryPhotos.findIndex(m => m.id === jenniferId); + // 3. HELPER FUNCTIONS + // Reusable logic, like creating a delay. + const wait = (ms: number) => new Promise(res => setTimeout(res, ms)); - // On the first click of the demo, I'll stop the autoplay and slide to Jennifer's photo. - if (currentStepIndex === 0) { - setHighlightedMemoryId(jenniferId); - if (carouselRef.current && jenniferIndex !== -1) { - carouselRef.current.autoplay.stop(); - carouselRef.current.slideToLoop(jenniferIndex); - } + useEffect(() => { + if (chatContainerRef.current) { + const { scrollHeight, clientHeight } = chatContainerRef.current; + chatContainerRef.current.scrollTo({ top: scrollHeight - clientHeight, behavior: 'smooth' }); } + }, [conversation]); + + // 4. EVENT HANDLERS + // All the functions that run when the user does something. + const handleInitiateConversation = async () => { + if (view !== 'home' || currentStepIndex !== 0) return; + setIsInitiated(true); + if (carouselRef.current) carouselRef.current.autoplay.stop(); + }; - // I'll display the current step of the conversation... + const switchToThinking = async () => { + await wait(1000); + setView('thinking'); + await wait(2000); + const firstStep = demoConversation[0]; + setConversation([{ sender: 'user', query: firstStep.query }]); + setCurrentStepIndex(1); + setView('chat'); + await wait(2500); + setConversation(prev => [...prev, { sender: 'agent', query: '', response: firstStep.response }]); + }; + + const runNextStepInDemo = async () => { const currentStep = demoConversation[currentStepIndex]; - setCurrentUserQuery(currentStep.query); - setCurrentAgentReply(currentStep.response); - - // ...and then get ready for the next click by advancing the step index. - setCurrentStepIndex((prevIndex) => (prevIndex + 1) % demoConversation.length); + if (!currentStep) return; + setConversation(prev => [ ...prev, { sender: 'user', query: currentStep.query }]); + await wait(1500); + setConversation(prev => [ ...prev, { sender: 'agent', query: '', response: currentStep.response } ]); + setCurrentStepIndex(prev => (prev + 1) % demoConversation.length); + }; + + const handleGoHome = () => { + setView('home'); + setConversation([]); + setCurrentStepIndex(0); + setIsInitiated(false); + if (carouselRef.current) { + carouselRef.current.autoplay?.start(); + } + }; + + // 5. HELPER COMPONENTS --- + // Small, reusable components used only on this page. + const TypewriterText = ({ text, onComplete, className }: { text: string, onComplete?: () => void, className?: string }) => { + const [displayedText, setDisplayedText] = useState(''); + useEffect(() => { + let currentText = ''; + setDisplayedText(''); + const words = text.split(' '); + let currentWordIndex = 0; + const intervalId = setInterval(() => { + if (currentWordIndex < words.length) { + currentText += (currentWordIndex > 0 ? ' ' : '') + words[currentWordIndex]; + setDisplayedText(currentText); + currentWordIndex++; + } else { + clearInterval(intervalId); + if (onComplete) onComplete(); + } + }, 120); + return () => clearInterval(intervalId); + }, [text, onComplete]); + return

{displayedText}

; }; + // --- 6. RENDER --- + // The actual JSX that gets rendered to the screen. return ( -
- -
-
Live Witness
- -
- -
- - { carouselRef.current = swiper; }} - effect={'coverflow'} - grabCursor={true} - centeredSlides={true} - slidesPerView={3} - loop={true} - autoplay={{ delay: 3000, disableOnInteraction: false }} - coverflowEffect={{ rotate: 0, stretch: 0, depth: 150, modifier: 1, slideShadows: false }} - pagination={false} - modules={[EffectCoverflow, Pagination, Autoplay]} - className="w-full max-w-5xl" - > - {memoryPhotos.map(photo => ( - -
- {photo.name - {photo.name} +
+ + {view === 'home' && ( + +
+
+
Life Witness
+ +
+
+
+
+
+

+ Hello, what memory +

+

+ would you like to relive today? +

+
+ { carouselRef.current = swiper; }} + onSlideChange={(swiper) => setActiveIndex(swiper.realIndex)} + effect={'coverflow'} grabCursor={true} centeredSlides={true} + slidesPerView={3} loop={true} autoplay={{ delay: 3000, disableOnInteraction: false }} + modules={[EffectCoverflow, Pagination, Autoplay]} className="w-full" + coverflowEffect={{ rotate: 0, stretch: 80, depth: 150, modifier: 1, slideShadows: false }} + > + {memoryPhotos.map((photo, index) => ( + +
+ {photo.name + {photo.name} +
+
+ ))} +
+
+
+ {isInitiated ? ( + + ) : ( +

Ask about a memory...

+ )} +
+ + +
+
+
+
+ )} +
+ + + {view === 'thinking' && ( + +

"{demoConversation[0].query}"

+
+ + + +
+
+ )} +
+ + + {view === 'chat' && ( + +
+
+ +
Jennifer Chen

Jennifer Chen

+
+
+
+ {conversation.map((chat, index) => (
{chat.sender === 'user' ? (
) : (
)}
))} +
+
+ +
- - ))} - - -
-
- -

"{currentUserQuery || 'Click the mic to start the conversation...'}"

-
- -
- - {currentAgentReply && ( -
- AI Response: -

{currentAgentReply}

+ )} -
+
); } \ No newline at end of file From 77a2a4e46acaca0010a3aecd828f369448c44e3b Mon Sep 17 00:00:00 2001 From: muhamed-aroui Date: Fri, 25 Jul 2025 21:34:04 -0400 Subject: [PATCH 20/25] VisionAgent class added --- backend/agents/vision_agent.py | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/backend/agents/vision_agent.py b/backend/agents/vision_agent.py index e69de29bb..d966d13c5 100644 --- a/backend/agents/vision_agent.py +++ b/backend/agents/vision_agent.py @@ -0,0 +1,64 @@ +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_core.messages import HumanMessage +from base_agent import BaseAgent +from dotenv import load_dotenv +import base64 + + +load_dotenv() + + +class VisionAgent(BaseAgent): + def __init__(self, model_name: str = "gemini-2.5-flash"): + """ + Initializes the VisionAgent with a specified Google Generative AI model. + + Args: + model_name (str): The name of the model to use. + """ + super().__init__() + self.model = ChatGoogleGenerativeAI(model=model_name) + print(f"VisionAgent initialized with model: {model_name}") + + def describe(self, image_path: str) -> str: + """ + Creates a detailed textual description of the image at the given file path. + + Args: + image_path (str): The file path to the image to be described. + + Returns: + str: A textual description of the image. + """ + # Read the image file and encode it in base64 + try: + with open(image_path, "rb") as image_file: + base64_image = base64.b64encode(image_file.read()).decode("utf-8") + except FileNotFoundError: + return f"Error: Image file not found at '{image_path}'" + + # Prepare the message for the model + message = HumanMessage( + content=[ + {"type": "text", "text": "Describe this image in detail."}, + { + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}, + }, + ] + ) + + # Invoke the model and return the content of the response + response = self.model.invoke([message]) + return response.content + +if __name__=="__main__": + + vision_agent = VisionAgent() + + image_file_path = "path_to_image" + + description = vision_agent.describe(image_file_path) + + print("\n--- Image Description ---") + print(description) From d84258a2ced7fa7f15a162e57a023f9ba3786d2d Mon Sep 17 00:00:00 2001 From: Likitha Date: Fri, 25 Jul 2025 23:53:19 -0400 Subject: [PATCH 21/25] mvp orchestrator single node --- backend/api/routes/voice_routes.py | 144 +++++++++++ backend/orchestrator_single.py/.env | 2 + .../orchestrator_single.cpython-313.pyc | Bin 0 -> 11096 bytes backend/orchestrator_single.py/main.py | 74 ++++++ .../orchestrator_single.py | 232 ++++++++++++++++++ .../orchestrator_single.py/requirements.txt | 7 + backend/orchestrator_single.py/test_mvp.py | 72 ++++++ .../voice_routes_single.py | 74 ++++++ backend/requirements.txt | 2 + 9 files changed, 607 insertions(+) create mode 100644 backend/orchestrator_single.py/.env create mode 100644 backend/orchestrator_single.py/__pycache__/orchestrator_single.cpython-313.pyc create mode 100644 backend/orchestrator_single.py/main.py create mode 100644 backend/orchestrator_single.py/orchestrator_single.py create mode 100644 backend/orchestrator_single.py/requirements.txt create mode 100644 backend/orchestrator_single.py/test_mvp.py create mode 100644 backend/orchestrator_single.py/voice_routes_single.py diff --git a/backend/api/routes/voice_routes.py b/backend/api/routes/voice_routes.py index e69de29bb..036794c93 100644 --- a/backend/api/routes/voice_routes.py +++ b/backend/api/routes/voice_routes.py @@ -0,0 +1,144 @@ +# api/routes/voice_routes.py - Voice processing endpoints +from fastapi import APIRouter, HTTPException, UploadFile, File, Depends +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any +import base64 +import logging + +# Import orchestrator +from orchestration.orchestrator import Orchestrator + +logger = logging.getLogger(__name__) + +# Create router +router = APIRouter(prefix="/api/voice", tags=["voice"]) + +# Request/Response models +class VoiceProcessRequest(BaseModel): + """Generic voice processing request""" + audio_data: Optional[str] = Field(None, description="Base64 encoded audio data") + text: Optional[str] = Field("", description="Text input if no audio") + metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata") + +class VoiceProcessResponse(BaseModel): + """Generic voice processing response""" + response: str + voice_data: Optional[Dict[str, Any]] = None + insights: Optional[list] = None + memory_stored: Optional[bool] = None + memories_retrieved: Optional[list] = None + error: Optional[str] = None + +# Dependency to get orchestrator instance +def get_orchestrator() -> Orchestrator: + """Get the orchestrator instance from app state""" + from main import app + if not hasattr(app.state, 'orchestrator'): + raise HTTPException(status_code=503, detail="Orchestrator not initialized") + return app.state.orchestrator + +@router.post("/process", response_model=VoiceProcessResponse) +async def process_voice( + request: VoiceProcessRequest, + orchestrator: Orchestrator = Depends(get_orchestrator) +): + """ + Process voice input through the orchestrator + + This endpoint simply forwards the request to the orchestrator + and returns its response. + """ + try: + # Build orchestrator input + orchestrator_input = { + "text": request.text or "", + "audio_data": request.audio_data, + "metadata": request.metadata + } + + # Process through orchestrator + result = await orchestrator.process(orchestrator_input) + + # Return orchestrator response directly + return VoiceProcessResponse( + response=result.get("response", ""), + voice_data=result.get("voice_data"), + insights=result.get("insights"), + memory_stored=result.get("memory_stored"), + memories_retrieved=result.get("memories_retrieved"), + error=result.get("error") + ) + + except Exception as e: + logger.error(f"Voice processing error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +# @router.post("/upload") +# async def upload_voice_file( +# file: UploadFile = File(...), +# metadata: Optional[str] = None, +# orchestrator: Orchestrator = Depends(get_orchestrator) +# ): +# """ +# Upload a voice file for processing + +# Converts the file to base64 and forwards to orchestrator. +# """ +# try: +# # Read and encode file +# audio_bytes = await file.read() +# audio_base64 = base64.b64encode(audio_bytes).decode('utf-8') + +# # Parse metadata if provided +# import json +# parsed_metadata = {} +# if metadata: +# try: +# parsed_metadata = json.loads(metadata) +# except: +# pass + +# # Add file info to metadata +# parsed_metadata["filename"] = file.filename +# parsed_metadata["content_type"] = file.content_type + +# # Build orchestrator input +# orchestrator_input = { +# "text": "", +# "audio_data": audio_base64, +# "metadata": parsed_metadata +# } + +# # Process through orchestrator +# result = await orchestrator.process(orchestrator_input) + +# # Return result +# return { +# "response": result.get("response", ""), +# "voice_data": result.get("voice_data"), +# "insights": result.get("insights"), +# "memory_stored": result.get("memory_stored"), +# "memories_retrieved": result.get("memories_retrieved"), +# "error": result.get("error") +# } + +# except Exception as e: +# logger.error(f"File upload error: {e}") +# raise HTTPException(status_code=500, detail=str(e)) + +# @router.get("/health") +# async def voice_health_check(orchestrator: Orchestrator = Depends(get_orchestrator)): +# """Check voice service health""" +# try: +# # Get agent pool stats +# pool_stats = orchestrator.agent_pool.get_pool_stats() + +# return { +# "status": "healthy", +# "voice_agent_stats": pool_stats.get("voice", {}), +# } +# except Exception as e: +# return { +# "status": "unhealthy", +# "error": str(e) +# } \ No newline at end of file diff --git a/backend/orchestrator_single.py/.env b/backend/orchestrator_single.py/.env new file mode 100644 index 000000000..26a0d00a0 --- /dev/null +++ b/backend/orchestrator_single.py/.env @@ -0,0 +1,2 @@ +GOOGLE_API_KEY=#add your api key +PORT=8000 \ No newline at end of file diff --git a/backend/orchestrator_single.py/__pycache__/orchestrator_single.cpython-313.pyc b/backend/orchestrator_single.py/__pycache__/orchestrator_single.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7554e668d7bd363add078735eebc5fe9c26a725 GIT binary patch literal 11096 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 literal 0 HcmV?d00001 diff --git a/backend/orchestrator_single.py/main.py b/backend/orchestrator_single.py/main.py new file mode 100644 index 000000000..815eb10d0 --- /dev/null +++ b/backend/orchestrator_single.py/main.py @@ -0,0 +1,74 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import base64 +from orchestrator_single import process_voice_sync +import os +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +CORS(app) + +@app.route('/') +def home(): + return jsonify({ + "message": "Life Witness Agent API", + "version": "1.0.0", + "endpoints": { + "/api/voice/process": "POST - Process voice input", + "/api/text/process": "POST - Process text input (for testing)", + "/api/health": "GET - Health check" + } + }) + +@app.route('/api/voice/process', methods=['POST']) +def process_voice(): + """Process voice input""" + try: + data = request.json + audio_base64 = data.get('audio') + + if not audio_base64: + return jsonify({"error": "No audio data provided"}), 400 + + # Decode base64 audio + audio_bytes = base64.b64decode(audio_base64) + + # Process through orchestrator + result = process_voice_sync(audio_data=audio_bytes) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/text/process', methods=['POST']) +def process_text(): + """Process text input (for testing without audio)""" + try: + data = request.json + text = data.get('text', '') + + if not text: + return jsonify({"error": "No text provided"}), 400 + + # Process through orchestrator + result = process_voice_sync(test_text=text) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/health', methods=['GET']) +def health(): + """Health check""" + return jsonify({ + "status": "healthy", + "service": "life-witness-agent" + }) + +if __name__ == '__main__': + port = int(os.getenv('PORT', 8000)) + app.run(host='0.0.0.0', port=port, debug=True) \ No newline at end of file diff --git a/backend/orchestrator_single.py/orchestrator_single.py b/backend/orchestrator_single.py/orchestrator_single.py new file mode 100644 index 000000000..b8cd10c6c --- /dev/null +++ b/backend/orchestrator_single.py/orchestrator_single.py @@ -0,0 +1,232 @@ +from typing import TypedDict, Dict, Any +from langgraph.graph import Graph, END +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain.memory import ConversationBufferMemory +from langchain.schema import HumanMessage, AIMessage +import google.generativeai as genai +import base64 +import os +from dotenv import load_dotenv +import asyncio + +load_dotenv() + +# Configure Gemini +genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + +# Initialize Gemini models +gemini_model = genai.GenerativeModel('gemini-1.5-pro') +gemini_flash = genai.GenerativeModel('gemini-1.5-flash') + +# Initialize LangChain Gemini for conversation +llm = ChatGoogleGenerativeAI( + model="gemini-1.5-flash", + google_api_key=os.getenv("GOOGLE_API_KEY"), + temperature=0.7 +) + +# Initialize conversational memory +memory = ConversationBufferMemory( + return_messages=True, + memory_key="chat_history" +) + +# Simple memory storage +stored_memories = [] + +class VoiceAgent: + """Handles voice operations using Gemini""" + def __init__(self): + self.gemini_flash = gemini_flash + + def transcribe(self, audio_data: bytes) -> str: + """For MVP, we'll skip actual audio transcription""" + # In production, you would upload audio and transcribe + # For now, return a placeholder + return "Transcribed audio content" + + def synthesize(self, text: str) -> bytes: + """Mock audio synthesis""" + return b"mock_audio_data" + +class MemoryAgent: + """Manages conversation memory""" + def __init__(self, memory: ConversationBufferMemory): + self.memory = memory + self.gemini_model = gemini_model + + def add_interaction(self, user_input: str, ai_response: str): + """Add user-AI interaction to memory""" + self.memory.chat_memory.add_user_message(user_input) + self.memory.chat_memory.add_ai_message(ai_response) + + # Extract and store key information + if "remember" in user_input.lower() or "don't forget" in user_input.lower(): + stored_memories.append({ + "id": len(stored_memories) + 1, + "content": user_input, + "response": ai_response, + "type": "explicit_memory" + }) + + def get_context(self) -> str: + """Get conversation history as context""" + messages = self.memory.chat_memory.messages + context = [] + for msg in messages[-10:]: # Last 10 messages + if isinstance(msg, HumanMessage): + context.append(f"User: {msg.content}") + elif isinstance(msg, AIMessage): + context.append(f"Assistant: {msg.content}") + return "\n".join(context) + + def search_memories(self, query: str) -> list: + """Simple memory search""" + results = [] + query_lower = query.lower() + for memory in stored_memories: + if query_lower in memory["content"].lower(): + results.append(memory) + return results + +class ResponseAgent: + """Generate responses using Gemini""" + def __init__(self, memory_agent): + self.memory_agent = memory_agent + self.gemini_model = gemini_model + + def generate(self, user_input: str) -> str: + """Generate contextual response""" + context = self.memory_agent.get_context() + + # Check if user is asking about memories + is_memory_query = any(word in user_input.lower() for word in ["what", "when", "who", "remember", "told", "said"]) + + memories = [] + if is_memory_query: + memories = self.memory_agent.search_memories(user_input) + + # Build prompt + prompt = f"""You are a Life Witness Agent - a personal AI companion that helps people capture and remember their life experiences. + +Previous conversation: +{context} + +{"Relevant memories found: " + str(memories) if memories else ""} + +User's current message: {user_input} + +Instructions: +- If the user wants to remember something, acknowledge it warmly and confirm you'll remember it +- If they're asking about past memories, help them recall based on conversation history +- Be conversational, warm, and show genuine interest +- Keep responses concise but meaningful + +Response:""" + + # Generate response using Gemini + response = self.gemini_model.generate_content(prompt) + return response.text.strip() + +# Initialize agents +voice_agent = VoiceAgent() +memory_agent = MemoryAgent(memory) +response_agent = ResponseAgent(memory_agent) + +# Define state schema +class AgentState(TypedDict): + audio_input: bytes + transcribed_text: str + response_text: str + audio_output: bytes + error: str + +# Node functions +def transcribe_audio(state: AgentState) -> AgentState: + """Convert audio to text""" + try: + if state.get("audio_input"): + text = voice_agent.transcribe(state["audio_input"]) + state["transcribed_text"] = text + else: + # For testing without audio + state["transcribed_text"] = state.get("test_text", "Hello") + except Exception as e: + state["error"] = f"Transcription error: {str(e)}" + state["transcribed_text"] = "Could not understand audio" + return state + +def generate_response(state: AgentState) -> AgentState: + """Generate AI response with memory context""" + try: + user_input = state["transcribed_text"] + response = response_agent.generate(user_input) + state["response_text"] = response + + # Update memory with this interaction + memory_agent.add_interaction(user_input, response) + + except Exception as e: + state["error"] = f"Response generation error: {str(e)}" + state["response_text"] = "I'm sorry, I couldn't process that request." + return state + +def synthesize_speech(state: AgentState) -> AgentState: + """Convert response text to speech""" + try: + audio_data = voice_agent.synthesize(state["response_text"]) + state["audio_output"] = audio_data + except Exception as e: + state["error"] = f"Speech synthesis error: {str(e)}" + state["audio_output"] = b"" + return state + +# Build the LangGraph workflow +workflow = Graph() + +# Add nodes +workflow.add_node("transcribe", transcribe_audio) +workflow.add_node("generate", generate_response) +workflow.add_node("synthesize", synthesize_speech) + +# Define edges +workflow.add_edge("transcribe", "generate") +workflow.add_edge("generate", "synthesize") +workflow.add_edge("synthesize", END) + +# Set entry point +workflow.set_entry_point("transcribe") + +# Compile the graph +app = workflow.compile() + +# Main orchestration function +async def process_voice_input(audio_data: bytes = None, test_text: str = None) -> Dict[str, Any]: + """Process voice input through the workflow""" + initial_state = { + "audio_input": audio_data, + "transcribed_text": "", + "response_text": "", + "audio_output": b"", + "error": "", + "test_text": test_text + } + + # Run the workflow + result = await app.ainvoke(initial_state) + + return { + "transcribed_text": result.get("transcribed_text"), + "response_text": result.get("response_text"), + "audio_output": base64.b64encode(result.get("audio_output", b"")).decode('utf-8'), + "error": result.get("error"), + "conversation_length": len(memory.chat_memory.messages), + "total_memories": len(stored_memories) + } + +# Synchronous wrapper for Flask +def process_voice_sync(audio_data: bytes = None, test_text: str = None) -> Dict[str, Any]: + """Synchronous wrapper for the async orchestration""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop.run_until_complete(process_voice_input(audio_data, test_text)) \ No newline at end of file diff --git a/backend/orchestrator_single.py/requirements.txt b/backend/orchestrator_single.py/requirements.txt new file mode 100644 index 000000000..03cdb635a --- /dev/null +++ b/backend/orchestrator_single.py/requirements.txt @@ -0,0 +1,7 @@ +flask==3.0.0 +flask-cors==4.0.0 +python-dotenv==1.0.0 +langchain==0.1.0 +langchain-google-genai==0.0.5 +langgraph==0.0.20 +google-generativeai>=0.3.2 \ No newline at end of file diff --git a/backend/orchestrator_single.py/test_mvp.py b/backend/orchestrator_single.py/test_mvp.py new file mode 100644 index 000000000..2028e5cf0 --- /dev/null +++ b/backend/orchestrator_single.py/test_mvp.py @@ -0,0 +1,72 @@ +import requests +import time +import json + +base_url = "http://localhost:8000" + +print("=== Life Witness Agent - Enhanced Gemini Test ===\n") + +# Test 1: Store a memory with automatic function calling +print("Test 1: Storing a detailed memory") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "I just had an amazing lunch with my colleague Sarah at Chez Pierre restaurant. We discussed the new AI project and she gave me great advice about leadership. I'm feeling really inspired!"} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"Function calls: {json.dumps(result.get('function_calls', []), indent=2)}\n") +time.sleep(2) + +# Test 2: Store another memory +print("Test 2: Family memory") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "Remember that my nephew Jake's birthday is next week. He's turning 9 and loves robotics. I should get him that Arduino kit we saw."} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"Function calls: {json.dumps(result.get('function_calls', []), indent=2)}\n") +time.sleep(2) + +# Test 3: Query memories +print("Test 3: Searching for memories") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "What did Sarah tell me about?"} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"Function calls: {json.dumps(result.get('function_calls', []), indent=2)}\n") +time.sleep(2) + +# Test 4: Complex query +print("Test 4: Complex memory query") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "What upcoming events do I need to remember?"} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"Function calls: {json.dumps(result.get('function_calls', []), indent=2)}\n") +time.sleep(2) + +# Test 5: Emotional memory +print("Test 5: Emotional context") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "I'm feeling a bit overwhelmed with work. My manager just assigned me three new projects and I don't know how I'll manage everything."} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"Function calls: {json.dumps(result.get('function_calls', []), indent=2)}\n") + +# Test 6: Memory statistics +print("\nTest 6: Conversation summary") +response = requests.post( + f"{base_url}/api/text/process", + json={"text": "Can you summarize what we've talked about today?"} +) +result = response.json() +print(f"Response: {result['response_text']}") +print(f"\nTotal memories stored: {result.get('total_memories', 0)}") +print(f"Conversation length: {result.get('conversation_length', 0)} messages") \ No newline at end of file diff --git a/backend/orchestrator_single.py/voice_routes_single.py b/backend/orchestrator_single.py/voice_routes_single.py new file mode 100644 index 000000000..815eb10d0 --- /dev/null +++ b/backend/orchestrator_single.py/voice_routes_single.py @@ -0,0 +1,74 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +import base64 +from orchestrator_single import process_voice_sync +import os +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +CORS(app) + +@app.route('/') +def home(): + return jsonify({ + "message": "Life Witness Agent API", + "version": "1.0.0", + "endpoints": { + "/api/voice/process": "POST - Process voice input", + "/api/text/process": "POST - Process text input (for testing)", + "/api/health": "GET - Health check" + } + }) + +@app.route('/api/voice/process', methods=['POST']) +def process_voice(): + """Process voice input""" + try: + data = request.json + audio_base64 = data.get('audio') + + if not audio_base64: + return jsonify({"error": "No audio data provided"}), 400 + + # Decode base64 audio + audio_bytes = base64.b64decode(audio_base64) + + # Process through orchestrator + result = process_voice_sync(audio_data=audio_bytes) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/text/process', methods=['POST']) +def process_text(): + """Process text input (for testing without audio)""" + try: + data = request.json + text = data.get('text', '') + + if not text: + return jsonify({"error": "No text provided"}), 400 + + # Process through orchestrator + result = process_voice_sync(test_text=text) + + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/health', methods=['GET']) +def health(): + """Health check""" + return jsonify({ + "status": "healthy", + "service": "life-witness-agent" + }) + +if __name__ == '__main__': + port = int(os.getenv('PORT', 8000)) + app.run(host='0.0.0.0', port=port, debug=True) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 65f5b1e77..f23bab0b4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,4 +3,6 @@ langchain-google-genai langgraph python-dotenv dataclasses-json +fastapi +pydantic # Standard library modules (no need to install): asyncio, logging, typing, datetime, enum, uuid, json, time, weakref, collections, importlib \ No newline at end of file From 57bcbf07307c7f78caf4cae70baf757197092337 Mon Sep 17 00:00:00 2001 From: Shivani Rupnawar Date: Fri, 25 Jul 2025 22:00:33 -0700 Subject: [PATCH 22/25] added speech-to-text feature on frontend app and modified the upload component to match the new design --- frontend/global.d.ts | 57 +++ frontend/src/app/page.tsx | 545 ++++++++++++++++----- frontend/src/components/photoUpload.tsx | 28 +- frontend/src/components/voiceInterface.tsx | 233 +++++++++ frontend/src/services/voiceService.ts | 170 +++++++ 5 files changed, 898 insertions(+), 135 deletions(-) create mode 100644 frontend/global.d.ts diff --git a/frontend/global.d.ts b/frontend/global.d.ts new file mode 100644 index 000000000..1b01b2ebf --- /dev/null +++ b/frontend/global.d.ts @@ -0,0 +1,57 @@ +// Add this to your global.d.ts file or at the top of your VoiceInterface component file + +interface SpeechRecognition extends EventTarget { + continuous: boolean; + interimResults: boolean; + lang: string; + start(): void; + stop(): void; + onstart: ((this: SpeechRecognition, ev: Event) => any) | null; + onend: ((this: SpeechRecognition, ev: Event) => any) | null; + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null; + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any) | null; +} + +interface SpeechRecognitionEvent extends Event { + resultIndex: number; + results: SpeechRecognitionResultList; +} + +interface SpeechRecognitionResultList { + length: number; + [index: number]: SpeechRecognitionResult; +} + +interface SpeechRecognitionResult { + length: number; + isFinal: boolean; + [index: number]: SpeechRecognitionAlternative; +} + +interface SpeechRecognitionAlternative { + transcript: string; + confidence: number; +} + +interface SpeechRecognitionErrorEvent extends Event { + error: string; + message: string; +} + +declare var SpeechRecognition: { + prototype: SpeechRecognition; + new(): SpeechRecognition; +}; + +declare var webkitSpeechRecognition: { + prototype: SpeechRecognition; + new(): SpeechRecognition; +}; + +// Extend the Window interface +declare global { + interface Window { + SpeechRecognition: typeof SpeechRecognition; + webkitSpeechRecognition: typeof SpeechRecognition; + } +} \ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index aa09722d4..d6bb36a5f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,34 +1,48 @@ -'use client'; - -import React, { useState, useRef, useEffect } from 'react'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { EffectCoverflow, Pagination, Autoplay } from 'swiper/modules'; -import type { Swiper as SwiperCore } from 'swiper'; -import { motion, AnimatePresence } from 'framer-motion'; -import 'swiper/css'; -import 'swiper/css/effect-coverflow'; -import 'swiper/css/pagination'; - -// SHIVANI ADDITION: Importing the photoUpload component +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { EffectCoverflow, Pagination, Autoplay } from "swiper/modules"; +import type { Swiper as SwiperCore } from "swiper"; +import { motion, AnimatePresence } from "framer-motion"; +import "swiper/css"; +import "swiper/css/effect-coverflow"; +import "swiper/css/pagination"; + +// SHIVANI ADDITION: Importing the photoUpload component import PhotoUploadModal from "../components/photoUpload"; +// Import the voice components and services +import VoiceInterface from "../components/voiceInterface"; +import { voiceService, type VoiceQueryResult } from "../services/voiceService"; // Helper component for the word-by-word typing effect -const TypewriterText = ({ text, onComplete, className }: { text: string, onComplete?: () => void, className?: string }) => { - const [displayedText, setDisplayedText] = useState(''); - +const TypewriterText = ({ + text, + onComplete, + className, +}: { + text: string; + onComplete?: () => void; + className?: string; +}) => { + const [displayedText, setDisplayedText] = useState(""); + useEffect(() => { - let currentText = ''; - setDisplayedText(''); - const words = text.split(' '); + let currentText = ""; + setDisplayedText(""); + const words = text.split(" "); let currentWordIndex = 0; const intervalId = setInterval(() => { if (currentWordIndex < words.length) { - currentText = currentText + (currentWordIndex > 0 ? ' ' : '') + words[currentWordIndex]; + currentText = + currentText + + (currentWordIndex > 0 ? " " : "") + + words[currentWordIndex]; setDisplayedText(currentText); currentWordIndex++; } else { clearInterval(intervalId); - if (onComplete) onComplete(); + if (onComplete) onComplete(); } }, 120); return () => clearInterval(intervalId); @@ -37,155 +51,444 @@ const TypewriterText = ({ text, onComplete, className }: { text: string, onCompl }; export default function Home() { - const [view, setView] = useState<'home' | 'thinking' | 'chat'>('home'); - const [conversation, setConversation] = useState<{ sender: 'user' | 'agent'; query: string; response?: string }[]>([]); + const [view, setView] = useState<"home" | "thinking" | "chat">("home"); + const [conversation, setConversation] = useState< + { sender: "user" | "agent"; query: string; response?: string }[] + >([]); const [currentStepIndex, setCurrentStepIndex] = useState(0); const [isInitiated, setIsInitiated] = useState(false); const carouselRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); const chatContainerRef = useRef(null); - - // SHIVANI ADDITION: State to manage the photo upload modal + + // SHIVANI ADDITION: State to manage the photo upload modal const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false); - const demoConversation = [ { query: "Who was that person I met at TechCrunch who worked on crypto payments?", response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team." }, { query: "Right! What else did we talk about?", response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role." }, { query: "Did I ever follow up with her?", response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'" }, { query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?" } ]; - const memoryPhotos = [ { id: 1, name: 'Jennifer Chen', imageUrl: '/placeholder.png' }, { id: 2, name: 'Jake\'s Bday', imageUrl: '/placeholder.png' }, { id: 3, name: 'Coffee Meetup', imageUrl: '/placeholder.png' }, { id: 4, name: 'Team Lunch', imageUrl: '/placeholder.png' }, { id: 5, name: 'Project Demo', imageUrl: '/placeholder.png' } ]; + // Voice interface state - UPDATED FOR CONTROLLED MODE + const [currentUserQuery, setCurrentUserQuery] = useState(""); + const [isVoiceListening, setIsVoiceListening] = useState(false); + const [isProcessingVoice, setIsProcessingVoice] = useState(false); + const [voiceTranscript, setVoiceTranscript] = useState(""); + + // NEW: Store the actual user queries and control demo vs real mode + const [actualUserQueries, setActualUserQueries] = useState([]); + const [isUsingRealQueries, setIsUsingRealQueries] = useState(false); + + const memoryPhotos = [ + { id: 1, name: "Jennifer Chen", imageUrl: "/placeholder.png" }, + { id: 2, name: "Jake's Bday", imageUrl: "/placeholder.png" }, + { id: 3, name: "Coffee Meetup", imageUrl: "/placeholder.png" }, + { id: 4, name: "Team Lunch", imageUrl: "/placeholder.png" }, + { id: 5, name: "Project Demo", imageUrl: "/placeholder.png" }, + ]; useEffect(() => { if (chatContainerRef.current) { const { scrollHeight, clientHeight } = chatContainerRef.current; - chatContainerRef.current.scrollTo({ top: scrollHeight - clientHeight, behavior: 'smooth' }); + chatContainerRef.current.scrollTo({ + top: scrollHeight - clientHeight, + behavior: "smooth", + }); } }, [conversation]); - const wait = (ms: number) => new Promise(res => setTimeout(res, ms)); + const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); + + // UPDATED: Voice interface handlers with manual control + const handleVoiceTranscript = async ( + transcript: string, + isFinal: boolean + ) => { + setCurrentUserQuery(transcript); + setVoiceTranscript(transcript); + }; + + const handleVoiceStart = () => { + setCurrentUserQuery(""); + setVoiceTranscript(""); + }; + + const handleVoiceEnd = () => { + // Voice ended - handled by controlled state + }; + + const handleVoiceError = (error: string) => { + console.error("Voice error:", error); + setIsVoiceListening(false); + setIsProcessingVoice(false); + }; + + // UPDATED: Toggle voice listening with manual processing + const toggleVoiceListening = async () => { + if (isProcessingVoice) return; + + if (isVoiceListening) { + // User is stopping the recording + setIsVoiceListening(false); + + // Process the accumulated transcript if we have any + if (voiceTranscript.trim()) { + setIsProcessingVoice(true); + + try { + // Process the voice query using the voice service + const result: VoiceQueryResult = await voiceService.processVoiceQuery( + voiceTranscript + ); - const handleInitiateConversation = async () => { - if (view !== 'home' || currentStepIndex !== 0) return; - setIsInitiated(true); - if (carouselRef.current) carouselRef.current.autoplay.stop(); + if (result.success) { + await handleVoiceQuery(voiceTranscript, result.response); + } else { + setCurrentUserQuery( + "I'm sorry, I couldn't process your request. Please try again." + ); + } + } catch (error) { + console.error("Error processing voice query:", error); + setCurrentUserQuery( + "I'm experiencing some technical difficulties. Please try again." + ); + } finally { + setIsProcessingVoice(false); + } + } + } else { + // User is starting to record + setIsVoiceListening(true); + } + }; + + // Handle voice query processing + const handleVoiceQuery = async (query: string, response: string) => { + if (view === "home") { + // If on home page, start the conversation with the voice query + setIsInitiated(true); + setIsUsingRealQueries(true); + setActualUserQueries([query]); + if (carouselRef.current) carouselRef.current.autoplay.stop(); + + // Simulate thinking phase + setView("thinking"); + await wait(2000); + + // Move to chat with the voice query + setConversation([{ sender: "user", query: query }]); + setView("chat"); + + // Wait for user query typewriter to complete before showing response + // Calculate approximate time for user query to finish typing (120ms per word + buffer) + const words = query.split(" "); + const typewriterDuration = words.length * 120 + 500; // 500ms buffer + await wait(typewriterDuration); + + // Add response after user query is fully displayed + setConversation((prev) => [ + ...prev, + { sender: "agent", query: "", response: response }, + ]); + setCurrentStepIndex(1); + } else if (view === "chat") { + // If already in chat, add the new query to conversation + const newQueryIndex = actualUserQueries.length; + setActualUserQueries((prev) => [...prev, query]); + setConversation((prev) => [...prev, { sender: "user", query: query }]); + + // Wait for user query typewriter to complete before showing response + const words = query.split(" "); + const typewriterDuration = words.length * 120 + 500; // 500ms buffer + await wait(typewriterDuration); + + // Add response after user query is fully displayed + setConversation((prev) => [ + ...prev, + { sender: "agent", query: "", response: response }, + ]); + setCurrentStepIndex((prev) => prev + 1); + } }; const switchToThinking = async () => { await wait(1000); - setView('thinking'); - await wait(2000); - const firstStep = demoConversation[0]; - setConversation([{ sender: 'user', query: firstStep.query }]); - setCurrentStepIndex(1); - setView('chat'); - await wait(2500); - setConversation(prev => [...prev, { sender: 'agent', query: '', response: firstStep.response }]); - }; + setView("thinking"); + await wait(2000); - const runNextStepInDemo = async () => { - const currentStep = demoConversation[currentStepIndex]; - if (!currentStep) return; - setConversation(prev => [ ...prev, { sender: 'user', query: currentStep.query }]); - await wait(1500); - setConversation(prev => [ ...prev, { sender: 'agent', query: '', response: currentStep.response } ]); - setCurrentStepIndex(prev => (prev + 1) % demoConversation.length); + // Use the first user query + const firstQuery = actualUserQueries[0]; + setConversation([{ sender: "user", query: firstQuery }]); + setView("chat"); + + // Wait for user query typewriter to complete before showing response + const words = firstQuery.split(" "); + const typewriterDuration = words.length * 120 + 500; // 500ms buffer + await wait(typewriterDuration); + + // Add a placeholder response (you can modify this based on your needs) + const firstResponse = "I'm processing your memory request..."; + setConversation((prev) => [ + ...prev, + { sender: "agent", query: "", response: firstResponse }, + ]); }; - + const handleGoHome = () => { - setView('home'); - setConversation([]); - setCurrentStepIndex(0); - setIsInitiated(false); - if (carouselRef.current) { - carouselRef.current.autoplay?.start(); - } + setView("home"); + setConversation([]); + setCurrentStepIndex(0); + setIsInitiated(false); + setIsUsingRealQueries(false); + setActualUserQueries([]); + setCurrentUserQuery(""); + setVoiceTranscript(""); + setIsVoiceListening(false); + setIsProcessingVoice(false); + voiceService.resetConversation(); + if (carouselRef.current) { + carouselRef.current.autoplay?.start(); + } }; return (
{/* This div blurs the background when the modal is open */} -
+
- {view === 'home' && ( - -
-
-
Life Witness
+ {view === "home" && ( + + +
+
-
-
-

- Hello, what memory -

-

- would you like to relive today? -

-
- { carouselRef.current = swiper; }} - onSlideChange={(swiper) => setActiveIndex(swiper.realIndex)} - effect={'coverflow'} grabCursor={true} centeredSlides={true} - slidesPerView={3} loop={true} autoplay={{ delay: 3000, disableOnInteraction: false }} - modules={[EffectCoverflow, Pagination, Autoplay]} className="w-full" - coverflowEffect={{ rotate: 0, stretch: 80, depth: 150, modifier: 1, slideShadows: false }} - > - {memoryPhotos.map((photo, index) => ( - -
- {photo.name - {photo.name} -
-
- ))} -
-
-
- {isInitiated ? ( - - ) : ( -

Ask about a memory...

- )} -
- - {/* --- PARTNER'S ADDITION: onClick handler for the upload button --- */} - -
-
+
+
+

+ + Hello, + {" "} + what memory +

+

+ would you like to relive today? +

+
+ { + carouselRef.current = swiper; + }} + onSlideChange={(swiper) => setActiveIndex(swiper.realIndex)} + effect={"coverflow"} + grabCursor={true} + centeredSlides={true} + slidesPerView={3} + loop={true} + autoplay={{ delay: 3000, disableOnInteraction: false }} + modules={[EffectCoverflow, Pagination, Autoplay]} + className="w-full" + coverflowEffect={{ + rotate: 0, + stretch: 80, + depth: 150, + modifier: 1, + slideShadows: false, + }} + > + {memoryPhotos.map((photo, index) => ( + +
+ {photo.name + + {photo.name} + +
+
+ ))} +
+
+
+ {isInitiated ? ( + + ) : ( +

+ {isProcessingVoice + ? "Processing your request..." + : isVoiceListening && voiceTranscript + ? voiceTranscript + : currentUserQuery || "Ask about a memory..."} +

+ )} +
+ {/* UPDATED: Use controlled VoiceInterface */} + + {/* --- PARTNER'S ADDITION: onClick handler for the upload button --- */} + +
+
)}
- {view === 'thinking' && ( - -

"{demoConversation[0].query}"

+ {view === "thinking" && ( + +

"{actualUserQueries[0] || ""}"

- - - + + +
)}
- {view === 'chat' && ( - + {view === "chat" && ( +
- -
Jennifer Chen

Jennifer Chen

+ +
+ Jennifer Chen +

Jennifer Chen

+
-
- {conversation.map((chat, index) => (
{chat.sender === 'user' ? (
) : (
)}
))} +
+ {conversation.map((chat, index) => ( +
+ {chat.sender === "user" ? ( +
+
+ +
+
+ ) : ( +
+
+ +
+
+ )} +
+ ))}
-
- + + {/* Chat input area */} +
+ {/* Voice interface */} +
+ +
@@ -206,4 +509,4 @@ export default function Home() { />
); -} \ No newline at end of file +} diff --git a/frontend/src/components/photoUpload.tsx b/frontend/src/components/photoUpload.tsx index f5102d092..95e23915e 100644 --- a/frontend/src/components/photoUpload.tsx +++ b/frontend/src/components/photoUpload.tsx @@ -131,14 +131,14 @@ const PhotoUploadModal: React.FC = ({ >
e.stopPropagation()} - className="bg-gray-900 border border-gray-700 rounded-xl mx-auto w-[30%]" + className="bg-white border border-gray-700 rounded-xl mx-auto w-[30%]" > {/* Header */} -
-

Upload Photo

+
+

Upload Photo

@@ -150,8 +150,8 @@ const PhotoUploadModal: React.FC = ({
= ({ Preview -

+

{uploadedPhoto.file.name}

@@ -207,7 +207,7 @@ const PhotoUploadModal: React.FC = ({ {/* Photo Name Input */} {uploadedPhoto && (
-
@@ -226,7 +226,7 @@ const PhotoUploadModal: React.FC = ({ @@ -234,7 +234,7 @@ const PhotoUploadModal: React.FC = ({ type="button" onClick={handleSubmit} disabled={!uploadedPhoto || !photoName.trim() || isUploading} - className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white py-2 px-4 rounded-lg transition-colors flex items-center justify-center gap-2" + className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500 text-white py-2 px-4 rounded-lg transition-colors flex items-center justify-center gap-2" > {isUploading ? ( <> diff --git a/frontend/src/components/voiceInterface.tsx b/frontend/src/components/voiceInterface.tsx index e69de29bb..a8cf8aecd 100644 --- a/frontend/src/components/voiceInterface.tsx +++ b/frontend/src/components/voiceInterface.tsx @@ -0,0 +1,233 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; + +interface VoiceInterfaceProps { + onTranscript: (transcript: string, isFinal: boolean) => void; + onVoiceStart: () => void; + onVoiceEnd: () => void; + onError: (error: string) => void; + disabled?: boolean; + // New props for external control + isListening?: boolean; + onToggleListening?: () => void; +} + +const VoiceInterface: React.FC = ({ + onTranscript, + onVoiceStart, + onVoiceEnd, + onError, + disabled = false, + isListening: externalIsListening, + onToggleListening, +}) => { + const [internalIsListening, setInternalIsListening] = useState(false); + const [isSupported, setIsSupported] = useState(false); + const recognitionRef = useRef(null); + + // Use external control if provided, otherwise fall back to internal state + const isListening = + externalIsListening !== undefined + ? externalIsListening + : internalIsListening; + const controlled = externalIsListening !== undefined; + + useEffect(() => { + // Check if speech recognition is supported + if (typeof window !== "undefined") { + const SpeechRecognitionClass = + window.SpeechRecognition || window.webkitSpeechRecognition; + if (SpeechRecognitionClass) { + setIsSupported(true); + + const recognition = new SpeechRecognitionClass(); + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = "en-US"; + + recognition.onstart = () => { + console.log("Speech recognition started"); + if (!controlled) { + setInternalIsListening(true); + } + onVoiceStart(); + }; + + recognition.onresult = (event: SpeechRecognitionEvent) => { + let interimTranscript = ""; + let finalTranscript = ""; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + finalTranscript += transcript; + } else { + interimTranscript += transcript; + } + } + + // Send interim results for real-time display + if (interimTranscript) { + onTranscript(interimTranscript, false); + } + + // Send final results (but don't auto-stop - wait for manual control) + if (finalTranscript) { + onTranscript(finalTranscript, true); + } + }; + + recognition.onerror = (event: SpeechRecognitionErrorEvent) => { + console.error("Speech recognition error:", event.error); + + // Handle "aborted" specifically - don't show as error to user + if (event.error === "aborted") { + console.log( + "Speech recognition was aborted - this is normal behavior" + ); + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); + return; + } + + // Handle "no-speech" - also normal + if (event.error === "no-speech") { + console.log("No speech detected"); + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); + return; + } + + // Only show actual errors to user + if (!["aborted", "no-speech"].includes(event.error)) { + if (!controlled) { + setInternalIsListening(false); + } + onError(`Speech recognition error: ${event.error}`); + onVoiceEnd(); + } + }; + + recognition.onend = () => { + console.log("Speech recognition ended"); + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); + }; + + recognitionRef.current = recognition; + } else { + setIsSupported(false); + } + } + }, [ + onTranscript, + onVoiceStart, + onVoiceEnd, + onError, + controlled, + internalIsListening, + ]); + + // Effect to handle external listening state changes + useEffect(() => { + if (controlled && recognitionRef.current) { + if (externalIsListening && !internalIsListening) { + startListening(); + } else if (!externalIsListening && internalIsListening) { + stopListening(); + } + } + }, [externalIsListening, controlled, internalIsListening]); + + const startListening = () => { + if (recognitionRef.current && !disabled) { + try { + recognitionRef.current.start(); + } catch (error: any) { + console.error("Error starting speech recognition:", error); + + // If recognition is already started, try to stop and restart + if (error.name === "InvalidStateError") { + console.log("Recognition already started, attempting restart..."); + try { + recognitionRef.current.stop(); + setTimeout(() => { + if (recognitionRef.current) { + recognitionRef.current.start(); + } + }, 200); + } catch (restartError) { + console.error("Failed to restart recognition:", restartError); + onError("Failed to start speech recognition"); + } + } else { + onError("Failed to start speech recognition"); + } + } + } + }; + + const stopListening = () => { + if (recognitionRef.current) { + recognitionRef.current.stop(); + } + }; + + const handleToggle = () => { + if (controlled && onToggleListening) { + onToggleListening(); + } else { + // Uncontrolled mode + if (isListening) { + stopListening(); + } else { + startListening(); + } + } + }; + + if (!isSupported) { + return ( +
+
+ Speech recognition not supported in this browser +
+
+ ); + } + + return ( + + ); +}; + +export default VoiceInterface; diff --git a/frontend/src/services/voiceService.ts b/frontend/src/services/voiceService.ts index e69de29bb..2637f5a88 100644 --- a/frontend/src/services/voiceService.ts +++ b/frontend/src/services/voiceService.ts @@ -0,0 +1,170 @@ +// VoiceService.ts - Mock service for handling voice queries + +export interface VoiceQueryResult { + success: boolean; + response: string; + matchedMemoryId?: number; + error?: string; +} + +class VoiceService { + private demoConversation = [ + { + query: "Who was that person I met at TechCrunch who worked on crypto payments?", + response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team.", + memoryId: 1 + }, + { + query: "Right! What else did we talk about?", + response: "You both bonded over being Stanford alumni, and she specifically mentioned their new payment APIs. You took a photo together at their booth, and she gave you her business card with instructions to reach out about the senior engineering role.", + memoryId: 1 + }, + { + query: "Did I ever follow up with her?", + response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'", + memoryId: 1 + }, + { + query: "Perfect. I'm interviewing at Coinbase tomorrow and want to mention my connection to Stripe's crypto work. Can you give me some talking points?", + response: "Based on your conversation with Jennifer, here are some relevant points: • Stripe is actively investing in crypto infrastructure with dedicated teams • They're focusing on enterprise-grade payment solutions (Jennifer's emphasis) • Their approach prioritizes regulatory compliance and security • Jennifer mentioned they're seeing huge demand from fintech companies. This shows you're plugged into industry trends and have insights from a senior leader in the space. Would you like me to find other crypto-related conversations from your memory?", + memoryId: 1 + }, + { + query: "Tell me about Jake's birthday party", + response: "Jake's birthday party was last month at his apartment downtown. You brought homemade cupcakes and played board games until midnight. Sarah was there too, and you all had a great time catching up. Jake mentioned he's planning to move to Seattle for a new job opportunity.", + memoryId: 2 + }, + { + query: "What happened at the coffee meetup?", + response: "At the coffee meetup last week, you discussed the new startup ideas with Mike and Lisa. You sketched out some wireframes on napkins and talked about potential funding sources. Lisa offered to introduce you to her contact at Y Combinator.", + memoryId: 3 + }, + { + query: "Remind me about the team lunch", + response: "The team lunch was at that Italian restaurant near the office. Everyone celebrated Tom's promotion, and you discussed the Q4 roadmap. The pasta was excellent, and you all decided to make it a monthly tradition.", + memoryId: 4 + }, + { + query: "What was the project demo about?", + response: "The project demo showcased your new AI-powered analytics dashboard. The stakeholders were impressed with the real-time data visualization features. You received approval to move forward with the full implementation, and the VP mentioned it could be a game-changer for the company.", + memoryId: 5 + } + ]; + + private currentConversationIndex = 0; + private usedQueries = new Set(); + + // Simulate processing delay like a real API + private async simulateDelay(min: number = 800, max: number = 2000): Promise { + const delay = Math.random() * (max - min) + min; + return new Promise(resolve => setTimeout(resolve, delay)); + } + + // Find the best matching response for the user's query + private findBestMatch(userQuery: string): { response: string; memoryId?: number } { + const queryLower = userQuery.toLowerCase(); + + // Keywords mapping for better matching + const keywordMatches = { + 'jennifer': [0, 1, 2, 3], + 'chen': [0, 1, 2, 3], + 'stripe': [0, 1, 2, 3], + 'techcrunch': [0], + 'crypto': [0, 3], + 'jake': [4], + 'birthday': [4], + 'coffee': [5], + 'meetup': [5], + 'lunch': [6], + 'team': [6], + 'demo': [7], + 'project': [7], + 'coinbase': [3], + 'interview': [3] + }; + + // Find matches based on keywords + let bestMatches: number[] = []; + for (const [keyword, indices] of Object.entries(keywordMatches)) { + if (queryLower.includes(keyword)) { + bestMatches = [...bestMatches, ...indices]; + } + } + + // Remove duplicates and sort + bestMatches = [...new Set(bestMatches)]; + + // If we have keyword matches, use the first unused one + for (const index of bestMatches) { + const item = this.demoConversation[index]; + if (!this.usedQueries.has(item.query)) { + this.usedQueries.add(item.query); + return { response: item.response, memoryId: item.memoryId }; + } + } + + // If no keyword matches or all used, use the next in sequence + if (this.currentConversationIndex < this.demoConversation.length) { + const item = this.demoConversation[this.currentConversationIndex]; + this.currentConversationIndex++; + return { response: item.response, memoryId: item.memoryId }; + } + + // Fallback response + return { + response: "I'm searching through your memories for that information. Could you provide more context or try asking about a specific person, event, or topic?", + memoryId: undefined + }; + } + + // Main method to process voice queries + async processVoiceQuery(userQuery: string): Promise { + try { + // Simulate processing time + await this.simulateDelay(); + + // Clean up the query + const cleanQuery = userQuery.trim(); + + if (!cleanQuery) { + return { + success: false, + response: "I didn't catch that. Could you please repeat your question?", + error: "Empty query" + }; + } + + // Find the best matching response + const match = this.findBestMatch(cleanQuery); + + return { + success: true, + response: match.response, + matchedMemoryId: match.memoryId + }; + + } catch (error) { + console.error('Error processing voice query:', error); + return { + success: false, + response: "I'm sorry, I encountered an error processing your request. Please try again.", + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + + // Reset the conversation state + resetConversation(): void { + this.currentConversationIndex = 0; + this.usedQueries.clear(); + } + + // Get all available demo queries (useful for testing) + getDemoQueries(): string[] { + return this.demoConversation.map(item => item.query); + } +} + +// Export a singleton instance +export const voiceService = new VoiceService(); +export default VoiceService; \ No newline at end of file From 0fd38b944f382976085b0445dc9c71d19dbe77d2 Mon Sep 17 00:00:00 2001 From: Tharak Vangalapati Date: Sat, 26 Jul 2025 02:48:02 -0500 Subject: [PATCH 23/25] app deployment to google cloud using docker --- .gitignore | 6 ++ DEPLOYMENT.md | 144 +++++++++++++++++++++++++++++ backend/agents/base_agent.py | 35 +++++++ backend/main.py | 22 +++++ backend/requirements.txt | 10 ++ docker-compose.yml | 18 ++++ frontend/.env.local.example | 1 + frontend/.gitignore | 2 +- frontend/Dockerfile | 13 +++ frontend/src/app/page.tsx | 20 +++- frontend/src/services/apiClient.ts | 9 ++ scripts/deploy.sh | 25 +++++ scripts/run_demo.sh | 8 ++ scripts/setup_dev.sh | 14 +++ 14 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 DEPLOYMENT.md create mode 100644 docker-compose.yml create mode 100755 frontend/.env.local.example create mode 100644 frontend/Dockerfile mode change 100644 => 100755 scripts/deploy.sh mode change 100644 => 100755 scripts/run_demo.sh mode change 100644 => 100755 scripts/setup_dev.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4ac29ba1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +venv/ +backend/ +frontend/migrations/ +frontend/node_modules/ +frontend/.next/ +frontend/.env.local \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 000000000..8b8dd4b2b --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,144 @@ +# Deployment Guide: agents-assemble +`Tharak Vangalapat` + +This guide describes how to build, deploy, and access both backend (FastAPI) and frontend (Next.js/React) services for the agents-assemble project on Google Cloud Run using Docker. + +--- + +## Prerequisites +- Google Cloud account and project +- `gcloud` CLI installed and authenticated +- Docker installed +- Node.js and Python installed locally for development +- Required files: `Dockerfile`, `requirements.txt` (backend), `package.json` (frontend), `.env.local` (frontend) + +--- + +## 1. Build and Deploy Backend (FastAPI) + +### a. Ensure dependencies +Your `backend/requirements.txt` should include: +``` +fastapi +pydantic +uvicorn[standard] +``` + +### b. Build Docker image locally (optional) +```sh +cd agents-assemble/backend +docker build -t life-witness-agent-backend . +``` + +### c. Deploy to Google Cloud Run +From the project root, run: +```sh +gcloud builds submit --tag gcr.io/$PROJECT_ID/life-witness-agent-backend ./backend + +gcloud run deploy life-witness-agent-backend \ + --image gcr.io/$PROJECT_ID/life-witness-agent-backend \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated +``` + +--- + +## 2. Build and Deploy Frontend (Next.js/React) + +### a. Ensure `.env.local` is present in `frontend/` and not excluded by `.dockerignore`. + +### b. Build Docker image locally (optional) +```sh +cd agents-assemble/frontend +docker build -t life-witness-agent-frontend . +``` + +### c. Deploy to Google Cloud Run +From the project root, run: +```sh +gcloud builds submit --tag gcr.io/$PROJECT_ID/life-witness-agent-frontend ./frontend + +gcloud run deploy life-witness-agent-frontend \ + --image gcr.io/$PROJECT_ID/life-witness-agent-frontend \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated +``` + +--- + +## 3. Accessing the Services +- **Backend API Docs:** + Visit the Cloud Run URL for `life-witness-agent-backend` (e.g., `https://life-witness-agent-backend-xxxxxx.a.run.app/docs`). +- **Frontend App:** + Visit the Cloud Run URL for `life-witness-agent-frontend` (e.g., `https://life-witness-agent-frontend-xxxxxx.a.run.app`). + +--- + +## 4. Environment Variables +- **Frontend:** + Set `NEXT_PUBLIC_AGENT_API_URL` in `.env.local` to your backend Cloud Run URL. +- **Backend:** + Use environment variables or Google Secret Manager for sensitive data if needed. + +--- + +## 5. Troubleshooting +- **500 Internal Server Error:** + Ensure all Python dependencies are installed and your backend code is correct. +- **Memory Issues:** + Increase memory allocation with `--memory 1Gi` or higher in the deploy command. +- **.env.local not found:** + Make sure it’s present in `frontend/` and not listed in `.dockerignore`. + +--- + +## 6. Useful Commands +- **List Cloud Run services:** + ```sh + gcloud run services list --region us-central1 --platform managed + ``` +- **Get service URL:** + ```sh + gcloud run services describe SERVICE_NAME --region us-central1 --platform managed --format 'value(status.url)' + ``` + +--- + +## 7. Updating Services +After code changes, repeat the build and deploy steps for the affected service. + +--- + +## 8. Running Deployment Scripts + +### a. Local Development +To run both backend and frontend locally using Docker Compose: + +```sh +cd agents-assemble +# Start both services +docker-compose up --build +``` + +Or, to use the provided demo script: +```sh +cd agents-assemble/scripts +./setup_dev.sh # Set up Python environment and install backend dependencies +./run_demo.sh # Start the FastAPI backend locally +``` + +### b. Cloud Deployment +To build and deploy both backend and frontend to Google Cloud Run using the provided script: + +```sh +cd agents-assemble/scripts +./deploy.sh +``` + +This will build Docker images and deploy both services to Cloud Run. Make sure you have authenticated with `gcloud` and set your project. + +--- + +For advanced topics (custom domains, load balancer, etc.), see Google Cloud Run documentation. diff --git a/backend/agents/base_agent.py b/backend/agents/base_agent.py index 84e87e6f3..8d93fa02b 100644 --- a/backend/agents/base_agent.py +++ b/backend/agents/base_agent.py @@ -5,3 +5,38 @@ class BaseAgent(ABC): def run(self, input_text: str) -> str: """Run the agent on the input text and return a response.""" pass + +class ReActAgent(BaseAgent): + def run(self, input_text: str) -> str: + # Simple demo: calculator tool + if "calculate" in input_text.lower(): + expr = input_text.lower().replace("calculate", "").strip() + try: + result = eval(expr, {"__builtins__": {}}) + return f"Result: {result}" + except Exception as e: + return f"Error: {str(e)}" + return "Sorry, I can only calculate for now." + + def reason_and_act(self, user_input: str) -> dict: + # Simple demo: calculator tool + if "calculate" in user_input.lower(): + expr = user_input.lower().replace("calculate", "").strip() + try: + result = eval(expr, {"__builtins__": {}}) + return { + "thought": "I need to calculate.", + "action": "calculator", + "result": f"Result: {result}" + } + except Exception as e: + return { + "thought": "Tried to calculate but failed.", + "action": "calculator", + "result": f"Error: {str(e)}" + } + return { + "thought": "I don't know how to handle this.", + "action": None, + "result": "Sorry, I can only calculate for now." + } \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index e69de29bb..317f776b3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from pydantic import BaseModel +from agents.base_agent import ReActAgent +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI(title="ReAct Agent API") +agent = ReActAgent() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Or specify your frontend URL + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class AgentRequest(BaseModel): + user_input: str + +@app.post("/react") +async def react_agent(request: AgentRequest): + return agent.reason_and_act(request.user_input) diff --git a/backend/requirements.txt b/backend/requirements.txt index e69de29bb..b94098c8e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi +pydantic +uvicorn[standard] +google-cloud-firestore==2.13.1 +google-cloud-storage==2.12.0 +google-cloud-tasks +google-cloud-secret-manager +google-api-python-client +google-auth +# Add other Google Cloud packages as needed diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..e366ef730 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + backend: + build: ./backend + ports: + - "8080:8080" + environment: + - PYTHONUNBUFFERED=1 + + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - CHOKIDAR_USEPOLLING=true + depends_on: + - backend \ No newline at end of file diff --git a/frontend/.env.local.example b/frontend/.env.local.example new file mode 100755 index 000000000..cb921520b --- /dev/null +++ b/frontend/.env.local.example @@ -0,0 +1 @@ +NEXT_PUBLIC_AGENT_API_URL="backend URL here" \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a5207..24c06b8c8 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env.local # vercel .vercel diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..0c25989ac --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +COPY package-lock.json ./ +RUN npm install + +COPY . . + +RUN npm run build + +CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8ed3d331f..fe2c0fff2 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -8,6 +8,8 @@ import type { Swiper as SwiperCore } from 'swiper'; import 'swiper/css'; import 'swiper/css/effect-coverflow'; import 'swiper/css/pagination'; +import { callAgentAPI } from "@/services/apiClient"; +import Image from 'next/image'; export default function Home() { // Here, I'm setting up the state for my component. This is the information @@ -16,7 +18,8 @@ export default function Home() { const [currentUserQuery, setCurrentUserQuery] = useState(''); const [currentAgentReply, setCurrentAgentReply] = useState(''); const [highlightedMemoryId, setHighlightedMemoryId] = useState(null); - + const [agentReply, setAgentReply] = useState(""); + // This ref gives me direct control over the carousel component. const carouselRef = useRef(null); @@ -72,6 +75,11 @@ export default function Home() { setCurrentStepIndex((prevIndex) => (prevIndex + 1) % demoConversation.length); }; + const handleAskAgent = async (query: string) => { + const result = await callAgentAPI(query); + setAgentReply(result.response || JSON.stringify(result)); + }; + return (
@@ -102,10 +110,10 @@ export default function Home() { {memoryPhotos.map(photo => (
- {photo.name 🎤 -

"{currentUserQuery || 'Click the mic to start the conversation...'}"

+

"{currentUserQuery || 'Click the mic to start the conversation...'}"

)} +
+ + {agentReply &&
{agentReply}
} +
); diff --git a/frontend/src/services/apiClient.ts b/frontend/src/services/apiClient.ts index e69de29bb..beb6a9cbc 100644 --- a/frontend/src/services/apiClient.ts +++ b/frontend/src/services/apiClient.ts @@ -0,0 +1,9 @@ +export async function callAgentAPI(userInput: string) { + const url = `${process.env.NEXT_PUBLIC_AGENT_API_URL}/react`; + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_input: userInput }), + }); + return response.json(); +} \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh old mode 100644 new mode 100755 index e69de29bb..8cb8bcc20 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e +PROJECT_ID=$(gcloud config get-value project) +REGION="us-central1" +BACKEND_SERVICE="life-witness-agent-backend" +FRONTEND_SERVICE="life-witness-agent-frontend" +echo "Building backend Docker image..." +gcloud builds submit --tag gcr.io/$PROJECT_ID/$BACKEND_SERVICE ./backend +echo "Building frontend Docker image..." +gcloud builds submit --tag gcr.io/$PROJECT_ID/$FRONTEND_SERVICE ./frontend +echo "Deploying backend to Cloud Run..." +gcloud run deploy $BACKEND_SERVICE \ + --image gcr.io/$PROJECT_ID/$BACKEND_SERVICE \ + --platform managed \ + --region $REGION \ + --allow-unauthenticated + +echo "Deploying frontend to Cloud Run..." +gcloud run deploy $FRONTEND_SERVICE \ + --image gcr.io/$PROJECT_ID/$FRONTEND_SERVICE \ + --platform managed \ + --region $REGION \ + --allow-unauthenticated \ + --memory 2Gi +echo "Deployment complete!" diff --git a/scripts/run_demo.sh b/scripts/run_demo.sh old mode 100644 new mode 100755 index e69de29bb..2a8ba6e04 --- a/scripts/run_demo.sh +++ b/scripts/run_demo.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +echo "Activating virtual environment..." +source venv/bin/activate + +echo "Starting FastAPI demo app..." +uvicorn ./backend/main:app --reload --host 0.0.0.0 --port 8080 diff --git a/scripts/setup_dev.sh b/scripts/setup_dev.sh old mode 100644 new mode 100755 index e69de29bb..237bbbde0 --- a/scripts/setup_dev.sh +++ b/scripts/setup_dev.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +echo "Creating Python virtual environment..." +python3 -m venv venv +source venv/bin/activate + +echo "Upgrading pip..." +pip install --upgrade pip + +echo "Installing requirements..." +pip install -r ./backend/requirements.txt + +echo "Setup complete!" From 4ec8b50c80f50e37ec095f4b0d85fec9bd1f7326 Mon Sep 17 00:00:00 2001 From: Shivani Rupnawar Date: Sat, 26 Jul 2025 01:38:54 -0700 Subject: [PATCH 24/25] resolved the pause issue between user speech --- frontend/src/app/page.tsx | 2 +- frontend/src/components/voiceInterface.tsx | 102 +++++- frontend/src/services/voiceService.ts | 404 ++++++++++++++++----- 3 files changed, 403 insertions(+), 105 deletions(-) diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d6bb36a5f..af64f9446 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -136,7 +136,7 @@ export default function Home() { voiceTranscript ); - if (result.success) { + if (result.success && result.response) { await handleVoiceQuery(voiceTranscript, result.response); } else { setCurrentUserQuery( diff --git a/frontend/src/components/voiceInterface.tsx b/frontend/src/components/voiceInterface.tsx index a8cf8aecd..fc0b3db8c 100644 --- a/frontend/src/components/voiceInterface.tsx +++ b/frontend/src/components/voiceInterface.tsx @@ -26,6 +26,10 @@ const VoiceInterface: React.FC = ({ const [isSupported, setIsSupported] = useState(false); const recognitionRef = useRef(null); + // Add accumulated transcript to handle pauses + const accumulatedTranscriptRef = useRef(""); + const lastFinalTranscriptRef = useRef(""); + // Use external control if provided, otherwise fall back to internal state const isListening = externalIsListening !== undefined @@ -48,6 +52,10 @@ const VoiceInterface: React.FC = ({ recognition.onstart = () => { console.log("Speech recognition started"); + // Reset accumulated transcript when starting + accumulatedTranscriptRef.current = ""; + lastFinalTranscriptRef.current = ""; + if (!controlled) { setInternalIsListening(true); } @@ -58,6 +66,7 @@ const VoiceInterface: React.FC = ({ let interimTranscript = ""; let finalTranscript = ""; + // Process all results from the last processed index for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { @@ -67,14 +76,26 @@ const VoiceInterface: React.FC = ({ } } - // Send interim results for real-time display - if (interimTranscript) { - onTranscript(interimTranscript, false); + // Handle final results - accumulate them + if (finalTranscript) { + // Only add new final transcript (avoid duplicates) + if (finalTranscript !== lastFinalTranscriptRef.current) { + accumulatedTranscriptRef.current += + (accumulatedTranscriptRef.current ? " " : "") + + finalTranscript.trim(); + lastFinalTranscriptRef.current = finalTranscript; + } } - // Send final results (but don't auto-stop - wait for manual control) - if (finalTranscript) { - onTranscript(finalTranscript, true); + // Combine accumulated final transcript with current interim + const fullTranscript = + accumulatedTranscriptRef.current + + (accumulatedTranscriptRef.current && interimTranscript ? " " : "") + + interimTranscript; + + // Send the complete transcript (accumulated + interim) + if (fullTranscript.trim()) { + onTranscript(fullTranscript.trim(), !interimTranscript); } }; @@ -93,13 +114,33 @@ const VoiceInterface: React.FC = ({ return; } - // Handle "no-speech" - also normal + // Handle "no-speech" - also normal, but restart recognition if still listening if (event.error === "no-speech") { console.log("No speech detected"); - if (!controlled) { - setInternalIsListening(false); + // If we're still supposed to be listening, restart recognition + if (isListening && !disabled) { + console.log("Restarting recognition due to no-speech..."); + setTimeout(() => { + if ( + recognitionRef.current && + (controlled ? externalIsListening : internalIsListening) + ) { + try { + recognitionRef.current.start(); + } catch (restartError) { + console.error( + "Failed to restart after no-speech:", + restartError + ); + } + } + }, 100); + } else { + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); } - onVoiceEnd(); return; } @@ -115,10 +156,35 @@ const VoiceInterface: React.FC = ({ recognition.onend = () => { console.log("Speech recognition ended"); - if (!controlled) { - setInternalIsListening(false); + + // If we're still supposed to be listening, restart recognition + const shouldStillListen = controlled + ? externalIsListening + : internalIsListening; + + if (shouldStillListen && !disabled) { + console.log( + "Recognition ended but should still be listening, restarting..." + ); + setTimeout(() => { + if (recognitionRef.current && shouldStillListen) { + try { + recognitionRef.current.start(); + } catch (restartError) { + console.error("Failed to restart recognition:", restartError); + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); + } + } + }, 100); + } else { + if (!controlled) { + setInternalIsListening(false); + } + onVoiceEnd(); } - onVoiceEnd(); }; recognitionRef.current = recognition; @@ -133,6 +199,8 @@ const VoiceInterface: React.FC = ({ onError, controlled, internalIsListening, + externalIsListening, + disabled, ]); // Effect to handle external listening state changes @@ -148,7 +216,12 @@ const VoiceInterface: React.FC = ({ const startListening = () => { if (recognitionRef.current && !disabled) { + // Reset transcript accumulation + accumulatedTranscriptRef.current = ""; + lastFinalTranscriptRef.current = ""; + try { + setInternalIsListening(true); recognitionRef.current.start(); } catch (error: any) { console.error("Error starting speech recognition:", error); @@ -165,9 +238,11 @@ const VoiceInterface: React.FC = ({ }, 200); } catch (restartError) { console.error("Failed to restart recognition:", restartError); + setInternalIsListening(false); onError("Failed to start speech recognition"); } } else { + setInternalIsListening(false); onError("Failed to start speech recognition"); } } @@ -176,6 +251,7 @@ const VoiceInterface: React.FC = ({ const stopListening = () => { if (recognitionRef.current) { + setInternalIsListening(false); recognitionRef.current.stop(); } }; diff --git a/frontend/src/services/voiceService.ts b/frontend/src/services/voiceService.ts index 2637f5a88..2772c0cd9 100644 --- a/frontend/src/services/voiceService.ts +++ b/frontend/src/services/voiceService.ts @@ -1,14 +1,24 @@ -// VoiceService.ts - Mock service for handling voice queries +export interface VoiceQuery { + query: string; + response: string; + memoryId: number; +} export interface VoiceQueryResult { success: boolean; - response: string; - matchedMemoryId?: number; + response?: string; + memoryId?: number; error?: string; } +export interface ConversationContext { + currentMemoryId?: number; + conversationHistory: VoiceQuery[]; + memorySequenceIndex: { [memoryId: number]: number }; +} + class VoiceService { - private demoConversation = [ + private demoConversation: VoiceQuery[] = [ { query: "Who was that person I met at TechCrunch who worked on crypto payments?", response: "That would be Jennifer Chen, VP of Engineering at Stripe! You met her at their booth on day 2 of TechCrunch conference. She mentioned they were hiring senior engineers for their crypto payments team.", @@ -21,7 +31,7 @@ class VoiceService { }, { query: "Did I ever follow up with her?", - response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.'", + response: "Yes, I reminded you on Tuesday morning as requested, and you sent her a LinkedIn message that same day. She responded within 2 hours suggesting a coffee meeting, which you scheduled for the next Friday. You met at Blue Bottle Coffee downtown and had what you described as a 'great conversation about team culture and technical challenges.", memoryId: 1 }, { @@ -51,120 +61,332 @@ class VoiceService { } ]; - private currentConversationIndex = 0; - private usedQueries = new Set(); + private conversationContext: ConversationContext = { + conversationHistory: [], + memorySequenceIndex: {} + }; + + /** + * Simulates processing a voice query and returns a response + * @param query - The transcribed text from speech + * @returns Promise with the response data + */ + async processVoiceQuery(query: string): Promise { + try { + // Simulate network delay + await this.delay(500 + Math.random() * 1000); + + // Check for topic triggers and get sequential response + const response = this.getSequentialResponse(query); + + if (response) { + // Add to conversation history + this.conversationContext.conversationHistory.push(response); + this.conversationContext.currentMemoryId = response.memoryId; + + return { + success: true, + response: response.response, + memoryId: response.memoryId + }; + } else { + // Fallback response for unmatched queries + const fallbackResponse = this.generateFallbackResponse(query); + const fallbackQuery: VoiceQuery = { + query, + response: fallbackResponse, + memoryId: 0 + }; + + this.conversationContext.conversationHistory.push(fallbackQuery); + + return { + success: true, + response: fallbackResponse, + memoryId: 0 + }; + } + } catch (error) { + return { + success: false, + error: 'Failed to process voice query' + }; + } + } - // Simulate processing delay like a real API - private async simulateDelay(min: number = 800, max: number = 2000): Promise { - const delay = Math.random() * (max - min) + min; - return new Promise(resolve => setTimeout(resolve, delay)); + /** + * Get conversation history + * @returns Array of previous queries and responses + */ + getConversationHistory(): VoiceQuery[] { + return this.conversationContext.conversationHistory; } - // Find the best matching response for the user's query - private findBestMatch(userQuery: string): { response: string; memoryId?: number } { - const queryLower = userQuery.toLowerCase(); - - // Keywords mapping for better matching - const keywordMatches = { - 'jennifer': [0, 1, 2, 3], - 'chen': [0, 1, 2, 3], - 'stripe': [0, 1, 2, 3], - 'techcrunch': [0], - 'crypto': [0, 3], - 'jake': [4], - 'birthday': [4], - 'coffee': [5], - 'meetup': [5], - 'lunch': [6], - 'team': [6], - 'demo': [7], - 'project': [7], - 'coinbase': [3], - 'interview': [3] + /** + * Clear conversation history and reset sequence indices + */ + clearConversationHistory(): void { + this.conversationContext = { + conversationHistory: [], + memorySequenceIndex: {} }; + } - // Find matches based on keywords - let bestMatches: number[] = []; - for (const [keyword, indices] of Object.entries(keywordMatches)) { - if (queryLower.includes(keyword)) { - bestMatches = [...bestMatches, ...indices]; - } + /** + * Reset conversation (alias for clearConversationHistory for backward compatibility) + */ + resetConversation(): void { + this.clearConversationHistory(); + } + + + /** + * Get current memory context ID + */ + getCurrentMemoryId(): number | undefined { + return this.conversationContext.currentMemoryId; + } + + /** + * Check if the service is available (simulate health check) + */ + async checkServiceHealth(): Promise { + try { + await this.delay(100); + return true; + } catch { + return false; } + } - // Remove duplicates and sort - bestMatches = [...new Set(bestMatches)]; + /** + * Get sequential response based on exact match first, then topic triggers + * Returns the exact matching response or next response in sequence for the detected topic + */ + private getSequentialResponse(query: string): VoiceQuery | null { + const normalizedQuery = query.toLowerCase().trim(); + + // First, try to find exact match + const exactMatch = this.demoConversation.find( + item => item.query.toLowerCase() === normalizedQuery + ); + + if (exactMatch) { + // Update sequence index to the position after this exact match + const memoryQueries = this.demoConversation.filter( + item => item.memoryId === exactMatch.memoryId + ); + const exactMatchIndex = memoryQueries.findIndex( + item => item.query.toLowerCase() === normalizedQuery + ); + if (exactMatchIndex !== -1) { + this.conversationContext.memorySequenceIndex[exactMatch.memoryId] = exactMatchIndex + 1; + } + return exactMatch; + } + + // If no exact match, try fuzzy matching with high similarity + const fuzzyMatch = this.findFuzzyMatch(normalizedQuery); + if (fuzzyMatch) { + // Update sequence index to the position after this fuzzy match + const memoryQueries = this.demoConversation.filter( + item => item.memoryId === fuzzyMatch.memoryId + ); + const fuzzyMatchIndex = memoryQueries.findIndex( + item => item.query === fuzzyMatch.query + ); + if (fuzzyMatchIndex !== -1) { + this.conversationContext.memorySequenceIndex[fuzzyMatch.memoryId] = fuzzyMatchIndex + 1; + } + return fuzzyMatch; + } + + // Define topic triggers and their corresponding memory IDs + const topicTriggers = { + techcrunch: 1, + stripe: 1, + crypto: 1, + jennifer: 1, + coinbase: 1, + jake: 2, + birthday: 2, + coffee: 3, + meetup: 3, + mike: 3, + lisa: 3, + lunch: 4, + team: 4, + italian: 4, + tom: 4, + demo: 5, + project: 5, + analytics: 5, + dashboard: 5 + }; - // If we have keyword matches, use the first unused one - for (const index of bestMatches) { - const item = this.demoConversation[index]; - if (!this.usedQueries.has(item.query)) { - this.usedQueries.add(item.query); - return { response: item.response, memoryId: item.memoryId }; + // Find which topic/memory this query relates to + let targetMemoryId: number | null = null; + + for (const [trigger, memoryId] of Object.entries(topicTriggers)) { + if (normalizedQuery.includes(trigger)) { + targetMemoryId = memoryId; + break; } } - // If no keyword matches or all used, use the next in sequence - if (this.currentConversationIndex < this.demoConversation.length) { - const item = this.demoConversation[this.currentConversationIndex]; - this.currentConversationIndex++; - return { response: item.response, memoryId: item.memoryId }; + if (targetMemoryId === null) { + return null; } - // Fallback response - return { - response: "I'm searching through your memories for that information. Could you provide more context or try asking about a specific person, event, or topic?", - memoryId: undefined - }; + // Get all queries for this memory ID in order + const memoryQueries = this.demoConversation.filter( + item => item.memoryId === targetMemoryId + ); + + if (memoryQueries.length === 0) { + return null; + } + + // Get current sequence index for this memory ID + const currentIndex = this.conversationContext.memorySequenceIndex[targetMemoryId] || 0; + + // If we've reached the end of the sequence, start over + const sequenceIndex = currentIndex >= memoryQueries.length ? 0 : currentIndex; + + // Update the sequence index for next time + this.conversationContext.memorySequenceIndex[targetMemoryId] = sequenceIndex + 1; + + return memoryQueries[sequenceIndex]; } - // Main method to process voice queries - async processVoiceQuery(userQuery: string): Promise { - try { - // Simulate processing time - await this.simulateDelay(); + /** + * Find fuzzy match for queries that are similar but not exactly the same + */ + private findFuzzyMatch(query: string): VoiceQuery | null { + const queryWords = query.split(' ').filter(word => word.length > 2); + let bestMatch: VoiceQuery | null = null; + let highestScore = 0; + const minMatchThreshold = 0.7; // 70% similarity threshold - // Clean up the query - const cleanQuery = userQuery.trim(); + for (const item of this.demoConversation) { + const itemWords = item.query.toLowerCase().split(' ').filter(word => word.length > 2); + let matchedWords = 0; - if (!cleanQuery) { - return { - success: false, - response: "I didn't catch that. Could you please repeat your question?", - error: "Empty query" - }; + for (const queryWord of queryWords) { + if (itemWords.some(itemWord => + itemWord.includes(queryWord) || + queryWord.includes(itemWord) || + this.calculateSimilarity(queryWord, itemWord) > 0.8 + )) { + matchedWords++; + } } + + const score = matchedWords / Math.max(queryWords.length, itemWords.length); + + if (score > highestScore && score >= minMatchThreshold) { + highestScore = score; + bestMatch = item; + } + } - // Find the best matching response - const match = this.findBestMatch(cleanQuery); + return bestMatch; + } - return { - success: true, - response: match.response, - matchedMemoryId: match.memoryId - }; + /** + * Calculate string similarity using Levenshtein distance + */ + private calculateSimilarity(str1: string, str2: string): number { + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const distance = this.levenshteinDistance(longer, shorter); + return (longer.length - distance) / longer.length; + } - } catch (error) { - console.error('Error processing voice query:', error); - return { - success: false, - response: "I'm sorry, I encountered an error processing your request. Please try again.", - error: error instanceof Error ? error.message : 'Unknown error' - }; + /** + * Calculate Levenshtein distance between two strings + */ + private levenshteinDistance(str1: string, str2: string): number { + const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); + + for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; + for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; + + for (let j = 1; j <= str2.length; j++) { + for (let i = 1; i <= str1.length; i++) { + const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; + matrix[j][i] = Math.min( + matrix[j][i - 1] + 1, + matrix[j - 1][i] + 1, + matrix[j - 1][i - 1] + indicator + ); + } } + + return matrix[str2.length][str1.length]; } - // Reset the conversation state - resetConversation(): void { - this.currentConversationIndex = 0; - this.usedQueries.clear(); + /** + * Generate fallback response for unmatched queries + */ + private generateFallbackResponse(query: string): string { + const fallbackResponses = [ + "I don't have that specific information in my memory right now. Could you provide more context or ask about something else?", + "I'm not finding that in my records. Can you rephrase your question or ask about a different topic?", + "That doesn't match anything I remember. Try asking about recent meetings, events, or conversations.", + "I don't have details about that. Would you like to ask about something else from your recent activities?" + ]; + + return fallbackResponses[Math.floor(Math.random() * fallbackResponses.length)]; + } + + /** + * Utility method to simulate async delays + */ + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Get suggested queries (for UI hints) + */ + getSuggestedQueries(): string[] { + return [ + "Tell me about TechCrunch", + "What about Jake?", + "Tell me about the coffee meetup", + "Remind me about the team lunch", + "What was the project demo about?" + ]; } - // Get all available demo queries (useful for testing) - getDemoQueries(): string[] { - return this.demoConversation.map(item => item.query); + /** + * Reset sequence for a specific memory ID + */ + resetMemorySequence(memoryId: number): void { + this.conversationContext.memorySequenceIndex[memoryId] = 0; + } + + /** + * Get current sequence index for a memory ID + */ + getCurrentSequenceIndex(memoryId: number): number { + return this.conversationContext.memorySequenceIndex[memoryId] || 0; + } + + /** + * Get queries by memory ID (for conversation grouping) + */ + getQueriesByMemoryId(memoryId: number): VoiceQuery[] { + return this.demoConversation.filter(item => item.memoryId === memoryId); } } -// Export a singleton instance +// Export singleton instance export const voiceService = new VoiceService(); + +// Export the class for testing purposes export default VoiceService; \ No newline at end of file From 962b0625be78d09871fd4fc407945bab37a0b998 Mon Sep 17 00:00:00 2001 From: Ayaan Agarwal Date: Sat, 26 Jul 2025 09:57:43 -0600 Subject: [PATCH 25/25] Finalize voice interface and memory recall flow with Jennifer convo --- frontend/public/Coffee Meetup.jpeg | Bin 0 -> 48869 bytes frontend/public/Day at park.jpeg | Bin 0 -> 61150 bytes frontend/public/Jakes Birthday.jpeg | Bin 0 -> 51272 bytes frontend/public/Jennifer.jpeg | Bin 0 -> 60107 bytes frontend/public/Team Lunch.jpeg | Bin 0 -> 66077 bytes frontend/src/app/page.tsx | 206 +++++++++++++--------------- 6 files changed, 98 insertions(+), 108 deletions(-) create mode 100644 frontend/public/Coffee Meetup.jpeg create mode 100644 frontend/public/Day at park.jpeg create mode 100644 frontend/public/Jakes Birthday.jpeg create mode 100644 frontend/public/Jennifer.jpeg create mode 100644 frontend/public/Team Lunch.jpeg diff --git a/frontend/public/Coffee Meetup.jpeg b/frontend/public/Coffee Meetup.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2bf12c5e34cc39bdbd163a6deeda2a91921ff9ec GIT binary patch literal 48869 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 KVEPdxfB)HX`MgK~ literal 0 HcmV?d00001 diff --git a/frontend/public/Day at park.jpeg b/frontend/public/Day at park.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..ce859af60bff89d81e50748ac72fc5f732529540 GIT binary patch literal 61150 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 F|Jed^lz;#L literal 0 HcmV?d00001 diff --git a/frontend/public/Jakes Birthday.jpeg b/frontend/public/Jakes Birthday.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d0b1e5f09367013062a6e0f10ecac5672d293f1e GIT binary patch literal 51272 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 K(H?xaKmXb0YW*+( literal 0 HcmV?d00001 diff --git a/frontend/public/Jennifer.jpeg b/frontend/public/Jennifer.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..323e7cb7ff0de3338baebd7facbbe98c62f795e4 GIT binary patch literal 60107 zcmbSybx<79*X7`@0fM^@5;QQl1b5dE+#$FI4-NwiKDfIDcMI-La3{FCWchvH)^64Q zwY#sY`=5T*^-lGzckeytynjpoHUZdjQnFG27#IKm=KTWvTLDM_kP#7)5D}1(kdRPN zkWta`FwoJ^(1~zAVBwJwQ;?Gplaf->vN2Fnv(S)|GV(I9uyb&8b5k&Yg!niG*|@kl z|ML+T6ciM6G;~4?3_?ySQYy~>=kc!_fP)Mh02=@YLk)n%fq}z;`S%+@@xD$3nEx7p z|2be_;ouPvk&sbP(cULCU;|)b;NW24;Sdnu;ooQbzP|^+;~?Ntafl&)P&G!PcEaNf zjL$`;5wGpRSDU_|1mx!V%*zMjm-r$nB`qT>r>>!?rLCi@ zXKH3{VQFP;9sDySG%P$K5|WUZl$?^92F=SaC@d;2DJ}a|SKrXs)ZEhA z+xNSFU~p)7WM+15eqnLx&+^vx&hFm+!QaE9%d6{~+q?UR$EW|`f&sw&H>~&Pe*^nJ zaN)e;f`x~NgGc%gE*Myk_Ztoe9)XGj5m!tV$=K-wHD@3)o_Kt2Z4U|!m)ZrsiSsln z0WJ3y-Q|Cv{TH(TJ77WoU&#I!u>Xx~8GsH4^ZxVTZ~!8J>zrfU-FYSpwm%FV{Ay+( zGGIY&q9dph)y{nEK@G{mplaD!uR67ANGB z$v=QjWq{0yq=bpEZ`VagV6}^YI`=94eS|S&2O= z*_f+iquWSO=Zq2$cFgHU=sfu{ny`M|2w?Y1t!Ql4wu_Fe(p}w)dC_^AN0-`owNa+UiHCX+)zp<^=FIj{o$GkI zg4CpUl;zXnZpXQM_uIZyV2Iz1cx&q1qEn

VhvRx(*bOv(`erXf-hG1d4=%>Vxw= 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 z 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 vS=4%1+KTtCbd~^lNk;LKlz@N|D~Xbddl7 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 S&0J%Z0-=>pnueNUXaCvfh2Fpb literal 0 HcmV?d00001 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index af64f9446..2c21910fa 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -9,13 +9,10 @@ import "swiper/css"; import "swiper/css/effect-coverflow"; import "swiper/css/pagination"; -// SHIVANI ADDITION: Importing the photoUpload component import PhotoUploadModal from "../components/photoUpload"; -// Import the voice components and services import VoiceInterface from "../components/voiceInterface"; import { voiceService, type VoiceQueryResult } from "../services/voiceService"; -// Helper component for the word-by-word typing effect const TypewriterText = ({ text, onComplete, @@ -61,25 +58,23 @@ export default function Home() { const [activeIndex, setActiveIndex] = useState(0); const chatContainerRef = useRef(null); - // SHIVANI ADDITION: State to manage the photo upload modal const [isPhotoModalOpen, setIsPhotoModalOpen] = useState(false); + const [showPhotoUploadLeft, setShowPhotoUploadLeft] = useState(false); - // Voice interface state - UPDATED FOR CONTROLLED MODE const [currentUserQuery, setCurrentUserQuery] = useState(""); const [isVoiceListening, setIsVoiceListening] = useState(false); const [isProcessingVoice, setIsProcessingVoice] = useState(false); const [voiceTranscript, setVoiceTranscript] = useState(""); - // NEW: Store the actual user queries and control demo vs real mode const [actualUserQueries, setActualUserQueries] = useState([]); const [isUsingRealQueries, setIsUsingRealQueries] = useState(false); const memoryPhotos = [ - { id: 1, name: "Jennifer Chen", imageUrl: "/placeholder.png" }, - { id: 2, name: "Jake's Bday", imageUrl: "/placeholder.png" }, - { id: 3, name: "Coffee Meetup", imageUrl: "/placeholder.png" }, - { id: 4, name: "Team Lunch", imageUrl: "/placeholder.png" }, - { id: 5, name: "Project Demo", imageUrl: "/placeholder.png" }, + { id: 1, name: "Jennifer Chen", imageUrl: "/Jennifer.jpeg" }, + { id: 2, name: "Jake's Bday", imageUrl: "/Jakes Birthday.jpeg" }, + { id: 3, name: "Coffee Meetup", imageUrl: "/Coffee Meetup.jpeg" }, + { id: 4, name: "Team Lunch", imageUrl: "/Team Lunch.jpeg" }, + { id: 5, name: "Day at Park", imageUrl: "/Day at park.jpeg" }, ]; useEffect(() => { @@ -94,11 +89,11 @@ export default function Home() { const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); - // UPDATED: Voice interface handlers with manual control - const handleVoiceTranscript = async ( - transcript: string, - isFinal: boolean - ) => { + const switchToThinking = () => { + setView("thinking"); + }; + + const handleVoiceTranscript = async (transcript: string, isFinal: boolean) => { setCurrentUserQuery(transcript); setVoiceTranscript(transcript); }; @@ -108,9 +103,7 @@ export default function Home() { setVoiceTranscript(""); }; - const handleVoiceEnd = () => { - // Voice ended - handled by controlled state - }; + const handleVoiceEnd = () => {}; const handleVoiceError = (error: string) => { console.error("Voice error:", error); @@ -118,26 +111,39 @@ export default function Home() { setIsProcessingVoice(false); }; - // UPDATED: Toggle voice listening with manual processing const toggleVoiceListening = async () => { if (isProcessingVoice) return; if (isVoiceListening) { - // User is stopping the recording setIsVoiceListening(false); - // Process the accumulated transcript if we have any if (voiceTranscript.trim()) { setIsProcessingVoice(true); try { - // Process the voice query using the voice service const result: VoiceQueryResult = await voiceService.processVoiceQuery( voiceTranscript ); - if (result.success && result.response) { - await handleVoiceQuery(voiceTranscript, result.response); + let adjustedResponse = result.response; + + // SPECIAL OVERRIDE for Jake's Bday + if ( + voiceTranscript + .toLowerCase() + .includes("jake") && + voiceTranscript.toLowerCase().includes("bday") + ) { + adjustedResponse = "I'm here! What's happening right now?"; + } + + if (result.success && adjustedResponse) { + if ( + voiceTranscript.toLowerCase().includes("i want to remember this moment") + ) { + setShowPhotoUploadLeft(true); + } + await handleVoiceQuery(voiceTranscript, adjustedResponse); } else { setCurrentUserQuery( "I'm sorry, I couldn't process your request. Please try again." @@ -153,52 +159,41 @@ export default function Home() { } } } else { - // User is starting to record setIsVoiceListening(true); } }; - // Handle voice query processing const handleVoiceQuery = async (query: string, response: string) => { if (view === "home") { - // If on home page, start the conversation with the voice query setIsInitiated(true); setIsUsingRealQueries(true); setActualUserQueries([query]); if (carouselRef.current) carouselRef.current.autoplay.stop(); - // Simulate thinking phase setView("thinking"); await wait(2000); - // Move to chat with the voice query setConversation([{ sender: "user", query: query }]); setView("chat"); - // Wait for user query typewriter to complete before showing response - // Calculate approximate time for user query to finish typing (120ms per word + buffer) const words = query.split(" "); - const typewriterDuration = words.length * 120 + 500; // 500ms buffer + const typewriterDuration = words.length * 120 + 500; await wait(typewriterDuration); - // Add response after user query is fully displayed setConversation((prev) => [ ...prev, { sender: "agent", query: "", response: response }, ]); setCurrentStepIndex(1); } else if (view === "chat") { - // If already in chat, add the new query to conversation const newQueryIndex = actualUserQueries.length; setActualUserQueries((prev) => [...prev, query]); setConversation((prev) => [...prev, { sender: "user", query: query }]); - // Wait for user query typewriter to complete before showing response const words = query.split(" "); - const typewriterDuration = words.length * 120 + 500; // 500ms buffer + const typewriterDuration = words.length * 120 + 500; await wait(typewriterDuration); - // Add response after user query is fully displayed setConversation((prev) => [ ...prev, { sender: "agent", query: "", response: response }, @@ -207,29 +202,6 @@ export default function Home() { } }; - const switchToThinking = async () => { - await wait(1000); - setView("thinking"); - await wait(2000); - - // Use the first user query - const firstQuery = actualUserQueries[0]; - setConversation([{ sender: "user", query: firstQuery }]); - setView("chat"); - - // Wait for user query typewriter to complete before showing response - const words = firstQuery.split(" "); - const typewriterDuration = words.length * 120 + 500; // 500ms buffer - await wait(typewriterDuration); - - // Add a placeholder response (you can modify this based on your needs) - const firstResponse = "I'm processing your memory request..."; - setConversation((prev) => [ - ...prev, - { sender: "agent", query: "", response: firstResponse }, - ]); - }; - const handleGoHome = () => { setView("home"); setConversation([]); @@ -241,6 +213,7 @@ export default function Home() { setVoiceTranscript(""); setIsVoiceListening(false); setIsProcessingVoice(false); + setShowPhotoUploadLeft(false); voiceService.resetConversation(); if (carouselRef.current) { carouselRef.current.autoplay?.start(); @@ -291,7 +264,7 @@ export default function Home() { what memory

- would you like to relive today? + would you like to inquire about today?

)}
@@ -427,53 +400,70 @@ export default function Home() { - {view === "chat" && ( - -
-
- -
- Jennifer Chen -

Jennifer Chen

+ {view === "chat" && ( + +
+ {/* Left Panel */} +
+ +
+ {showPhotoUploadLeft ? ( +
+ {}} + onPhotoUpload={(photo) => + console.log("Photo uploaded during memory creation:", photo) + } + /> +
+ ) : ( +
+ Jennifer Chen +

Jennifer Chen

+
+ )} +
+
+ + {/* Right Panel */} +
+
+ {conversation.map((chat, index) => ( +
+ {chat.sender === "user" ? ( +
+
+ +
-
-
-
- {conversation.map((chat, index) => ( -
- {chat.sender === "user" ? ( -
-
- -
-
- ) : ( -
-
- -
-
- )} -
- ))} + ) : ( +
+
+ +
+ )} +
+ ))} +
{/* Chat input area */}