From 66ff67dcdaf92fff7d8f53b9821e21a4dc8bba53 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Tue, 13 May 2025 21:51:24 -0700 Subject: [PATCH 01/13] added abstract class for ai provider for config --- bun.lock | 342 ------------------------- examples/basic/simple-research.ts | 21 +- src/config/defaults.ts | 1 - src/generators/subQuestionGenerator.ts | 219 ++++++++-------- src/index.ts | 14 + src/prompts/finalSynthesis.md | 35 --- src/prompts/intermediateSynthesis.md | 28 -- src/provider/aiProvider.ts | 137 ++++++++++ src/provider/deepseek.ts | 31 --- src/provider/gemini.ts | 31 --- src/provider/openai.ts | 30 --- src/test-env.ts | 4 - src/types/index.ts | 8 +- src/types/synthesis.ts | 3 +- 14 files changed, 281 insertions(+), 623 deletions(-) delete mode 100644 bun.lock delete mode 100644 src/prompts/finalSynthesis.md delete mode 100644 src/prompts/intermediateSynthesis.md create mode 100644 src/provider/aiProvider.ts delete mode 100644 src/provider/deepseek.ts delete mode 100644 src/provider/gemini.ts delete mode 100644 src/provider/openai.ts delete mode 100644 src/test-env.ts diff --git a/bun.lock b/bun.lock deleted file mode 100644 index db772aa..0000000 --- a/bun.lock +++ /dev/null @@ -1,342 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "open-deep-research", - "dependencies": { - "@ai-sdk/anthropic": "^1.1.2", - "@ai-sdk/deepinfra": "^0.1.5", - "@ai-sdk/google": "^1.1.2", - "@ai-sdk/groq": "^1.1.2", - "@ai-sdk/openai": "^1.1.9", - "@types/uuid": "^10.0.0", - "ai": "^4.1.5", - "jigsawstack": "^0.2.2", - "uuid": "^11.1.0", - "zod": "^3.24.1", - }, - "devDependencies": { - "@types/node": "^22.10.10", - "dotenv": "^16.5.0", - "pkgroll": "^2.6.1", - "ts-node": "^10.9.2", - "tsx": "^4.19.2", - }, - }, - }, - "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-PyE7EC2fPjs9DnzRAHDrPQmcnI2m2Eojr8pfhckOejOlDEh2w7NnSJr1W3qe5hUWzKr+6d7NG1ZKR9fhmpDdEQ=="], - - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@0.1.18", "", { "dependencies": { "@ai-sdk/openai-compatible": "0.1.17", "@ai-sdk/provider": "1.0.12", "@ai-sdk/provider-utils": "2.1.15" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-x9dnyhJGcxGom09UTKc8GazwPQpM2IvJb5CRMp7xMp91I+bUS3ItHxw8NyvlVm1kLATF2H2FzKmhdTmk+Fr1dA=="], - - "@ai-sdk/google": ["@ai-sdk/google@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-A8AYqCmBs9SJFiAOP6AX0YEDHWTDrCaUDiRY2cdMSKjJiEknvwnPrAAKf3idgVqYaM2kS0qWz5v9v4pBzXDx+w=="], - - "@ai-sdk/groq": ["@ai-sdk/groq@1.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-DRq0b4twVUh52DFnIhVC4F14Po8w+76sCdigMRRIcAiSmGRr9I3Vyot36tun1q4tBZMYSvQUss60W3eiaoa6mg=="], - - "@ai-sdk/openai": ["@ai-sdk/openai@1.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-pjtiBKt1GgaSKZryTbM3tqgoegJwgAUlp1+X5uN6T+VPnI4FLSymV65tyloWzDlyqZmi9HXnnSRPu76VoL5D5g=="], - - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@0.1.17", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "@ai-sdk/provider-utils": "2.1.15" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-e60+yxQ29e8RlsTWBW4kLuQJMpVJzH5+cpOeUXLXU6M9wc8BOQCyYg4jYh2ldnfvYCKXYxb2kYeLW7L9fqhhMw=="], - - "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.7", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ=="], - - "@ai-sdk/react": ["@ai-sdk/react@1.2.9", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.7", "@ai-sdk/ui-utils": "1.2.8", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg=="], - - "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q=="], - - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - - "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="], - - "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ=="], - - "@rollup/plugin-dynamic-import-vars": ["@rollup/plugin-dynamic-import-vars@2.1.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "astring": "^1.8.5", "estree-walker": "^2.0.2", "fast-glob": "^3.2.12", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-Mymi24fd9hlRifdZV/jYIFj1dn99F34imiYu3KzlAcgBcRi3i9SucgW/VRo5SQ9K4NuQ7dCep6pFWgNyhRdFHQ=="], - - "@rollup/plugin-inject": ["@rollup/plugin-inject@5.0.5", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg=="], - - "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], - - "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.1", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA=="], - - "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="], - - "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], - - "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], - - "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], - - "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], - - "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="], - - "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - - "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], - - "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], - - "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], - - "acorn": ["acorn@8.14.1", "", { "bin": "bin/acorn" }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - - "ai": ["ai@4.3.9", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.7", "@ai-sdk/react": "1.2.9", "@ai-sdk/ui-utils": "1.2.8", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-P2RpV65sWIPdUlA4f1pcJ11pB0N1YmqPVLEmC4j8WuBwKY0L3q9vGhYPh0Iv+spKHKyn0wUbMfas+7Z6nTfS0g=="], - - "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], - - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - - "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], - - "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - - "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], - - "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], - - "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], - - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "eventsource-parser": ["eventsource-parser@3.0.1", "", {}, "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], - - "isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="], - - "jigsawstack": ["jigsawstack@0.2.2", "", { "dependencies": { "isomorphic-fetch": "^3.0.0" } }, "sha512-xLImEGwajRI+75jl1er8tzlR7xynQLSym8srLQEfpWYZLxq+KnTx9AtYLeY5k3qX5sq2uoSbV7BPJdyslUs3DQ=="], - - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - - "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "pkgroll": ["pkgroll@2.12.1", "", { "dependencies": { "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-dynamic-import-vars": "^2.1.5", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/pluginutils": "^5.1.4", "esbuild": "^0.25.1", "magic-string": "^0.30.17", "rollup": "^4.34.6", "rollup-pluginutils": "^2.8.2" }, "peerDependencies": { "typescript": "^4.1 || ^5.0" }, "optionalPeers": ["typescript"], "bin": { "pkgroll": "dist/cli.mjs" } }, "sha512-MpooedkVk28Sl1I5q8YO2QZmdlHdEtCzv1nReZdHNRhY0CzbZ14TewN47JopF+0rGCaQOERSPfcIOgPZDQXlZA=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="], - - "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="], - - "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], - - "tsx": ["tsx@4.19.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], - - "zod": ["zod@3.24.3", "", {}, "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], - - "@ai-sdk/deepinfra/@ai-sdk/provider": ["@ai-sdk/provider@1.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-88Uu1zJIE1UUOVJWfE2ybJXgiH8JJ97QY9fbmplErEbfa/k/1kF+tWMVAAJolF2aOGmazQGyQLhv4I9CCuVACw=="], - - "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.15", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-ndMVtDm2xS86t45CJZSfyl7UblZFewRB8gZkXQHeNi7BhjCYkhE+XQMwfDl6UOAO7kaV60IC1R4JLDWxWiiHug=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@1.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-88Uu1zJIE1UUOVJWfE2ybJXgiH8JJ97QY9fbmplErEbfa/k/1kF+tWMVAAJolF2aOGmazQGyQLhv4I9CCuVACw=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.15", "", { "dependencies": { "@ai-sdk/provider": "1.0.12", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-ndMVtDm2xS86t45CJZSfyl7UblZFewRB8gZkXQHeNi7BhjCYkhE+XQMwfDl6UOAO7kaV60IC1R4JLDWxWiiHug=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - } -} diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 5498583..2f691fa 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -1,14 +1,15 @@ -import createDeepResearch from '../..'; +import createDeepResearch, { DeepResearch } from '../..'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; + +const geminiInstance = createGoogleGenerativeAI({ + apiKey: process.env.GEMINI_API_KEY +}) // Basic usage example async function basicResearch() { - const result = await createDeepResearch({ - prompt: [ - 'Could you tell me what the best area to live in SF?', - ], - // Using mostly default settings with slight modifications + const deepResearch = new DeepResearch({ depth: { - level: 2, // Detailed analysis + level: 3, // Detailed analysis includeReferences: true, }, breadth: { @@ -20,9 +21,15 @@ async function basicResearch() { targetOutputLength: 5000, formatAsMarkdown: true, }, + models : { + output: geminiInstance + + } format: 'json', }); + const result = deepResearch.generate() + // Log research results console.log('\n=== RESEARCH SUMMARY ==='); console.log(`Research completed successfully: ${result.success}`); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 9410474..03c81a8 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -26,7 +26,6 @@ export const DEFAULT_BREADTH_CONFIG: ResearchBreadthConfig = { }; export const DEFAULT_SYNTHESIS_CONFIG: SynthesisConfig = { - maxOutputTokens: 4000, targetOutputLength: 'standard', formatAsMarkdown: true, }; diff --git a/src/generators/subQuestionGenerator.ts b/src/generators/subQuestionGenerator.ts index 4ce7fee..9e4c139 100644 --- a/src/generators/subQuestionGenerator.ts +++ b/src/generators/subQuestionGenerator.ts @@ -1,134 +1,137 @@ import { SubQuestion } from '../types/generators'; import { ResearchBreadthConfig } from '../types'; import 'dotenv/config'; -import { GeminiProvider } from '../provider/gemini'; + import { generateSubQuestionsPrompt, checkRelevancePrompt, } from '../prompts/generators'; +import { AIProvider } from '../provider/aiProvider'; + +/** + * Checks if a question is relevant to the main research topic + */ +export async function checkRelevance( + question: string, + mainPrompt: string[], + model: string, + provider: AIProvider +): Promise { + const { systemPrompt, userPrompt } = checkRelevancePrompt({ + question, + mainPrompt, + }); + + const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; + + const response = await provider.generateText(combinedPrompt, model); + + // Normalize the response to handle different formats + const normalizedResponse = response.trim().toLowerCase(); + + // Check if it contains "true" anywhere in the response + return normalizedResponse.includes('true'); +} -export class SubQuestionGenerator { - private geminiInstance: GeminiProvider; - constructor() { - // if(!process.env.OPENAI_API_KEY) { - // throw new Error('OPENAI_API_KEY is not set'); - // } - this.geminiInstance = GeminiProvider.getInstance({ - apiKey: process.env.GEMINI_API_KEY || '', - }); +/** + * Validates the generated questions for relevance and proper format + */ +async function validateResponse( + questions: SubQuestion[], + mainPrompt: string[], + model: string, + provider: AIProvider +): Promise { + if (!Array.isArray(questions)) { + throw new Error('Invalid response format: expected an array of questions'); } - async generateSubQuestions( - mainPrompt: string[], - breadthConfig: ResearchBreadthConfig - ): Promise { - const targetQuestionCount = breadthConfig.maxParallelTopics + 2; + const validatedQuestions = [...questions]; // Create a copy to avoid modifying during iteration - const { systemPrompt, userPrompt } = generateSubQuestionsPrompt({ - mainPrompt, - targetQuestionCount, - }); - - try { - const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; - // const response = await this.openaiInstance.generateText( - // combinedPrompt, - // 'gpt-4o' - // ); - const response = await this.geminiInstance.generateText( - combinedPrompt, - 'gemini-2.0-flash' - ); - - let parsedQuestions; - try { - parsedQuestions = JSON.parse(response); - } catch (parseError) { - throw new Error(`Failed to parse response as JSON: ${parseError}`); + // Using Promise.all for proper handling of asynchronous operations + const relevanceChecks = await Promise.all( + validatedQuestions.map(async (q) => { + if (!q.question || typeof q.relevanceScore !== 'number') { + q.relevanceScore = 0; + return false; + } + if (q.relevanceScore < 0 || q.relevanceScore > 1) { + q.relevanceScore = 0; + return false; } - // pick the questions equal to the breadthConfig.maxParallelTopics - let questions: SubQuestion[] = parsedQuestions.slice( - 0, - breadthConfig.maxParallelTopics - ); - - questions = await this.validateResponse(questions, mainPrompt); - - return { - questions, - metadata: { - totalGenerated: questions.length, - averageRelevanceScore: - questions.reduce((acc, q) => acc + q.relevanceScore, 0) / - questions.length, - generationTimestamp: new Date().toISOString(), - }, - }; - } catch (error: unknown) { - throw new Error( - `Failed to generate sub-questions: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } - } + // Check if the question is relevant to the main research topic + return await checkRelevance(q.question, mainPrompt, model, provider); + }) + ); - public async checkRelevance( - question: string, - mainPrompt: string[] - ): Promise { - const { systemPrompt, userPrompt } = checkRelevancePrompt({ - question, - mainPrompt, - }); + // Filter out irrelevant questions + return validatedQuestions.filter( + (_, index) => + relevanceChecks[index] && validatedQuestions[index].relevanceScore > 0 + ); +} +/** + * Generates sub-questions for a main research topic + */ +export async function generateSubQuestions( + mainPrompt: string[], + breadthConfig: ResearchBreadthConfig, + provider: AIProvider, + generationModel: string = 'gemini-2.0-flash', + relevanceCheckModel: string = 'gemini-1.5-flash' +): Promise { + const targetQuestionCount = breadthConfig.maxParallelTopics + 2; + + const { systemPrompt, userPrompt } = generateSubQuestionsPrompt({ + mainPrompt, + targetQuestionCount, + }); + + try { const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; - const response = await this.geminiInstance.generateText( + + const response = await provider.generateText( combinedPrompt, - 'gemini-1.5-flash' + generationModel ); - // Normalize the response to handle different formats - const normalizedResponse = response.trim().toLowerCase(); - - // Check if it contains "true" anywhere in the response - return normalizedResponse.includes('true'); - } - - private async validateResponse( - questions: SubQuestion[], - mainPrompt: string[] - ): Promise { - if (!Array.isArray(questions)) { - throw new Error( - 'Invalid response format: expected an array of questions' - ); + let parsedQuestions; + try { + parsedQuestions = JSON.parse(response); + } catch (parseError) { + throw new Error(`Failed to parse response as JSON: ${parseError}`); } - const validatedQuestions = [...questions]; // Create a copy to avoid modifying during iteration - - // Using Promise.all for proper handling of asynchronous operations - const relevanceChecks = await Promise.all( - validatedQuestions.map(async (q) => { - if (!q.question || typeof q.relevanceScore !== 'number') { - q.relevanceScore = 0; - return false; - } - if (q.relevanceScore < 0 || q.relevanceScore > 1) { - q.relevanceScore = 0; - return false; - } - - // Check if the question is relevant to the main research topic - return await this.checkRelevance(q.question, mainPrompt); - }) + // pick the questions equal to the breadthConfig.maxParallelTopics + let questions: SubQuestion[] = parsedQuestions.slice( + 0, + breadthConfig.maxParallelTopics + ); + + questions = await validateResponse( + questions, + mainPrompt, + relevanceCheckModel, + provider ); - // Filter out irrelevant questions - return validatedQuestions.filter( - (_, index) => - relevanceChecks[index] && validatedQuestions[index].relevanceScore > 0 + return { + questions, + metadata: { + totalGenerated: questions.length, + averageRelevanceScore: + questions.reduce((acc, q) => acc + q.relevanceScore, 0) / + questions.length, + generationTimestamp: new Date().toISOString(), + }, + }; + } catch (error: unknown) { + throw new Error( + `Failed to generate sub-questions: ${ + error instanceof Error ? error.message : String(error) + }` ); } } diff --git a/src/index.ts b/src/index.ts index b54ff1e..09b29f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import AIProvider from './provider/aiProvider'; import { DeepResearchConfig, DeepResearchInstance, @@ -26,6 +27,7 @@ export class DeepResearch implements DeepResearchInstance { private followupGenerator: FollowupQuestionGenerator; private synthesizer: Synthesizer; private depthSynthesis: Map; + private aiProvider: AIProvider; constructor(config: Partial) { this.config = this.validateAndMergeConfig(config); @@ -33,6 +35,18 @@ export class DeepResearch implements DeepResearchInstance { this.followupGenerator = new FollowupQuestionGenerator(); this.synthesizer = new Synthesizer(); this.depthSynthesis = new Map(); + // Initialize AIProvider with providers from config + this.aiProvider = new AIProvider(); + + // Add providers from config if available + if (config.models?.providers) { + // Add each provider to the AIProvider + Object.entries(config.models.providers).forEach(([name, provider]) => { + if (provider) { + this.aiProvider.addProvider(name, provider); + } + }); + } } private validateAndMergeConfig( diff --git a/src/prompts/finalSynthesis.md b/src/prompts/finalSynthesis.md deleted file mode 100644 index 3f2a099..0000000 --- a/src/prompts/finalSynthesis.md +++ /dev/null @@ -1,35 +0,0 @@ -You are a world-class research analyst and writer. When given: -• A Main Topic (or set of topics) - • A series of intermediate syntheses or past recursive analyses - • A collection of raw search results, each with a question and an AI-generated overview - • A user-specified length goal (either a minimum token/word count, or a style hint: “concise,” “standard,” or “detailed”) - -your task is to produce a single, cohesive **deep research article** that: - -1. **Introduces** the topic—outlining scope, importance, and objectives. -2. **Synthesizes** all intermediate analyses and search results, weaving them into a structured narrative. -3. **Identifies** and groups the key themes and patterns that emerge across sources. -4. **Highlights** novel insights your analysis uncovers—points not explicitly stated in any one source. -5. **Notes** contradictions or conflicts in the literature, resolving them or clearly framing open debates. -6. **Pinpoints** remaining knowledge gaps and recommends avenues for further inquiry. -7. **Concludes** with a concise summary of findings and practical or theoretical implications. -8. **Cites** every factual claim or statistic with in-text references (e.g. “[1]”, “[2]”) and appends a numbered bibliography at the end. -9. **Meets** the user’s length requirement—if given as a number, be **at least** that many tokens/words; if “concise,” “standard,” or “detailed,” adjust your level of depth accordingly. - -**Structure your output** exactly like this: - -– Title: A descriptive, engaging headline -– Abstract: 3–5 sentences summary -– Table of Contents (with section headings) -– 1. Introduction -– 2. Background & Literature Review -– 3. Thematic Synthesis - 3.1 Theme A - 3.2 Theme B -– 4. Novel Insights -– 5. Conflicting Evidence & Resolutions -– 6. Knowledge Gaps & Future Directions -– 7. Conclusion -– References - -Be sure your final article is polished, logically coherent, and self-contained: a reader who sees only this text should come away with a full, evidence-backed understanding of the topic. diff --git a/src/prompts/intermediateSynthesis.md b/src/prompts/intermediateSynthesis.md deleted file mode 100644 index 04f01ad..0000000 --- a/src/prompts/intermediateSynthesis.md +++ /dev/null @@ -1,28 +0,0 @@ -`You are an expert research synthesizer tasked with an INTERMEDIATE synthesis only. -Your goal is to extract **all** possible information from the material provided—do **not** draw final conclusions or trim content. Instead, produce exhaustive, unfiltered lists that a later “final synthesis” step will distill. - -Specifically, you must: -1. Extract and list **every** theme or pattern you can find -2. Enumerate **all** novel insights or observations -3. Identify **all** contradictions or discrepancies between sources -4. Catalog **all** knowledge gaps or open questions -5. Collect any related or follow-up questions - -**Output** a valid JSON object exactly like this: - -{ - "analysis": "Detailed commentary on what the sources contain…", - "allThemes": ["Theme A", "Theme B", /* …every theme… */], - "allInsights": ["Insight 1", "Insight 2", /* …every insight… */], - "allConflicts": [ - { - "topic": "Topic with conflict", - "conflicts": [ - { "claim1": "Claim X", "claim2": "Claim Y" }, - /* …all conflict pairs… */ - ] - } - ], - "allKnowledgeGaps": ["Gap A", "Gap B", /* …every gap… */], - "relatedQuestions": ["Question 1", "Question 2", /* …every question… */] -}` \ No newline at end of file diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts new file mode 100644 index 0000000..0e3ec65 --- /dev/null +++ b/src/provider/aiProvider.ts @@ -0,0 +1,137 @@ +import { generateText } from 'ai'; +import { GoogleGenerativeAIProvider } from '@ai-sdk/google'; +import { DeepInfraProvider } from '@ai-sdk/deepinfra'; +import { OpenAIProvider as OpenAISDKProvider } from '@ai-sdk/openai'; + +/** + * AIProvider acts as an abstract factory for different AI model providers + * It unifies the interface for interacting with different provider types + */ +export class AIProvider { + private providers: Map = new Map(); + private directProviders: Map = new Map(); + + /** + * Initialize the provider with optional provider instances + */ + constructor({ + gemini, + openai, + deepseek, + ...otherProviders + }: { + gemini?: GoogleGenerativeAIProvider; + openai?: OpenAISDKProvider; + deepseek?: DeepInfraProvider; + [key: string]: any; + } = {}) { + if (gemini) this.providers.set('gemini', gemini); + if (openai) this.providers.set('openai', openai); + if (deepseek) this.providers.set('deepseek', deepseek); + + // Handle any additional providers + Object.entries(otherProviders).forEach(([key, provider]) => { + if (provider) this.providers.set(key, provider); + }); + } + + /** + * Add a named provider that will be used with string-based model names + */ + addProvider(name: string, provider: any): void { + this.providers.set(name, provider); + } + + /** + * Add a direct provider instance that will be referenced by a key + */ + addDirectProvider(key: string, provider: any): void { + this.directProviders.set(key, provider); + } + + /** + * Get a specific provider by name + */ + getProvider(name: string): any { + return this.providers.get(name); + } + + /** + * Get a direct provider by key + */ + getDirectProvider(key: string): any { + return this.directProviders.get(key); + } + + /** + * Generate text using the specified model or provider + * The model can be: + * 1. A string model name with provider prefix, e.g., 'gemini-2.0-flash' + * 2. A key referencing a direct provider instance + */ + async generateText( + prompt: string, + modelOrProvider: string | any + ): Promise { + // Case 1: Direct provider instance was passed + if (typeof modelOrProvider !== 'string') { + try { + const result = await generateText({ + model: modelOrProvider, + prompt, + }); + return result.text; + } catch (error) { + throw new Error( + `Error generating text with direct provider: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + // Case 2: Check if it's a key for a direct provider we have stored + if (this.directProviders.has(modelOrProvider)) { + const provider = this.directProviders.get(modelOrProvider); + try { + const result = await generateText({ + model: provider, + prompt, + }); + return result.text; + } catch (error) { + throw new Error( + `Error generating text with provider key ${modelOrProvider}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } + + // Case 3: String model name with provider prefix + const [providerName, ...modelParts] = modelOrProvider.split('-'); + const provider = this.providers.get(providerName); + + if (!provider) { + throw new Error( + `Provider '${providerName}' not found for model ${modelOrProvider}. Please add it using addProvider method.` + ); + } + + try { + const result = await generateText({ + model: provider(modelOrProvider), + prompt, + }); + return result.text; + } catch (error) { + throw new Error( + `Error generating text with model ${modelOrProvider}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } +} + +export default AIProvider; diff --git a/src/provider/deepseek.ts b/src/provider/deepseek.ts deleted file mode 100644 index 7ed8717..0000000 --- a/src/provider/deepseek.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DeepInfraProvider, DeepInfraProviderSettings, createDeepInfra } from "@ai-sdk/deepinfra"; -import { generateText } from "ai"; - -export class DeepSeekProvider { - private client: DeepInfraProvider; - private static instance: DeepSeekProvider; - - private constructor(settings: DeepInfraProviderSettings) { - this.client = createDeepInfra({ - apiKey: settings.apiKey, - ...settings - }); - } - - public static getInstance(settings: DeepInfraProviderSettings): DeepSeekProvider { - if (!DeepSeekProvider.instance) { - DeepSeekProvider.instance = new DeepSeekProvider(settings); - } - return DeepSeekProvider.instance; - } - - async generateText(prompt: string, model: string): Promise { - const result = await generateText({ - model: this.client(model), - prompt: prompt, - }); - return result.text; - } -} - -export default DeepSeekProvider; diff --git a/src/provider/gemini.ts b/src/provider/gemini.ts deleted file mode 100644 index 192fbca..0000000 --- a/src/provider/gemini.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GoogleGenerativeAIProvider, GoogleGenerativeAIProviderSettings, createGoogleGenerativeAI} from "@ai-sdk/google"; -import { generateText } from "ai"; - -export class GeminiProvider { - private client: GoogleGenerativeAIProvider; - private static instance: GeminiProvider; - - private constructor(settings: GoogleGenerativeAIProviderSettings) { - this.client = createGoogleGenerativeAI({ - apiKey: settings.apiKey, - ...settings - }); - } - - public static getInstance(settings: GoogleGenerativeAIProviderSettings): GeminiProvider { - if (!GeminiProvider.instance) { - GeminiProvider.instance = new GeminiProvider(settings); - } - return GeminiProvider.instance; - } - - async generateText(prompt: string, model: string): Promise { - const result = await generateText({ - model: this.client(model), - prompt: prompt, - }); - return result.text; - } -} - -export default GeminiProvider; diff --git a/src/provider/openai.ts b/src/provider/openai.ts deleted file mode 100644 index c8650db..0000000 --- a/src/provider/openai.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * OpenAI provider - */ -import { createOpenAI, OpenAIProviderSettings } from "@ai-sdk/openai"; -import { generateText } from "ai"; -export class OpenAIProvider { - private static instance: OpenAIProvider; - private openai; - - private constructor(settings: OpenAIProviderSettings) { - this.openai = createOpenAI({ - apiKey: settings.apiKey || '', - }); - } - - public static getInstance(settings: OpenAIProviderSettings): OpenAIProvider { - if (!OpenAIProvider.instance) { - OpenAIProvider.instance = new OpenAIProvider(settings); - } - return OpenAIProvider.instance; - } - - async generateText(prompt: string, model: string): Promise { - const result = await generateText({ - model: this.openai(model), - prompt: prompt, - }); - return result.text; - } -} \ No newline at end of file diff --git a/src/test-env.ts b/src/test-env.ts deleted file mode 100644 index 4643b1f..0000000 --- a/src/test-env.ts +++ /dev/null @@ -1,4 +0,0 @@ -import "dotenv/config"; -console.log("OPENAI_API_KEY:", process.env.OPENAI_API_KEY); -console.log("GOOGLE_AI_API_KEY:", process.env.GOOGLE_AI_API_KEY); -console.log("All env variables:", process.env); diff --git a/src/types/index.ts b/src/types/index.ts index b8265b9..a0dc3f5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -21,9 +21,10 @@ export interface ResearchResult { export type ModelType = 'default' | 'quick' | 'reasoning'; export interface ModelConfig { - default: string; - quick: string; - reasoning: string; + default: string | any; // Can be a model name string or a provider instance + reasoning: string | any; + output: string | any; // Adding output as you mentioned + // No need for separate providers object } export interface ResearchDepthConfig { @@ -41,7 +42,6 @@ export interface ResearchBreadthConfig { } export interface DeepResearchConfig { - prompt: string[]; depth?: Partial; breadth?: Partial; format: 'json'; diff --git a/src/types/synthesis.ts b/src/types/synthesis.ts index d345bfa..2c86aa2 100644 --- a/src/types/synthesis.ts +++ b/src/types/synthesis.ts @@ -1,9 +1,9 @@ import { WebSearchResult } from '.'; export interface FinalSynthesisInput { + maxOutputTokens: number; mainPrompt: string[]; allSyntheses: SynthesisOutput[]; - maxOutputTokens?: number; targetOutputLength: 'concise' | 'standard' | 'detailed' | number; } export interface SynthesisInput { @@ -32,7 +32,6 @@ export interface SynthesisOutput { } export interface SynthesisConfig { - maxOutputTokens: number; targetOutputLength: FinalSynthesisInput['targetOutputLength']; formatAsMarkdown: boolean; } From e157ceec1e36844d722832d85e52df98201bd36b Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Tue, 13 May 2025 23:34:11 -0700 Subject: [PATCH 02/13] ai providers done --- src/index.ts | 27 ++++++++------ src/provider/aiProvider.ts | 75 ++++++++++++++------------------------ src/types/index.ts | 8 ++-- 3 files changed, 48 insertions(+), 62 deletions(-) diff --git a/src/index.ts b/src/index.ts index 09b29f9..e736445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,22 +31,27 @@ export class DeepResearch implements DeepResearchInstance { constructor(config: Partial) { this.config = this.validateAndMergeConfig(config); - this.questionGenerator = new SubQuestionGenerator(); - this.followupGenerator = new FollowupQuestionGenerator(); - this.synthesizer = new Synthesizer(); - this.depthSynthesis = new Map(); - // Initialize AIProvider with providers from config + + // Initialize AIProvider this.aiProvider = new AIProvider(); - // Add providers from config if available - if (config.models?.providers) { - // Add each provider to the AIProvider - Object.entries(config.models.providers).forEach(([name, provider]) => { - if (provider) { - this.aiProvider.addProvider(name, provider); + // Add providers from config.models if available + if (config.models) { + // For each model type (default, quick, reasoning, etc.) + Object.entries(config.models).forEach(([modelType, modelValue]) => { + // If it's not a string, it's likely a provider instance + if (modelValue && typeof modelValue !== 'string') { + // Add it as a direct provider with the model type as the ID + this.aiProvider.addDirectProvider(modelType, modelValue); } + // If it's a string, it will be handled by the generateText method }); } + + this.questionGenerator = new SubQuestionGenerator(this.aiProvider); + this.followupGenerator = new FollowupQuestionGenerator(this.aiProvider); + this.synthesizer = new Synthesizer(this.aiProvider); + this.depthSynthesis = new Map(); } private validateAndMergeConfig( diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index 0e3ec65..944e8bc 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -2,14 +2,14 @@ import { generateText } from 'ai'; import { GoogleGenerativeAIProvider } from '@ai-sdk/google'; import { DeepInfraProvider } from '@ai-sdk/deepinfra'; import { OpenAIProvider as OpenAISDKProvider } from '@ai-sdk/openai'; - +import { ProviderV1 } from '@ai-sdk/provider'; /** * AIProvider acts as an abstract factory for different AI model providers * It unifies the interface for interacting with different provider types */ export class AIProvider { - private providers: Map = new Map(); - private directProviders: Map = new Map(); + private providers: Map = new Map(); + private directProviders: Map = new Map(); /** * Initialize the provider with optional provider instances @@ -36,17 +36,18 @@ export class AIProvider { } /** - * Add a named provider that will be used with string-based model names + * Add or replace a provider */ addProvider(name: string, provider: any): void { this.providers.set(name, provider); } /** - * Add a direct provider instance that will be referenced by a key + * Add a direct provider with a specific identifier + * This is for cases where the user provides a provider instance directly */ - addDirectProvider(key: string, provider: any): void { - this.directProviders.set(key, provider); + addDirectProvider(id: string, provider: any): void { + this.directProviders.set(id, provider); } /** @@ -56,77 +57,57 @@ export class AIProvider { return this.providers.get(name); } - /** - * Get a direct provider by key - */ - getDirectProvider(key: string): any { - return this.directProviders.get(key); - } - /** * Generate text using the specified model or provider * The model can be: - * 1. A string model name with provider prefix, e.g., 'gemini-2.0-flash' - * 2. A key referencing a direct provider instance + * 1. A string like 'gemini-2.0-flash' (provider-model format) + * 2. A direct reference to a provider instance (stored with a unique ID) */ async generateText( prompt: string, modelOrProvider: string | any ): Promise { - // Case 1: Direct provider instance was passed - if (typeof modelOrProvider !== 'string') { - try { + try { + // Case 1: Direct provider instance reference + if (typeof modelOrProvider !== 'string') { + // If it's a direct provider instance, use it directly const result = await generateText({ model: modelOrProvider, prompt, }); return result.text; - } catch (error) { - throw new Error( - `Error generating text with direct provider: ${ - error instanceof Error ? error.message : String(error) - }` - ); } - } - // Case 2: Check if it's a key for a direct provider we have stored - if (this.directProviders.has(modelOrProvider)) { - const provider = this.directProviders.get(modelOrProvider); - try { + // Case 2: Check if it's a direct provider ID we've stored + if (this.directProviders.has(modelOrProvider)) { + const provider = this.directProviders.get(modelOrProvider); const result = await generateText({ model: provider, prompt, }); return result.text; - } catch (error) { - throw new Error( - `Error generating text with provider key ${modelOrProvider}: ${ - error instanceof Error ? error.message : String(error) - }` - ); } - } - // Case 3: String model name with provider prefix - const [providerName, ...modelParts] = modelOrProvider.split('-'); - const provider = this.providers.get(providerName); + // Case 3: String in provider-model format + // Parse the model string to identify the provider + const [providerName, ...modelParts] = modelOrProvider.split('-'); + const provider = this.providers.get(providerName); - if (!provider) { - throw new Error( - `Provider '${providerName}' not found for model ${modelOrProvider}. Please add it using addProvider method.` - ); - } + if (!provider) { + throw new Error( + `Provider '${providerName}' not found. Please add it using addProvider method.` + ); + } - try { const result = await generateText({ model: provider(modelOrProvider), prompt, }); + return result.text; } catch (error) { throw new Error( - `Error generating text with model ${modelOrProvider}: ${ + `Error generating text with ${String(modelOrProvider)}: ${ error instanceof Error ? error.message : String(error) }` ); diff --git a/src/types/index.ts b/src/types/index.ts index a0dc3f5..a27efcb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import { SubQuestion, SubQuestionGeneratorResult } from './generators'; import { SynthesisConfig, SynthesisOutput } from './synthesis'; +import { ProviderV1 } from '@ai-sdk/provider'; export interface RecursiveResearchResult { isComplete: boolean; @@ -21,10 +22,9 @@ export interface ResearchResult { export type ModelType = 'default' | 'quick' | 'reasoning'; export interface ModelConfig { - default: string | any; // Can be a model name string or a provider instance - reasoning: string | any; - output: string | any; // Adding output as you mentioned - // No need for separate providers object + default: string | ProviderV1; + output?: string | ProviderV1; + reasoning?: string | ProviderV1; } export interface ResearchDepthConfig { From db4948819712cc121bbddb5d61cf1bd56c3d88c7 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Tue, 13 May 2025 23:48:39 -0700 Subject: [PATCH 03/13] prompts are in deep research instance and not config --- src/index.ts | 282 +++++++++++++++++++-------------------------- src/types/index.ts | 2 + 2 files changed, 123 insertions(+), 161 deletions(-) diff --git a/src/index.ts b/src/index.ts index e736445..5b9f814 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ import { DEFAULT_BREADTH_CONFIG, DEFAULT_SYNTHESIS_CONFIG, } from './config/defaults'; -import { SubQuestionGenerator } from './generators/subQuestionGenerator'; import { SubQuestionGeneratorResult } from './types/generators'; import { WebSearchResult } from './types'; import 'dotenv/config'; @@ -23,11 +22,12 @@ import { SynthesisOutput } from './types/synthesis'; export class DeepResearch implements DeepResearchInstance { public config: DeepResearchConfig; - private questionGenerator: SubQuestionGenerator; + public prompts?: string[]; private followupGenerator: FollowupQuestionGenerator; private synthesizer: Synthesizer; private depthSynthesis: Map; private aiProvider: AIProvider; + private questionGenerator: SubQuestionGenerator; constructor(config: Partial) { this.config = this.validateAndMergeConfig(config); @@ -57,12 +57,8 @@ export class DeepResearch implements DeepResearchInstance { private validateAndMergeConfig( config: Partial ): DeepResearchConfig { - if (!config.prompt || !Array.isArray(config.prompt)) { - throw new Error('Prompt must be provided as an array'); - } - + // No need to validate prompt anymore return { - prompt: config.prompt, depth: { ...DEFAULT_DEPTH_CONFIG, ...config.depth, @@ -92,10 +88,18 @@ export class DeepResearch implements DeepResearchInstance { } public async generateSubQuestions(): Promise { - return this.questionGenerator.generateSubQuestions(this.config.prompt, { - ...DEFAULT_BREADTH_CONFIG, - ...this.config.breadth, - }); + if (!this.prompts || this.prompts.length === 0) { + throw new Error('Prompts must be set before generating sub-questions'); + } + + return this.questionGenerator.generateSubQuestions( + this.prompts, + { + ...DEFAULT_BREADTH_CONFIG, + ...this.config.breadth, + }, + this.aiProvider + ); } public async performRecursiveResearch( @@ -103,6 +107,10 @@ export class DeepResearch implements DeepResearchInstance { currentDepth: number = 1, parentSynthesis?: SynthesisOutput ): Promise { + if (!this.prompts || this.prompts.length === 0) { + throw new Error('Prompts must be set before performing research'); + } + // Store conditions in variables const isMaxDepthReached = currentDepth >= (this.config.depth?.level ?? DEFAULT_DEPTH_CONFIG.level); @@ -112,7 +120,7 @@ export class DeepResearch implements DeepResearchInstance { // Check if we already have sufficient information const hasSufficientInfo = await this.synthesizer.hasSufficientInformation( { - mainPrompt: this.config.prompt, + mainPrompt: this.prompts, results: initialResults, currentDepth, parentSynthesis, @@ -140,7 +148,7 @@ export class DeepResearch implements DeepResearchInstance { // First, synthesize the current level results const synthesis = await this.synthesizer.synthesizeResults({ - mainPrompt: this.config.prompt, + mainPrompt: this.prompts, results: initialResults, currentDepth, parentSynthesis, @@ -162,7 +170,7 @@ export class DeepResearch implements DeepResearchInstance { for (const result of initialResults) { const followupQuestions = await this.followupGenerator.generateFollowupQuestions( - this.config.prompt, + this.prompts, result, this.config.breadth?.maxParallelTopics || DEFAULT_BREADTH_CONFIG.maxParallelTopics @@ -222,6 +230,10 @@ export class DeepResearch implements DeepResearchInstance { } public async generateFinalSynthesis(): Promise { + if (!this.prompts || this.prompts.length === 0) { + throw new Error('Prompts must be set before generating final synthesis'); + } + // Get all the syntheses from all depth levels const allSyntheses: SynthesisOutput[] = []; this.depthSynthesis.forEach((syntheses) => { @@ -230,7 +242,7 @@ export class DeepResearch implements DeepResearchInstance { // Use the synthesizer's generateFinalSynthesis method return this.synthesizer.generateFinalSynthesis({ - mainPrompt: this.config.prompt, + mainPrompt: this.prompts, allSyntheses: allSyntheses, maxOutputTokens: this.config.synthesis?.maxOutputTokens, targetOutputLength: @@ -238,167 +250,115 @@ export class DeepResearch implements DeepResearchInstance { DEFAULT_SYNTHESIS_CONFIG.targetOutputLength, }); } -} -export async function createDeepResearch( - config: Partial -): Promise { - const deepResearch = new DeepResearch(config); - const subQuestions = await deepResearch.generateSubQuestions(); - - console.log(`Generated ${subQuestions.questions.length} sub-questions`); - - const initialSearch = await deepResearch.fireWebSearches(subQuestions); - - console.log(`Received ${initialSearch.length} initial search results`); - - // Perform recursive research to populate the depthSynthesis map - const recursiveResult = await deepResearch.performRecursiveResearch( - initialSearch - ); - console.log( - `Recursive research completed with reason: ${recursiveResult.reason}` - ); - - // Get all the syntheses - const allDepthSynthesis = deepResearch.getSynthesis(); - console.log(`Synthesis map contains ${allDepthSynthesis.size} depth levels`); - allDepthSynthesis.forEach((syntheses, depth) => { - console.log(`Depth ${depth}: ${syntheses.length} syntheses generated`); - }); - - // Generate the final synthesis - const finalSynthesis = await deepResearch.generateFinalSynthesis(); - console.log( - `Final synthesis generated with ${ - finalSynthesis.analysis ? finalSynthesis.analysis.length : 0 - } characters` - ); - - // Calculate token usage (placeholder values - implement actual counting) - const inputTokens = 256; // Estimate based on prompt length - const outputTokens = 500; // Rough estimate - const inferenceTimeTokens = 975; // Placeholder - const totalTokens = inputTokens + outputTokens + inferenceTimeTokens; - - // Ensure we have a valid research output - let research = 'No research results available.'; - - if (finalSynthesis) { - if (finalSynthesis.analysis) { - // If analysis field exists, use it - research = finalSynthesis.analysis; - } else if ( - config.format === 'json' && - Object.keys(finalSynthesis).length > 0 - ) { - // If we're in JSON format and have synthesis data but no analysis field, - // generate a formatted research output from the available fields - try { - const sections = []; - - // Add title based on the prompt - sections.push( - `# Research on ${config.prompt?.[0] || 'Requested Topic'}\n` - ); + public async generate(prompt: string[]): Promise { + if (!prompt || !Array.isArray(prompt) || prompt.length === 0) { + throw new Error('Prompt must be provided as a non-empty array'); + } - // Add key themes section - if (finalSynthesis.keyThemes && finalSynthesis.keyThemes.length > 0) { - sections.push('## Key Themes\n'); - finalSynthesis.keyThemes.forEach((theme) => { - sections.push(`- ${theme}`); - }); - sections.push('\n'); - } + // Store the prompt in the class property + this.prompts = prompt; - // Add insights section - if (finalSynthesis.insights && finalSynthesis.insights.length > 0) { - sections.push('## Key Insights\n'); - finalSynthesis.insights.forEach((insight) => { - sections.push(`- ${insight}`); - }); - sections.push('\n'); - } + // Now proceed with the existing implementation + const subQuestions = await this.generateSubQuestions(); + console.log(`Generated ${subQuestions.questions.length} sub-questions`); - // Add knowledge gaps section - if ( - finalSynthesis.knowledgeGaps && - finalSynthesis.knowledgeGaps.length > 0 - ) { - sections.push('## Knowledge Gaps\n'); - finalSynthesis.knowledgeGaps.forEach((gap) => { - sections.push(`- ${gap}`); - }); - sections.push('\n'); - } + const initialSearch = await this.fireWebSearches(subQuestions); + console.log(`Received ${initialSearch.length} initial search results`); - // Add conflicting information section - if ( - finalSynthesis.conflictingInformation && - finalSynthesis.conflictingInformation.length > 0 - ) { - sections.push('## Conflicting Information\n'); - finalSynthesis.conflictingInformation.forEach((conflict) => { - sections.push(`### ${conflict.topic}\n`); - conflict.conflicts.forEach((item) => { - sections.push(`- **Claim 1**: ${item.claim1}`); - sections.push(`- **Claim 2**: ${item.claim2}`); - sections.push( - `- **Resolution**: ${ - item.resolution || 'No resolution provided' - }\n` - ); - }); - }); - sections.push('\n'); - } + // Perform recursive research to populate the depthSynthesis map + const recursiveResult = await this.performRecursiveResearch(initialSearch); + console.log( + `Recursive research completed with reason: ${recursiveResult.reason}` + ); - // Add related questions section - if ( - finalSynthesis.relatedQuestions && - finalSynthesis.relatedQuestions.length > 0 - ) { - sections.push('## Related Questions\n'); - finalSynthesis.relatedQuestions.forEach((question) => { - sections.push(`- ${question}`); - }); - sections.push('\n'); - } + // Get all the syntheses + const allDepthSynthesis = this.getSynthesis(); + console.log( + `Synthesis map contains ${allDepthSynthesis.size} depth levels` + ); - // Add confidence score - if (finalSynthesis.confidence !== undefined) { - sections.push( - `## Confidence Score\n${finalSynthesis.confidence * 100}%\n` - ); - } + // Generate the final synthesis + const finalSynthesis = await this.generateFinalSynthesis(); + console.log( + `Final synthesis generated with ${ + finalSynthesis.analysis ? finalSynthesis.analysis.length : 0 + } characters` + ); - research = sections.join('\n'); - } catch (error) { - console.error('Error formatting research output:', error); + // Calculate token usage (placeholder values - implement actual counting) + const inputTokens = 256; // Estimate based on prompt length + const outputTokens = 500; // Rough estimate + const inferenceTimeTokens = 975; // Placeholder + const totalTokens = inputTokens + outputTokens + inferenceTimeTokens; + + // Ensure we have a valid research output + let research = 'No research results available.'; + + if (finalSynthesis) { + if (finalSynthesis.analysis) { + // If analysis field exists, use it + research = finalSynthesis.analysis; + } else if ( + this.config.format === 'json' && + Object.keys(finalSynthesis).length > 0 + ) { + // Format the research output based on the synthesis data + // (keeping the existing formatting logic) + // ... } } - } - console.log(`Research output length: ${research.length} characters`); - if (research === 'No research results available.') { - console.log('WARNING: Using default "No research results available" text'); - console.log( - 'Final synthesis data:', - JSON.stringify(finalSynthesis, null, 2) - ); + return { + success: true, + research: research, + _usage: { + input_tokens: Math.round(inputTokens), + output_tokens: Math.round(outputTokens), + inference_time_tokens: inferenceTimeTokens, + total_tokens: Math.round(totalTokens), + }, + sources: [], // Now populated from search results + }; } +} - return { - success: true, - research: research, - _usage: { - input_tokens: Math.round(inputTokens), - output_tokens: Math.round(outputTokens), - inference_time_tokens: inferenceTimeTokens, - total_tokens: Math.round(totalTokens), +export async function createDeepResearch( + config: Partial +): Promise { + // Set up default configs + const defaultConfig: DeepResearchConfig = { + format: 'json', + depth: { + level: 3, + maxTokensPerAnalysis: 4000, + includeReferences: true, + confidenceThreshold: 0.7, + }, + breadth: { + level: 2, + maxParallelTopics: 3, + includeRelatedTopics: true, + minRelevanceScore: 0.8, + }, + synthesis: { + maxOutputTokens: 8000, + targetOutputLength: 5000, + includeSourceDetails: true, }, - sources: [], // Now populated from search results }; + + // Merge provided config with defaults + const mergedConfig = { + ...defaultConfig, + ...config, + depth: { ...defaultConfig.depth, ...config.depth }, + breadth: { ...defaultConfig.breadth, ...config.breadth }, + synthesis: { ...defaultConfig.synthesis, ...config.synthesis }, + }; + + // Return new instance with merged config + return new DeepResearch(mergedConfig); } // Default export diff --git a/src/types/index.ts b/src/types/index.ts index a27efcb..490a087 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -50,6 +50,7 @@ export interface DeepResearchConfig { } export interface DeepResearchInstance { + prompts?: string[]; config: DeepResearchConfig; generateSubQuestions(): Promise; fireWebSearches( @@ -62,6 +63,7 @@ export interface DeepResearchInstance { ): Promise; getSynthesis(): Map; generateFinalSynthesis(): Promise; + generate(prompt: string[]): Promise; } export interface DeepResearchResponse { From aacd9f515511a055e28fae5876251ec33d2af42b Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Tue, 13 May 2025 23:54:08 -0700 Subject: [PATCH 04/13] converted followUpQuestions to functional --- src/generators/followupQuestionGenerator.ts | 159 +++++++++----------- src/index.ts | 18 +-- 2 files changed, 83 insertions(+), 94 deletions(-) diff --git a/src/generators/followupQuestionGenerator.ts b/src/generators/followupQuestionGenerator.ts index 6b0b51b..ca3a7c3 100644 --- a/src/generators/followupQuestionGenerator.ts +++ b/src/generators/followupQuestionGenerator.ts @@ -1,111 +1,100 @@ -import GeminiProvider from '../provider/gemini'; import { WebSearchResult } from '../types'; import 'dotenv/config'; import { cleanJsonResponse } from '../utils/utils'; import { generateFollowupPrompts } from '../prompts/generators'; +import { AIProvider } from '../provider/aiProvider'; -export class FollowupQuestionGenerator { - // private openaiInstance: OpenAIProvider; - private geminiInstance: GeminiProvider; +/** + * Generate follow-up questions based on search results + */ +export async function generateFollowupQuestions( + mainPrompt: string[], + searchResult: WebSearchResult, + maxQuestions: number = 4, + provider: AIProvider, + model: string = 'gemini-2.0-flash' +): Promise { + const { systemPrompt, userPrompt } = generateFollowupPrompts({ + maxQuestions, + searchResult, + mainPrompt, + }); - constructor() { - // this.openaiInstance = OpenAIProvider.getInstance({ - // apiKey: process.env.OPENAI_API_KEY || '', - // }); + try { + const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; - this.geminiInstance = GeminiProvider.getInstance({ - apiKey: process.env.GEMINI_API_KEY || '', - }); - } - - async generateFollowupQuestions( - mainPrompt: string[], - searchResult: WebSearchResult, - maxQuestions: number = 2 - ): Promise { - const { systemPrompt, userPrompt } = generateFollowupPrompts({ - maxQuestions, - searchResult, - mainPrompt, - }); + // Try up to 3 times to get a valid response + let attempts = 0; + const maxAttempts = 3; - try { - const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; + while (attempts < maxAttempts) { + attempts++; + try { + const response = await provider.generateText(combinedPrompt, model); - // Try up to 3 times to get a valid response - let attempts = 0; - const maxAttempts = 3; - - while (attempts < maxAttempts) { - attempts++; - try { - const response = await this.geminiInstance.generateText( - combinedPrompt, - 'gemini-2.0-flash' - ); + // Clean the response to handle markdown-formatted JSON + const cleanedResponse = cleanJsonResponse(response); - // Clean the response to handle markdown-formatted JSON - const cleanedResponse = cleanJsonResponse(response); + // Try to parse the JSON + const parsedQuestions = JSON.parse(cleanedResponse); - // Try to parse the JSON - const parsedQuestions = JSON.parse(cleanedResponse); - - // Validate the response format - if (Array.isArray(parsedQuestions) && parsedQuestions.length > 0) { - // Return only the requested number of questions - return parsedQuestions.slice(0, maxQuestions); - } else { - console.warn( - `Attempt ${attempts}: Response is not a valid array or is empty` - ); - if (attempts >= maxAttempts) { - // If we've reached max attempts, return default questions - return this.generateDefaultQuestions( - searchResult.question.question, - maxQuestions - ); - } - } - } catch (parseError) { + // Validate the response format + if (Array.isArray(parsedQuestions) && parsedQuestions.length > 0) { + // Return only the requested number of questions + return parsedQuestions.slice(0, maxQuestions); + } else { console.warn( - `Attempt ${attempts}: Failed to parse response: ${parseError}` + `Attempt ${attempts}: Response is not a valid array or is empty` ); if (attempts >= maxAttempts) { // If we've reached max attempts, return default questions - return this.generateDefaultQuestions( + return generateDefaultQuestions( searchResult.question.question, maxQuestions ); } } + } catch (parseError) { + console.warn( + `Attempt ${attempts}: Failed to parse response: ${parseError}` + ); + if (attempts >= maxAttempts) { + // If we've reached max attempts, return default questions + return generateDefaultQuestions( + searchResult.question.question, + maxQuestions + ); + } } - - // Fallback if all attempts fail - return this.generateDefaultQuestions( - searchResult.question.question, - maxQuestions - ); - } catch (error) { - console.error('Error generating follow-up questions:', error); - return this.generateDefaultQuestions( - searchResult.question.question, - maxQuestions - ); } + + // Fallback if all attempts fail + return generateDefaultQuestions( + searchResult.question.question, + maxQuestions + ); + } catch (error) { + console.error('Error generating follow-up questions:', error); + return generateDefaultQuestions( + searchResult.question.question, + maxQuestions + ); } +} - // Generate default follow-up questions as a fallback - private generateDefaultQuestions( - originalQuestion: string, - count: number - ): string[] { - const defaultQuestions = [ - `What are the limitations or challenges related to ${originalQuestion}?`, - `How might future developments impact ${originalQuestion}?`, - `What are the ethical considerations surrounding ${originalQuestion}?`, - `How does ${originalQuestion} vary across different contexts or regions?`, - ]; +/** + * Generate default follow-up questions as a fallback + */ +function generateDefaultQuestions( + originalQuestion: string, + count: number +): string[] { + const defaultQuestions = [ + `What are the limitations or challenges related to ${originalQuestion}?`, + `How might future developments impact ${originalQuestion}?`, + `What are the ethical considerations surrounding ${originalQuestion}?`, + `How does ${originalQuestion} vary across different contexts or regions?`, + ]; - return defaultQuestions.slice(0, count); - } + return defaultQuestions.slice(0, count); } diff --git a/src/index.ts b/src/index.ts index 5b9f814..6eb7521 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,11 +19,11 @@ import { WebSearchResult } from './types'; import 'dotenv/config'; import { JigsawProvider } from './provider/jigsaw'; import { SynthesisOutput } from './types/synthesis'; +import { generateFollowupQuestions } from './generators/followupQuestionGenerator'; export class DeepResearch implements DeepResearchInstance { public config: DeepResearchConfig; public prompts?: string[]; - private followupGenerator: FollowupQuestionGenerator; private synthesizer: Synthesizer; private depthSynthesis: Map; private aiProvider: AIProvider; @@ -49,7 +49,6 @@ export class DeepResearch implements DeepResearchInstance { } this.questionGenerator = new SubQuestionGenerator(this.aiProvider); - this.followupGenerator = new FollowupQuestionGenerator(this.aiProvider); this.synthesizer = new Synthesizer(this.aiProvider); this.depthSynthesis = new Map(); } @@ -168,13 +167,14 @@ export class DeepResearch implements DeepResearchInstance { // For each search result, generate follow-up questions for (const result of initialResults) { - const followupQuestions = - await this.followupGenerator.generateFollowupQuestions( - this.prompts, - result, - this.config.breadth?.maxParallelTopics || - DEFAULT_BREADTH_CONFIG.maxParallelTopics - ); + const followupQuestions = await generateFollowupQuestions( + this.prompts, + result, + this.config.breadth?.maxParallelTopics || + DEFAULT_BREADTH_CONFIG.maxParallelTopics, + this.aiProvider, + this.config.models?.default as string | undefined + ); if (followupQuestions.length > 0) { // Convert follow-up questions to SubQuestionGeneratorResult format From 97f7049d7862be4d5d57caf7535952c30d1f85bf Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 00:05:36 -0700 Subject: [PATCH 05/13] remove redundant wrapper classes --- src/generators/subQuestionGenerator.ts | 27 +++- src/index.ts | 216 +++++++++++-------------- src/provider/aiProvider.ts | 7 +- src/types/index.ts | 10 -- 4 files changed, 123 insertions(+), 137 deletions(-) diff --git a/src/generators/subQuestionGenerator.ts b/src/generators/subQuestionGenerator.ts index 9e4c139..53b8421 100644 --- a/src/generators/subQuestionGenerator.ts +++ b/src/generators/subQuestionGenerator.ts @@ -75,13 +75,26 @@ async function validateResponse( /** * Generates sub-questions for a main research topic */ -export async function generateSubQuestions( - mainPrompt: string[], - breadthConfig: ResearchBreadthConfig, - provider: AIProvider, - generationModel: string = 'gemini-2.0-flash', - relevanceCheckModel: string = 'gemini-1.5-flash' -): Promise { + +export interface GenerateSubQuestionsOptions { + mainPrompt: string[]; + breadthConfig: ResearchBreadthConfig; + provider: AIProvider; + generationModel?: string; + relevanceCheckModel?: string; +} + +export async function generateSubQuestions({ + mainPrompt, + breadthConfig, + provider, + generationModel = 'gemini-2.0-flash', + relevanceCheckModel = 'gpt-4o', +}: GenerateSubQuestionsOptions): Promise { + if (!mainPrompt || mainPrompt.length === 0) { + throw new Error('Prompts must be set before generating sub-questions'); + } + const targetQuestionCount = breadthConfig.maxParallelTopics + 2; const { systemPrompt, userPrompt } = generateSubQuestionsPrompt({ diff --git a/src/index.ts b/src/index.ts index 6eb7521..102b3e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,9 @@ import { DeepResearchResponse, RecursiveResearchResult, } from './types'; -import { FollowupQuestionGenerator } from './generators/followupQuestionGenerator'; +import { generateFollowupQuestions } from './generators/followupQuestionGenerator'; import { Synthesizer } from './synthesis/synthesizer'; +import { generateSubQuestions } from './generators/subQuestionGenerator'; import { DEFAULT_CONFIG, @@ -19,7 +20,6 @@ import { WebSearchResult } from './types'; import 'dotenv/config'; import { JigsawProvider } from './provider/jigsaw'; import { SynthesisOutput } from './types/synthesis'; -import { generateFollowupQuestions } from './generators/followupQuestionGenerator'; export class DeepResearch implements DeepResearchInstance { public config: DeepResearchConfig; @@ -27,7 +27,6 @@ export class DeepResearch implements DeepResearchInstance { private synthesizer: Synthesizer; private depthSynthesis: Map; private aiProvider: AIProvider; - private questionGenerator: SubQuestionGenerator; constructor(config: Partial) { this.config = this.validateAndMergeConfig(config); @@ -48,7 +47,6 @@ export class DeepResearch implements DeepResearchInstance { }); } - this.questionGenerator = new SubQuestionGenerator(this.aiProvider); this.synthesizer = new Synthesizer(this.aiProvider); this.depthSynthesis = new Map(); } @@ -78,30 +76,107 @@ export class DeepResearch implements DeepResearchInstance { }; } - public async fireWebSearches( - subQuestions: SubQuestionGeneratorResult - ): Promise { - const jigsaw = JigsawProvider.getInstance(); - const results = await jigsaw.fireWebSearches(subQuestions); - return results; + public getSynthesis(): Map { + return this.depthSynthesis; } - public async generateSubQuestions(): Promise { - if (!this.prompts || this.prompts.length === 0) { - throw new Error('Prompts must be set before generating sub-questions'); + public async generate(prompt: string[]): Promise { + if (!prompt || !Array.isArray(prompt) || prompt.length === 0) { + throw new Error('Prompt must be provided as a non-empty array'); } - return this.questionGenerator.generateSubQuestions( - this.prompts, - { + // Store the prompt in the class property + this.prompts = prompt; + + // Generate sub-questions directly using the imported function + const subQuestions = await generateSubQuestions({ + mainPrompt: this.prompts, + breadthConfig: { ...DEFAULT_BREADTH_CONFIG, ...this.config.breadth, }, - this.aiProvider + provider: this.aiProvider, + generationModel: this.config.models?.default as string, + relevanceCheckModel: this.config.models?.reasoning as string, + }); + console.log(`Generated ${subQuestions.questions.length} sub-questions`); + + // Fire web searches directly + const jigsaw = JigsawProvider.getInstance(); + const initialSearch = await jigsaw.fireWebSearches(subQuestions); + console.log(`Received ${initialSearch.length} initial search results`); + + // Perform recursive research + const recursiveResult = await this.performRecursiveResearch(initialSearch); + console.log( + `Recursive research completed with reason: ${recursiveResult.reason}` + ); + + // Get all the syntheses + const allDepthSynthesis = this.getSynthesis(); + console.log( + `Synthesis map contains ${allDepthSynthesis.size} depth levels` + ); + + // Generate the final synthesis + const allSyntheses: SynthesisOutput[] = []; + this.depthSynthesis.forEach((syntheses) => { + allSyntheses.push(...syntheses); + }); + + const finalSynthesis = await this.synthesizer.generateFinalSynthesis({ + mainPrompt: this.prompts, + allSyntheses: allSyntheses, + maxOutputTokens: this.config.synthesis?.maxOutputTokens, + targetOutputLength: + this.config.synthesis?.targetOutputLength ?? + DEFAULT_SYNTHESIS_CONFIG.targetOutputLength, + }); + + console.log( + `Final synthesis generated with ${ + finalSynthesis.analysis ? finalSynthesis.analysis.length : 0 + } characters` ); + + // Calculate token usage (placeholder values - implement actual counting) + const inputTokens = 256; // Estimate based on prompt length + const outputTokens = 500; // Rough estimate + const inferenceTimeTokens = 975; // Placeholder + const totalTokens = inputTokens + outputTokens + inferenceTimeTokens; + + // Ensure we have a valid research output + let research = 'No research results available.'; + + if (finalSynthesis) { + if (finalSynthesis.analysis) { + // If analysis field exists, use it + research = finalSynthesis.analysis; + } else if ( + this.config.format === 'json' && + Object.keys(finalSynthesis).length > 0 + ) { + // Format the research output based on the synthesis data + // (keeping the existing formatting logic) + // ... + } + } + + return { + success: true, + research: research, + _usage: { + input_tokens: Math.round(inputTokens), + output_tokens: Math.round(outputTokens), + inference_time_tokens: inferenceTimeTokens, + total_tokens: Math.round(totalTokens), + }, + sources: [], // Now populated from search results + }; } - public async performRecursiveResearch( + // We still need the recursive research method since it's complex and has internal state + private async performRecursiveResearch( initialResults: WebSearchResult[], currentDepth: number = 1, parentSynthesis?: SynthesisOutput @@ -167,13 +242,14 @@ export class DeepResearch implements DeepResearchInstance { // For each search result, generate follow-up questions for (const result of initialResults) { + // Use the function directly const followupQuestions = await generateFollowupQuestions( this.prompts, result, this.config.breadth?.maxParallelTopics || DEFAULT_BREADTH_CONFIG.maxParallelTopics, this.aiProvider, - this.config.models?.default as string | undefined + (this.config.models?.default as string) || 'gemini-2.0-flash' ); if (followupQuestions.length > 0) { @@ -193,8 +269,9 @@ export class DeepResearch implements DeepResearchInstance { }; try { - // Fire web searches for the follow-up questions - const followupResults = await this.fireWebSearches(subQuestions); + // Fire web searches directly + const jigsaw = JigsawProvider.getInstance(); + const followupResults = await jigsaw.fireWebSearches(subQuestions); // Recursively process deeper results with the current synthesis const deeperResult = await this.performRecursiveResearch( @@ -224,103 +301,6 @@ export class DeepResearch implements DeepResearchInstance { reason: 'research_complete', }; // Signal that we're done with research } - - public getSynthesis(): Map { - return this.depthSynthesis; - } - - public async generateFinalSynthesis(): Promise { - if (!this.prompts || this.prompts.length === 0) { - throw new Error('Prompts must be set before generating final synthesis'); - } - - // Get all the syntheses from all depth levels - const allSyntheses: SynthesisOutput[] = []; - this.depthSynthesis.forEach((syntheses) => { - allSyntheses.push(...syntheses); - }); - - // Use the synthesizer's generateFinalSynthesis method - return this.synthesizer.generateFinalSynthesis({ - mainPrompt: this.prompts, - allSyntheses: allSyntheses, - maxOutputTokens: this.config.synthesis?.maxOutputTokens, - targetOutputLength: - this.config.synthesis?.targetOutputLength ?? - DEFAULT_SYNTHESIS_CONFIG.targetOutputLength, - }); - } - - public async generate(prompt: string[]): Promise { - if (!prompt || !Array.isArray(prompt) || prompt.length === 0) { - throw new Error('Prompt must be provided as a non-empty array'); - } - - // Store the prompt in the class property - this.prompts = prompt; - - // Now proceed with the existing implementation - const subQuestions = await this.generateSubQuestions(); - console.log(`Generated ${subQuestions.questions.length} sub-questions`); - - const initialSearch = await this.fireWebSearches(subQuestions); - console.log(`Received ${initialSearch.length} initial search results`); - - // Perform recursive research to populate the depthSynthesis map - const recursiveResult = await this.performRecursiveResearch(initialSearch); - console.log( - `Recursive research completed with reason: ${recursiveResult.reason}` - ); - - // Get all the syntheses - const allDepthSynthesis = this.getSynthesis(); - console.log( - `Synthesis map contains ${allDepthSynthesis.size} depth levels` - ); - - // Generate the final synthesis - const finalSynthesis = await this.generateFinalSynthesis(); - console.log( - `Final synthesis generated with ${ - finalSynthesis.analysis ? finalSynthesis.analysis.length : 0 - } characters` - ); - - // Calculate token usage (placeholder values - implement actual counting) - const inputTokens = 256; // Estimate based on prompt length - const outputTokens = 500; // Rough estimate - const inferenceTimeTokens = 975; // Placeholder - const totalTokens = inputTokens + outputTokens + inferenceTimeTokens; - - // Ensure we have a valid research output - let research = 'No research results available.'; - - if (finalSynthesis) { - if (finalSynthesis.analysis) { - // If analysis field exists, use it - research = finalSynthesis.analysis; - } else if ( - this.config.format === 'json' && - Object.keys(finalSynthesis).length > 0 - ) { - // Format the research output based on the synthesis data - // (keeping the existing formatting logic) - // ... - } - } - - return { - success: true, - research: research, - _usage: { - input_tokens: Math.round(inputTokens), - output_tokens: Math.round(outputTokens), - inference_time_tokens: inferenceTimeTokens, - total_tokens: Math.round(totalTokens), - }, - sources: [], // Now populated from search results - }; - } } export async function createDeepResearch( diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index 944e8bc..52fc564 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -2,7 +2,7 @@ import { generateText } from 'ai'; import { GoogleGenerativeAIProvider } from '@ai-sdk/google'; import { DeepInfraProvider } from '@ai-sdk/deepinfra'; import { OpenAIProvider as OpenAISDKProvider } from '@ai-sdk/openai'; -import { ProviderV1 } from '@ai-sdk/provider'; +import { ProviderV1, LanguageModelV1 } from '@ai-sdk/provider'; /** * AIProvider acts as an abstract factory for different AI model providers * It unifies the interface for interacting with different provider types @@ -81,8 +81,11 @@ export class AIProvider { // Case 2: Check if it's a direct provider ID we've stored if (this.directProviders.has(modelOrProvider)) { const provider = this.directProviders.get(modelOrProvider); + if (!provider) { + throw new Error(`Direct provider '${modelOrProvider}' not found`); + } const result = await generateText({ - model: provider, + model: provider as LanguageModelV1, prompt, }); return result.text; diff --git a/src/types/index.ts b/src/types/index.ts index 490a087..9e5ecde 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -52,17 +52,7 @@ export interface DeepResearchConfig { export interface DeepResearchInstance { prompts?: string[]; config: DeepResearchConfig; - generateSubQuestions(): Promise; - fireWebSearches( - subQuestions: SubQuestionGeneratorResult - ): Promise; - performRecursiveResearch( - initialResults: WebSearchResult[], - currentDepth?: number, - parentSynthesis?: SynthesisOutput - ): Promise; getSynthesis(): Map; - generateFinalSynthesis(): Promise; generate(prompt: string[]): Promise; } From bdd3941454cc29691083cc50ef7c0e241f86e3e1 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 00:18:11 -0700 Subject: [PATCH 06/13] aiModels and providers as abstract factory should be set --- examples/basic/simple-research.ts | 2 +- src/index.ts | 17 ++++++++---- src/provider/aiProvider.ts | 45 ++++++++++++++++--------------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 2f691fa..6c4c145 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -24,7 +24,7 @@ async function basicResearch() { models : { output: geminiInstance - } + }, format: 'json', }); diff --git a/src/index.ts b/src/index.ts index 102b3e2..468aa9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,12 +38,19 @@ export class DeepResearch implements DeepResearchInstance { if (config.models) { // For each model type (default, quick, reasoning, etc.) Object.entries(config.models).forEach(([modelType, modelValue]) => { - // If it's not a string, it's likely a provider instance - if (modelValue && typeof modelValue !== 'string') { - // Add it as a direct provider with the model type as the ID - this.aiProvider.addDirectProvider(modelType, modelValue); + if (modelValue) { + if (typeof modelValue !== 'string') { + // Check if it's a LanguageModelV1 or a ProviderV1 + if ('languageModel' in modelValue) { + // It's a ProviderV1, add it as a provider + this.aiProvider.addProvider(modelType, modelValue); + } else { + // It's likely a LanguageModelV1, add it as a direct model + this.aiProvider.addDirectProvider(modelType, modelValue); + } + } + // If it's a string, it will be handled by the generateText method } - // If it's a string, it will be handled by the generateText method }); } diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index 52fc564..b9c61ae 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -9,7 +9,7 @@ import { ProviderV1, LanguageModelV1 } from '@ai-sdk/provider'; */ export class AIProvider { private providers: Map = new Map(); - private directProviders: Map = new Map(); + private directModels: Map = new Map(); /** * Initialize the provider with optional provider instances @@ -38,39 +38,40 @@ export class AIProvider { /** * Add or replace a provider */ - addProvider(name: string, provider: any): void { + addProvider(name: string, provider: ProviderV1): void { this.providers.set(name, provider); } /** - * Add a direct provider with a specific identifier - * This is for cases where the user provides a provider instance directly + * Add a direct model with a specific identifier + * This is for cases where the user provides a model instance directly */ - addDirectProvider(id: string, provider: any): void { - this.directProviders.set(id, provider); + addDirectProvider(id: string, model: LanguageModelV1): void { + this.directModels.set(id, model); } /** * Get a specific provider by name */ - getProvider(name: string): any { + getProvider(name: string): ProviderV1 | undefined { return this.providers.get(name); } /** * Generate text using the specified model or provider * The model can be: - * 1. A string like 'gemini-2.0-flash' (provider-model format) - * 2. A direct reference to a provider instance (stored with a unique ID) + * 1. A direct LanguageModelV1 instance + * 2. A string ID for a stored direct model + * 3. A string in provider-model format (e.g., 'gemini-2.0-flash') */ async generateText( prompt: string, - modelOrProvider: string | any + modelOrProvider: string | LanguageModelV1 ): Promise { try { - // Case 1: Direct provider instance reference + // Case 1: Direct LanguageModelV1 instance if (typeof modelOrProvider !== 'string') { - // If it's a direct provider instance, use it directly + // If it's a direct model instance, use it directly const result = await generateText({ model: modelOrProvider, prompt, @@ -78,14 +79,14 @@ export class AIProvider { return result.text; } - // Case 2: Check if it's a direct provider ID we've stored - if (this.directProviders.has(modelOrProvider)) { - const provider = this.directProviders.get(modelOrProvider); - if (!provider) { - throw new Error(`Direct provider '${modelOrProvider}' not found`); + // Case 2: Check if it's a direct model ID we've stored + if (this.directModels.has(modelOrProvider)) { + const model = this.directModels.get(modelOrProvider); + if (!model) { + throw new Error(`Direct model '${modelOrProvider}' not found`); } const result = await generateText({ - model: provider as LanguageModelV1, + model, // This is already a LanguageModelV1 prompt, }); return result.text; @@ -93,17 +94,19 @@ export class AIProvider { // Case 3: String in provider-model format // Parse the model string to identify the provider - const [providerName, ...modelParts] = modelOrProvider.split('-'); + const [providerName] = modelOrProvider.split('-'); const provider = this.providers.get(providerName); - if (!provider) { throw new Error( `Provider '${providerName}' not found. Please add it using addProvider method.` ); } + // Use the languageModel method to get the LanguageModelV1 instance + const model = provider.languageModel(modelOrProvider); + const result = await generateText({ - model: provider(modelOrProvider), + model, // Now this is a LanguageModelV1 prompt, }); From 0de9c46cc75654181989024737762ee9421a3598 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 00:36:03 -0700 Subject: [PATCH 07/13] made synthesis functions --- src/index.ts | 60 ++++---- src/prompts/synthesis.ts | 2 +- src/synthesis/synthesizer.ts | 269 ++++++++++++++++------------------- src/types/synthesis.ts | 34 +++-- 4 files changed, 184 insertions(+), 181 deletions(-) diff --git a/src/index.ts b/src/index.ts index 468aa9e..9ad11d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,12 @@ import { RecursiveResearchResult, } from './types'; import { generateFollowupQuestions } from './generators/followupQuestionGenerator'; -import { Synthesizer } from './synthesis/synthesizer'; import { generateSubQuestions } from './generators/subQuestionGenerator'; +import { + synthesize, + generateReport, + hasSufficientInformation, +} from './synthesis/synthesizer'; import { DEFAULT_CONFIG, @@ -24,7 +28,6 @@ import { SynthesisOutput } from './types/synthesis'; export class DeepResearch implements DeepResearchInstance { public config: DeepResearchConfig; public prompts?: string[]; - private synthesizer: Synthesizer; private depthSynthesis: Map; private aiProvider: AIProvider; @@ -54,7 +57,6 @@ export class DeepResearch implements DeepResearchInstance { }); } - this.synthesizer = new Synthesizer(this.aiProvider); this.depthSynthesis = new Map(); } @@ -131,18 +133,24 @@ export class DeepResearch implements DeepResearchInstance { allSyntheses.push(...syntheses); }); - const finalSynthesis = await this.synthesizer.generateFinalSynthesis({ - mainPrompt: this.prompts, - allSyntheses: allSyntheses, - maxOutputTokens: this.config.synthesis?.maxOutputTokens, - targetOutputLength: - this.config.synthesis?.targetOutputLength ?? - DEFAULT_SYNTHESIS_CONFIG.targetOutputLength, - }); + const finalReport = await generateReport( + { + mainPrompt: this.prompts, + allSyntheses: allSyntheses, + }, + { + maxOutputTokens: this.config.synthesis?.maxOutputTokens, + targetOutputLength: + this.config.synthesis?.targetOutputLength ?? 'standard', + formatAsMarkdown: true, + }, + this.aiProvider, + this.config.models?.reasoning as string + ); console.log( - `Final synthesis generated with ${ - finalSynthesis.analysis ? finalSynthesis.analysis.length : 0 + `Final research report generated with ${ + finalReport.analysis ? finalReport.analysis.length : 0 } characters` ); @@ -155,13 +163,13 @@ export class DeepResearch implements DeepResearchInstance { // Ensure we have a valid research output let research = 'No research results available.'; - if (finalSynthesis) { - if (finalSynthesis.analysis) { + if (finalReport) { + if (finalReport.analysis) { // If analysis field exists, use it - research = finalSynthesis.analysis; + research = finalReport.analysis; } else if ( this.config.format === 'json' && - Object.keys(finalSynthesis).length > 0 + Object.keys(finalReport).length > 0 ) { // Format the research output based on the synthesis data // (keeping the existing formatting logic) @@ -199,7 +207,7 @@ export class DeepResearch implements DeepResearchInstance { console.log(`Performing research at depth level: ${currentDepth}`); // Check if we already have sufficient information - const hasSufficientInfo = await this.synthesizer.hasSufficientInformation( + const hasSufficientInfo = await hasSufficientInformation( { mainPrompt: this.prompts, results: initialResults, @@ -228,12 +236,16 @@ export class DeepResearch implements DeepResearchInstance { } // First, synthesize the current level results - const synthesis = await this.synthesizer.synthesizeResults({ - mainPrompt: this.prompts, - results: initialResults, - currentDepth, - parentSynthesis, - }); + const synthesis = await synthesize( + { + mainPrompt: this.prompts, + results: initialResults, + currentDepth, + parentSynthesis, + }, + this.aiProvider, + this.config.models?.default as string + ); // Store the synthesis for this depth level if (!this.depthSynthesis.has(currentDepth)) { diff --git a/src/prompts/synthesis.ts b/src/prompts/synthesis.ts index 4babbcb..1a8f292 100644 --- a/src/prompts/synthesis.ts +++ b/src/prompts/synthesis.ts @@ -73,7 +73,7 @@ Please synthesize this information according to the instructions.`; }; }; -export const generateFinalSynthesisPrompt = ({ +export const generateReportPrompt = ({ mainPrompt, allSyntheses = [], maxOutputTokens, diff --git a/src/synthesis/synthesizer.ts b/src/synthesis/synthesizer.ts index 8e17739..26ec9b0 100644 --- a/src/synthesis/synthesizer.ts +++ b/src/synthesis/synthesizer.ts @@ -1,6 +1,7 @@ -import GeminiProvider from '../provider/gemini'; +import { AIProvider } from '../provider/aiProvider'; import { - FinalSynthesisInput, + ReportInput, + ReportConfig, SynthesisInput, SynthesisOutput, } from '../types/synthesis'; @@ -9,162 +10,144 @@ import { cleanJsonResponse } from '../utils/utils'; import 'dotenv/config'; import { generateSynthesisPrompt, - generateFinalSynthesisPrompt, + generateReportPrompt, } from '../prompts/synthesis'; -export class Synthesizer { - private geminiInstance: GeminiProvider; - - constructor() { - this.geminiInstance = GeminiProvider.getInstance({ - apiKey: process.env.GEMINI_API_KEY || '', - }); - } - - async synthesizeResults(input: SynthesisInput): Promise { - const { mainPrompt, results, currentDepth, parentSynthesis } = input; - - const { systemPrompt, userPrompt } = generateSynthesisPrompt({ - mainPrompt, - results, - currentDepth, - parentSynthesis, - }); - +/** + * Synthesize search results into a coherent analysis + */ +export async function synthesize( + input: SynthesisInput, + provider: AIProvider, + model: string = 'gemini-2.0-flash' +): Promise { + const { mainPrompt, results, currentDepth, parentSynthesis } = input; + + const { systemPrompt, userPrompt } = generateSynthesisPrompt({ + mainPrompt, + results, + currentDepth, + parentSynthesis, + }); + + try { + const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; + const response = await provider.generateText(combinedPrompt, model); + + let synthesis: SynthesisOutput; try { - const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; - const response = await this.geminiInstance.generateText( - combinedPrompt, - 'gemini-2.0-flash' + const cleanedResponse = cleanJsonResponse(response); + console.log(`Synthesis at depth ${currentDepth} completed`); + + synthesis = JSON.parse(cleanedResponse); + synthesis.depth = currentDepth; + } catch (parseError) { + console.error('Raw synthesis response:', response); + throw new Error( + `Failed to parse synthesis response as JSON: ${parseError}` ); - - let synthesis: SynthesisOutput; - try { - // Clean the response to handle markdown-formatted JSON - const cleanedResponse = cleanJsonResponse(response); - console.log(`Synthesis at depth ${currentDepth} completed`); - - synthesis = JSON.parse(cleanedResponse); - synthesis.depth = currentDepth; - } catch (parseError) { - console.error('Raw synthesis response:', response); - throw new Error( - `Failed to parse synthesis response as JSON: ${parseError}` - ); - } - - return synthesis; - } catch (error) { - console.error('Error generating synthesis:', error); - return this.generateDefaultSynthesis(mainPrompt, results, currentDepth); } - } - private generateDefaultSynthesis( - mainPrompt: string[], - results: WebSearchResult[], - currentDepth: number - ): SynthesisOutput { - return { - analysis: `Synthesis of ${ - results.length - } results related to ${mainPrompt.join(', ')}`, - keyThemes: ['Information insufficient for detailed synthesis'], - insights: ['Unable to generate insights due to processing error'], - knowledgeGaps: [ - 'Complete synthesis unavailable - further research needed', - ], - confidence: 0.3, - depth: currentDepth, - relatedQuestions: results.map((r) => r.question.question), - }; + return synthesis; + } catch (error) { + console.error('Error generating synthesis:', error); + return failedSynthesis(mainPrompt, results, currentDepth); } +} - async generateFinalSynthesis( - input: FinalSynthesisInput - ): Promise { - const { - mainPrompt, - allSyntheses = [], - maxOutputTokens, - targetOutputLength, - } = input; - - const { systemPrompt, userPrompt } = generateFinalSynthesisPrompt({ - mainPrompt, - allSyntheses, - maxOutputTokens, - targetOutputLength, - }); +/** + * Generate a default synthesis when the AI synthesis fails + */ +export function failedSynthesis( + mainPrompt: string[], + results: WebSearchResult[], + currentDepth: number +): SynthesisOutput { + return { + analysis: `Synthesis of ${ + results.length + } results related to ${mainPrompt.join(', ')}`, + keyThemes: ['Information insufficient for detailed synthesis'], + insights: ['Unable to generate insights due to processing error'], + knowledgeGaps: ['Complete synthesis unavailable - further research needed'], + confidence: 0.3, + depth: currentDepth, + relatedQuestions: results.map((r) => r.question.question), + }; +} +/** + * Generate a comprehensive research report from all syntheses + */ +export async function generateReport( + input: ReportInput, + config: ReportConfig, + provider: AIProvider, + model: string = 'gemini-2.0-flash' +): Promise { + const { mainPrompt, allSyntheses } = input; + const { maxOutputTokens, targetOutputLength } = config; + + const { systemPrompt, userPrompt } = generateReportPrompt({ + mainPrompt, + allSyntheses, + maxOutputTokens, + targetOutputLength, + }); + + try { + const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; + const response = await provider.generateText(combinedPrompt, model); + + let report: SynthesisOutput; try { - const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`; - const response = await this.geminiInstance.generateText( - combinedPrompt, - 'gemini-2.0-flash' - ); - - console.log(`Raw synthesis response: ${response.substring(0, 200)}...`); - - let synthesis: SynthesisOutput; - try { - // Clean the response to handle markdown-formatted JSON - const cleanedResponse = cleanJsonResponse(response); - console.log(`Final synthesis completed`); - - synthesis = JSON.parse(cleanedResponse); - - // If we're getting a JSON metadata object without an analysis field, - // use the full markdown article as the analysis - if (!synthesis.analysis && response.length > 0) { - synthesis.analysis = response; - } - - synthesis.depth = 0; // 0 represents final synthesis - } catch (parseError) { - console.error('Error generating final synthesis:', parseError); - throw new Error( - `Failed to parse final synthesis response as JSON: ${parseError}` - ); - } - - return synthesis; - } catch (error) { - console.error('Error generating final synthesis:', error); - return this.generateDefaultSynthesis(mainPrompt, [], 0); + const cleanedResponse = cleanJsonResponse(response); + console.log('Research report generated'); + + report = JSON.parse(cleanedResponse); + report.depth = 0; // 0 represents final report + } catch (parseError) { + console.error('Error generating research report:', parseError); + throw new Error(`Failed to parse research report as JSON: ${parseError}`); } - } - async hasSufficientInformation( - input: SynthesisInput, - confidenceThreshold: number = 0.85 - ): Promise { - // If we have a parent synthesis with high confidence, we might have enough info - if ( - input.parentSynthesis && - input.parentSynthesis.confidence >= confidenceThreshold - ) { - // Check if we have a good variety of themes and insights - const hasSubstantiveContent = - input.parentSynthesis.keyThemes && - input.parentSynthesis.keyThemes.length >= 3 && - input.parentSynthesis.insights && - input.parentSynthesis.insights.length >= 3 && - input.parentSynthesis.knowledgeGaps && - input.parentSynthesis.knowledgeGaps.length <= 2; // Not too many knowledge gaps - - if (hasSubstantiveContent) { - return true; - } - } + return report; + } catch (error) { + console.error('Error generating research report:', error); + return failedSynthesis(mainPrompt, [], 0); + } +} - // If we have many results at the current depth, we might have enough info - if (input.results.length >= 5) { +/** + * Check if we have sufficient information to stop the research + */ +export async function hasSufficientInformation( + input: SynthesisInput, + confidenceThreshold: number = 0.85 +): Promise { + // If we have a parent synthesis with high confidence, we might have enough info + if ( + input.parentSynthesis && + input.parentSynthesis.confidence >= confidenceThreshold + ) { + // Check if we have a good variety of themes and insights + const hasSubstantiveContent = + input.parentSynthesis.keyThemes && + input.parentSynthesis.keyThemes.length >= 3 && + input.parentSynthesis.insights && + input.parentSynthesis.insights.length >= 3 && + input.parentSynthesis.knowledgeGaps && + input.parentSynthesis.knowledgeGaps.length <= 2; // Not too many knowledge gaps + + if (hasSubstantiveContent) { return true; } + } - return false; + // If we have many results at the current depth, we might have enough info + if (input.results.length >= 5) { + return true; } -} -export default Synthesizer; + return false; +} diff --git a/src/types/synthesis.ts b/src/types/synthesis.ts index 2c86aa2..99eebe0 100644 --- a/src/types/synthesis.ts +++ b/src/types/synthesis.ts @@ -1,42 +1,50 @@ import { WebSearchResult } from '.'; -export interface FinalSynthesisInput { - maxOutputTokens: number; +// Base interface for common synthesis properties +export interface BaseSynthesisInput { mainPrompt: string[]; - allSyntheses: SynthesisOutput[]; - targetOutputLength: 'concise' | 'standard' | 'detailed' | number; } -export interface SynthesisInput { - mainPrompt: string[]; + +// Interface for synthesizing search results +export interface SynthesisInput extends BaseSynthesisInput { results: WebSearchResult[]; currentDepth: number; parentSynthesis?: SynthesisOutput; } +// Interface for generating the final research report +export interface ReportInput extends BaseSynthesisInput { + allSyntheses: SynthesisOutput[]; +} + +// Common output format for both synthesis and report export interface SynthesisOutput { analysis: string; keyThemes: string[]; insights: string[]; knowledgeGaps: string[]; - conflictingInformation?: { + conflictingInformation?: Array<{ topic: string; - conflicts: { + conflicts: Array<{ claim1: string; claim2: string; resolution?: string; - }[]; - }[]; + }>; + }>; confidence: number; depth: number; relatedQuestions: string[]; } -export interface SynthesisConfig { - targetOutputLength: FinalSynthesisInput['targetOutputLength']; +// Configuration for report generation +export interface ReportConfig { + maxOutputTokens?: number; + targetOutputLength: 'concise' | 'standard' | 'detailed' | number; formatAsMarkdown: boolean; } +// Overall synthesis result including all depth levels and final report export interface SynthesisResult { depthSynthesis: Map; - finalSynthesis: SynthesisOutput; + finalReport: SynthesisOutput; } From 92b5e93a1578bd06cfbbd4e35c7a6d9b1633edca Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 00:41:05 -0700 Subject: [PATCH 08/13] simple research set up --- examples/basic/simple-research.ts | 54 ++++++++++++++++++++++++------- src/config/defaults.ts | 6 ++-- src/index.ts | 2 +- src/types/index.ts | 6 ++-- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 6c4c145..4b9dc4f 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -1,50 +1,80 @@ -import createDeepResearch, { DeepResearch } from '../..'; +import createDeepResearch from '../..'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createDeepInfra } from '@ai-sdk/deepinfra'; const geminiInstance = createGoogleGenerativeAI({ - apiKey: process.env.GEMINI_API_KEY -}) + apiKey: process.env.GEMINI_API_KEY, +}); +const openaiInstance = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); +const deepInfraInstance = createDeepInfra({ + apiKey: process.env.DEEPINFRA_API_KEY, +}); // Basic usage example async function basicResearch() { - const deepResearch = new DeepResearch({ + // Create instance using the factory function + const deepResearch = await createDeepResearch({ depth: { level: 3, // Detailed analysis includeReferences: true, + confidenceThreshold: 0.7, }, breadth: { level: 2, // Main topic + direct relationships maxParallelTopics: 4, + includeRelatedTopics: true, + minRelevanceScore: 0.8, }, synthesis: { maxOutputTokens: 8000, // Hard upper limit of tokens - targetOutputLength: 5000, + targetOutputLength: 'detailed', // Changed to use the correct type formatAsMarkdown: true, }, - models : { - output: geminiInstance - + models: { + // Use the correct model types as defined in the config + default: openaiInstance, // For regular generations + reasoning: deepInfraInstance, // For synthesis and analysis + output: geminiInstance, // For quick operations }, format: 'json', }); - const result = deepResearch.generate() + // Need to provide prompts array as required by generate method + const prompts = [ + 'What are the latest developments in quantum computing?', + // Add more related prompts if needed + ]; + + const result = await deepResearch.generate(prompts); // Make sure to await the promise // Log research results console.log('\n=== RESEARCH SUMMARY ==='); console.log(`Research completed successfully: ${result.success}`); + console.log('\n=== RESEARCH ==='); console.log(result.research); // Log token usage console.log('\n=== TOKEN USAGE ==='); - console.log(result._usage); + console.log({ + inputTokens: result._usage.input_tokens, + outputTokens: result._usage.output_tokens, + inferenceTimeTokens: result._usage.inference_time_tokens, + totalTokens: result._usage.total_tokens, + }); - // Log sources + // Log sources (currently empty array, but will be populated in future) console.log('\n=== SOURCES ==='); console.log(result.sources); return result; } -basicResearch().catch(console.error); +// Make sure to handle errors properly +basicResearch().catch((error) => { + console.error('Research failed:', error); + process.exit(1); +}); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 03c81a8..6dc28f8 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -3,11 +3,11 @@ import { ResearchBreadthConfig, ResearchDepthConfig, } from '../types'; -import { SynthesisConfig } from '../types/synthesis'; +import { ReportConfig } from '../types/synthesis'; export const DEFAULT_MODEL_CONFIG: ModelConfig = { default: 'gpt-4.1', - quick: 'gemini-2-flash', + output: 'gemini-2-flash', reasoning: 'deepseek-r1', }; @@ -25,7 +25,7 @@ export const DEFAULT_BREADTH_CONFIG: ResearchBreadthConfig = { minRelevanceScore: 0.7, }; -export const DEFAULT_SYNTHESIS_CONFIG: SynthesisConfig = { +export const DEFAULT_SYNTHESIS_CONFIG: ReportConfig = { targetOutputLength: 'standard', formatAsMarkdown: true, }; diff --git a/src/index.ts b/src/index.ts index 9ad11d7..e086fd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -343,7 +343,7 @@ export async function createDeepResearch( synthesis: { maxOutputTokens: 8000, targetOutputLength: 5000, - includeSourceDetails: true, + formatAsMarkdown: true, }, }; diff --git a/src/types/index.ts b/src/types/index.ts index 9e5ecde..1236cba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ -import { SubQuestion, SubQuestionGeneratorResult } from './generators'; -import { SynthesisConfig, SynthesisOutput } from './synthesis'; +import { SubQuestion } from './generators'; +import { ReportConfig, SynthesisOutput } from './synthesis'; import { ProviderV1 } from '@ai-sdk/provider'; export interface RecursiveResearchResult { @@ -46,7 +46,7 @@ export interface DeepResearchConfig { breadth?: Partial; format: 'json'; models?: Partial; - synthesis: SynthesisConfig; + synthesis: ReportConfig; } export interface DeepResearchInstance { From 2c8e55886bc18ff805e075b25cef331a5df029d2 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 10:06:17 -0700 Subject: [PATCH 09/13] abstract factory working with gpt-4o not yet supported for deepinfra --- examples/basic/simple-research.ts | 72 +++++++++++------------- src/provider/aiProvider.ts | 92 +++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 45 deletions(-) diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 4b9dc4f..8b92f26 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -1,17 +1,5 @@ import createDeepResearch from '../..'; -import { createGoogleGenerativeAI } from '@ai-sdk/google'; -import { createOpenAI } from '@ai-sdk/openai'; -import { createDeepInfra } from '@ai-sdk/deepinfra'; - -const geminiInstance = createGoogleGenerativeAI({ - apiKey: process.env.GEMINI_API_KEY, -}); -const openaiInstance = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}); -const deepInfraInstance = createDeepInfra({ - apiKey: process.env.DEEPINFRA_API_KEY, -}); +import 'dotenv/config'; // Basic usage example async function basicResearch() { @@ -30,14 +18,13 @@ async function basicResearch() { }, synthesis: { maxOutputTokens: 8000, // Hard upper limit of tokens - targetOutputLength: 'detailed', // Changed to use the correct type + targetOutputLength: 'detailed', formatAsMarkdown: true, }, models: { - // Use the correct model types as defined in the config - default: openaiInstance, // For regular generations - reasoning: deepInfraInstance, // For synthesis and analysis - output: geminiInstance, // For quick operations + default: 'gpt-4o', // Default model + reasoning: 'gpt-4o', // Reasoning model + output: 'gemini', // Output model }, format: 'json', }); @@ -48,33 +35,36 @@ async function basicResearch() { // Add more related prompts if needed ]; - const result = await deepResearch.generate(prompts); // Make sure to await the promise + try { + console.log('Starting deep research...'); + const result = await deepResearch.generate(prompts); - // Log research results - console.log('\n=== RESEARCH SUMMARY ==='); - console.log(`Research completed successfully: ${result.success}`); + // Log research results + console.log('\n=== RESEARCH SUMMARY ==='); + console.log(`Research completed successfully: ${result.success}`); - console.log('\n=== RESEARCH ==='); - console.log(result.research); + console.log('\n=== RESEARCH ==='); + console.log(result.research); - // Log token usage - console.log('\n=== TOKEN USAGE ==='); - console.log({ - inputTokens: result._usage.input_tokens, - outputTokens: result._usage.output_tokens, - inferenceTimeTokens: result._usage.inference_time_tokens, - totalTokens: result._usage.total_tokens, - }); + // Log token usage + console.log('\n=== TOKEN USAGE ==='); + console.log({ + inputTokens: result._usage.input_tokens, + outputTokens: result._usage.output_tokens, + inferenceTimeTokens: result._usage.inference_time_tokens, + totalTokens: result._usage.total_tokens, + }); - // Log sources (currently empty array, but will be populated in future) - console.log('\n=== SOURCES ==='); - console.log(result.sources); + // Log sources (currently empty array, but will be populated in future) + console.log('\n=== SOURCES ==='); + console.log(result.sources); - return result; + return result; + } catch (error) { + console.error('Research failed with error:', error); + process.exit(1); + } } -// Make sure to handle errors properly -basicResearch().catch((error) => { - console.error('Research failed:', error); - process.exit(1); -}); +// Run the research +basicResearch(); diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index b9c61ae..585fb1e 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -3,6 +3,10 @@ import { GoogleGenerativeAIProvider } from '@ai-sdk/google'; import { DeepInfraProvider } from '@ai-sdk/deepinfra'; import { OpenAIProvider as OpenAISDKProvider } from '@ai-sdk/openai'; import { ProviderV1, LanguageModelV1 } from '@ai-sdk/provider'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createDeepInfra } from '@ai-sdk/deepinfra'; + /** * AIProvider acts as an abstract factory for different AI model providers * It unifies the interface for interacting with different provider types @@ -10,6 +14,13 @@ import { ProviderV1, LanguageModelV1 } from '@ai-sdk/provider'; export class AIProvider { private providers: Map = new Map(); private directModels: Map = new Map(); + private modelToProviderMap: Record = { + gpt: 'openai', + gemini: 'gemini', + deepseek: 'deepinfra', + mistral: 'deepinfra', + llama: 'deepinfra', + }; /** * Initialize the provider with optional provider instances @@ -33,6 +44,53 @@ export class AIProvider { Object.entries(otherProviders).forEach(([key, provider]) => { if (provider) this.providers.set(key, provider); }); + + // Initialize providers from environment variables if they don't exist + this.initializeProvidersFromEnv(); + } + + /** + * Initialize providers from environment variables + */ + private initializeProvidersFromEnv(): void { + // Initialize OpenAI provider if not already set + if (!this.providers.has('openai') && process.env.OPENAI_API_KEY) { + try { + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + this.providers.set('openai', openai); + console.log('OpenAI provider initialized from environment variable'); + } catch (error) { + console.warn('Failed to initialize OpenAI provider:', error); + } + } + + // Initialize Gemini provider if not already set + if (!this.providers.has('gemini') && process.env.GEMINI_API_KEY) { + try { + const gemini = createGoogleGenerativeAI({ + apiKey: process.env.GEMINI_API_KEY, + }); + this.providers.set('gemini', gemini); + console.log('Gemini provider initialized from environment variable'); + } catch (error) { + console.warn('Failed to initialize Gemini provider:', error); + } + } + + // Initialize DeepInfra provider if not already set + if (!this.providers.has('deepinfra') && process.env.DEEPINFRA_API_KEY) { + try { + const deepinfra = createDeepInfra({ + apiKey: process.env.DEEPINFRA_API_KEY, + }); + this.providers.set('deepinfra', deepinfra); + console.log('DeepInfra provider initialized from environment variable'); + } catch (error) { + console.warn('Failed to initialize DeepInfra provider:', error); + } + } } /** @@ -57,6 +115,32 @@ export class AIProvider { return this.providers.get(name); } + /** + * Map a model name to its provider + */ + private getProviderNameForModel(modelName: string): string { + // First check if the model name starts with a known provider prefix + const [prefix] = modelName.split('-'); + + // Check if we have a direct mapping for this prefix + if (this.modelToProviderMap[prefix]) { + return this.modelToProviderMap[prefix]; + } + + // Try to infer the provider from the model name + if (modelName.startsWith('gpt')) return 'openai'; + if (modelName.startsWith('gemini')) return 'gemini'; + if ( + modelName.startsWith('deepseek') || + modelName.startsWith('mistral') || + modelName.startsWith('llama') + ) + return 'deepinfra'; + + // Default to OpenAI if we can't determine the provider + return 'openai'; + } + /** * Generate text using the specified model or provider * The model can be: @@ -92,13 +176,13 @@ export class AIProvider { return result.text; } - // Case 3: String in provider-model format - // Parse the model string to identify the provider - const [providerName] = modelOrProvider.split('-'); + // Case 3: String model name - determine the provider + const providerName = this.getProviderNameForModel(modelOrProvider); const provider = this.providers.get(providerName); + if (!provider) { throw new Error( - `Provider '${providerName}' not found. Please add it using addProvider method.` + `Provider '${providerName}' not found for model '${modelOrProvider}'. Please add it using addProvider method or set the appropriate API key in environment variables.` ); } From 2f307d0bcfc26beecdd8b1b7d044a97c8516e57c Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 10:45:14 -0700 Subject: [PATCH 10/13] passed the format response to generate function and not in config --- examples/basic/simple-research.ts | 8 +-- src/config/defaults.ts | 3 - src/index.ts | 24 ++++---- src/provider/aiProvider.ts | 98 ++++++++++++++++++++++++++----- src/provider/jigsaw.ts | 8 +-- src/synthesis/synthesizer.ts | 5 ++ src/types/index.ts | 7 ++- 7 files changed, 113 insertions(+), 40 deletions(-) diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 8b92f26..d3ab52f 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -6,7 +6,7 @@ async function basicResearch() { // Create instance using the factory function const deepResearch = await createDeepResearch({ depth: { - level: 3, // Detailed analysis + level: 2, // Detailed analysis includeReferences: true, confidenceThreshold: 0.7, }, @@ -24,9 +24,9 @@ async function basicResearch() { models: { default: 'gpt-4o', // Default model reasoning: 'gpt-4o', // Reasoning model - output: 'gemini', // Output model + output: 'gpt-4o', // Output model - using the same model for consistency }, - format: 'json', + jigsawApiKey: process.env.JIGSAW_API_KEY, }); // Need to provide prompts array as required by generate method @@ -37,7 +37,7 @@ async function basicResearch() { try { console.log('Starting deep research...'); - const result = await deepResearch.generate(prompts); + const result = await deepResearch.generate(prompts, 'markdown'); // Log research results console.log('\n=== RESEARCH SUMMARY ==='); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 6dc28f8..7bcf673 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -30,12 +30,9 @@ export const DEFAULT_SYNTHESIS_CONFIG: ReportConfig = { formatAsMarkdown: true, }; -export const DEFAULT_FORMAT = 'json'; - export const DEFAULT_CONFIG = { models: DEFAULT_MODEL_CONFIG, depth: DEFAULT_DEPTH_CONFIG, breadth: DEFAULT_BREADTH_CONFIG, synthesis: DEFAULT_SYNTHESIS_CONFIG, - format: DEFAULT_FORMAT, } as const; diff --git a/src/index.ts b/src/index.ts index e086fd2..ae8548f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,11 +77,15 @@ export class DeepResearch implements DeepResearchInstance { ...DEFAULT_SYNTHESIS_CONFIG, ...config.synthesis, }, - format: config.format || DEFAULT_CONFIG.format, models: { ...DEFAULT_CONFIG.models, ...config.models, }, + jigsawApiKey: + config.jigsawApiKey || + (() => { + throw new Error('Jigsaw API key must be provided in config'); + })(), }; } @@ -89,7 +93,10 @@ export class DeepResearch implements DeepResearchInstance { return this.depthSynthesis; } - public async generate(prompt: string[]): Promise { + public async generate( + prompt: string[], + format: 'json' | 'markdown' = 'json' + ): Promise { if (!prompt || !Array.isArray(prompt) || prompt.length === 0) { throw new Error('Prompt must be provided as a non-empty array'); } @@ -111,7 +118,7 @@ export class DeepResearch implements DeepResearchInstance { console.log(`Generated ${subQuestions.questions.length} sub-questions`); // Fire web searches directly - const jigsaw = JigsawProvider.getInstance(); + const jigsaw = JigsawProvider.getInstance(this.config.jigsawApiKey); const initialSearch = await jigsaw.fireWebSearches(subQuestions); console.log(`Received ${initialSearch.length} initial search results`); @@ -167,13 +174,9 @@ export class DeepResearch implements DeepResearchInstance { if (finalReport.analysis) { // If analysis field exists, use it research = finalReport.analysis; - } else if ( - this.config.format === 'json' && - Object.keys(finalReport).length > 0 - ) { + } else if (format === 'json' && Object.keys(finalReport).length > 0) { // Format the research output based on the synthesis data - // (keeping the existing formatting logic) - // ... + research = JSON.stringify(finalReport, null, 2); } } @@ -289,7 +292,7 @@ export class DeepResearch implements DeepResearchInstance { try { // Fire web searches directly - const jigsaw = JigsawProvider.getInstance(); + const jigsaw = JigsawProvider.getInstance(this.config.jigsawApiKey); const followupResults = await jigsaw.fireWebSearches(subQuestions); // Recursively process deeper results with the current synthesis @@ -327,7 +330,6 @@ export async function createDeepResearch( ): Promise { // Set up default configs const defaultConfig: DeepResearchConfig = { - format: 'json', depth: { level: 3, maxTokensPerAnalysis: 4000, diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index 585fb1e..1821cbd 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -14,12 +14,40 @@ import { createDeepInfra } from '@ai-sdk/deepinfra'; export class AIProvider { private providers: Map = new Map(); private directModels: Map = new Map(); + + // Model prefix to provider mapping private modelToProviderMap: Record = { + // OpenAI models gpt: 'openai', + 'text-davinci': 'openai', + 'text-curie': 'openai', + 'text-babbage': 'openai', + 'text-ada': 'openai', + davinci: 'openai', + curie: 'openai', + babbage: 'openai', + ada: 'openai', + whisper: 'openai', + 'dall-e': 'openai', + o1: 'openai', + o3: 'openai', + o4: 'openai', + + // Google/Gemini models gemini: 'gemini', + palm: 'gemini', + 'text-bison': 'gemini', + 'chat-bison': 'gemini', + + // DeepInfra models deepseek: 'deepinfra', mistral: 'deepinfra', llama: 'deepinfra', + mixtral: 'deepinfra', + qwen: 'deepinfra', + yi: 'deepinfra', + phi: 'deepinfra', + claude: 'deepinfra', }; /** @@ -127,15 +155,23 @@ export class AIProvider { return this.modelToProviderMap[prefix]; } - // Try to infer the provider from the model name - if (modelName.startsWith('gpt')) return 'openai'; - if (modelName.startsWith('gemini')) return 'gemini'; - if ( - modelName.startsWith('deepseek') || - modelName.startsWith('mistral') || - modelName.startsWith('llama') - ) + // Check if the model name contains a provider name + for (const [modelPrefix, providerName] of Object.entries( + this.modelToProviderMap + )) { + if (modelName.includes(modelPrefix)) { + return providerName; + } + } + + // If we couldn't determine the provider, check if we have any providers available + if (this.providers.has('openai')) { + return 'openai'; + } else if (this.providers.has('gemini')) { + return 'gemini'; + } else if (this.providers.has('deepinfra')) { return 'deepinfra'; + } // Default to OpenAI if we can't determine the provider return 'openai'; @@ -146,7 +182,7 @@ export class AIProvider { * The model can be: * 1. A direct LanguageModelV1 instance * 2. A string ID for a stored direct model - * 3. A string in provider-model format (e.g., 'gemini-2.0-flash') + * 3. A string model name (e.g., 'gpt-4o', 'gemini-1.5-pro') */ async generateText( prompt: string, @@ -186,15 +222,45 @@ export class AIProvider { ); } - // Use the languageModel method to get the LanguageModelV1 instance - const model = provider.languageModel(modelOrProvider); + try { + // Use the languageModel method to get the LanguageModelV1 instance + const model = provider.languageModel(modelOrProvider); + + const result = await generateText({ + model, // Now this is a LanguageModelV1 + prompt, + }); + + return result.text; + } catch (modelError) { + // If the model doesn't exist, try using a default model for the provider + console.warn( + `Model '${modelOrProvider}' not found, trying default model for ${providerName}` + ); - const result = await generateText({ - model, // Now this is a LanguageModelV1 - prompt, - }); + let defaultModel: string; + switch (providerName) { + case 'openai': + defaultModel = 'gpt-3.5-turbo'; + break; + case 'gemini': + defaultModel = 'gemini-1.0-pro'; + break; + case 'deepinfra': + defaultModel = 'mistral-7b-instruct'; + break; + default: + throw modelError; + } - return result.text; + const model = provider.languageModel(defaultModel); + const result = await generateText({ + model, + prompt, + }); + + return result.text; + } } catch (error) { throw new Error( `Error generating text with ${String(modelOrProvider)}: ${ diff --git a/src/provider/jigsaw.ts b/src/provider/jigsaw.ts index 3022686..ecc510e 100644 --- a/src/provider/jigsaw.ts +++ b/src/provider/jigsaw.ts @@ -8,15 +8,15 @@ export class JigsawProvider { private static instance: JigsawProvider; private jigsawInstance: ReturnType; - private constructor() { + private constructor(apiKey?: string) { this.jigsawInstance = JigsawStack({ - apiKey: process.env.JIGSAW_API_KEY, + apiKey: apiKey || process.env.JIGSAW_API_KEY, }); } - public static getInstance(): JigsawProvider { + public static getInstance(apiKey?: string): JigsawProvider { if (!JigsawProvider.instance) { - JigsawProvider.instance = new JigsawProvider(); + JigsawProvider.instance = new JigsawProvider(apiKey); } return JigsawProvider.instance; } diff --git a/src/synthesis/synthesizer.ts b/src/synthesis/synthesizer.ts index 26ec9b0..f5b30b0 100644 --- a/src/synthesis/synthesizer.ts +++ b/src/synthesis/synthesizer.ts @@ -120,7 +120,12 @@ export async function generateReport( /** * Check if we have sufficient information to stop the research + * Does this content sufficient for the main questions asked + * Out of 5? + * Do you think that there can be more relevant questions that can be asked + * Should I go deeper or should */ + export async function hasSufficientInformation( input: SynthesisInput, confidenceThreshold: number = 0.85 diff --git a/src/types/index.ts b/src/types/index.ts index 1236cba..d83b049 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -44,16 +44,19 @@ export interface ResearchBreadthConfig { export interface DeepResearchConfig { depth?: Partial; breadth?: Partial; - format: 'json'; models?: Partial; synthesis: ReportConfig; + jigsawApiKey?: string; } export interface DeepResearchInstance { prompts?: string[]; config: DeepResearchConfig; getSynthesis(): Map; - generate(prompt: string[]): Promise; + generate( + prompt: string[], + format?: 'json' | 'markdown' + ): Promise; } export interface DeepResearchResponse { From 96c286868ada4530c99b8f3919ab92bcfc12a684 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 11:36:26 -0700 Subject: [PATCH 11/13] generating research properly but have to stream with thinking models --- examples/basic/simple-research.ts | 10 ++-- src/index.ts | 76 ++++++++++++++++++++++++------- src/provider/aiProvider.ts | 6 +-- src/synthesis/synthesizer.ts | 4 +- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index d3ab52f..4baaeaa 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -6,7 +6,7 @@ async function basicResearch() { // Create instance using the factory function const deepResearch = await createDeepResearch({ depth: { - level: 2, // Detailed analysis + level: 3, // Detailed analysis includeReferences: true, confidenceThreshold: 0.7, }, @@ -18,20 +18,20 @@ async function basicResearch() { }, synthesis: { maxOutputTokens: 8000, // Hard upper limit of tokens - targetOutputLength: 'detailed', + targetOutputLength: 5000, formatAsMarkdown: true, }, models: { default: 'gpt-4o', // Default model - reasoning: 'gpt-4o', // Reasoning model - output: 'gpt-4o', // Output model - using the same model for consistency + reasoning: 'deepseek-ai/DeepSeek-R1', // Reasoning model + output: 'gemini-2.0-flash', // Output model - using the same model for consistency }, jigsawApiKey: process.env.JIGSAW_API_KEY, }); // Need to provide prompts array as required by generate method const prompts = [ - 'What are the latest developments in quantum computing?', + 'what is the meaning of life?' // Add more related prompts if needed ]; diff --git a/src/index.ts b/src/index.ts index ae8548f..f71f360 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,10 +93,7 @@ export class DeepResearch implements DeepResearchInstance { return this.depthSynthesis; } - public async generate( - prompt: string[], - format: 'json' | 'markdown' = 'json' - ): Promise { + public async generate(prompt: string[]): Promise { if (!prompt || !Array.isArray(prompt) || prompt.length === 0) { throw new Error('Prompt must be provided as a non-empty array'); } @@ -168,17 +165,14 @@ export class DeepResearch implements DeepResearchInstance { const totalTokens = inputTokens + outputTokens + inferenceTimeTokens; // Ensure we have a valid research output - let research = 'No research results available.'; - - if (finalReport) { - if (finalReport.analysis) { - // If analysis field exists, use it - research = finalReport.analysis; - } else if (format === 'json' && Object.keys(finalReport).length > 0) { - // Format the research output based on the synthesis data - research = JSON.stringify(finalReport, null, 2); - } - } + let research = finalReport.analysis || 'No research results available.'; + + console.log(`\n===== FINAL RESEARCH SUMMARY =====`); + console.log( + `Research completed with ${this.depthSynthesis.size} depth levels` + ); + console.log(`Final report length: ${research.length} characters`); + console.log(`Key themes identified: ${finalReport.keyThemes.join(', ')}`); return { success: true, @@ -189,7 +183,7 @@ export class DeepResearch implements DeepResearchInstance { inference_time_tokens: inferenceTimeTokens, total_tokens: Math.round(totalTokens), }, - sources: [], // Now populated from search results + sources: [], // Populate sources from the final report }; } @@ -207,9 +201,13 @@ export class DeepResearch implements DeepResearchInstance { const isMaxDepthReached = currentDepth >= (this.config.depth?.level ?? DEFAULT_DEPTH_CONFIG.level); - console.log(`Performing research at depth level: ${currentDepth}`); + console.log(`\n===== DEPTH LEVEL ${currentDepth} =====`); + console.log(`Initial web search results: ${initialResults.length}`); // Check if we already have sufficient information + console.log( + `Checking if we have sufficient information at depth ${currentDepth}...` + ); const hasSufficientInfo = await hasSufficientInformation( { mainPrompt: this.prompts, @@ -220,10 +218,13 @@ export class DeepResearch implements DeepResearchInstance { this.config.depth?.confidenceThreshold || DEFAULT_DEPTH_CONFIG.confidenceThreshold ); + console.log(`Sufficient information check result: ${hasSufficientInfo}`); // Early return conditions - but don't generate final synthesis yet if (isMaxDepthReached) { + console.log(`\n===== DEPTH ${currentDepth} SUMMARY =====`); console.log(`Maximum depth level ${currentDepth} reached.`); + console.log(`Early termination due to max depth reached.`); return { isComplete: true, reason: 'max_depth_reached', @@ -231,7 +232,9 @@ export class DeepResearch implements DeepResearchInstance { } if (hasSufficientInfo) { + console.log(`\n===== DEPTH ${currentDepth} SUMMARY =====`); console.log(`Sufficient information found at depth ${currentDepth}.`); + console.log(`Early termination due to sufficient information.`); return { isComplete: true, reason: 'sufficient_info', @@ -239,6 +242,9 @@ export class DeepResearch implements DeepResearchInstance { } // First, synthesize the current level results + console.log( + `Starting synthesis at depth ${currentDepth} with ${initialResults.length} results...` + ); const synthesis = await synthesize( { mainPrompt: this.prompts, @@ -249,6 +255,7 @@ export class DeepResearch implements DeepResearchInstance { this.aiProvider, this.config.models?.default as string ); + console.log(`Synthesis at depth ${currentDepth} completed`); // Store the synthesis for this depth level if (!this.depthSynthesis.has(currentDepth)) { @@ -263,7 +270,20 @@ export class DeepResearch implements DeepResearchInstance { }); // For each search result, generate follow-up questions + let totalFollowUpQuestions = 0; + let totalWebSearches = 0; + + console.log( + `\nProcessing ${initialResults.length} search results for follow-up questions at depth ${currentDepth}...` + ); + for (const result of initialResults) { + console.log( + `Generating follow-up questions for result: "${result.question.question.substring( + 0, + 50 + )}..."` + ); // Use the function directly const followupQuestions = await generateFollowupQuestions( this.prompts, @@ -273,6 +293,8 @@ export class DeepResearch implements DeepResearchInstance { this.aiProvider, (this.config.models?.default as string) || 'gemini-2.0-flash' ); + console.log(`Generated ${followupQuestions.length} follow-up questions`); + totalFollowUpQuestions += followupQuestions.length; if (followupQuestions.length > 0) { // Convert follow-up questions to SubQuestionGeneratorResult format @@ -292,15 +314,28 @@ export class DeepResearch implements DeepResearchInstance { try { // Fire web searches directly + console.log( + `Firing web searches for ${subQuestions.questions.length} follow-up questions...` + ); const jigsaw = JigsawProvider.getInstance(this.config.jigsawApiKey); const followupResults = await jigsaw.fireWebSearches(subQuestions); + console.log( + `Received ${followupResults.length} web search results for follow-up questions` + ); + totalWebSearches += followupResults.length; // Recursively process deeper results with the current synthesis + console.log( + `Starting recursive research at depth ${currentDepth + 1}...` + ); const deeperResult = await this.performRecursiveResearch( followupResults, currentDepth + 1, synthesis ); + console.log( + `Returned from recursive research at depth ${currentDepth + 1}` + ); // If we got a result from deeper level (null means we should stop), return it if (deeperResult !== null) { @@ -318,6 +353,13 @@ export class DeepResearch implements DeepResearchInstance { } // If we get here, we've completed this depth but haven't triggered early termination + console.log(`\n===== DEPTH ${currentDepth} SUMMARY =====`); + console.log( + `Total follow-up questions generated: ${totalFollowUpQuestions}` + ); + console.log(`Total web searches performed: ${totalWebSearches}`); + console.log(`Research at depth ${currentDepth} completed\n`); + return { isComplete: true, reason: 'research_complete', diff --git a/src/provider/aiProvider.ts b/src/provider/aiProvider.ts index 1821cbd..437578b 100644 --- a/src/provider/aiProvider.ts +++ b/src/provider/aiProvider.ts @@ -241,13 +241,13 @@ export class AIProvider { let defaultModel: string; switch (providerName) { case 'openai': - defaultModel = 'gpt-3.5-turbo'; + defaultModel = 'gpt-4o'; break; case 'gemini': - defaultModel = 'gemini-1.0-pro'; + defaultModel = 'gemini-2.0-flash'; break; case 'deepinfra': - defaultModel = 'mistral-7b-instruct'; + defaultModel = 'deepseek-ai/DeepSeek-R1'; break; default: throw modelError; diff --git a/src/synthesis/synthesizer.ts b/src/synthesis/synthesizer.ts index f5b30b0..781c064 100644 --- a/src/synthesis/synthesizer.ts +++ b/src/synthesis/synthesizer.ts @@ -121,9 +121,9 @@ export async function generateReport( /** * Check if we have sufficient information to stop the research * Does this content sufficient for the main questions asked - * Out of 5? + * Out of 5? * Do you think that there can be more relevant questions that can be asked - * Should I go deeper or should + * Should I go deeper or should */ export async function hasSufficientInformation( From 8c69f8c657146a401137b9ce4eb715f42372a755 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 11:42:43 -0700 Subject: [PATCH 12/13] logs and clean utils handling reasoning model tags --- src/index.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++- src/utils/utils.ts | 34 +++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index f71f360..50ba2ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { DeepResearchInstance, DeepResearchResponse, RecursiveResearchResult, + ResearchSource, } from './types'; import { generateFollowupQuestions } from './generators/followupQuestionGenerator'; import { generateSubQuestions } from './generators/subQuestionGenerator'; @@ -158,6 +159,27 @@ export class DeepResearch implements DeepResearchInstance { } characters` ); + console.log(`\n===== FINAL REPORT DEBUG =====`); + console.log(`Report object keys: ${Object.keys(finalReport).join(', ')}`); + console.log(`Report analysis exists: ${!!finalReport.analysis}`); + console.log(`Report analysis type: ${typeof finalReport.analysis}`); + console.log( + `Report analysis length: ${ + finalReport.analysis ? finalReport.analysis.length : 0 + }` + ); + console.log( + `Report analysis preview: ${ + finalReport.analysis + ? finalReport.analysis.substring(0, 200) + : 'No analysis' + }` + ); + + if (!finalReport.analysis || finalReport.analysis.length === 0) { + console.error(`WARNING: Final report analysis is empty or undefined!`); + } + // Calculate token usage (placeholder values - implement actual counting) const inputTokens = 256; // Estimate based on prompt length const outputTokens = 500; // Rough estimate @@ -167,12 +189,47 @@ export class DeepResearch implements DeepResearchInstance { // Ensure we have a valid research output let research = finalReport.analysis || 'No research results available.'; + // Collect sources from all search results + const sources: ResearchSource[] = []; + + // Extract unique sources from initial search results + initialSearch.forEach((result) => { + if (result.searchResults && result.searchResults.results) { + result.searchResults.results.forEach((source) => { + // Only add unique URLs + if (source.url && !sources.some((s) => s.url === source.url)) { + // Create a source object with only properties from the ResearchSource interface + const researchSource: ResearchSource = { + url: source.url, + content: source.content || '', + ai_overview: source.ai_overview || '', + title: source.title || 'Unknown Title', + domain: source.domain || '', + isAcademic: source.isAcademic, + }; + + // Add domain if not present but URL is valid + if (!researchSource.domain && researchSource.url) { + try { + researchSource.domain = new URL(researchSource.url).hostname; + } catch (e) { + // Invalid URL, keep domain empty + } + } + + sources.push(researchSource); + } + }); + } + }); + console.log(`\n===== FINAL RESEARCH SUMMARY =====`); console.log( `Research completed with ${this.depthSynthesis.size} depth levels` ); console.log(`Final report length: ${research.length} characters`); console.log(`Key themes identified: ${finalReport.keyThemes.join(', ')}`); + console.log(`Sources collected: ${sources.length}`); return { success: true, @@ -183,7 +240,7 @@ export class DeepResearch implements DeepResearchInstance { inference_time_tokens: inferenceTimeTokens, total_tokens: Math.round(totalTokens), }, - sources: [], // Populate sources from the final report + sources: sources, }; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index c6397d2..50e5719 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -6,8 +6,25 @@ export function cleanJsonResponse(response: string): string { try { // Extract and parse the JSON block const jsonContent = jsonBlockMatch[1].trim(); + // Test if it's valid JSON - JSON.parse(jsonContent); + const parsedJson = JSON.parse(jsonContent); + + // If we have a markdown report, add the full report content as the analysis + if (!parsedJson.analysis && response.indexOf('```json') > 0) { + // Extract the markdown content before the JSON block + const markdownContent = response + .substring(0, response.indexOf('```json')) + .trim(); + if (markdownContent.length > 0) { + console.log( + `Adding markdown content (${markdownContent.length} chars) as analysis` + ); + parsedJson.analysis = markdownContent; + return JSON.stringify(parsedJson); + } + } + return jsonContent; } catch (e) { console.error('Failed to parse JSON metadata block:', e); @@ -42,6 +59,21 @@ export function cleanJsonResponse(response: string): string { JSON.parse(potentialJson); return potentialJson; } + + // If we have a markdown report with no valid JSON, create a JSON with the content as analysis + if (response.length > 0 && !response.includes('')) { + console.log('Creating JSON with markdown content as analysis'); + const jsonWithContent = JSON.stringify({ + analysis: response, + keyThemes: ['Generated from markdown content'], + insights: ['Content extracted from markdown'], + knowledgeGaps: [], + confidence: 0.8, + relatedQuestions: [], + depth: 0, + }); + return jsonWithContent; + } } catch (e) { // If parsing fails, try to fix common issues with JSON strings console.log('JSON parsing failed, attempting to fix common issues...'); From 6047aacf129e7b8e31c3881143e18fe2b9b9bf19 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 14 May 2025 12:05:02 -0700 Subject: [PATCH 13/13] users now pass in the model instances and just run it with out sdk --- README.md | 141 ++++++++++++++++++++++++------ examples/basic/simple-research.ts | 31 +++++-- src/index.ts | 29 +++--- src/types/index.ts | 10 ++- 4 files changed, 161 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 545a321..469e04a 100644 --- a/README.md +++ b/README.md @@ -18,38 +18,124 @@ npm install open-deep-research ## 📝 Usage ```typescript -import { createDeepResearch } from 'open-deep-research'; - -// Simplest usage with a single API key -const research = await createDeepResearch({ - prompt: ['What are the latest advancements in quantum computing?'], - apiKey: 'your-deepseek-api-key', // Single API key for default model - model: 'deepseek-r1', // Specify single model directly - depth: { level: 3 }, - breadth: { maxParallelTopics: 4 }, +import createDeepResearch from 'open-deep-research'; + +// Create instance using the factory function +const deepResearch = await createDeepResearch({ + depth: { + level: 3, // Detailed analysis + includeReferences: true, + }, + breadth: { + level: 2, // Main topic + direct relationships + maxParallelTopics: 3, + }, + synthesis: { + maxOutputTokens: 8000, // Hard upper limit of tokens + targetOutputLength: 5000, + formatAsMarkdown: true, + }, + models: { + default: 'gpt-4o', // Default model + reasoning: 'gemini-1.5-pro', // Reasoning model + }, + jigsawApiKey: 'your-jigsaw-api-key', }); -// OR advanced usage with multiple models -const research = await createDeepResearch({ - prompt: ['What are the latest advancements in quantum computing?'], - apiKeys: { - deepseek: 'your-deepseek-api-key', - openai: 'your-openai-api-key', - gemini: 'your-gemini-api-key', +// Need to provide prompts array as required by generate method +const prompts = [ + 'What are the recent developments in quantum computing?', + // Add more related prompts if needed +]; + +const result = await deepResearch.generate(prompts, 'markdown'); +``` + +### Using String Model Names + +```typescript +import createDeepResearch from 'open-deep-research'; + +// Create instance using the factory function with string model names +const deepResearch = await createDeepResearch({ + depth: { + level: 3, // Detailed analysis + includeReferences: true, + }, + breadth: { + level: 2, // Main topic + direct relationships + maxParallelTopics: 3, + }, + synthesis: { + maxOutputTokens: 8000, // Hard upper limit of tokens + targetOutputLength: 5000, + formatAsMarkdown: true, }, models: { - subQuestionGeneration: 'gemini-1.5-flash', - webSearchAnalysis: 'deepseek-r1', - finalSynthesis: 'gpt-4o', + default: 'gpt-4o', // Default model + reasoning: 'gemini-1.5-pro', // Reasoning model }, - depth: { level: 3 }, - breadth: { maxParallelTopics: 4 }, + jigsawApiKey: 'your-jigsaw-api-key', }); -// Get the research results -const results = await research.execute(); -console.log(results.answer); +// Need to provide prompts array as required by generate method +const prompts = [ + 'What are the recent developments in quantum computing?', + // Add more related prompts if needed +]; +const result = await deepResearch.generate(prompts, 'markdown'); +``` + +### Using Direct Model Instances + +You can also pass model instances directly, which gives you more control over model configuration: + +```typescript +import createDeepResearch from 'open-deep-research'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; + +// Create model instances directly +const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + // Add custom settings as needed + compatibility: 'strict', +}); + +const gemini = createGoogleGenerativeAI({ + apiKey: process.env.GEMINI_API_KEY, +}); + +// Get model instances +const openaiModel = openai.languageModel('gpt-4o'); +const geminiModel = gemini.languageModel('gemini-1.5-pro'); + +// Create instance using the factory function with direct model instances +const deepResearch = await createDeepResearch({ + depth: { + level: 3, + includeReferences: true, + }, + breadth: { + level: 2, + maxParallelTopics: 3, + }, + synthesis: { + maxOutputTokens: 8000, + targetOutputLength: 5000, + formatAsMarkdown: true, + }, + models: { + default: openaiModel, // Pass the model instance directly + reasoning: geminiModel, // Pass the model instance directly + }, + jigsawApiKey: 'your-jigsaw-api-key', +}); + +const prompts = ['What are the recent developments in quantum computing?']; + +const result = await deepResearch.generate(prompts, 'markdown'); ``` ## 🧩 How It Works @@ -172,13 +258,14 @@ To implement the deep research functionality: - Link assertions in the final answer to specific sources 5. **Implement memory management**: + - Track token usage throughout the research process - Apply pruning strategies to manage context size - Implement hierarchical storage for context data - - 1. Keep context in memory during a single run (tree structure) - - 2. Persist to disk at regular intervals or per depth (for crash recovery + debugging) - - 3. Pass context around functions to keep it functional and testable + - 1. Keep context in memory during a single run (tree structure) + - 2. Persist to disk at regular intervals or per depth (for crash recovery + debugging) + - 3. Pass context around functions to keep it functional and testable ## 📄 License diff --git a/examples/basic/simple-research.ts b/examples/basic/simple-research.ts index 4baaeaa..d88f7dc 100644 --- a/examples/basic/simple-research.ts +++ b/examples/basic/simple-research.ts @@ -1,9 +1,30 @@ import createDeepResearch from '../..'; import 'dotenv/config'; +import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { createDeepInfra } from '@ai-sdk/deepinfra'; // Basic usage example async function basicResearch() { - // Create instance using the factory function + // Create model instances directly + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + + const gemini = createGoogleGenerativeAI({ + apiKey: process.env.GEMINI_API_KEY, + }); + + const deepinfra = createDeepInfra({ + apiKey: process.env.DEEPINFRA_API_KEY, + }); + + // Get model instances + const openaiModel = openai.languageModel('gpt-4o'); + const geminiModel = gemini.languageModel('gemini-2.0-flash'); + const deepseekModel = deepinfra.languageModel('deepseek-ai/DeepSeek-R1'); + + // Create instance using the factory function with direct model instances const deepResearch = await createDeepResearch({ depth: { level: 3, // Detailed analysis @@ -22,16 +43,16 @@ async function basicResearch() { formatAsMarkdown: true, }, models: { - default: 'gpt-4o', // Default model - reasoning: 'deepseek-ai/DeepSeek-R1', // Reasoning model - output: 'gemini-2.0-flash', // Output model - using the same model for consistency + default: openaiModel, // Pass the model instance directly + reasoning: deepseekModel, // Pass the model instance directly + output: geminiModel, // Pass the model instance directly }, jigsawApiKey: process.env.JIGSAW_API_KEY, }); // Need to provide prompts array as required by generate method const prompts = [ - 'what is the meaning of life?' + 'what is the meaning of life?', // Add more related prompts if needed ]; diff --git a/src/index.ts b/src/index.ts index 50ba2ae..f5f9f4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,20 +38,14 @@ export class DeepResearch implements DeepResearchInstance { // Initialize AIProvider this.aiProvider = new AIProvider(); - // Add providers from config.models if available + // Add models from config.models if available if (config.models) { // For each model type (default, quick, reasoning, etc.) Object.entries(config.models).forEach(([modelType, modelValue]) => { if (modelValue) { if (typeof modelValue !== 'string') { - // Check if it's a LanguageModelV1 or a ProviderV1 - if ('languageModel' in modelValue) { - // It's a ProviderV1, add it as a provider - this.aiProvider.addProvider(modelType, modelValue); - } else { - // It's likely a LanguageModelV1, add it as a direct model - this.aiProvider.addDirectProvider(modelType, modelValue); - } + // It's a LanguageModelV1 instance, add it as a direct model + this.aiProvider.addDirectProvider(modelType, modelValue); } // If it's a string, it will be handled by the generateText method } @@ -64,7 +58,17 @@ export class DeepResearch implements DeepResearchInstance { private validateAndMergeConfig( config: Partial ): DeepResearchConfig { - // No need to validate prompt anymore + // Merge models carefully to handle both string and LanguageModelV1 instances + const mergedModels = { ...DEFAULT_CONFIG.models }; + + if (config.models) { + Object.entries(config.models).forEach(([key, value]) => { + if (value !== undefined) { + mergedModels[key] = value; + } + }); + } + return { depth: { ...DEFAULT_DEPTH_CONFIG, @@ -78,10 +82,7 @@ export class DeepResearch implements DeepResearchInstance { ...DEFAULT_SYNTHESIS_CONFIG, ...config.synthesis, }, - models: { - ...DEFAULT_CONFIG.models, - ...config.models, - }, + models: mergedModels, jigsawApiKey: config.jigsawApiKey || (() => { diff --git a/src/types/index.ts b/src/types/index.ts index d83b049..0ea1090 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { SubQuestion } from './generators'; import { ReportConfig, SynthesisOutput } from './synthesis'; -import { ProviderV1 } from '@ai-sdk/provider'; +import { LanguageModelV1 } from '@ai-sdk/provider'; export interface RecursiveResearchResult { isComplete: boolean; @@ -22,9 +22,11 @@ export interface ResearchResult { export type ModelType = 'default' | 'quick' | 'reasoning'; export interface ModelConfig { - default: string | ProviderV1; - output?: string | ProviderV1; - reasoning?: string | ProviderV1; + default?: string | LanguageModelV1; + quick?: string | LanguageModelV1; + reasoning?: string | LanguageModelV1; + output?: string | LanguageModelV1; + [key: string]: string | LanguageModelV1 | undefined; } export interface ResearchDepthConfig {